+ ]
+ }
+ ]
+ },
+ [ScopeNames.WITH_STATS]: {
+ attributes: {
+ include: [
+ [
+ literal(
+ '(' +
+ UserModel.generateUserQuotaBaseSQL({
+ withSelect: false,
+ whereUserId: '"UserModel"."id"'
+ }) +
+ ')'
+ ),
+ 'videoQuotaUsed'
+ ],
+ [
+ literal(
+ '(' +
+ 'SELECT COUNT("video"."id") ' +
+ 'FROM "video" ' +
+ 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+ 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
+ 'WHERE "account"."userId" = "UserModel"."id"' +
+ ')'
+ ),
+ 'videosCount'
+ ],
+ [
+ literal(
+ '(' +
+ `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
+ 'FROM (' +
+ 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
+ `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
+ 'FROM "videoAbuse" ' +
+ 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
+ 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+ 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
+ 'WHERE "account"."userId" = "UserModel"."id"' +
+ ') t' +
+ ')'
+ ),
+ 'videoAbusesCount'
+ ],
+ [
+ literal(
+ '(' +
+ 'SELECT COUNT("videoAbuse"."id") ' +
+ 'FROM "videoAbuse" ' +
+ 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
+ 'WHERE "account"."userId" = "UserModel"."id"' +
+ ')'
+ ),
+ 'videoAbusesCreatedCount'
+ ],
+ [
+ literal(
+ '(' +
+ 'SELECT COUNT("videoComment"."id") ' +
+ 'FROM "videoComment" ' +
+ 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
+ 'WHERE "account"."userId" = "UserModel"."id"' +
+ ')'
+ ),
+ 'videoCommentsCount'
+ ]
+ ]
+ }
+ }
+}))
+@Table({
+ tableName: 'user',
+ indexes: [
+ {
+ fields: [ 'username' ],
+ unique: true
+ },
+ {
+ fields: [ 'email' ],
+ unique: true
+ }
+ ]
+})
+export class UserModel extends Model<UserModel> {
+
+ @AllowNull(true)
+ @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
+ @Column
+ password: string
+
+ @AllowNull(false)
+ @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
+ @Column
+ username: string
+
+ @AllowNull(false)
+ @IsEmail
+ @Column(DataType.STRING(400))
+ email: string
+
+ @AllowNull(true)
+ @IsEmail
+ @Column(DataType.STRING(400))
+ pendingEmail: string
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
+ @Column
+ emailVerified: boolean
+
+ @AllowNull(false)
+ @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
+ @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
+ nsfwPolicy: NSFWPolicyType
+
+ @AllowNull(false)
+ @Default(true)
+ @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
+ @Column
+ webTorrentEnabled: boolean
+
+ @AllowNull(false)
+ @Default(true)
+ @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
+ @Column
+ videosHistoryEnabled: boolean
+
+ @AllowNull(false)
+ @Default(true)
+ @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
+ @Column
+ autoPlayVideo: boolean
+
+ @AllowNull(false)
+ @Default(false)
+ @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
+ @Column
+ autoPlayNextVideo: boolean
+
+ @AllowNull(false)
+ @Default(true)
+ @Is(
+ 'UserAutoPlayNextVideoPlaylist',
+ value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')
+ )
+ @Column
+ autoPlayNextVideoPlaylist: boolean
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
+ @Column(DataType.ARRAY(DataType.STRING))
+ videoLanguages: string[]
+
+ @AllowNull(false)
+ @Default(UserAdminFlag.NONE)
+ @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
+ @Column
+ adminFlags?: UserAdminFlag
+
+ @AllowNull(false)
+ @Default(false)
+ @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
+ @Column
+ blocked: boolean
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
+ @Column
+ blockedReason: string
+
+ @AllowNull(false)
+ @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
+ @Column
+ role: number
+
+ @AllowNull(false)
+ @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
+ @Column(DataType.BIGINT)
+ videoQuota: number
+
+ @AllowNull(false)
+ @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
+ @Column(DataType.BIGINT)
+ videoQuotaDaily: number
+
+ @AllowNull(false)
+ @Default(DEFAULT_USER_THEME_NAME)
+ @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
+ @Column
+ theme: string
+
+ @AllowNull(false)
+ @Default(false)
+ @Is(
+ 'UserNoInstanceConfigWarningModal',
+ value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
+ )
+ @Column
+ noInstanceConfigWarningModal: boolean
+
+ @AllowNull(false)
+ @Default(false)
+ @Is(
+ 'UserNoInstanceConfigWarningModal',
+ value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
+ )
+ @Column
+ noWelcomeModal: boolean
+
+ @AllowNull(true)
+ @Default(null)
+ @Column
+ pluginAuth: string
+
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @HasOne(() => AccountModel, {
+ foreignKey: 'userId',
+ onDelete: 'cascade',
+ hooks: true
+ })
+ Account: AccountModel
+
+ @HasOne(() => UserNotificationSettingModel, {
+ foreignKey: 'userId',
+ onDelete: 'cascade',
+ hooks: true
+ })
+ NotificationSetting: UserNotificationSettingModel
+
+ @HasMany(() => VideoImportModel, {
+ foreignKey: 'userId',
+ onDelete: 'cascade'
+ })
+ VideoImports: VideoImportModel[]
+
+ @HasMany(() => OAuthTokenModel, {
+ foreignKey: 'userId',
+ onDelete: 'cascade'
+ })
+ OAuthTokens: OAuthTokenModel[]
+
+ @BeforeCreate
+ @BeforeUpdate
+ static cryptPasswordIfNeeded (instance: UserModel) {
+ if (instance.changed('password')) {
+ return cryptPassword(instance.password)
+ .then(hash => {
+ instance.password = hash
+ return undefined
+ })
+ }
+ }
+
+ @AfterUpdate
+ @AfterDestroy
+ static removeTokenCache (instance: UserModel) {
+ return clearCacheByUserId(instance.id)
+ }
+
+ static countTotal () {
+ return this.count()
+ }
+
+ static listForApi (start: number, count: number, sort: string, search?: string) {
+ let where: WhereOptions
+
+ if (search) {
+ where = {
+ [Op.or]: [
+ {
+ email: {
+ [Op.iLike]: '%' + search + '%'
+ }
+ },
+ {
+ username: {
+ [Op.iLike]: '%' + search + '%'
+ }