]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/account/user.ts
Fix notification icon position
[github/Chocobozzz/PeerTube.git] / server / models / account / user.ts
1 import * as Sequelize from 'sequelize'
2 import {
3 AfterDestroy,
4 AfterUpdate,
5 AllowNull,
6 BeforeCreate,
7 BeforeUpdate,
8 Column,
9 CreatedAt,
10 DataType,
11 Default,
12 DefaultScope,
13 HasMany,
14 HasOne,
15 Is,
16 IsEmail,
17 Model,
18 Scopes,
19 Table,
20 UpdatedAt
21 } from 'sequelize-typescript'
22 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
23 import { User, UserRole } from '../../../shared/models/users'
24 import {
25 isUserAutoPlayVideoValid,
26 isUserBlockedReasonValid,
27 isUserBlockedValid,
28 isUserEmailVerifiedValid,
29 isUserNSFWPolicyValid,
30 isUserPasswordValid,
31 isUserRoleValid,
32 isUserUsernameValid,
33 isUserVideoQuotaDailyValid,
34 isUserVideoQuotaValid,
35 isUserVideosHistoryEnabledValid,
36 isUserWebTorrentEnabledValid
37 } from '../../helpers/custom-validators/users'
38 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
39 import { OAuthTokenModel } from '../oauth/oauth-token'
40 import { getSort, throwIfNotValid } from '../utils'
41 import { VideoChannelModel } from '../video/video-channel'
42 import { AccountModel } from './account'
43 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
44 import { values } from 'lodash'
45 import { NSFW_POLICY_TYPES } from '../../initializers'
46 import { clearCacheByUserId } from '../../lib/oauth-model'
47 import { UserNotificationSettingModel } from './user-notification-setting'
48 import { VideoModel } from '../video/video'
49 import { ActorModel } from '../activitypub/actor'
50 import { ActorFollowModel } from '../activitypub/actor-follow'
51
52 enum ScopeNames {
53 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
54 }
55
56 @DefaultScope({
57 include: [
58 {
59 model: () => AccountModel,
60 required: true
61 },
62 {
63 model: () => UserNotificationSettingModel,
64 required: true
65 }
66 ]
67 })
68 @Scopes({
69 [ScopeNames.WITH_VIDEO_CHANNEL]: {
70 include: [
71 {
72 model: () => AccountModel,
73 required: true,
74 include: [ () => VideoChannelModel ]
75 },
76 {
77 model: () => UserNotificationSettingModel,
78 required: true
79 }
80 ]
81 }
82 })
83 @Table({
84 tableName: 'user',
85 indexes: [
86 {
87 fields: [ 'username' ],
88 unique: true
89 },
90 {
91 fields: [ 'email' ],
92 unique: true
93 }
94 ]
95 })
96 export class UserModel extends Model<UserModel> {
97
98 @AllowNull(false)
99 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
100 @Column
101 password: string
102
103 @AllowNull(false)
104 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
105 @Column
106 username: string
107
108 @AllowNull(false)
109 @IsEmail
110 @Column(DataType.STRING(400))
111 email: string
112
113 @AllowNull(true)
114 @Default(null)
115 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean'))
116 @Column
117 emailVerified: boolean
118
119 @AllowNull(false)
120 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
121 @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
122 nsfwPolicy: NSFWPolicyType
123
124 @AllowNull(false)
125 @Default(true)
126 @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
127 @Column
128 webTorrentEnabled: boolean
129
130 @AllowNull(false)
131 @Default(true)
132 @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
133 @Column
134 videosHistoryEnabled: boolean
135
136 @AllowNull(false)
137 @Default(true)
138 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
139 @Column
140 autoPlayVideo: boolean
141
142 @AllowNull(false)
143 @Default(false)
144 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
145 @Column
146 blocked: boolean
147
148 @AllowNull(true)
149 @Default(null)
150 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason'))
151 @Column
152 blockedReason: string
153
154 @AllowNull(false)
155 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
156 @Column
157 role: number
158
159 @AllowNull(false)
160 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
161 @Column(DataType.BIGINT)
162 videoQuota: number
163
164 @AllowNull(false)
165 @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
166 @Column(DataType.BIGINT)
167 videoQuotaDaily: number
168
169 @CreatedAt
170 createdAt: Date
171
172 @UpdatedAt
173 updatedAt: Date
174
175 @HasOne(() => AccountModel, {
176 foreignKey: 'userId',
177 onDelete: 'cascade',
178 hooks: true
179 })
180 Account: AccountModel
181
182 @HasOne(() => UserNotificationSettingModel, {
183 foreignKey: 'userId',
184 onDelete: 'cascade',
185 hooks: true
186 })
187 NotificationSetting: UserNotificationSettingModel
188
189 @HasMany(() => OAuthTokenModel, {
190 foreignKey: 'userId',
191 onDelete: 'cascade'
192 })
193 OAuthTokens: OAuthTokenModel[]
194
195 @BeforeCreate
196 @BeforeUpdate
197 static cryptPasswordIfNeeded (instance: UserModel) {
198 if (instance.changed('password')) {
199 return cryptPassword(instance.password)
200 .then(hash => {
201 instance.password = hash
202 return undefined
203 })
204 }
205 }
206
207 @AfterUpdate
208 @AfterDestroy
209 static removeTokenCache (instance: UserModel) {
210 return clearCacheByUserId(instance.id)
211 }
212
213 static countTotal () {
214 return this.count()
215 }
216
217 static listForApi (start: number, count: number, sort: string, search?: string) {
218 let where = undefined
219 if (search) {
220 where = {
221 [Sequelize.Op.or]: [
222 {
223 email: {
224 [Sequelize.Op.iLike]: '%' + search + '%'
225 }
226 },
227 {
228 username: {
229 [ Sequelize.Op.iLike ]: '%' + search + '%'
230 }
231 }
232 ]
233 }
234 }
235
236 const query = {
237 attributes: {
238 include: [
239 [
240 Sequelize.literal(
241 '(' +
242 'SELECT COALESCE(SUM("size"), 0) ' +
243 'FROM (' +
244 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
245 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
246 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
247 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
248 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
249 ') t' +
250 ')'
251 ),
252 'videoQuotaUsed'
253 ] as any // FIXME: typings
254 ]
255 },
256 offset: start,
257 limit: count,
258 order: getSort(sort),
259 where
260 }
261
262 return UserModel.findAndCountAll(query)
263 .then(({ rows, count }) => {
264 return {
265 data: rows,
266 total: count
267 }
268 })
269 }
270
271 static listWithRight (right: UserRight) {
272 const roles = Object.keys(USER_ROLE_LABELS)
273 .map(k => parseInt(k, 10) as UserRole)
274 .filter(role => hasUserRight(role, right))
275
276 const query = {
277 where: {
278 role: {
279 [Sequelize.Op.in]: roles
280 }
281 }
282 }
283
284 return UserModel.findAll(query)
285 }
286
287 static listUserSubscribersOf (actorId: number) {
288 const query = {
289 include: [
290 {
291 model: UserNotificationSettingModel.unscoped(),
292 required: true
293 },
294 {
295 attributes: [ 'userId' ],
296 model: AccountModel.unscoped(),
297 required: true,
298 include: [
299 {
300 attributes: [ ],
301 model: ActorModel.unscoped(),
302 required: true,
303 where: {
304 serverId: null
305 },
306 include: [
307 {
308 attributes: [ ],
309 as: 'ActorFollowings',
310 model: ActorFollowModel.unscoped(),
311 required: true,
312 where: {
313 targetActorId: actorId
314 }
315 }
316 ]
317 }
318 ]
319 }
320 ]
321 }
322
323 return UserModel.unscoped().findAll(query)
324 }
325
326 static loadById (id: number) {
327 return UserModel.findById(id)
328 }
329
330 static loadByUsername (username: string) {
331 const query = {
332 where: {
333 username
334 }
335 }
336
337 return UserModel.findOne(query)
338 }
339
340 static loadByUsernameAndPopulateChannels (username: string) {
341 const query = {
342 where: {
343 username
344 }
345 }
346
347 return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
348 }
349
350 static loadByEmail (email: string) {
351 const query = {
352 where: {
353 email
354 }
355 }
356
357 return UserModel.findOne(query)
358 }
359
360 static loadByUsernameOrEmail (username: string, email?: string) {
361 if (!email) email = username
362
363 const query = {
364 where: {
365 [ Sequelize.Op.or ]: [ { username }, { email } ]
366 }
367 }
368
369 return UserModel.findOne(query)
370 }
371
372 static loadByVideoId (videoId: number) {
373 const query = {
374 include: [
375 {
376 required: true,
377 attributes: [ 'id' ],
378 model: AccountModel.unscoped(),
379 include: [
380 {
381 required: true,
382 attributes: [ 'id' ],
383 model: VideoChannelModel.unscoped(),
384 include: [
385 {
386 required: true,
387 attributes: [ 'id' ],
388 model: VideoModel.unscoped(),
389 where: {
390 id: videoId
391 }
392 }
393 ]
394 }
395 ]
396 }
397 ]
398 }
399
400 return UserModel.findOne(query)
401 }
402
403 static getOriginalVideoFileTotalFromUser (user: UserModel) {
404 // Don't use sequelize because we need to use a sub query
405 const query = UserModel.generateUserQuotaBaseSQL()
406
407 return UserModel.getTotalRawQuery(query, user.id)
408 }
409
410 // Returns cumulative size of all video files uploaded in the last 24 hours.
411 static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
412 // Don't use sequelize because we need to use a sub query
413 const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
414
415 return UserModel.getTotalRawQuery(query, user.id)
416 }
417
418 static async getStats () {
419 const totalUsers = await UserModel.count()
420
421 return {
422 totalUsers
423 }
424 }
425
426 static autoComplete (search: string) {
427 const query = {
428 where: {
429 username: {
430 [ Sequelize.Op.like ]: `%${search}%`
431 }
432 },
433 limit: 10
434 }
435
436 return UserModel.findAll(query)
437 .then(u => u.map(u => u.username))
438 }
439
440 hasRight (right: UserRight) {
441 return hasUserRight(this.role, right)
442 }
443
444 isPasswordMatch (password: string) {
445 return comparePassword(password, this.password)
446 }
447
448 toFormattedJSON (): User {
449 const videoQuotaUsed = this.get('videoQuotaUsed')
450 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
451
452 const json = {
453 id: this.id,
454 username: this.username,
455 email: this.email,
456 emailVerified: this.emailVerified,
457 nsfwPolicy: this.nsfwPolicy,
458 webTorrentEnabled: this.webTorrentEnabled,
459 videosHistoryEnabled: this.videosHistoryEnabled,
460 autoPlayVideo: this.autoPlayVideo,
461 role: this.role,
462 roleLabel: USER_ROLE_LABELS[ this.role ],
463 videoQuota: this.videoQuota,
464 videoQuotaDaily: this.videoQuotaDaily,
465 createdAt: this.createdAt,
466 blocked: this.blocked,
467 blockedReason: this.blockedReason,
468 account: this.Account.toFormattedJSON(),
469 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
470 videoChannels: [],
471 videoQuotaUsed: videoQuotaUsed !== undefined
472 ? parseInt(videoQuotaUsed, 10)
473 : undefined,
474 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
475 ? parseInt(videoQuotaUsedDaily, 10)
476 : undefined
477 }
478
479 if (Array.isArray(this.Account.VideoChannels) === true) {
480 json.videoChannels = this.Account.VideoChannels
481 .map(c => c.toFormattedJSON())
482 .sort((v1, v2) => {
483 if (v1.createdAt < v2.createdAt) return -1
484 if (v1.createdAt === v2.createdAt) return 0
485
486 return 1
487 })
488 }
489
490 return json
491 }
492
493 async isAbleToUploadVideo (videoFile: { size: number }) {
494 if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
495
496 const [ totalBytes, totalBytesDaily ] = await Promise.all([
497 UserModel.getOriginalVideoFileTotalFromUser(this),
498 UserModel.getOriginalVideoFileTotalDailyFromUser(this)
499 ])
500
501 const uploadedTotal = videoFile.size + totalBytes
502 const uploadedDaily = videoFile.size + totalBytesDaily
503 if (this.videoQuotaDaily === -1) {
504 return uploadedTotal < this.videoQuota
505 }
506 if (this.videoQuota === -1) {
507 return uploadedDaily < this.videoQuotaDaily
508 }
509
510 return (uploadedTotal < this.videoQuota) &&
511 (uploadedDaily < this.videoQuotaDaily)
512 }
513
514 private static generateUserQuotaBaseSQL (where?: string) {
515 const andWhere = where ? 'AND ' + where : ''
516
517 return 'SELECT SUM("size") AS "total" ' +
518 'FROM (' +
519 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
520 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
521 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
522 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
523 'WHERE "account"."userId" = $userId ' + andWhere +
524 'GROUP BY "video"."id"' +
525 ') t'
526 }
527
528 private static getTotalRawQuery (query: string, userId: number) {
529 const options = {
530 bind: { userId },
531 type: Sequelize.QueryTypes.SELECT
532 }
533
534 return UserModel.sequelize.query(query, options)
535 .then(([ { total } ]) => {
536 if (total === null) return 0
537
538 return parseInt(total, 10)
539 })
540 }
541 }