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