From 7d9ba5c08999c6482f0bc5e0c09c6f55b7724090 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 May 2021 11:15:29 +0200 Subject: Cleanup models directory organization --- server/models/user/user-notification-setting.ts | 221 ++++++ server/models/user/user-notification.ts | 665 ++++++++++++++++ server/models/user/user-video-history.ts | 100 +++ server/models/user/user.ts | 967 ++++++++++++++++++++++++ 4 files changed, 1953 insertions(+) create mode 100644 server/models/user/user-notification-setting.ts create mode 100644 server/models/user/user-notification.ts create mode 100644 server/models/user/user-video-history.ts create mode 100644 server/models/user/user.ts (limited to 'server/models/user') diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts new file mode 100644 index 000000000..138051528 --- /dev/null +++ b/server/models/user/user-notification-setting.ts @@ -0,0 +1,221 @@ +import { + AfterDestroy, + AfterUpdate, + AllowNull, + BelongsTo, + Column, + CreatedAt, + Default, + ForeignKey, + Is, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { TokensCache } from '@server/lib/auth/tokens-cache' +import { MNotificationSettingFormattable } from '@server/types/models' +import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' +import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' +import { throwIfNotValid } from '../utils' +import { UserModel } from './user' + +@Table({ + tableName: 'userNotificationSetting', + indexes: [ + { + fields: [ 'userId' ], + unique: true + } + ] +}) +export class UserNotificationSettingModel extends Model { + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewVideoFromSubscription', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') + ) + @Column + newVideoFromSubscription: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewCommentOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') + ) + @Column + newCommentOnMyVideo: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingAbuseAsModerator', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator') + ) + @Column + abuseAsModerator: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingVideoAutoBlacklistAsModerator', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator') + ) + @Column + videoAutoBlacklistAsModerator: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingBlacklistOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') + ) + @Column + blacklistOnMyVideo: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingMyVideoPublished', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') + ) + @Column + myVideoPublished: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingMyVideoImportFinished', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') + ) + @Column + myVideoImportFinished: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewUserRegistration', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') + ) + @Column + newUserRegistration: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewInstanceFollower', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newInstanceFollower') + ) + @Column + newInstanceFollower: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewInstanceFollower', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing') + ) + @Column + autoInstanceFollowing: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewFollow', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') + ) + @Column + newFollow: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingCommentMention', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') + ) + @Column + commentMention: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingAbuseStateChange', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange') + ) + @Column + abuseStateChange: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingAbuseNewMessage', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage') + ) + @Column + abuseNewMessage: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewPeerTubeVersion', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion') + ) + @Column + newPeerTubeVersion: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewPeerPluginVersion', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion') + ) + @Column + newPluginVersion: UserNotificationSettingValue + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AfterUpdate + @AfterDestroy + static removeTokenCache (instance: UserNotificationSettingModel) { + return TokensCache.Instance.clearCacheByUserId(instance.userId) + } + + toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { + return { + newCommentOnMyVideo: this.newCommentOnMyVideo, + newVideoFromSubscription: this.newVideoFromSubscription, + abuseAsModerator: this.abuseAsModerator, + videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, + blacklistOnMyVideo: this.blacklistOnMyVideo, + myVideoPublished: this.myVideoPublished, + myVideoImportFinished: this.myVideoImportFinished, + newUserRegistration: this.newUserRegistration, + commentMention: this.commentMention, + newFollow: this.newFollow, + newInstanceFollower: this.newInstanceFollower, + autoInstanceFollowing: this.autoInstanceFollowing, + abuseNewMessage: this.abuseNewMessage, + abuseStateChange: this.abuseStateChange, + newPeerTubeVersion: this.newPeerTubeVersion, + newPluginVersion: this.newPluginVersion + } + } +} diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts new file mode 100644 index 000000000..f7f9ac867 --- /dev/null +++ b/server/models/user/user-notification.ts @@ -0,0 +1,665 @@ +import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' +import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' +import { UserNotification, UserNotificationType } from '../../../shared' +import { isBooleanValid } from '../../helpers/custom-validators/misc' +import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' +import { AbuseModel } from '../abuse/abuse' +import { VideoAbuseModel } from '../abuse/video-abuse' +import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' +import { AccountModel } from '../account/account' +import { ActorModel } from '../actor/actor' +import { ActorFollowModel } from '../actor/actor-follow' +import { ActorImageModel } from '../actor/actor-image' +import { ApplicationModel } from '../application/application' +import { PluginModel } from '../server/plugin' +import { ServerModel } from '../server/server' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' +import { VideoBlacklistModel } from '../video/video-blacklist' +import { VideoChannelModel } from '../video/video-channel' +import { VideoCommentModel } from '../video/video-comment' +import { VideoImportModel } from '../video/video-import' +import { UserModel } from './user' + +enum ScopeNames { + WITH_ALL = 'WITH_ALL' +} + +function buildActorWithAvatarInclude () { + return { + attributes: [ 'preferredUsername' ], + model: ActorModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'filename' ], + as: 'Avatar', + model: ActorImageModel.unscoped(), + required: false + }, + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + } +} + +function buildVideoInclude (required: boolean) { + return { + attributes: [ 'id', 'uuid', 'name' ], + model: VideoModel.unscoped(), + required + } +} + +function buildChannelInclude (required: boolean, withActor = false) { + return { + required, + attributes: [ 'id', 'name' ], + model: VideoChannelModel.unscoped(), + include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] + } +} + +function buildAccountInclude (required: boolean, withActor = false) { + return { + required, + attributes: [ 'id', 'name' ], + model: AccountModel.unscoped(), + include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] + } +} + +@Scopes(() => ({ + [ScopeNames.WITH_ALL]: { + include: [ + Object.assign(buildVideoInclude(false), { + include: [ buildChannelInclude(true, true) ] + }), + + { + attributes: [ 'id', 'originCommentId' ], + model: VideoCommentModel.unscoped(), + required: false, + include: [ + buildAccountInclude(true, true), + buildVideoInclude(true) + ] + }, + + { + attributes: [ 'id', 'state' ], + model: AbuseModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id' ], + model: VideoAbuseModel.unscoped(), + required: false, + include: [ buildVideoInclude(false) ] + }, + { + attributes: [ 'id' ], + model: VideoCommentAbuseModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'originCommentId' ], + model: VideoCommentModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'name', 'uuid' ], + model: VideoModel.unscoped(), + required: false + } + ] + } + ] + }, + { + model: AccountModel, + as: 'FlaggedAccount', + required: false, + include: [ buildActorWithAvatarInclude() ] + } + ] + }, + + { + attributes: [ 'id' ], + model: VideoBlacklistModel.unscoped(), + required: false, + include: [ buildVideoInclude(true) ] + }, + + { + attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], + model: VideoImportModel.unscoped(), + required: false, + include: [ buildVideoInclude(false) ] + }, + + { + attributes: [ 'id', 'name', 'type', 'latestVersion' ], + model: PluginModel.unscoped(), + required: false + }, + + { + attributes: [ 'id', 'latestPeerTubeVersion' ], + model: ApplicationModel.unscoped(), + required: false + }, + + { + attributes: [ 'id', 'state' ], + model: ActorFollowModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'preferredUsername' ], + model: ActorModel.unscoped(), + required: true, + as: 'ActorFollower', + include: [ + { + attributes: [ 'id', 'name' ], + model: AccountModel.unscoped(), + required: true + }, + { + attributes: [ 'filename' ], + as: 'Avatar', + model: ActorImageModel.unscoped(), + required: false + }, + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + }, + { + attributes: [ 'preferredUsername', 'type' ], + model: ActorModel.unscoped(), + required: true, + as: 'ActorFollowing', + include: [ + buildChannelInclude(false), + buildAccountInclude(false), + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + } + ] + }, + + buildAccountInclude(false, true) + ] + } +})) +@Table({ + tableName: 'userNotification', + indexes: [ + { + fields: [ 'userId' ] + }, + { + fields: [ 'videoId' ], + where: { + videoId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'commentId' ], + where: { + commentId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'abuseId' ], + where: { + abuseId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoBlacklistId' ], + where: { + videoBlacklistId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoImportId' ], + where: { + videoImportId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'accountId' ], + where: { + accountId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'actorFollowId' ], + where: { + actorFollowId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'pluginId' ], + where: { + pluginId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'applicationId' ], + where: { + applicationId: { + [Op.ne]: null + } + } + } + ] as (ModelIndexesOptions & { where?: WhereOptions })[] +}) +export class UserNotificationModel extends Model { + + @AllowNull(false) + @Default(null) + @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) + @Column + type: UserNotificationType + + @AllowNull(false) + @Default(false) + @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) + @Column + read: boolean + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Video: VideoModel + + @ForeignKey(() => VideoCommentModel) + @Column + commentId: number + + @BelongsTo(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Comment: VideoCommentModel + + @ForeignKey(() => AbuseModel) + @Column + abuseId: number + + @BelongsTo(() => AbuseModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Abuse: AbuseModel + + @ForeignKey(() => VideoBlacklistModel) + @Column + videoBlacklistId: number + + @BelongsTo(() => VideoBlacklistModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoBlacklist: VideoBlacklistModel + + @ForeignKey(() => VideoImportModel) + @Column + videoImportId: number + + @BelongsTo(() => VideoImportModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoImport: VideoImportModel + + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Account: AccountModel + + @ForeignKey(() => ActorFollowModel) + @Column + actorFollowId: number + + @BelongsTo(() => ActorFollowModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + ActorFollow: ActorFollowModel + + @ForeignKey(() => PluginModel) + @Column + pluginId: number + + @BelongsTo(() => PluginModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Plugin: PluginModel + + @ForeignKey(() => ApplicationModel) + @Column + applicationId: number + + @BelongsTo(() => ApplicationModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Application: ApplicationModel + + static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { + const where = { userId } + + const query: FindOptions = { + offset: start, + limit: count, + order: getSort(sort), + where + } + + if (unread !== undefined) query.where['read'] = !unread + + return Promise.all([ + UserNotificationModel.count({ where }) + .then(count => count || 0), + + count === 0 + ? [] + : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query) + ]).then(([ total, data ]) => ({ total, data })) + } + + static markAsRead (userId: number, notificationIds: number[]) { + const query = { + where: { + userId, + id: { + [Op.in]: notificationIds + } + } + } + + return UserNotificationModel.update({ read: true }, query) + } + + static markAllAsRead (userId: number) { + const query = { where: { userId } } + + return UserNotificationModel.update({ read: true }, query) + } + + static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) { + const id = parseInt(options.id + '', 10) + + function buildAccountWhereQuery (base: string) { + const whereSuffix = options.forUserId + ? ` AND "userNotification"."userId" = ${options.forUserId}` + : '' + + if (options.type === 'account') { + return base + + ` WHERE "account"."id" = ${id} ${whereSuffix}` + } + + return base + + ` WHERE "actor"."serverId" = ${id} ${whereSuffix}` + } + + const queries = [ + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + ), + + // Remove notifications from muted accounts that followed ours + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` + ), + + // Remove notifications from muted accounts that commented something + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` + ), + + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + + `INNER JOIN account ON account.id = "videoComment"."accountId" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + ) + ] + + const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})` + + return UserNotificationModel.sequelize.query(query) + } + + toFormattedJSON (this: UserNotificationModelForApi): UserNotification { + const video = this.Video + ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) + : undefined + + const videoImport = this.VideoImport + ? { + id: this.VideoImport.id, + video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, + torrentName: this.VideoImport.torrentName, + magnetUri: this.VideoImport.magnetUri, + targetUrl: this.VideoImport.targetUrl + } + : undefined + + const comment = this.Comment + ? { + id: this.Comment.id, + threadId: this.Comment.getThreadId(), + account: this.formatActor(this.Comment.Account), + video: this.formatVideo(this.Comment.Video) + } + : undefined + + const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined + + const videoBlacklist = this.VideoBlacklist + ? { + id: this.VideoBlacklist.id, + video: this.formatVideo(this.VideoBlacklist.Video) + } + : undefined + + const account = this.Account ? this.formatActor(this.Account) : undefined + + const actorFollowingType = { + Application: 'instance' as 'instance', + Group: 'channel' as 'channel', + Person: 'account' as 'account' + } + const actorFollow = this.ActorFollow + ? { + id: this.ActorFollow.id, + state: this.ActorFollow.state, + follower: { + id: this.ActorFollow.ActorFollower.Account.id, + displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), + name: this.ActorFollow.ActorFollower.preferredUsername, + avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined, + host: this.ActorFollow.ActorFollower.getHost() + }, + following: { + type: actorFollowingType[this.ActorFollow.ActorFollowing.type], + displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), + name: this.ActorFollow.ActorFollowing.preferredUsername, + host: this.ActorFollow.ActorFollowing.getHost() + } + } + : undefined + + const plugin = this.Plugin + ? { + name: this.Plugin.name, + type: this.Plugin.type, + latestVersion: this.Plugin.latestVersion + } + : undefined + + const peertube = this.Application + ? { latestVersion: this.Application.latestPeerTubeVersion } + : undefined + + return { + id: this.id, + type: this.type, + read: this.read, + video, + videoImport, + comment, + abuse, + videoBlacklist, + account, + actorFollow, + plugin, + peertube, + createdAt: this.createdAt.toISOString(), + updatedAt: this.updatedAt.toISOString() + } + } + + formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) { + return { + id: video.id, + uuid: video.uuid, + name: video.name + } + } + + formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { + const commentAbuse = abuse.VideoCommentAbuse?.VideoComment + ? { + threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), + + video: abuse.VideoCommentAbuse.VideoComment.Video + ? { + id: abuse.VideoCommentAbuse.VideoComment.Video.id, + name: abuse.VideoCommentAbuse.VideoComment.Video.name, + uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid + } + : undefined + } + : undefined + + const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined + + const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined + + return { + id: abuse.id, + state: abuse.state, + video: videoAbuse, + comment: commentAbuse, + account: accountAbuse + } + } + + formatActor ( + this: UserNotificationModelForApi, + accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor + ) { + const avatar = accountOrChannel.Actor.Avatar + ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } + : undefined + + return { + id: accountOrChannel.id, + displayName: accountOrChannel.getDisplayName(), + name: accountOrChannel.Actor.preferredUsername, + host: accountOrChannel.Actor.getHost(), + avatar + } + } +} diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts new file mode 100644 index 000000000..6be1d65ea --- /dev/null +++ b/server/models/user/user-video-history.ts @@ -0,0 +1,100 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { VideoModel } from '../video/video' +import { UserModel } from './user' +import { DestroyOptions, Op, Transaction } from 'sequelize' +import { MUserAccountId, MUserId } from '@server/types/models' + +@Table({ + tableName: 'userVideoHistory', + indexes: [ + { + fields: [ 'userId', 'videoId' ], + unique: true + }, + { + fields: [ 'userId' ] + }, + { + fields: [ 'videoId' ] + } + ] +}) +export class UserVideoHistoryModel extends Model { + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(false) + @IsInt + @Column + currentTime: number + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + Video: VideoModel + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + User: UserModel + + static listForApi (user: MUserAccountId, start: number, count: number, search?: string) { + return VideoModel.listForApi({ + start, + count, + search, + sort: '-"userVideoHistory"."updatedAt"', + nsfw: null, // All + includeLocalVideos: true, + withFiles: false, + user, + historyOfUser: user + }) + } + + static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) { + const query: DestroyOptions = { + where: { + userId: user.id + }, + transaction: t + } + + if (beforeDate) { + query.where['updatedAt'] = { + [Op.lt]: beforeDate + } + } + + return UserVideoHistoryModel.destroy(query) + } + + static removeOldHistory (beforeDate: string) { + const query: DestroyOptions = { + where: { + updatedAt: { + [Op.lt]: beforeDate + } + } + } + + return UserVideoHistoryModel.destroy(query) + } +} diff --git a/server/models/user/user.ts b/server/models/user/user.ts new file mode 100644 index 000000000..8d2564e54 --- /dev/null +++ b/server/models/user/user.ts @@ -0,0 +1,967 @@ +import { values } from 'lodash' +import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize' +import { + AfterDestroy, + AfterUpdate, + AllowNull, + BeforeCreate, + BeforeUpdate, + Column, + CreatedAt, + DataType, + Default, + DefaultScope, + HasMany, + HasOne, + Is, + IsEmail, + IsUUID, + Model, + Scopes, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { TokensCache } from '@server/lib/auth/tokens-cache' +import { + MMyUserFormattable, + MUser, + MUserDefault, + MUserFormattable, + MUserNotifSettingChannelDefault, + MUserWithNotificationSetting, + MVideoWithRights +} from '@server/types/models' +import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' +import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' +import { User, UserRole } from '../../../shared/models/users' +import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' +import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' +import { isThemeNameValid } from '../../helpers/custom-validators/plugins' +import { + isNoInstanceConfigWarningModal, + isNoWelcomeModal, + isUserAdminFlagsValid, + isUserAutoPlayNextVideoPlaylistValid, + isUserAutoPlayNextVideoValid, + isUserAutoPlayVideoValid, + isUserBlockedReasonValid, + isUserBlockedValid, + isUserEmailVerifiedValid, + isUserNSFWPolicyValid, + isUserPasswordValid, + isUserRoleValid, + isUserUsernameValid, + isUserVideoLanguages, + isUserVideoQuotaDailyValid, + isUserVideoQuotaValid, + isUserVideosHistoryEnabledValid, + isUserWebTorrentEnabledValid +} from '../../helpers/custom-validators/users' +import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' +import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' +import { AccountModel } from '../account/account' +import { ActorModel } from '../actor/actor' +import { ActorFollowModel } from '../actor/actor-follow' +import { ActorImageModel } from '../actor/actor-image' +import { OAuthTokenModel } from '../oauth/oauth-token' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' +import { VideoChannelModel } from '../video/video-channel' +import { VideoImportModel } from '../video/video-import' +import { VideoLiveModel } from '../video/video-live' +import { VideoPlaylistModel } from '../video/video-playlist' +import { UserNotificationSettingModel } from './user-notification-setting' + +enum ScopeNames { + FOR_ME_API = 'FOR_ME_API', + WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', + WITH_STATS = 'WITH_STATS' +} + +@DefaultScope(() => ({ + include: [ + { + model: AccountModel, + required: true + }, + { + model: UserNotificationSettingModel, + required: true + } + ] +})) +@Scopes(() => ({ + [ScopeNames.FOR_ME_API]: { + include: [ + { + model: AccountModel, + include: [ + { + model: VideoChannelModel.unscoped(), + include: [ + { + model: ActorModel, + required: true, + include: [ + { + model: ActorImageModel, + as: 'Banner', + required: false + } + ] + } + ] + }, + { + attributes: [ 'id', 'name', 'type' ], + model: VideoPlaylistModel.unscoped(), + required: true, + where: { + type: { + [Op.ne]: VideoPlaylistType.REGULAR + } + } + } + ] + }, + { + model: UserNotificationSettingModel, + required: true + } + ] + }, + [ScopeNames.WITH_VIDEOCHANNELS]: { + include: [ + { + model: AccountModel, + include: [ + { + model: VideoChannelModel + }, + { + attributes: [ 'id', 'name', 'type' ], + model: VideoPlaylistModel.unscoped(), + required: true, + where: { + type: { + [Op.ne]: VideoPlaylistType.REGULAR + } + } + } + ] + } + ] + }, + [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("abuse"."id") AS "abuses", ' + + `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + + 'WHERE "account"."userId" = "UserModel"."id"' + + ') t' + + ')' + ), + 'abusesCount' + ], + [ + literal( + '(' + + 'SELECT COUNT("abuse"."id") ' + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + + 'WHERE "account"."userId" = "UserModel"."id"' + + ')' + ), + 'abusesCreatedCount' + ], + [ + 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 { + + @AllowNull(true) + @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) + @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 + + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + feedToken: string + + @AllowNull(true) + @Default(null) + @Column + lastLoginDate: Date + + @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') && instance.password) { + return cryptPassword(instance.password) + .then(hash => { + instance.password = hash + return undefined + }) + } + } + + @AfterUpdate + @AfterDestroy + static removeTokenCache (instance: UserModel) { + return TokensCache.Instance.clearCacheByUserId(instance.id) + } + + static countTotal () { + return this.count() + } + + static listForApi (parameters: { + start: number + count: number + sort: string + search?: string + blocked?: boolean + }) { + const { start, count, sort, search, blocked } = parameters + const where: WhereOptions = {} + + if (search) { + Object.assign(where, { + [Op.or]: [ + { + email: { + [Op.iLike]: '%' + search + '%' + } + }, + { + username: { + [Op.iLike]: '%' + search + '%' + } + } + ] + }) + } + + if (blocked !== undefined) { + Object.assign(where, { + blocked: blocked + }) + } + + const query: FindOptions = { + attributes: { + include: [ + [ + literal( + '(' + + UserModel.generateUserQuotaBaseSQL({ + withSelect: false, + whereUserId: '"UserModel"."id"' + }) + + ')' + ), + 'videoQuotaUsed' + ] as any // FIXME: typings + ] + }, + offset: start, + limit: count, + order: getSort(sort), + where + } + + return UserModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static listWithRight (right: UserRight): Promise { + const roles = Object.keys(USER_ROLE_LABELS) + .map(k => parseInt(k, 10) as UserRole) + .filter(role => hasUserRight(role, right)) + + const query = { + where: { + role: { + [Op.in]: roles + } + } + } + + return UserModel.findAll(query) + } + + static listUserSubscribersOf (actorId: number): Promise { + 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[]): Promise { + const query = { + where: { + username: usernames + } + } + + return UserModel.findAll(query) + } + + static loadById (id: number): Promise { + return UserModel.unscoped().findByPk(id) + } + + static loadByIdFull (id: number): Promise { + return UserModel.findByPk(id) + } + + static loadByIdWithChannels (id: number, withStats = false): Promise { + const scopes = [ + ScopeNames.WITH_VIDEOCHANNELS + ] + + if (withStats) scopes.push(ScopeNames.WITH_STATS) + + return UserModel.scope(scopes).findByPk(id) + } + + static loadByUsername (username: string): Promise { + const query = { + where: { + username + } + } + + return UserModel.findOne(query) + } + + static loadForMeAPI (id: number): Promise { + const query = { + where: { + id + } + } + + return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query) + } + + static loadByEmail (email: string): Promise { + const query = { + where: { + email + } + } + + return UserModel.findOne(query) + } + + static loadByUsernameOrEmail (username: string, email?: string): Promise { + if (!email) email = username + + const query = { + where: { + [Op.or]: [ + where(fn('lower', col('username')), fn('lower', username)), + + { email } + ] + } + } + + return UserModel.findOne(query) + } + + static loadByVideoId (videoId: number): Promise { + 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): Promise { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoImportModel.unscoped(), + where: { + id: videoImportId + } + } + ] + } + + return UserModel.findOne(query) + } + + static loadByChannelActorId (videoChannelActorId: number): Promise { + 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): Promise { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + where: { + actorId: accountActorId + } + } + ] + } + + return UserModel.findOne(query) + } + + static loadByLiveId (liveId: number): Promise { + const query = { + include: [ + { + attributes: [ 'id' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id' ], + model: VideoModel.unscoped(), + required: true, + include: [ + { + attributes: [], + model: VideoLiveModel.unscoped(), + required: true, + where: { + id: liveId + } + } + ] + } + ] + } + ] + } + ] + } + + return UserModel.unscoped().findOne(query) + } + + static generateUserQuotaBaseSQL (options: { + whereUserId: '$userId' | '"UserModel"."id"' + withSelect: boolean + where?: string + }) { + const andWhere = options.where + ? 'AND ' + options.where + : '' + + const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + + 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + + `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` + + const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + + 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + + videoChannelJoin + + const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + + 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' + + 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' + + videoChannelJoin + + return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + + 'FROM (' + + `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` + + 'GROUP BY "t1"."videoId"' + + ') t2' + } + + static getTotalRawQuery (query: string, userId: number) { + const options = { + bind: { userId }, + type: QueryTypes.SELECT as QueryTypes.SELECT + } + + return UserModel.sequelize.query<{ total: string }>(query, options) + .then(([ { total } ]) => { + if (total === null) return 0 + + return parseInt(total, 10) + }) + } + + static async getStats () { + function getActiveUsers (days: number) { + const query = { + where: { + [Op.and]: [ + literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`) + ] + } + } + + return UserModel.count(query) + } + + const totalUsers = await UserModel.count() + const totalDailyActiveUsers = await getActiveUsers(1) + const totalWeeklyActiveUsers = await getActiveUsers(7) + const totalMonthlyActiveUsers = await getActiveUsers(30) + const totalHalfYearActiveUsers = await getActiveUsers(180) + + return { + totalUsers, + totalDailyActiveUsers, + totalWeeklyActiveUsers, + totalMonthlyActiveUsers, + totalHalfYearActiveUsers + } + } + + static autoComplete (search: string) { + const query = { + where: { + username: { + [Op.like]: `%${search}%` + } + }, + limit: 10 + } + + return UserModel.findAll(query) + .then(u => u.map(u => u.username)) + } + + canGetVideo (video: MVideoWithRights) { + const videoUserId = video.VideoChannel.Account.userId + + if (video.isBlacklisted()) { + return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) + } + + if (video.privacy === VideoPrivacy.PRIVATE) { + return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) + } + + if (video.privacy === VideoPrivacy.INTERNAL) return true + + return false + } + + hasRight (right: UserRight) { + return hasUserRight(this.role, right) + } + + hasAdminFlag (flag: UserAdminFlag) { + return this.adminFlags & flag + } + + isPasswordMatch (password: string) { + return comparePassword(password, this.password) + } + + toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { + const videoQuotaUsed = this.get('videoQuotaUsed') + const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') + const videosCount = this.get('videosCount') + const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':') + const abusesCreatedCount = this.get('abusesCreatedCount') + const videoCommentsCount = this.get('videoCommentsCount') + + const json: User = { + id: this.id, + username: this.username, + email: this.email, + theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME), + + pendingEmail: this.pendingEmail, + emailVerified: this.emailVerified, + + nsfwPolicy: this.nsfwPolicy, + webTorrentEnabled: this.webTorrentEnabled, + videosHistoryEnabled: this.videosHistoryEnabled, + autoPlayVideo: this.autoPlayVideo, + autoPlayNextVideo: this.autoPlayNextVideo, + autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist, + videoLanguages: this.videoLanguages, + + role: this.role, + roleLabel: USER_ROLE_LABELS[this.role], + + videoQuota: this.videoQuota, + videoQuotaDaily: this.videoQuotaDaily, + videoQuotaUsed: videoQuotaUsed !== undefined + ? parseInt(videoQuotaUsed + '', 10) + : undefined, + videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined + ? parseInt(videoQuotaUsedDaily + '', 10) + : undefined, + videosCount: videosCount !== undefined + ? parseInt(videosCount + '', 10) + : undefined, + abusesCount: abusesCount + ? parseInt(abusesCount, 10) + : undefined, + abusesAcceptedCount: abusesAcceptedCount + ? parseInt(abusesAcceptedCount, 10) + : undefined, + abusesCreatedCount: abusesCreatedCount !== undefined + ? parseInt(abusesCreatedCount + '', 10) + : undefined, + videoCommentsCount: videoCommentsCount !== undefined + ? parseInt(videoCommentsCount + '', 10) + : undefined, + + noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, + noWelcomeModal: this.noWelcomeModal, + + blocked: this.blocked, + blockedReason: this.blockedReason, + + account: this.Account.toFormattedJSON(), + + notificationSettings: this.NotificationSetting + ? this.NotificationSetting.toFormattedJSON() + : undefined, + + videoChannels: [], + + createdAt: this.createdAt, + + pluginAuth: this.pluginAuth, + + lastLoginDate: this.lastLoginDate + } + + 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()) + .sort((v1, v2) => { + if (v1.createdAt < v2.createdAt) return -1 + if (v1.createdAt === v2.createdAt) return 0 + + return 1 + }) + } + + return json + } + + toMeFormattedJSON (this: MMyUserFormattable): MyUser { + const formatted = this.toFormattedJSON() + + const specialPlaylists = this.Account.VideoPlaylists + .map(p => ({ id: p.id, name: p.name, type: p.type })) + + return Object.assign(formatted, { specialPlaylists }) + } +} -- cgit v1.2.3