From cef534ed53e4518fe0acf581bfe880788d42fc36 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 26 Dec 2018 10:36:24 +0100 Subject: Add user notification base code --- server/models/account/user-notification-setting.ts | 100 ++++++++ server/models/account/user-notification.ts | 256 +++++++++++++++++++++ server/models/account/user.ts | 101 +++++++- server/models/activitypub/actor-follow.ts | 14 +- server/models/activitypub/actor.ts | 1 + server/models/video/video-abuse.ts | 5 - server/models/video/video-blacklist.ts | 10 - server/models/video/video-comment.ts | 4 + server/models/video/video.ts | 4 + 9 files changed, 466 insertions(+), 29 deletions(-) create mode 100644 server/models/account/user-notification-setting.ts create mode 100644 server/models/account/user-notification.ts (limited to 'server/models') diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts new file mode 100644 index 000000000..bc24b1e33 --- /dev/null +++ b/server/models/account/user-notification-setting.ts @@ -0,0 +1,100 @@ +import { + AfterDestroy, + AfterUpdate, + AllowNull, + BelongsTo, + Column, + CreatedAt, + Default, + ForeignKey, + Is, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { throwIfNotValid } from '../utils' +import { UserModel } from './user' +import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' +import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' +import { clearCacheByUserId } from '../../lib/oauth-model' + +@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( + 'UserNotificationSettingVideoAbuseAsModerator', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') + ) + @Column + videoAbuseAsModerator: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingBlacklistOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') + ) + @Column + blacklistOnMyVideo: 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 clearCacheByUserId(instance.userId) + } + + toFormattedJSON (): UserNotificationSetting { + return { + newCommentOnMyVideo: this.newCommentOnMyVideo, + newVideoFromSubscription: this.newVideoFromSubscription, + videoAbuseAsModerator: this.videoAbuseAsModerator, + blacklistOnMyVideo: this.blacklistOnMyVideo + } + } +} diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts new file mode 100644 index 000000000..e22f0d57f --- /dev/null +++ b/server/models/account/user-notification.ts @@ -0,0 +1,256 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { UserNotification, UserNotificationType } from '../../../shared' +import { getSort, throwIfNotValid } from '../utils' +import { isBooleanValid } from '../../helpers/custom-validators/misc' +import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' +import { UserModel } from './user' +import { VideoModel } from '../video/video' +import { VideoCommentModel } from '../video/video-comment' +import { Op } from 'sequelize' +import { VideoChannelModel } from '../video/video-channel' +import { AccountModel } from './account' +import { VideoAbuseModel } from '../video/video-abuse' +import { VideoBlacklistModel } from '../video/video-blacklist' + +enum ScopeNames { + WITH_ALL = 'WITH_ALL' +} + +@Scopes({ + [ScopeNames.WITH_ALL]: { + include: [ + { + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'name' ], + model: () => VideoChannelModel.unscoped() + } + ] + }, + { + attributes: [ 'id' ], + model: () => VideoCommentModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped() + }, + { + required: true, + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped() + } + ] + }, + { + attributes: [ 'id' ], + model: () => VideoAbuseModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped() + } + ] + }, + { + attributes: [ 'id' ], + model: () => VideoBlacklistModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped() + } + ] + } + ] + } +}) +@Table({ + tableName: 'userNotification', + indexes: [ + { + fields: [ 'videoId' ] + }, + { + fields: [ 'commentId' ] + } + ] +}) +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(() => VideoAbuseModel) + @Column + videoAbuseId: number + + @BelongsTo(() => VideoAbuseModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoAbuse: VideoAbuseModel + + @ForeignKey(() => VideoBlacklistModel) + @Column + videoBlacklistId: number + + @BelongsTo(() => VideoBlacklistModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoBlacklist: VideoBlacklistModel + + static listForApi (userId: number, start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: getSort(sort), + where: { + userId + } + } + + return UserNotificationModel.scope(ScopeNames.WITH_ALL) + .findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static markAsRead (userId: number, notificationIds: number[]) { + const query = { + where: { + userId, + id: { + [Op.any]: notificationIds + } + } + } + + return UserNotificationModel.update({ read: true }, query) + } + + toFormattedJSON (): UserNotification { + const video = this.Video ? { + id: this.Video.id, + uuid: this.Video.uuid, + name: this.Video.name, + channel: { + id: this.Video.VideoChannel.id, + displayName: this.Video.VideoChannel.getDisplayName() + } + } : undefined + + const comment = this.Comment ? { + id: this.Comment.id, + account: { + id: this.Comment.Account.id, + displayName: this.Comment.Account.getDisplayName() + }, + video: { + id: this.Comment.Video.id, + uuid: this.Comment.Video.uuid, + name: this.Comment.Video.name + } + } : undefined + + const videoAbuse = this.VideoAbuse ? { + id: this.VideoAbuse.id, + video: { + id: this.VideoAbuse.Video.id, + uuid: this.VideoAbuse.Video.uuid, + name: this.VideoAbuse.Video.name + } + } : undefined + + const videoBlacklist = this.VideoBlacklist ? { + id: this.VideoBlacklist.id, + video: { + id: this.VideoBlacklist.Video.id, + uuid: this.VideoBlacklist.Video.uuid, + name: this.VideoBlacklist.Video.name + } + } : undefined + + return { + id: this.id, + type: this.type, + read: this.read, + video, + comment, + videoAbuse, + videoBlacklist, + createdAt: this.createdAt.toISOString(), + updatedAt: this.updatedAt.toISOString() + } + } +} diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 180ced810..55ec14d05 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -32,8 +32,8 @@ import { isUserUsernameValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid, - isUserWebTorrentEnabledValid, - isUserVideosHistoryEnabledValid + isUserVideosHistoryEnabledValid, + isUserWebTorrentEnabledValid } from '../../helpers/custom-validators/users' import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' import { OAuthTokenModel } from '../oauth/oauth-token' @@ -44,6 +44,10 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' import { values } from 'lodash' import { NSFW_POLICY_TYPES } from '../../initializers' 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' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' @@ -54,6 +58,10 @@ enum ScopeNames { { model: () => AccountModel, required: true + }, + { + model: () => UserNotificationSettingModel, + required: true } ] }) @@ -64,6 +72,10 @@ enum ScopeNames { model: () => AccountModel, required: true, include: [ () => VideoChannelModel ] + }, + { + model: () => UserNotificationSettingModel, + required: true } ] } @@ -167,6 +179,13 @@ export class UserModel extends Model { }) Account: AccountModel + @HasOne(() => UserNotificationSettingModel, { + foreignKey: 'userId', + onDelete: 'cascade', + hooks: true + }) + NotificationSetting: UserNotificationSettingModel + @HasMany(() => OAuthTokenModel, { foreignKey: 'userId', onDelete: 'cascade' @@ -249,13 +268,12 @@ 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 @@ -263,9 +281,46 @@ export class UserModel extends Model { } } - 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 loadById (id: number) { @@ -314,6 +369,37 @@ export class UserModel extends Model { 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 getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() @@ -380,6 +466,7 @@ 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) diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 994f791de..796e07a42 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -307,7 +307,7 @@ export class ActorFollowModel extends Model { }) } - static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { + static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) { const query = { distinct: true, offset: start, @@ -335,7 +335,7 @@ export class ActorFollowModel extends Model { as: 'ActorFollowing', required: true, where: { - id + id: actorId } } ] @@ -350,7 +350,7 @@ export class ActorFollowModel extends Model { }) } - static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { + static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { const query = { attributes: [], distinct: true, @@ -358,7 +358,7 @@ export class ActorFollowModel extends Model { limit: count, order: getSort(sort), where: { - actorId: id + actorId: actorId }, include: [ { @@ -451,9 +451,9 @@ export class ActorFollowModel extends Model { static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + 'WHERE id IN (' + - 'SELECT "actorFollow"."id" FROM "actorFollow" ' + - 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + - `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + + 'SELECT "actorFollow"."id" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + + `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + ')' const options = { diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 12b83916e..dda57a8ba 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -219,6 +219,7 @@ export class ActorModel extends Model { name: 'actorId', allowNull: false }, + as: 'ActorFollowings', onDelete: 'cascade' }) ActorFollowing: ActorFollowModel[] diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index dbb88ca45..4c9e2d05e 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -86,11 +86,6 @@ export class VideoAbuseModel extends Model { }) Video: VideoModel - @AfterCreate - static sendEmailNotification (instance: VideoAbuseModel) { - return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) - } - static loadByIdAndVideoId (id: number, videoId: number) { const query = { where: { diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 67f7cd487..23e992685 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -53,16 +53,6 @@ export class VideoBlacklistModel extends Model { }) Video: VideoModel - @AfterCreate - static sendBlacklistEmailNotification (instance: VideoBlacklistModel) { - return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason) - } - - @AfterDestroy - static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) { - return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId) - } - static listForApi (start: number, count: number, sort: SortType) { const query = { offset: start, diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index dd6d08139..d8fc2a564 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -448,6 +448,10 @@ export class VideoCommentModel extends Model { } } + getCommentStaticPath () { + return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() + } + getThreadId (): number { return this.originCommentId || this.id } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index bcf327f32..fc200e5d1 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1527,6 +1527,10 @@ export class VideoModel extends Model { videoFile.infoHash = parsedTorrent.infoHash } + getWatchStaticPath () { + return '/videos/watch/' + this.uuid + } + getEmbedStaticPath () { return '/videos/embed/' + this.uuid } -- cgit v1.2.3