1 import { FindOptions, literal, Op, QueryTypes } from 'sequelize'
21 } from 'sequelize-typescript'
22 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
23 import { User, UserRole } from '../../../shared/models/users'
25 isUserAdminFlagsValid,
26 isUserAutoPlayVideoValid,
27 isUserBlockedReasonValid,
29 isUserEmailVerifiedValid,
30 isUserNSFWPolicyValid,
35 isUserVideoQuotaDailyValid,
36 isUserVideoQuotaValid,
37 isUserVideosHistoryEnabledValid,
38 isUserWebTorrentEnabledValid
39 } from '../../helpers/custom-validators/users'
40 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
41 import { OAuthTokenModel } from '../oauth/oauth-token'
42 import { getSort, throwIfNotValid } from '../utils'
43 import { VideoChannelModel } from '../video/video-channel'
44 import { AccountModel } from './account'
45 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
46 import { values } from 'lodash'
47 import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
48 import { clearCacheByUserId } from '../../lib/oauth-model'
49 import { UserNotificationSettingModel } from './user-notification-setting'
50 import { VideoModel } from '../video/video'
51 import { ActorModel } from '../activitypub/actor'
52 import { ActorFollowModel } from '../activitypub/actor-follow'
53 import { VideoImportModel } from '../video/video-import'
54 import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
55 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
56 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
57 import * as Bluebird from 'bluebird'
62 MUserNotifSettingChannelDefault,
63 MUserWithNotificationSetting
64 } from '@server/typings/models'
67 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
70 @DefaultScope(() => ({
77 model: UserNotificationSettingModel,
83 [ScopeNames.WITH_VIDEO_CHANNEL]: {
88 include: [ VideoChannelModel ]
91 model: UserNotificationSettingModel,
101 fields: [ 'username' ],
110 export class UserModel extends Model<UserModel> {
113 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
118 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
124 @Column(DataType.STRING(400))
129 @Column(DataType.STRING(400))
134 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
136 emailVerified: boolean
139 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
140 @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
141 nsfwPolicy: NSFWPolicyType
145 @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
147 webTorrentEnabled: boolean
151 @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
153 videosHistoryEnabled: boolean
157 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
159 autoPlayVideo: boolean
163 @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
164 @Column(DataType.ARRAY(DataType.STRING))
165 videoLanguages: string[]
168 @Default(UserAdminFlag.NONE)
169 @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
171 adminFlags?: UserAdminFlag
175 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
181 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
183 blockedReason: string
186 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
191 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
192 @Column(DataType.BIGINT)
196 @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
197 @Column(DataType.BIGINT)
198 videoQuotaDaily: number
201 @Default(DEFAULT_THEME_NAME)
202 @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
212 @HasOne(() => AccountModel, {
213 foreignKey: 'userId',
217 Account: AccountModel
219 @HasOne(() => UserNotificationSettingModel, {
220 foreignKey: 'userId',
224 NotificationSetting: UserNotificationSettingModel
226 @HasMany(() => VideoImportModel, {
227 foreignKey: 'userId',
230 VideoImports: VideoImportModel[]
232 @HasMany(() => OAuthTokenModel, {
233 foreignKey: 'userId',
236 OAuthTokens: OAuthTokenModel[]
240 static cryptPasswordIfNeeded (instance: UserModel) {
241 if (instance.changed('password')) {
242 return cryptPassword(instance.password)
244 instance.password = hash
252 static removeTokenCache (instance: UserModel) {
253 return clearCacheByUserId(instance.id)
256 static countTotal () {
260 static listForApi (start: number, count: number, sort: string, search?: string) {
261 let where = undefined
267 [Op.iLike]: '%' + search + '%'
272 [ Op.iLike ]: '%' + search + '%'
279 const query: FindOptions = {
285 'SELECT COALESCE(SUM("size"), 0) ' +
287 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
288 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
289 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
290 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
291 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
301 order: getSort(sort),
305 return UserModel.findAndCountAll(query)
306 .then(({ rows, count }) => {
314 static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
315 const roles = Object.keys(USER_ROLE_LABELS)
316 .map(k => parseInt(k, 10) as UserRole)
317 .filter(role => hasUserRight(role, right))
327 return UserModel.findAll(query)
330 static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
334 model: UserNotificationSettingModel.unscoped(),
338 attributes: [ 'userId' ],
339 model: AccountModel.unscoped(),
344 model: ActorModel.unscoped(),
352 as: 'ActorFollowings',
353 model: ActorFollowModel.unscoped(),
356 targetActorId: actorId
366 return UserModel.unscoped().findAll(query)
369 static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
376 return UserModel.findAll(query)
379 static loadById (id: number): Bluebird<MUserDefault> {
380 return UserModel.findByPk(id)
383 static loadByUsername (username: string): Bluebird<MUserDefault> {
386 username: { [ Op.iLike ]: username }
390 return UserModel.findOne(query)
393 static loadByUsernameAndPopulateChannels (username: string): Bluebird<MUserNotifSettingChannelDefault> {
396 username: { [ Op.iLike ]: username }
400 return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
403 static loadByEmail (email: string): Bluebird<MUserDefault> {
410 return UserModel.findOne(query)
413 static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
414 if (!email) email = username
418 [ Op.or ]: [ { username: { [ Op.iLike ]: username } }, { email } ]
422 return UserModel.findOne(query)
425 static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
430 attributes: [ 'id' ],
431 model: AccountModel.unscoped(),
435 attributes: [ 'id' ],
436 model: VideoChannelModel.unscoped(),
440 attributes: [ 'id' ],
441 model: VideoModel.unscoped(),
453 return UserModel.findOne(query)
456 static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
461 attributes: [ 'id' ],
462 model: VideoImportModel.unscoped(),
470 return UserModel.findOne(query)
473 static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
478 attributes: [ 'id' ],
479 model: AccountModel.unscoped(),
483 attributes: [ 'id' ],
484 model: VideoChannelModel.unscoped(),
486 actorId: videoChannelActorId
494 return UserModel.findOne(query)
497 static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
502 attributes: [ 'id' ],
503 model: AccountModel.unscoped(),
505 actorId: accountActorId
511 return UserModel.findOne(query)
514 static getOriginalVideoFileTotalFromUser (user: MUserId) {
515 // Don't use sequelize because we need to use a sub query
516 const query = UserModel.generateUserQuotaBaseSQL()
518 return UserModel.getTotalRawQuery(query, user.id)
521 // Returns cumulative size of all video files uploaded in the last 24 hours.
522 static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
523 // Don't use sequelize because we need to use a sub query
524 const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
526 return UserModel.getTotalRawQuery(query, user.id)
529 static async getStats () {
530 const totalUsers = await UserModel.count()
537 static autoComplete (search: string) {
541 [ Op.like ]: `%${search}%`
547 return UserModel.findAll(query)
548 .then(u => u.map(u => u.username))
551 hasRight (right: UserRight) {
552 return hasUserRight(this.role, right)
555 hasAdminFlag (flag: UserAdminFlag) {
556 return this.adminFlags & flag
559 isPasswordMatch (password: string) {
560 return comparePassword(password, this.password)
565 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
566 const videoQuotaUsed = this.get('videoQuotaUsed')
567 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
571 username: this.username,
573 pendingEmail: this.pendingEmail,
574 emailVerified: this.emailVerified,
575 nsfwPolicy: this.nsfwPolicy,
576 webTorrentEnabled: this.webTorrentEnabled,
577 videosHistoryEnabled: this.videosHistoryEnabled,
578 autoPlayVideo: this.autoPlayVideo,
579 videoLanguages: this.videoLanguages,
581 theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
582 roleLabel: USER_ROLE_LABELS[ this.role ],
583 videoQuota: this.videoQuota,
584 videoQuotaDaily: this.videoQuotaDaily,
585 createdAt: this.createdAt,
586 blocked: this.blocked,
587 blockedReason: this.blockedReason,
588 account: this.Account.toFormattedJSON(),
589 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
591 videoQuotaUsed: videoQuotaUsed !== undefined
592 ? parseInt(videoQuotaUsed + '', 10)
594 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
595 ? parseInt(videoQuotaUsedDaily + '', 10)
599 if (parameters.withAdminFlags) {
600 Object.assign(json, { adminFlags: this.adminFlags })
603 if (Array.isArray(this.Account.VideoChannels) === true) {
604 json.videoChannels = this.Account.VideoChannels
605 .map(c => c.toFormattedJSON())
607 if (v1.createdAt < v2.createdAt) return -1
608 if (v1.createdAt === v2.createdAt) return 0
617 async isAbleToUploadVideo (videoFile: { size: number }) {
618 if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
620 const [ totalBytes, totalBytesDaily ] = await Promise.all([
621 UserModel.getOriginalVideoFileTotalFromUser(this),
622 UserModel.getOriginalVideoFileTotalDailyFromUser(this)
625 const uploadedTotal = videoFile.size + totalBytes
626 const uploadedDaily = videoFile.size + totalBytesDaily
628 if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
629 if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
631 return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
634 private static generateUserQuotaBaseSQL (where?: string) {
635 const andWhere = where ? 'AND ' + where : ''
637 return 'SELECT SUM("size") AS "total" ' +
639 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
640 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
641 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
642 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
643 'WHERE "account"."userId" = $userId ' + andWhere +
644 'GROUP BY "video"."id"' +
648 private static getTotalRawQuery (query: string, userId: number) {
651 type: QueryTypes.SELECT as QueryTypes.SELECT
654 return UserModel.sequelize.query<{ total: string }>(query, options)
655 .then(([ { total } ]) => {
656 if (total === null) return 0
658 return parseInt(total, 10)