X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Fuser.ts;h=6f0b0e00f4f477599ce47134318e768d3ecf75bc;hb=ffb321bedca46d6987c7b31dd58e5dea96ea2ea2;hp=1843603f1e935ec16c37241e75fe42c4357ffa94;hpb=d175a6f7ab9dd53e36f9f52769ac02dbfdc57e3e;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 1843603f1..6f0b0e00f 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -1,4 +1,4 @@ -import * as Sequelize from 'sequelize' +import { FindOptions, literal, Op, QueryTypes } from 'sequelize' import { AfterDestroy, AfterUpdate, @@ -22,6 +22,7 @@ import { import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' import { User, UserRole } from '../../../shared/models/users' import { + isUserAdminFlagsValid, isUserAutoPlayVideoValid, isUserBlockedReasonValid, isUserBlockedValid, @@ -30,8 +31,10 @@ import { isUserPasswordValid, isUserRoleValid, isUserUsernameValid, + isUserVideoLanguages, isUserVideoQuotaDailyValid, isUserVideoQuotaValid, + isUserVideosHistoryEnabledValid, isUserWebTorrentEnabledValid } from '../../helpers/custom-validators/users' import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' @@ -41,32 +44,48 @@ import { VideoChannelModel } from '../video/video-channel' import { AccountModel } from './account' import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' import { values } from 'lodash' -import { NSFW_POLICY_TYPES } from '../../initializers' +import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' import { clearCacheByUserId } from '../../lib/oauth-model' +import { UserNotificationSettingModel } from './user-notification-setting' +import { VideoModel } from '../video/video' +import { ActorModel } from '../activitypub/actor' +import { ActorFollowModel } from '../activitypub/actor-follow' +import { VideoImportModel } from '../video/video-import' +import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' +import { isThemeValid } from '../../helpers/custom-validators/plugins' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' } -@DefaultScope({ +@DefaultScope(() => ({ include: [ { - model: () => AccountModel, + model: AccountModel, + required: true + }, + { + model: UserNotificationSettingModel, required: true } ] -}) -@Scopes({ +})) +@Scopes(() => ({ [ScopeNames.WITH_VIDEO_CHANNEL]: { include: [ { - model: () => AccountModel, + model: AccountModel, required: true, - include: [ () => VideoChannelModel ] + include: [ VideoChannelModel ] + }, + { + model: UserNotificationSettingModel, + required: true } ] } -}) +})) @Table({ tableName: 'user', indexes: [ @@ -97,15 +116,20 @@ export class UserModel extends Model { @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')) + @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))) + @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES))) nsfwPolicy: NSFWPolicyType @AllowNull(false) @@ -114,12 +138,30 @@ export class UserModel extends Model { @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(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')) @@ -128,7 +170,7 @@ export class UserModel extends Model { @AllowNull(true) @Default(null) - @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason')) + @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true)) @Column blockedReason: string @@ -147,6 +189,12 @@ export class UserModel extends Model { @Column(DataType.BIGINT) videoQuotaDaily: number + @AllowNull(false) + @Default(DEFAULT_THEME_NAME) + @Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme')) + @Column + theme: string + @CreatedAt createdAt: Date @@ -160,6 +208,19 @@ export class UserModel extends Model { }) 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' @@ -192,26 +253,26 @@ export class UserModel extends Model { let where = undefined if (search) { where = { - [Sequelize.Op.or]: [ + [Op.or]: [ { email: { - [Sequelize.Op.iLike]: '%' + search + '%' + [Op.iLike]: '%' + search + '%' } }, { username: { - [ Sequelize.Op.iLike ]: '%' + search + '%' + [ Op.iLike ]: '%' + search + '%' } } ] } } - const query = { + const query: FindOptions = { attributes: { include: [ [ - Sequelize.literal( + literal( '(' + 'SELECT COALESCE(SUM("size"), 0) ' + 'FROM (' + @@ -224,7 +285,7 @@ export class UserModel extends Model { ')' ), 'videoQuotaUsed' - ] as any // FIXME: typings + ] ] }, offset: start, @@ -242,33 +303,79 @@ export class UserModel extends Model { }) } - static listEmailsWithRight (right: UserRight) { + static listWithRight (right: UserRight) { const roles = Object.keys(USER_ROLE_LABELS) .map(k => parseInt(k, 10) as UserRole) .filter(role => hasUserRight(role, right)) const query = { - attribute: [ 'email' ], where: { role: { - [Sequelize.Op.in]: roles + [Op.in]: roles } } } - return UserModel.unscoped() - .findAll(query) - .then(u => u.map(u => u.email)) + return UserModel.findAll(query) + } + + static listUserSubscribersOf (actorId: number) { + const query = { + include: [ + { + model: UserNotificationSettingModel.unscoped(), + required: true + }, + { + attributes: [ 'userId' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ ], + model: ActorModel.unscoped(), + required: true, + where: { + serverId: null + }, + include: [ + { + attributes: [ ], + as: 'ActorFollowings', + model: ActorFollowModel.unscoped(), + required: true, + where: { + targetActorId: actorId + } + } + ] + } + ] + } + ] + } + + return UserModel.unscoped().findAll(query) + } + + static listByUsernames (usernames: string[]) { + const query = { + where: { + username: usernames + } + } + + return UserModel.findAll(query) } static loadById (id: number) { - return UserModel.findById(id) + return UserModel.findByPk(id) } static loadByUsername (username: string) { const query = { where: { - username + username: { [ Op.iLike ]: username } } } @@ -278,7 +385,7 @@ export class UserModel extends Model { static loadByUsernameAndPopulateChannels (username: string) { const query = { where: { - username + username: { [ Op.iLike ]: username } } } @@ -300,13 +407,102 @@ export class UserModel extends Model { const query = { where: { - [ Sequelize.Op.or ]: [ { username }, { email } ] + [ Op.or ]: [ { username: { [ Op.iLike ]: username } }, { email } ] } } return UserModel.findOne(query) } + static loadByVideoId (videoId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] + } + ] + } + + return UserModel.findOne(query) + } + + static loadByVideoImportId (videoImportId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoImportModel.unscoped(), + where: { + id: videoImportId + } + } + ] + } + + return UserModel.findOne(query) + } + + static loadByChannelActorId (videoChannelActorId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + where: { + actorId: videoChannelActorId + } + } + ] + } + ] + } + + return UserModel.findOne(query) + } + + static loadByAccountActorId (accountActorId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + where: { + actorId: accountActorId + } + } + ] + } + + return UserModel.findOne(query) + } + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() @@ -334,7 +530,7 @@ export class UserModel extends Model { const query = { where: { username: { - [ Sequelize.Op.like ]: `%${search}%` + [ Op.like ]: `%${search}%` } }, limit: 10 @@ -348,11 +544,15 @@ export class UserModel extends Model { return hasUserRight(this.role, right) } + hasAdminFlag (flag: UserAdminFlag) { + return this.adminFlags & flag + } + isPasswordMatch (password: string) { return comparePassword(password, this.password) } - toFormattedJSON (): User { + toFormattedJSON (parameters: { withAdminFlags?: boolean } = {}): User { const videoQuotaUsed = this.get('videoQuotaUsed') const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') @@ -360,11 +560,15 @@ export class UserModel extends Model { id: this.id, username: this.username, email: this.email, + pendingEmail: this.pendingEmail, emailVerified: this.emailVerified, nsfwPolicy: this.nsfwPolicy, webTorrentEnabled: this.webTorrentEnabled, + videosHistoryEnabled: this.videosHistoryEnabled, autoPlayVideo: this.autoPlayVideo, + videoLanguages: this.videoLanguages, role: this.role, + theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME), roleLabel: USER_ROLE_LABELS[ this.role ], videoQuota: this.videoQuota, videoQuotaDaily: this.videoQuotaDaily, @@ -372,15 +576,20 @@ export class UserModel extends Model { blocked: this.blocked, blockedReason: this.blockedReason, account: this.Account.toFormattedJSON(), + notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, videoChannels: [], videoQuotaUsed: videoQuotaUsed !== undefined - ? parseInt(videoQuotaUsed, 10) + ? parseInt(videoQuotaUsed + '', 10) : undefined, videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined - ? parseInt(videoQuotaUsedDaily, 10) + ? parseInt(videoQuotaUsedDaily + '', 10) : undefined } + if (parameters.withAdminFlags) { + Object.assign(json, { adminFlags: this.adminFlags }) + } + if (Array.isArray(this.Account.VideoChannels) === true) { json.videoChannels = this.Account.VideoChannels .map(c => c.toFormattedJSON()) @@ -405,15 +614,11 @@ export class UserModel extends Model { const uploadedTotal = videoFile.size + totalBytes const uploadedDaily = videoFile.size + totalBytesDaily - if (this.videoQuotaDaily === -1) { - return uploadedTotal < this.videoQuota - } - if (this.videoQuota === -1) { - return uploadedDaily < this.videoQuotaDaily - } - return (uploadedTotal < this.videoQuota) && - (uploadedDaily < this.videoQuotaDaily) + if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota + if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily + + return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily } private static generateUserQuotaBaseSQL (where?: string) { @@ -433,10 +638,10 @@ export class UserModel extends Model { private static getTotalRawQuery (query: string, userId: number) { const options = { bind: { userId }, - type: Sequelize.QueryTypes.SELECT + type: QueryTypes.SELECT as QueryTypes.SELECT } - return UserModel.sequelize.query(query, options) + return UserModel.sequelize.query<{ total: string }>(query, options) .then(([ { total } ]) => { if (total === null) return 0