1 import { SortMeta } from 'primeng/api'
2 import { from, Observable, of } from 'rxjs'
3 import { catchError, concatMap, filter, first, map, shareReplay, throttleTime, toArray } from 'rxjs/operators'
4 import { HttpClient, HttpParams } from '@angular/common/http'
5 import { Injectable } from '@angular/core'
6 import { AuthService } from '@app/core/auth'
7 import { getBytes } from '@root-helpers/bytes'
8 import { UserLocalStorageKeys } from '@root-helpers/users'
13 User as UserServerModel,
20 } from '@shared/models'
21 import { environment } from '../../../environments/environment'
22 import { RestExtractor, RestPagination, RestService } from '../rest'
23 import { LocalStorageService, SessionStorageService } from '../wrappers/storage.service'
24 import { User } from './user.model'
27 export class UserService {
28 static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/'
30 private userCache: { [ id: number ]: Observable<UserServerModel> } = {}
33 private authHttp: HttpClient,
34 private authService: AuthService,
35 private restExtractor: RestExtractor,
36 private restService: RestService,
37 private localStorageService: LocalStorageService,
38 private sessionStorageService: SessionStorageService
41 changePassword (currentPassword: string, newPassword: string) {
42 const url = UserService.BASE_USERS_URL + 'me'
43 const body: UserUpdateMe = {
48 return this.authHttp.put(url, body)
50 map(this.restExtractor.extractDataBool),
51 catchError(err => this.restExtractor.handleError(err))
55 changeEmail (password: string, newEmail: string) {
56 const url = UserService.BASE_USERS_URL + 'me'
57 const body: UserUpdateMe = {
58 currentPassword: password,
62 return this.authHttp.put(url, body)
64 map(this.restExtractor.extractDataBool),
65 catchError(err => this.restExtractor.handleError(err))
69 updateMyProfile (profile: UserUpdateMe) {
70 const url = UserService.BASE_USERS_URL + 'me'
72 return this.authHttp.put(url, profile)
74 map(this.restExtractor.extractDataBool),
75 catchError(err => this.restExtractor.handleError(err))
79 updateMyAnonymousProfile (profile: UserUpdateMe) {
80 const localStorageKeys: { [ id in keyof UserUpdateMe ]: string } = {
81 nsfwPolicy: UserLocalStorageKeys.NSFW_POLICY,
82 webTorrentEnabled: UserLocalStorageKeys.WEBTORRENT_ENABLED,
83 autoPlayNextVideo: UserLocalStorageKeys.AUTO_PLAY_VIDEO,
84 autoPlayNextVideoPlaylist: UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST,
85 theme: UserLocalStorageKeys.THEME,
86 videoLanguages: UserLocalStorageKeys.VIDEO_LANGUAGES
89 const obj = Object.keys(localStorageKeys)
90 .filter(key => key in profile)
91 .map(key => ([ localStorageKeys[key], profile[key] ]))
93 for (const [ key, value ] of obj) {
95 if (value === undefined) {
96 this.localStorageService.removeItem(key)
100 const localStorageValue = typeof value === 'string'
102 : JSON.stringify(value)
104 this.localStorageService.setItem(key, localStorageValue)
106 console.error(`Cannot set ${key}->${value} in localStorage. Likely due to a value impossible to stringify.`, err)
111 listenAnonymousUpdate () {
112 return this.localStorageService.watch([
113 UserLocalStorageKeys.NSFW_POLICY,
114 UserLocalStorageKeys.WEBTORRENT_ENABLED,
115 UserLocalStorageKeys.AUTO_PLAY_VIDEO,
116 UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST,
117 UserLocalStorageKeys.THEME,
118 UserLocalStorageKeys.VIDEO_LANGUAGES
121 filter(() => this.authService.isLoggedIn() !== true),
122 map(() => this.getAnonymousUser())
127 const url = UserService.BASE_USERS_URL + 'me'
129 return this.authHttp.delete(url)
131 map(this.restExtractor.extractDataBool),
132 catchError(err => this.restExtractor.handleError(err))
136 changeAvatar (avatarForm: FormData) {
137 const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
139 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
140 .pipe(catchError(err => this.restExtractor.handleError(err)))
144 const url = UserService.BASE_USERS_URL + 'me/avatar'
146 return this.authHttp.delete(url)
148 map(this.restExtractor.extractDataBool),
149 catchError(err => this.restExtractor.handleError(err))
153 signup (userCreate: UserRegister) {
154 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
156 map(this.restExtractor.extractDataBool),
157 catchError(err => this.restExtractor.handleError(err))
161 getMyVideoQuotaUsed () {
162 const url = UserService.BASE_USERS_URL + 'me/video-quota-used'
164 return this.authHttp.get<UserVideoQuota>(url)
165 .pipe(catchError(err => this.restExtractor.handleError(err)))
168 askResetPassword (email: string) {
169 const url = UserService.BASE_USERS_URL + '/ask-reset-password'
171 return this.authHttp.post(url, { email })
173 map(this.restExtractor.extractDataBool),
174 catchError(err => this.restExtractor.handleError(err))
178 resetPassword (userId: number, verificationString: string, password: string) {
179 const url = `${UserService.BASE_USERS_URL}/${userId}/reset-password`
185 return this.authHttp.post(url, body)
187 map(this.restExtractor.extractDataBool),
188 catchError(res => this.restExtractor.handleError(res))
192 verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) {
193 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
199 return this.authHttp.post(url, body)
201 map(this.restExtractor.extractDataBool),
202 catchError(res => this.restExtractor.handleError(res))
206 askSendVerifyEmail (email: string) {
207 const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
209 return this.authHttp.post(url, { email })
211 map(this.restExtractor.extractDataBool),
212 catchError(err => this.restExtractor.handleError(err))
216 autocomplete (search: string): Observable<string[]> {
217 const url = UserService.BASE_USERS_URL + 'autocomplete'
218 const params = new HttpParams().append('search', search)
221 .get<string[]>(url, { params })
222 .pipe(catchError(res => this.restExtractor.handleError(res)))
225 getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
226 // Don't update display name, the user seems to have changed it
227 if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
229 return this.displayNameToUsername(newDisplayName)
232 displayNameToUsername (displayName: string) {
233 if (!displayName) return ''
238 .replace(/[^a-z0-9_.]/g, '')
241 /* ###### Admin methods ###### */
243 addUser (userCreate: UserCreate) {
244 return this.authHttp.post(UserService.BASE_USERS_URL, userCreate)
246 map(this.restExtractor.extractDataBool),
247 catchError(err => this.restExtractor.handleError(err))
251 updateUser (userId: number, userUpdate: UserUpdate) {
252 return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
254 map(this.restExtractor.extractDataBool),
255 catchError(err => this.restExtractor.handleError(err))
259 updateUsers (users: UserServerModel[], userUpdate: UserUpdate) {
262 concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
264 catchError(err => this.restExtractor.handleError(err))
268 getUserWithCache (userId: number) {
269 if (!this.userCache[userId]) {
270 this.userCache[ userId ] = this.getUser(userId).pipe(shareReplay())
273 return this.userCache[userId]
276 getUser (userId: number, withStats = false) {
277 const params = new HttpParams().append('withStats', withStats + '')
278 return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId, { params })
279 .pipe(catchError(err => this.restExtractor.handleError(err)))
282 getAnonymousUser () {
283 let videoLanguages: string[]
286 const languagesString = this.localStorageService.getItem(UserLocalStorageKeys.VIDEO_LANGUAGES)
287 videoLanguages = languagesString && languagesString !== 'undefined'
288 ? JSON.parse(languagesString)
291 videoLanguages = null
292 console.error('Cannot parse desired video languages from localStorage.', err)
296 // local storage keys
297 nsfwPolicy: this.localStorageService.getItem(UserLocalStorageKeys.NSFW_POLICY),
298 webTorrentEnabled: this.localStorageService.getItem(UserLocalStorageKeys.WEBTORRENT_ENABLED) !== 'false',
299 theme: this.localStorageService.getItem(UserLocalStorageKeys.THEME) || 'instance-default',
302 autoPlayNextVideoPlaylist: this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST) !== 'false',
303 autoPlayVideo: this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO) === 'true',
305 // session storage keys
306 autoPlayNextVideo: this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
310 getUsers (parameters: {
311 pagination: RestPagination
314 }): Observable<ResultList<UserServerModel>> {
315 const { pagination, sort, search } = parameters
317 let params = new HttpParams()
318 params = this.restService.addRestGetParams(params, pagination, sort)
321 const filters = this.restService.parseQueryStringFilter(search, {
326 if (v === 'true') return v
327 if (v === 'false') return v
334 params = this.restService.addObjectParams(params, filters)
337 return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
339 map(res => this.restExtractor.convertResultListDateToHuman(res)),
340 map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))),
341 catchError(err => this.restExtractor.handleError(err))
345 removeUser (usersArg: UserServerModel | UserServerModel[]) {
346 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
350 concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)),
352 catchError(err => this.restExtractor.handleError(err))
356 banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
357 const body = reason ? { reason } : {}
358 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
362 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)),
364 catchError(err => this.restExtractor.handleError(err))
368 unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
369 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
373 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})),
375 catchError(err => this.restExtractor.handleError(err))
379 getAnonymousOrLoggedUser () {
380 if (!this.authService.isLoggedIn()) {
381 return of(this.getAnonymousUser())
384 return this.authService.userInformationLoaded
387 map(() => this.authService.getUser())
391 private formatUser (user: UserServerModel) {
393 if (user.videoQuota === -1) {
396 videoQuota = getBytes(user.videoQuota, 0)
399 const videoQuotaUsed = getBytes(user.videoQuotaUsed, 0)
401 let videoQuotaDaily: string
402 let videoQuotaUsedDaily: string
403 if (user.videoQuotaDaily === -1) {
404 videoQuotaDaily = '∞'
405 videoQuotaUsedDaily = getBytes(0, 0) + ''
407 videoQuotaDaily = getBytes(user.videoQuotaDaily, 0) + ''
408 videoQuotaUsedDaily = getBytes(user.videoQuotaUsedDaily || 0, 0) + ''
411 const roleLabels: { [ id in UserRole ]: string } = {
412 [UserRole.USER]: $localize`User`,
413 [UserRole.ADMINISTRATOR]: $localize`Administrator`,
414 [UserRole.MODERATOR]: $localize`Moderator`
417 return Object.assign(user, {
418 roleLabel: roleLabels[user.role],
421 rawVideoQuota: user.videoQuota,
422 rawVideoQuotaUsed: user.videoQuotaUsed,
425 rawVideoQuotaDaily: user.videoQuotaDaily,
426 rawVideoQuotaUsedDaily: user.videoQuotaUsedDaily