1 import * as Sequelize from 'sequelize'
21 } from 'sequelize-typescript'
22 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
23 import { User, UserRole } from '../../../shared/models/users'
25 isUserAutoPlayVideoValid,
26 isUserBlockedReasonValid,
28 isUserEmailVerifiedValid,
29 isUserNSFWPolicyValid,
33 isUserVideoQuotaDailyValid,
34 isUserVideoQuotaValid,
35 isUserVideosHistoryEnabledValid,
36 isUserWebTorrentEnabledValid
37 } from '../../helpers/custom-validators/users'
38 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
39 import { OAuthTokenModel } from '../oauth/oauth-token'
40 import { getSort, throwIfNotValid } from '../utils'
41 import { VideoChannelModel } from '../video/video-channel'
42 import { AccountModel } from './account'
43 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
44 import { values } from 'lodash'
45 import { NSFW_POLICY_TYPES } from '../../initializers'
46 import { clearCacheByUserId } from '../../lib/oauth-model'
47 import { UserNotificationSettingModel } from './user-notification-setting'
48 import { VideoModel } from '../video/video'
49 import { ActorModel } from '../activitypub/actor'
50 import { ActorFollowModel } from '../activitypub/actor-follow'
53 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
59 model: () => AccountModel,
63 model: () => UserNotificationSettingModel,
69 [ScopeNames.WITH_VIDEO_CHANNEL]: {
72 model: () => AccountModel,
74 include: [ () => VideoChannelModel ]
77 model: () => UserNotificationSettingModel,
87 fields: [ 'username' ],
96 export class UserModel extends Model<UserModel> {
99 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
104 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
110 @Column(DataType.STRING(400))
115 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean'))
117 emailVerified: boolean
120 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
121 @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
122 nsfwPolicy: NSFWPolicyType
126 @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
128 webTorrentEnabled: boolean
132 @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
134 videosHistoryEnabled: boolean
138 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
140 autoPlayVideo: boolean
144 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
150 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason'))
152 blockedReason: string
155 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
160 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
161 @Column(DataType.BIGINT)
165 @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
166 @Column(DataType.BIGINT)
167 videoQuotaDaily: number
175 @HasOne(() => AccountModel, {
176 foreignKey: 'userId',
180 Account: AccountModel
182 @HasOne(() => UserNotificationSettingModel, {
183 foreignKey: 'userId',
187 NotificationSetting: UserNotificationSettingModel
189 @HasMany(() => OAuthTokenModel, {
190 foreignKey: 'userId',
193 OAuthTokens: OAuthTokenModel[]
197 static cryptPasswordIfNeeded (instance: UserModel) {
198 if (instance.changed('password')) {
199 return cryptPassword(instance.password)
201 instance.password = hash
209 static removeTokenCache (instance: UserModel) {
210 return clearCacheByUserId(instance.id)
213 static countTotal () {
217 static listForApi (start: number, count: number, sort: string, search?: string) {
218 let where = undefined
224 [Sequelize.Op.iLike]: '%' + search + '%'
229 [ Sequelize.Op.iLike ]: '%' + search + '%'
242 'SELECT COALESCE(SUM("size"), 0) ' +
244 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
245 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
246 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
247 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
248 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
253 ] as any // FIXME: typings
258 order: getSort(sort),
262 return UserModel.findAndCountAll(query)
263 .then(({ rows, count }) => {
271 static listWithRight (right: UserRight) {
272 const roles = Object.keys(USER_ROLE_LABELS)
273 .map(k => parseInt(k, 10) as UserRole)
274 .filter(role => hasUserRight(role, right))
279 [Sequelize.Op.in]: roles
284 return UserModel.findAll(query)
287 static listUserSubscribersOf (actorId: number) {
291 model: UserNotificationSettingModel.unscoped(),
295 attributes: [ 'userId' ],
296 model: AccountModel.unscoped(),
301 model: ActorModel.unscoped(),
309 as: 'ActorFollowings',
310 model: ActorFollowModel.unscoped(),
313 targetActorId: actorId
323 return UserModel.unscoped().findAll(query)
326 static loadById (id: number) {
327 return UserModel.findById(id)
330 static loadByUsername (username: string) {
337 return UserModel.findOne(query)
340 static loadByUsernameAndPopulateChannels (username: string) {
347 return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
350 static loadByEmail (email: string) {
357 return UserModel.findOne(query)
360 static loadByUsernameOrEmail (username: string, email?: string) {
361 if (!email) email = username
365 [ Sequelize.Op.or ]: [ { username }, { email } ]
369 return UserModel.findOne(query)
372 static loadByVideoId (videoId: number) {
377 attributes: [ 'id' ],
378 model: AccountModel.unscoped(),
382 attributes: [ 'id' ],
383 model: VideoChannelModel.unscoped(),
387 attributes: [ 'id' ],
388 model: VideoModel.unscoped(),
400 return UserModel.findOne(query)
403 static getOriginalVideoFileTotalFromUser (user: UserModel) {
404 // Don't use sequelize because we need to use a sub query
405 const query = UserModel.generateUserQuotaBaseSQL()
407 return UserModel.getTotalRawQuery(query, user.id)
410 // Returns cumulative size of all video files uploaded in the last 24 hours.
411 static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
412 // Don't use sequelize because we need to use a sub query
413 const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
415 return UserModel.getTotalRawQuery(query, user.id)
418 static async getStats () {
419 const totalUsers = await UserModel.count()
426 static autoComplete (search: string) {
430 [ Sequelize.Op.like ]: `%${search}%`
436 return UserModel.findAll(query)
437 .then(u => u.map(u => u.username))
440 hasRight (right: UserRight) {
441 return hasUserRight(this.role, right)
444 isPasswordMatch (password: string) {
445 return comparePassword(password, this.password)
448 toFormattedJSON (): User {
449 const videoQuotaUsed = this.get('videoQuotaUsed')
450 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
454 username: this.username,
456 emailVerified: this.emailVerified,
457 nsfwPolicy: this.nsfwPolicy,
458 webTorrentEnabled: this.webTorrentEnabled,
459 videosHistoryEnabled: this.videosHistoryEnabled,
460 autoPlayVideo: this.autoPlayVideo,
462 roleLabel: USER_ROLE_LABELS[ this.role ],
463 videoQuota: this.videoQuota,
464 videoQuotaDaily: this.videoQuotaDaily,
465 createdAt: this.createdAt,
466 blocked: this.blocked,
467 blockedReason: this.blockedReason,
468 account: this.Account.toFormattedJSON(),
469 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
471 videoQuotaUsed: videoQuotaUsed !== undefined
472 ? parseInt(videoQuotaUsed, 10)
474 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
475 ? parseInt(videoQuotaUsedDaily, 10)
479 if (Array.isArray(this.Account.VideoChannels) === true) {
480 json.videoChannels = this.Account.VideoChannels
481 .map(c => c.toFormattedJSON())
483 if (v1.createdAt < v2.createdAt) return -1
484 if (v1.createdAt === v2.createdAt) return 0
493 async isAbleToUploadVideo (videoFile: { size: number }) {
494 if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
496 const [ totalBytes, totalBytesDaily ] = await Promise.all([
497 UserModel.getOriginalVideoFileTotalFromUser(this),
498 UserModel.getOriginalVideoFileTotalDailyFromUser(this)
501 const uploadedTotal = videoFile.size + totalBytes
502 const uploadedDaily = videoFile.size + totalBytesDaily
503 if (this.videoQuotaDaily === -1) {
504 return uploadedTotal < this.videoQuota
506 if (this.videoQuota === -1) {
507 return uploadedDaily < this.videoQuotaDaily
510 return (uploadedTotal < this.videoQuota) &&
511 (uploadedDaily < this.videoQuotaDaily)
514 private static generateUserQuotaBaseSQL (where?: string) {
515 const andWhere = where ? 'AND ' + where : ''
517 return 'SELECT SUM("size") AS "total" ' +
519 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
520 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
521 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
522 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
523 'WHERE "account"."userId" = $userId ' + andWhere +
524 'GROUP BY "video"."id"' +
528 private static getTotalRawQuery (query: string, userId: number) {
531 type: Sequelize.QueryTypes.SELECT
534 return UserModel.sequelize.query(query, options)
535 .then(([ { total } ]) => {
536 if (total === null) return 0
538 return parseInt(total, 10)