]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/core/users/user.service.ts
33cc1f668d13138bdfb8412b1edc2d96e8fe4b69
[github/Chocobozzz/PeerTube.git] / client / src / app / core / users / user.service.ts
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'
9 import {
10 Avatar,
11 NSFWPolicyType,
12 ResultList,
13 User as UserServerModel,
14 UserCreate,
15 UserRegister,
16 UserRole,
17 UserUpdate,
18 UserUpdateMe,
19 UserVideoQuota
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'
25
26 @Injectable()
27 export class UserService {
28 static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/'
29
30 private userCache: { [ id: number ]: Observable<UserServerModel> } = {}
31
32 constructor (
33 private authHttp: HttpClient,
34 private authService: AuthService,
35 private restExtractor: RestExtractor,
36 private restService: RestService,
37 private localStorageService: LocalStorageService,
38 private sessionStorageService: SessionStorageService
39 ) { }
40
41 changePassword (currentPassword: string, newPassword: string) {
42 const url = UserService.BASE_USERS_URL + 'me'
43 const body: UserUpdateMe = {
44 currentPassword,
45 password: newPassword
46 }
47
48 return this.authHttp.put(url, body)
49 .pipe(
50 map(this.restExtractor.extractDataBool),
51 catchError(err => this.restExtractor.handleError(err))
52 )
53 }
54
55 changeEmail (password: string, newEmail: string) {
56 const url = UserService.BASE_USERS_URL + 'me'
57 const body: UserUpdateMe = {
58 currentPassword: password,
59 email: newEmail
60 }
61
62 return this.authHttp.put(url, body)
63 .pipe(
64 map(this.restExtractor.extractDataBool),
65 catchError(err => this.restExtractor.handleError(err))
66 )
67 }
68
69 updateMyProfile (profile: UserUpdateMe) {
70 const url = UserService.BASE_USERS_URL + 'me'
71
72 return this.authHttp.put(url, profile)
73 .pipe(
74 map(this.restExtractor.extractDataBool),
75 catchError(err => this.restExtractor.handleError(err))
76 )
77 }
78
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
87 }
88
89 const obj = Object.keys(localStorageKeys)
90 .filter(key => key in profile)
91 .map(key => ([ localStorageKeys[key], profile[key] ]))
92
93 for (const [ key, value ] of obj) {
94 try {
95 if (value === undefined) {
96 this.localStorageService.removeItem(key)
97 continue
98 }
99
100 const localStorageValue = typeof value === 'string'
101 ? value
102 : JSON.stringify(value)
103
104 this.localStorageService.setItem(key, localStorageValue)
105 } catch (err) {
106 console.error(`Cannot set ${key}->${value} in localStorage. Likely due to a value impossible to stringify.`, err)
107 }
108 }
109 }
110
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
119 ]).pipe(
120 throttleTime(200),
121 filter(() => this.authService.isLoggedIn() !== true),
122 map(() => this.getAnonymousUser())
123 )
124 }
125
126 deleteMe () {
127 const url = UserService.BASE_USERS_URL + 'me'
128
129 return this.authHttp.delete(url)
130 .pipe(
131 map(this.restExtractor.extractDataBool),
132 catchError(err => this.restExtractor.handleError(err))
133 )
134 }
135
136 changeAvatar (avatarForm: FormData) {
137 const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
138
139 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
140 .pipe(catchError(err => this.restExtractor.handleError(err)))
141 }
142
143 deleteAvatar () {
144 const url = UserService.BASE_USERS_URL + 'me/avatar'
145
146 return this.authHttp.delete(url)
147 .pipe(
148 map(this.restExtractor.extractDataBool),
149 catchError(err => this.restExtractor.handleError(err))
150 )
151 }
152
153 signup (userCreate: UserRegister) {
154 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
155 .pipe(
156 map(this.restExtractor.extractDataBool),
157 catchError(err => this.restExtractor.handleError(err))
158 )
159 }
160
161 getMyVideoQuotaUsed () {
162 const url = UserService.BASE_USERS_URL + 'me/video-quota-used'
163
164 return this.authHttp.get<UserVideoQuota>(url)
165 .pipe(catchError(err => this.restExtractor.handleError(err)))
166 }
167
168 askResetPassword (email: string) {
169 const url = UserService.BASE_USERS_URL + '/ask-reset-password'
170
171 return this.authHttp.post(url, { email })
172 .pipe(
173 map(this.restExtractor.extractDataBool),
174 catchError(err => this.restExtractor.handleError(err))
175 )
176 }
177
178 resetPassword (userId: number, verificationString: string, password: string) {
179 const url = `${UserService.BASE_USERS_URL}/${userId}/reset-password`
180 const body = {
181 verificationString,
182 password
183 }
184
185 return this.authHttp.post(url, body)
186 .pipe(
187 map(this.restExtractor.extractDataBool),
188 catchError(res => this.restExtractor.handleError(res))
189 )
190 }
191
192 verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) {
193 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
194 const body = {
195 verificationString,
196 isPendingEmail
197 }
198
199 return this.authHttp.post(url, body)
200 .pipe(
201 map(this.restExtractor.extractDataBool),
202 catchError(res => this.restExtractor.handleError(res))
203 )
204 }
205
206 askSendVerifyEmail (email: string) {
207 const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
208
209 return this.authHttp.post(url, { email })
210 .pipe(
211 map(this.restExtractor.extractDataBool),
212 catchError(err => this.restExtractor.handleError(err))
213 )
214 }
215
216 autocomplete (search: string): Observable<string[]> {
217 const url = UserService.BASE_USERS_URL + 'autocomplete'
218 const params = new HttpParams().append('search', search)
219
220 return this.authHttp
221 .get<string[]>(url, { params })
222 .pipe(catchError(res => this.restExtractor.handleError(res)))
223 }
224
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
228
229 return this.displayNameToUsername(newDisplayName)
230 }
231
232 displayNameToUsername (displayName: string) {
233 if (!displayName) return ''
234
235 return displayName
236 .toLowerCase()
237 .replace(/\s/g, '_')
238 .replace(/[^a-z0-9_.]/g, '')
239 }
240
241 /* ###### Admin methods ###### */
242
243 addUser (userCreate: UserCreate) {
244 return this.authHttp.post(UserService.BASE_USERS_URL, userCreate)
245 .pipe(
246 map(this.restExtractor.extractDataBool),
247 catchError(err => this.restExtractor.handleError(err))
248 )
249 }
250
251 updateUser (userId: number, userUpdate: UserUpdate) {
252 return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
253 .pipe(
254 map(this.restExtractor.extractDataBool),
255 catchError(err => this.restExtractor.handleError(err))
256 )
257 }
258
259 updateUsers (users: UserServerModel[], userUpdate: UserUpdate) {
260 return from(users)
261 .pipe(
262 concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
263 toArray(),
264 catchError(err => this.restExtractor.handleError(err))
265 )
266 }
267
268 getUserWithCache (userId: number) {
269 if (!this.userCache[userId]) {
270 this.userCache[ userId ] = this.getUser(userId).pipe(shareReplay())
271 }
272
273 return this.userCache[userId]
274 }
275
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)))
280 }
281
282 getAnonymousUser () {
283 let videoLanguages: string[]
284
285 try {
286 const languagesString = this.localStorageService.getItem(UserLocalStorageKeys.VIDEO_LANGUAGES)
287 videoLanguages = languagesString && languagesString !== 'undefined'
288 ? JSON.parse(languagesString)
289 : null
290 } catch (err) {
291 videoLanguages = null
292 console.error('Cannot parse desired video languages from localStorage.', err)
293 }
294
295 return new User({
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',
300 videoLanguages,
301
302 autoPlayNextVideoPlaylist: this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST) !== 'false',
303 autoPlayVideo: this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO) === 'true',
304
305 // session storage keys
306 autoPlayNextVideo: this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
307 })
308 }
309
310 getUsers (parameters: {
311 pagination: RestPagination
312 sort: SortMeta
313 search?: string
314 }): Observable<ResultList<UserServerModel>> {
315 const { pagination, sort, search } = parameters
316
317 let params = new HttpParams()
318 params = this.restService.addRestGetParams(params, pagination, sort)
319
320 if (search) {
321 const filters = this.restService.parseQueryStringFilter(search, {
322 blocked: {
323 prefix: 'banned:',
324 isBoolean: true,
325 handler: v => {
326 if (v === 'true') return v
327 if (v === 'false') return v
328
329 return undefined
330 }
331 }
332 })
333
334 params = this.restService.addObjectParams(params, filters)
335 }
336
337 return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
338 .pipe(
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))
342 )
343 }
344
345 removeUser (usersArg: UserServerModel | UserServerModel[]) {
346 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
347
348 return from(users)
349 .pipe(
350 concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)),
351 toArray(),
352 catchError(err => this.restExtractor.handleError(err))
353 )
354 }
355
356 banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
357 const body = reason ? { reason } : {}
358 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
359
360 return from(users)
361 .pipe(
362 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)),
363 toArray(),
364 catchError(err => this.restExtractor.handleError(err))
365 )
366 }
367
368 unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
369 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
370
371 return from(users)
372 .pipe(
373 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})),
374 toArray(),
375 catchError(err => this.restExtractor.handleError(err))
376 )
377 }
378
379 getAnonymousOrLoggedUser () {
380 if (!this.authService.isLoggedIn()) {
381 return of(this.getAnonymousUser())
382 }
383
384 return this.authService.userInformationLoaded
385 .pipe(
386 first(),
387 map(() => this.authService.getUser())
388 )
389 }
390
391 private formatUser (user: UserServerModel) {
392 let videoQuota
393 if (user.videoQuota === -1) {
394 videoQuota = '∞'
395 } else {
396 videoQuota = getBytes(user.videoQuota, 0)
397 }
398
399 const videoQuotaUsed = getBytes(user.videoQuotaUsed, 0)
400
401 let videoQuotaDaily: string
402 let videoQuotaUsedDaily: string
403 if (user.videoQuotaDaily === -1) {
404 videoQuotaDaily = '∞'
405 videoQuotaUsedDaily = getBytes(0, 0) + ''
406 } else {
407 videoQuotaDaily = getBytes(user.videoQuotaDaily, 0) + ''
408 videoQuotaUsedDaily = getBytes(user.videoQuotaUsedDaily || 0, 0) + ''
409 }
410
411 const roleLabels: { [ id in UserRole ]: string } = {
412 [UserRole.USER]: $localize`User`,
413 [UserRole.ADMINISTRATOR]: $localize`Administrator`,
414 [UserRole.MODERATOR]: $localize`Moderator`
415 }
416
417 return Object.assign(user, {
418 roleLabel: roleLabels[user.role],
419 videoQuota,
420 videoQuotaUsed,
421 rawVideoQuota: user.videoQuota,
422 rawVideoQuotaUsed: user.videoQuotaUsed,
423 videoQuotaDaily,
424 videoQuotaUsedDaily,
425 rawVideoQuotaDaily: user.videoQuotaDaily,
426 rawVideoQuotaUsedDaily: user.videoQuotaUsedDaily
427 })
428 }
429 }