Support logout and add id and pass tests
[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/typings/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   @CreatedAt
357   createdAt: Date
358
359   @UpdatedAt
360   updatedAt: Date
361
362   @HasOne(() => AccountModel, {
363     foreignKey: 'userId',
364     onDelete: 'cascade',
365     hooks: true
366   })
367   Account: AccountModel
368
369   @HasOne(() => UserNotificationSettingModel, {
370     foreignKey: 'userId',
371     onDelete: 'cascade',
372     hooks: true
373   })
374   NotificationSetting: UserNotificationSettingModel
375
376   @HasMany(() => VideoImportModel, {
377     foreignKey: 'userId',
378     onDelete: 'cascade'
379   })
380   VideoImports: VideoImportModel[]
381
382   @HasMany(() => OAuthTokenModel, {
383     foreignKey: 'userId',
384     onDelete: 'cascade'
385   })
386   OAuthTokens: OAuthTokenModel[]
387
388   @BeforeCreate
389   @BeforeUpdate
390   static cryptPasswordIfNeeded (instance: UserModel) {
391     if (instance.changed('password') && instance.password) {
392       return cryptPassword(instance.password)
393         .then(hash => {
394           instance.password = hash
395           return undefined
396         })
397     }
398   }
399
400   @AfterUpdate
401   @AfterDestroy
402   static removeTokenCache (instance: UserModel) {
403     return clearCacheByUserId(instance.id)
404   }
405
406   static countTotal () {
407     return this.count()
408   }
409
410   static listForApi (start: number, count: number, sort: string, search?: string) {
411     let where: WhereOptions
412
413     if (search) {
414       where = {
415         [Op.or]: [
416           {
417             email: {
418               [Op.iLike]: '%' + search + '%'
419             }
420           },
421           {
422             username: {
423               [Op.iLike]: '%' + search + '%'
424             }
425           }
426         ]
427       }
428     }
429
430     const query: FindOptions = {
431       attributes: {
432         include: [
433           [
434             literal(
435               '(' +
436                 UserModel.generateUserQuotaBaseSQL({
437                   withSelect: false,
438                   whereUserId: '"UserModel"."id"'
439                 }) +
440               ')'
441             ),
442             'videoQuotaUsed'
443           ] as any // FIXME: typings
444         ]
445       },
446       offset: start,
447       limit: count,
448       order: getSort(sort),
449       where
450     }
451
452     return UserModel.findAndCountAll(query)
453                     .then(({ rows, count }) => {
454                       return {
455                         data: rows,
456                         total: count
457                       }
458                     })
459   }
460
461   static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
462     const roles = Object.keys(USER_ROLE_LABELS)
463                         .map(k => parseInt(k, 10) as UserRole)
464                         .filter(role => hasUserRight(role, right))
465
466     const query = {
467       where: {
468         role: {
469           [Op.in]: roles
470         }
471       }
472     }
473
474     return UserModel.findAll(query)
475   }
476
477   static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
478     const query = {
479       include: [
480         {
481           model: UserNotificationSettingModel.unscoped(),
482           required: true
483         },
484         {
485           attributes: [ 'userId' ],
486           model: AccountModel.unscoped(),
487           required: true,
488           include: [
489             {
490               attributes: [],
491               model: ActorModel.unscoped(),
492               required: true,
493               where: {
494                 serverId: null
495               },
496               include: [
497                 {
498                   attributes: [],
499                   as: 'ActorFollowings',
500                   model: ActorFollowModel.unscoped(),
501                   required: true,
502                   where: {
503                     targetActorId: actorId
504                   }
505                 }
506               ]
507             }
508           ]
509         }
510       ]
511     }
512
513     return UserModel.unscoped().findAll(query)
514   }
515
516   static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
517     const query = {
518       where: {
519         username: usernames
520       }
521     }
522
523     return UserModel.findAll(query)
524   }
525
526   static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
527     const scopes = [
528       ScopeNames.WITH_VIDEOCHANNELS
529     ]
530
531     if (withStats) scopes.push(ScopeNames.WITH_STATS)
532
533     return UserModel.scope(scopes).findByPk(id)
534   }
535
536   static loadByUsername (username: string): Bluebird<MUserDefault> {
537     const query = {
538       where: {
539         username: { [Op.iLike]: username }
540       }
541     }
542
543     return UserModel.findOne(query)
544   }
545
546   static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> {
547     const query = {
548       where: {
549         username: { [Op.iLike]: username }
550       }
551     }
552
553     return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
554   }
555
556   static loadByEmail (email: string): Bluebird<MUserDefault> {
557     const query = {
558       where: {
559         email
560       }
561     }
562
563     return UserModel.findOne(query)
564   }
565
566   static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
567     if (!email) email = username
568
569     const query = {
570       where: {
571         [Op.or]: [
572           where(fn('lower', col('username')), fn('lower', username)),
573
574           { email }
575         ]
576       }
577     }
578
579     return UserModel.findOne(query)
580   }
581
582   static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
583     const query = {
584       include: [
585         {
586           required: true,
587           attributes: [ 'id' ],
588           model: AccountModel.unscoped(),
589           include: [
590             {
591               required: true,
592               attributes: [ 'id' ],
593               model: VideoChannelModel.unscoped(),
594               include: [
595                 {
596                   required: true,
597                   attributes: [ 'id' ],
598                   model: VideoModel.unscoped(),
599                   where: {
600                     id: videoId
601                   }
602                 }
603               ]
604             }
605           ]
606         }
607       ]
608     }
609
610     return UserModel.findOne(query)
611   }
612
613   static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
614     const query = {
615       include: [
616         {
617           required: true,
618           attributes: [ 'id' ],
619           model: VideoImportModel.unscoped(),
620           where: {
621             id: videoImportId
622           }
623         }
624       ]
625     }
626
627     return UserModel.findOne(query)
628   }
629
630   static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
631     const query = {
632       include: [
633         {
634           required: true,
635           attributes: [ 'id' ],
636           model: AccountModel.unscoped(),
637           include: [
638             {
639               required: true,
640               attributes: [ 'id' ],
641               model: VideoChannelModel.unscoped(),
642               where: {
643                 actorId: videoChannelActorId
644               }
645             }
646           ]
647         }
648       ]
649     }
650
651     return UserModel.findOne(query)
652   }
653
654   static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
655     const query = {
656       include: [
657         {
658           required: true,
659           attributes: [ 'id' ],
660           model: AccountModel.unscoped(),
661           where: {
662             actorId: accountActorId
663           }
664         }
665       ]
666     }
667
668     return UserModel.findOne(query)
669   }
670
671   static getOriginalVideoFileTotalFromUser (user: MUserId) {
672     // Don't use sequelize because we need to use a sub query
673     const query = UserModel.generateUserQuotaBaseSQL({
674       withSelect: true,
675       whereUserId: '$userId'
676     })
677
678     return UserModel.getTotalRawQuery(query, user.id)
679   }
680
681   // Returns cumulative size of all video files uploaded in the last 24 hours.
682   static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
683     // Don't use sequelize because we need to use a sub query
684     const query = UserModel.generateUserQuotaBaseSQL({
685       withSelect: true,
686       whereUserId: '$userId',
687       where: '"video"."createdAt" > now() - interval \'24 hours\''
688     })
689
690     return UserModel.getTotalRawQuery(query, user.id)
691   }
692
693   static async getStats () {
694     const totalUsers = await UserModel.count()
695
696     return {
697       totalUsers
698     }
699   }
700
701   static autoComplete (search: string) {
702     const query = {
703       where: {
704         username: {
705           [Op.like]: `%${search}%`
706         }
707       },
708       limit: 10
709     }
710
711     return UserModel.findAll(query)
712                     .then(u => u.map(u => u.username))
713   }
714
715   canGetVideo (video: MVideoFullLight) {
716     const videoUserId = video.VideoChannel.Account.userId
717
718     if (video.isBlacklisted()) {
719       return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
720     }
721
722     if (video.privacy === VideoPrivacy.PRIVATE) {
723       return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
724     }
725
726     if (video.privacy === VideoPrivacy.INTERNAL) return true
727
728     return false
729   }
730
731   hasRight (right: UserRight) {
732     return hasUserRight(this.role, right)
733   }
734
735   hasAdminFlag (flag: UserAdminFlag) {
736     return this.adminFlags & flag
737   }
738
739   isPasswordMatch (password: string) {
740     return comparePassword(password, this.password)
741   }
742
743   toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
744     const videoQuotaUsed = this.get('videoQuotaUsed')
745     const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
746     const videosCount = this.get('videosCount')
747     const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
748     const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
749     const videoCommentsCount = this.get('videoCommentsCount')
750
751     const json: User = {
752       id: this.id,
753       username: this.username,
754       email: this.email,
755       theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
756
757       pendingEmail: this.pendingEmail,
758       emailVerified: this.emailVerified,
759
760       nsfwPolicy: this.nsfwPolicy,
761       webTorrentEnabled: this.webTorrentEnabled,
762       videosHistoryEnabled: this.videosHistoryEnabled,
763       autoPlayVideo: this.autoPlayVideo,
764       autoPlayNextVideo: this.autoPlayNextVideo,
765       autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
766       videoLanguages: this.videoLanguages,
767
768       role: this.role,
769       roleLabel: USER_ROLE_LABELS[this.role],
770
771       videoQuota: this.videoQuota,
772       videoQuotaDaily: this.videoQuotaDaily,
773       videoQuotaUsed: videoQuotaUsed !== undefined
774         ? parseInt(videoQuotaUsed + '', 10)
775         : undefined,
776       videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
777         ? parseInt(videoQuotaUsedDaily + '', 10)
778         : undefined,
779       videosCount: videosCount !== undefined
780         ? parseInt(videosCount + '', 10)
781         : undefined,
782       videoAbusesCount: videoAbusesCount
783         ? parseInt(videoAbusesCount, 10)
784         : undefined,
785       videoAbusesAcceptedCount: videoAbusesAcceptedCount
786         ? parseInt(videoAbusesAcceptedCount, 10)
787         : undefined,
788       videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
789         ? parseInt(videoAbusesCreatedCount + '', 10)
790         : undefined,
791       videoCommentsCount: videoCommentsCount !== undefined
792         ? parseInt(videoCommentsCount + '', 10)
793         : undefined,
794
795       noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
796       noWelcomeModal: this.noWelcomeModal,
797
798       blocked: this.blocked,
799       blockedReason: this.blockedReason,
800
801       account: this.Account.toFormattedJSON(),
802
803       notificationSettings: this.NotificationSetting
804         ? this.NotificationSetting.toFormattedJSON()
805         : undefined,
806
807       videoChannels: [],
808
809       createdAt: this.createdAt
810     }
811
812     if (parameters.withAdminFlags) {
813       Object.assign(json, { adminFlags: this.adminFlags })
814     }
815
816     if (Array.isArray(this.Account.VideoChannels) === true) {
817       json.videoChannels = this.Account.VideoChannels
818                                .map(c => c.toFormattedJSON())
819                                .sort((v1, v2) => {
820                                  if (v1.createdAt < v2.createdAt) return -1
821                                  if (v1.createdAt === v2.createdAt) return 0
822
823                                  return 1
824                                })
825     }
826
827     return json
828   }
829
830   toMeFormattedJSON (this: MMyUserFormattable): MyUser {
831     const formatted = this.toFormattedJSON()
832
833     const specialPlaylists = this.Account.VideoPlaylists
834                                  .map(p => ({ id: p.id, name: p.name, type: p.type }))
835
836     return Object.assign(formatted, { specialPlaylists })
837   }
838
839   async isAbleToUploadVideo (videoFile: { size: number }) {
840     if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
841
842     const [ totalBytes, totalBytesDaily ] = await Promise.all([
843       UserModel.getOriginalVideoFileTotalFromUser(this),
844       UserModel.getOriginalVideoFileTotalDailyFromUser(this)
845     ])
846
847     const uploadedTotal = videoFile.size + totalBytes
848     const uploadedDaily = videoFile.size + totalBytesDaily
849
850     if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
851     if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
852
853     return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
854   }
855
856   private static generateUserQuotaBaseSQL (options: {
857     whereUserId: '$userId' | '"UserModel"."id"'
858     withSelect: boolean
859     where?: string
860   }) {
861     const andWhere = options.where
862       ? 'AND ' + options.where
863       : ''
864
865     const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
866       'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
867       `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
868
869     const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
870       'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
871       videoChannelJoin
872
873     const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
874       'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
875       'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
876       videoChannelJoin
877
878     return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
879       'FROM (' +
880         `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
881         'GROUP BY "t1"."videoId"' +
882       ') t2'
883   }
884
885   private static getTotalRawQuery (query: string, userId: number) {
886     const options = {
887       bind: { userId },
888       type: QueryTypes.SELECT as QueryTypes.SELECT
889     }
890
891     return UserModel.sequelize.query<{ total: string }>(query, options)
892                     .then(([ { total } ]) => {
893                       if (total === null) return 0
894
895                       return parseInt(total, 10)
896                     })
897   }
898 }