Split types and typings
[oweals/peertube.git] / server / models / account / user.ts
1 import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } 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, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
23 import { User, UserRole } from '../../../shared/models/users'
24 import {
25   isNoInstanceConfigWarningModal,
26   isNoWelcomeModal,
27   isUserAdminFlagsValid,
28   isUserAutoPlayNextVideoPlaylistValid,
29   isUserAutoPlayNextVideoValid,
30   isUserAutoPlayVideoValid,
31   isUserBlockedReasonValid,
32   isUserBlockedValid,
33   isUserEmailVerifiedValid,
34   isUserNSFWPolicyValid,
35   isUserPasswordValid,
36   isUserRoleValid,
37   isUserUsernameValid,
38   isUserVideoLanguages,
39   isUserVideoQuotaDailyValid,
40   isUserVideoQuotaValid,
41   isUserVideosHistoryEnabledValid,
42   isUserWebTorrentEnabledValid
43 } from '../../helpers/custom-validators/users'
44 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
45 import { OAuthTokenModel } from '../oauth/oauth-token'
46 import { getSort, throwIfNotValid } from '../utils'
47 import { VideoChannelModel } from '../video/video-channel'
48 import { VideoPlaylistModel } from '../video/video-playlist'
49 import { AccountModel } from './account'
50 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
51 import { values } from 'lodash'
52 import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
53 import { clearCacheByUserId } from '../../lib/oauth-model'
54 import { UserNotificationSettingModel } from './user-notification-setting'
55 import { VideoModel } from '../video/video'
56 import { ActorModel } from '../activitypub/actor'
57 import { ActorFollowModel } from '../activitypub/actor-follow'
58 import { VideoImportModel } from '../video/video-import'
59 import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
60 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
61 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
62 import * as Bluebird from 'bluebird'
63 import {
64   MMyUserFormattable,
65   MUserDefault,
66   MUserFormattable,
67   MUserId,
68   MUserNotifSettingChannelDefault,
69   MUserWithNotificationSetting,
70   MVideoFullLight
71 } from '@server/types/models'
72
73 enum ScopeNames {
74   FOR_ME_API = 'FOR_ME_API',
75   WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
76   WITH_STATS = 'WITH_STATS'
77 }
78
79 @DefaultScope(() => ({
80   include: [
81     {
82       model: AccountModel,
83       required: true
84     },
85     {
86       model: UserNotificationSettingModel,
87       required: true
88     }
89   ]
90 }))
91 @Scopes(() => ({
92   [ScopeNames.FOR_ME_API]: {
93     include: [
94       {
95         model: AccountModel,
96         include: [
97           {
98             model: VideoChannelModel
99           },
100           {
101             attributes: [ 'id', 'name', 'type' ],
102             model: VideoPlaylistModel.unscoped(),
103             required: true,
104             where: {
105               type: {
106                 [Op.ne]: VideoPlaylistType.REGULAR
107               }
108             }
109           }
110         ]
111       },
112       {
113         model: UserNotificationSettingModel,
114         required: true
115       }
116     ]
117   },
118   [ScopeNames.WITH_VIDEOCHANNELS]: {
119     include: [
120       {
121         model: AccountModel,
122         include: [
123           {
124             model: VideoChannelModel
125           },
126           {
127             attributes: [ 'id', 'name', 'type' ],
128             model: VideoPlaylistModel.unscoped(),
129             required: true,
130             where: {
131               type: {
132                 [Op.ne]: VideoPlaylistType.REGULAR
133               }
134             }
135           }
136         ]
137       }
138     ]
139   },
140   [ScopeNames.WITH_STATS]: {
141     attributes: {
142       include: [
143         [
144           literal(
145             '(' +
146               UserModel.generateUserQuotaBaseSQL({
147                 withSelect: false,
148                 whereUserId: '"UserModel"."id"'
149               }) +
150             ')'
151           ),
152           'videoQuotaUsed'
153         ],
154         [
155           literal(
156             '(' +
157               'SELECT COUNT("video"."id") ' +
158               'FROM "video" ' +
159               'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
160               'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
161               'WHERE "account"."userId" = "UserModel"."id"' +
162             ')'
163           ),
164           'videosCount'
165         ],
166         [
167           literal(
168             '(' +
169               `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
170               'FROM (' +
171                 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
172                        `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173                 'FROM "videoAbuse" ' +
174                 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
175                 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
176                 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
177                 'WHERE "account"."userId" = "UserModel"."id"' +
178               ') t' +
179             ')'
180           ),
181           'videoAbusesCount'
182         ],
183         [
184           literal(
185             '(' +
186               'SELECT COUNT("videoAbuse"."id") ' +
187               'FROM "videoAbuse" ' +
188               'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
189               'WHERE "account"."userId" = "UserModel"."id"' +
190             ')'
191           ),
192           'videoAbusesCreatedCount'
193         ],
194         [
195           literal(
196             '(' +
197               'SELECT COUNT("videoComment"."id") ' +
198               'FROM "videoComment" ' +
199               'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
200               'WHERE "account"."userId" = "UserModel"."id"' +
201             ')'
202           ),
203           'videoCommentsCount'
204         ]
205       ]
206     }
207   }
208 }))
209 @Table({
210   tableName: 'user',
211   indexes: [
212     {
213       fields: [ 'username' ],
214       unique: true
215     },
216     {
217       fields: [ 'email' ],
218       unique: true
219     }
220   ]
221 })
222 export class UserModel extends Model<UserModel> {
223
224   @AllowNull(true)
225   @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
226   @Column
227   password: string
228
229   @AllowNull(false)
230   @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
231   @Column
232   username: string
233
234   @AllowNull(false)
235   @IsEmail
236   @Column(DataType.STRING(400))
237   email: string
238
239   @AllowNull(true)
240   @IsEmail
241   @Column(DataType.STRING(400))
242   pendingEmail: string
243
244   @AllowNull(true)
245   @Default(null)
246   @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
247   @Column
248   emailVerified: boolean
249
250   @AllowNull(false)
251   @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
252   @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
253   nsfwPolicy: NSFWPolicyType
254
255   @AllowNull(false)
256   @Default(true)
257   @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
258   @Column
259   webTorrentEnabled: boolean
260
261   @AllowNull(false)
262   @Default(true)
263   @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
264   @Column
265   videosHistoryEnabled: boolean
266
267   @AllowNull(false)
268   @Default(true)
269   @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
270   @Column
271   autoPlayVideo: boolean
272
273   @AllowNull(false)
274   @Default(false)
275   @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
276   @Column
277   autoPlayNextVideo: boolean
278
279   @AllowNull(false)
280   @Default(true)
281   @Is(
282     'UserAutoPlayNextVideoPlaylist',
283     value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')
284   )
285   @Column
286   autoPlayNextVideoPlaylist: boolean
287
288   @AllowNull(true)
289   @Default(null)
290   @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
291   @Column(DataType.ARRAY(DataType.STRING))
292   videoLanguages: string[]
293
294   @AllowNull(false)
295   @Default(UserAdminFlag.NONE)
296   @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
297   @Column
298   adminFlags?: UserAdminFlag
299
300   @AllowNull(false)
301   @Default(false)
302   @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
303   @Column
304   blocked: boolean
305
306   @AllowNull(true)
307   @Default(null)
308   @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
309   @Column
310   blockedReason: string
311
312   @AllowNull(false)
313   @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
314   @Column
315   role: number
316
317   @AllowNull(false)
318   @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
319   @Column(DataType.BIGINT)
320   videoQuota: number
321
322   @AllowNull(false)
323   @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
324   @Column(DataType.BIGINT)
325   videoQuotaDaily: number
326
327   @AllowNull(false)
328   @Default(DEFAULT_USER_THEME_NAME)
329   @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
330   @Column
331   theme: string
332
333   @AllowNull(false)
334   @Default(false)
335   @Is(
336     'UserNoInstanceConfigWarningModal',
337     value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
338   )
339   @Column
340   noInstanceConfigWarningModal: boolean
341
342   @AllowNull(false)
343   @Default(false)
344   @Is(
345     'UserNoInstanceConfigWarningModal',
346     value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
347   )
348   @Column
349   noWelcomeModal: boolean
350
351   @AllowNull(true)
352   @Default(null)
353   @Column
354   pluginAuth: string
355
356   @AllowNull(true)
357   @Default(null)
358   @Column
359   lastLoginDate: Date
360
361   @CreatedAt
362   createdAt: Date
363
364   @UpdatedAt
365   updatedAt: Date
366
367   @HasOne(() => AccountModel, {
368     foreignKey: 'userId',
369     onDelete: 'cascade',
370     hooks: true
371   })
372   Account: AccountModel
373
374   @HasOne(() => UserNotificationSettingModel, {
375     foreignKey: 'userId',
376     onDelete: 'cascade',
377     hooks: true
378   })
379   NotificationSetting: UserNotificationSettingModel
380
381   @HasMany(() => VideoImportModel, {
382     foreignKey: 'userId',
383     onDelete: 'cascade'
384   })
385   VideoImports: VideoImportModel[]
386
387   @HasMany(() => OAuthTokenModel, {
388     foreignKey: 'userId',
389     onDelete: 'cascade'
390   })
391   OAuthTokens: OAuthTokenModel[]
392
393   @BeforeCreate
394   @BeforeUpdate
395   static cryptPasswordIfNeeded (instance: UserModel) {
396     if (instance.changed('password') && instance.password) {
397       return cryptPassword(instance.password)
398         .then(hash => {
399           instance.password = hash
400           return undefined
401         })
402     }
403   }
404
405   @AfterUpdate
406   @AfterDestroy
407   static removeTokenCache (instance: UserModel) {
408     return clearCacheByUserId(instance.id)
409   }
410
411   static countTotal () {
412     return this.count()
413   }
414
415   static listForApi (start: number, count: number, sort: string, search?: string) {
416     let where: WhereOptions
417
418     if (search) {
419       where = {
420         [Op.or]: [
421           {
422             email: {
423               [Op.iLike]: '%' + search + '%'
424             }
425           },
426           {
427             username: {
428               [Op.iLike]: '%' + search + '%'
429             }
430           }
431         ]
432       }
433     }
434
435     const query: FindOptions = {
436       attributes: {
437         include: [
438           [
439             literal(
440               '(' +
441                 UserModel.generateUserQuotaBaseSQL({
442                   withSelect: false,
443                   whereUserId: '"UserModel"."id"'
444                 }) +
445               ')'
446             ),
447             'videoQuotaUsed'
448           ] as any // FIXME: typings
449         ]
450       },
451       offset: start,
452       limit: count,
453       order: getSort(sort),
454       where
455     }
456
457     return UserModel.findAndCountAll(query)
458                     .then(({ rows, count }) => {
459                       return {
460                         data: rows,
461                         total: count
462                       }
463                     })
464   }
465
466   static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
467     const roles = Object.keys(USER_ROLE_LABELS)
468                         .map(k => parseInt(k, 10) as UserRole)
469                         .filter(role => hasUserRight(role, right))
470
471     const query = {
472       where: {
473         role: {
474           [Op.in]: roles
475         }
476       }
477     }
478
479     return UserModel.findAll(query)
480   }
481
482   static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
483     const query = {
484       include: [
485         {
486           model: UserNotificationSettingModel.unscoped(),
487           required: true
488         },
489         {
490           attributes: [ 'userId' ],
491           model: AccountModel.unscoped(),
492           required: true,
493           include: [
494             {
495               attributes: [],
496               model: ActorModel.unscoped(),
497               required: true,
498               where: {
499                 serverId: null
500               },
501               include: [
502                 {
503                   attributes: [],
504                   as: 'ActorFollowings',
505                   model: ActorFollowModel.unscoped(),
506                   required: true,
507                   where: {
508                     targetActorId: actorId
509                   }
510                 }
511               ]
512             }
513           ]
514         }
515       ]
516     }
517
518     return UserModel.unscoped().findAll(query)
519   }
520
521   static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
522     const query = {
523       where: {
524         username: usernames
525       }
526     }
527
528     return UserModel.findAll(query)
529   }
530
531   static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
532     const scopes = [
533       ScopeNames.WITH_VIDEOCHANNELS
534     ]
535
536     if (withStats) scopes.push(ScopeNames.WITH_STATS)
537
538     return UserModel.scope(scopes).findByPk(id)
539   }
540
541   static loadByUsername (username: string): Bluebird<MUserDefault> {
542     const query = {
543       where: {
544         username: { [Op.iLike]: username }
545       }
546     }
547
548     return UserModel.findOne(query)
549   }
550
551   static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> {
552     const query = {
553       where: {
554         username: { [Op.iLike]: username }
555       }
556     }
557
558     return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
559   }
560
561   static loadByEmail (email: string): Bluebird<MUserDefault> {
562     const query = {
563       where: {
564         email
565       }
566     }
567
568     return UserModel.findOne(query)
569   }
570
571   static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
572     if (!email) email = username
573
574     const query = {
575       where: {
576         [Op.or]: [
577           where(fn('lower', col('username')), fn('lower', username)),
578
579           { email }
580         ]
581       }
582     }
583
584     return UserModel.findOne(query)
585   }
586
587   static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
588     const query = {
589       include: [
590         {
591           required: true,
592           attributes: [ 'id' ],
593           model: AccountModel.unscoped(),
594           include: [
595             {
596               required: true,
597               attributes: [ 'id' ],
598               model: VideoChannelModel.unscoped(),
599               include: [
600                 {
601                   required: true,
602                   attributes: [ 'id' ],
603                   model: VideoModel.unscoped(),
604                   where: {
605                     id: videoId
606                   }
607                 }
608               ]
609             }
610           ]
611         }
612       ]
613     }
614
615     return UserModel.findOne(query)
616   }
617
618   static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
619     const query = {
620       include: [
621         {
622           required: true,
623           attributes: [ 'id' ],
624           model: VideoImportModel.unscoped(),
625           where: {
626             id: videoImportId
627           }
628         }
629       ]
630     }
631
632     return UserModel.findOne(query)
633   }
634
635   static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
636     const query = {
637       include: [
638         {
639           required: true,
640           attributes: [ 'id' ],
641           model: AccountModel.unscoped(),
642           include: [
643             {
644               required: true,
645               attributes: [ 'id' ],
646               model: VideoChannelModel.unscoped(),
647               where: {
648                 actorId: videoChannelActorId
649               }
650             }
651           ]
652         }
653       ]
654     }
655
656     return UserModel.findOne(query)
657   }
658
659   static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
660     const query = {
661       include: [
662         {
663           required: true,
664           attributes: [ 'id' ],
665           model: AccountModel.unscoped(),
666           where: {
667             actorId: accountActorId
668           }
669         }
670       ]
671     }
672
673     return UserModel.findOne(query)
674   }
675
676   static getOriginalVideoFileTotalFromUser (user: MUserId) {
677     // Don't use sequelize because we need to use a sub query
678     const query = UserModel.generateUserQuotaBaseSQL({
679       withSelect: true,
680       whereUserId: '$userId'
681     })
682
683     return UserModel.getTotalRawQuery(query, user.id)
684   }
685
686   // Returns cumulative size of all video files uploaded in the last 24 hours.
687   static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
688     // Don't use sequelize because we need to use a sub query
689     const query = UserModel.generateUserQuotaBaseSQL({
690       withSelect: true,
691       whereUserId: '$userId',
692       where: '"video"."createdAt" > now() - interval \'24 hours\''
693     })
694
695     return UserModel.getTotalRawQuery(query, user.id)
696   }
697
698   static async getStats () {
699     function getActiveUsers (days: number) {
700       const query = {
701         where: {
702           [Op.and]: [
703             literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`)
704           ]
705         }
706       }
707
708       return UserModel.count(query)
709     }
710
711     const totalUsers = await UserModel.count()
712     const totalDailyActiveUsers = await getActiveUsers(1)
713     const totalWeeklyActiveUsers = await getActiveUsers(7)
714     const totalMonthlyActiveUsers = await getActiveUsers(30)
715
716     return {
717       totalUsers,
718       totalDailyActiveUsers,
719       totalWeeklyActiveUsers,
720       totalMonthlyActiveUsers
721     }
722   }
723
724   static autoComplete (search: string) {
725     const query = {
726       where: {
727         username: {
728           [Op.like]: `%${search}%`
729         }
730       },
731       limit: 10
732     }
733
734     return UserModel.findAll(query)
735                     .then(u => u.map(u => u.username))
736   }
737
738   canGetVideo (video: MVideoFullLight) {
739     const videoUserId = video.VideoChannel.Account.userId
740
741     if (video.isBlacklisted()) {
742       return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
743     }
744
745     if (video.privacy === VideoPrivacy.PRIVATE) {
746       return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
747     }
748
749     if (video.privacy === VideoPrivacy.INTERNAL) return true
750
751     return false
752   }
753
754   hasRight (right: UserRight) {
755     return hasUserRight(this.role, right)
756   }
757
758   hasAdminFlag (flag: UserAdminFlag) {
759     return this.adminFlags & flag
760   }
761
762   isPasswordMatch (password: string) {
763     return comparePassword(password, this.password)
764   }
765
766   toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
767     const videoQuotaUsed = this.get('videoQuotaUsed')
768     const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
769     const videosCount = this.get('videosCount')
770     const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
771     const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
772     const videoCommentsCount = this.get('videoCommentsCount')
773
774     const json: User = {
775       id: this.id,
776       username: this.username,
777       email: this.email,
778       theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
779
780       pendingEmail: this.pendingEmail,
781       emailVerified: this.emailVerified,
782
783       nsfwPolicy: this.nsfwPolicy,
784       webTorrentEnabled: this.webTorrentEnabled,
785       videosHistoryEnabled: this.videosHistoryEnabled,
786       autoPlayVideo: this.autoPlayVideo,
787       autoPlayNextVideo: this.autoPlayNextVideo,
788       autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
789       videoLanguages: this.videoLanguages,
790
791       role: this.role,
792       roleLabel: USER_ROLE_LABELS[this.role],
793
794       videoQuota: this.videoQuota,
795       videoQuotaDaily: this.videoQuotaDaily,
796       videoQuotaUsed: videoQuotaUsed !== undefined
797         ? parseInt(videoQuotaUsed + '', 10)
798         : undefined,
799       videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
800         ? parseInt(videoQuotaUsedDaily + '', 10)
801         : undefined,
802       videosCount: videosCount !== undefined
803         ? parseInt(videosCount + '', 10)
804         : undefined,
805       videoAbusesCount: videoAbusesCount
806         ? parseInt(videoAbusesCount, 10)
807         : undefined,
808       videoAbusesAcceptedCount: videoAbusesAcceptedCount
809         ? parseInt(videoAbusesAcceptedCount, 10)
810         : undefined,
811       videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
812         ? parseInt(videoAbusesCreatedCount + '', 10)
813         : undefined,
814       videoCommentsCount: videoCommentsCount !== undefined
815         ? parseInt(videoCommentsCount + '', 10)
816         : undefined,
817
818       noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
819       noWelcomeModal: this.noWelcomeModal,
820
821       blocked: this.blocked,
822       blockedReason: this.blockedReason,
823
824       account: this.Account.toFormattedJSON(),
825
826       notificationSettings: this.NotificationSetting
827         ? this.NotificationSetting.toFormattedJSON()
828         : undefined,
829
830       videoChannels: [],
831
832       createdAt: this.createdAt,
833
834       pluginAuth: this.pluginAuth,
835
836       lastLoginDate: this.lastLoginDate
837     }
838
839     if (parameters.withAdminFlags) {
840       Object.assign(json, { adminFlags: this.adminFlags })
841     }
842
843     if (Array.isArray(this.Account.VideoChannels) === true) {
844       json.videoChannels = this.Account.VideoChannels
845                                .map(c => c.toFormattedJSON())
846                                .sort((v1, v2) => {
847                                  if (v1.createdAt < v2.createdAt) return -1
848                                  if (v1.createdAt === v2.createdAt) return 0
849
850                                  return 1
851                                })
852     }
853
854     return json
855   }
856
857   toMeFormattedJSON (this: MMyUserFormattable): MyUser {
858     const formatted = this.toFormattedJSON()
859
860     const specialPlaylists = this.Account.VideoPlaylists
861                                  .map(p => ({ id: p.id, name: p.name, type: p.type }))
862
863     return Object.assign(formatted, { specialPlaylists })
864   }
865
866   async isAbleToUploadVideo (videoFile: { size: number }) {
867     if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
868
869     const [ totalBytes, totalBytesDaily ] = await Promise.all([
870       UserModel.getOriginalVideoFileTotalFromUser(this),
871       UserModel.getOriginalVideoFileTotalDailyFromUser(this)
872     ])
873
874     const uploadedTotal = videoFile.size + totalBytes
875     const uploadedDaily = videoFile.size + totalBytesDaily
876
877     if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
878     if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
879
880     return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
881   }
882
883   private static generateUserQuotaBaseSQL (options: {
884     whereUserId: '$userId' | '"UserModel"."id"'
885     withSelect: boolean
886     where?: string
887   }) {
888     const andWhere = options.where
889       ? 'AND ' + options.where
890       : ''
891
892     const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
893       'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
894       `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
895
896     const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
897       'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
898       videoChannelJoin
899
900     const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
901       'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
902       'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
903       videoChannelJoin
904
905     return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
906       'FROM (' +
907         `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
908         'GROUP BY "t1"."videoId"' +
909       ') t2'
910   }
911
912   private static getTotalRawQuery (query: string, userId: number) {
913     const options = {
914       bind: { userId },
915       type: QueryTypes.SELECT as QueryTypes.SELECT
916     }
917
918     return UserModel.sequelize.query<{ total: string }>(query, options)
919                     .then(([ { total } ]) => {
920                       if (total === null) return 0
921
922                       return parseInt(total, 10)
923                     })
924   }
925 }