14 } from 'sequelize-typescript'
15 import { UserNotification, UserNotificationType } from '../../../shared'
16 import { getSort, throwIfNotValid } from '../utils'
17 import { isBooleanValid } from '../../helpers/custom-validators/misc'
18 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
19 import { UserModel } from './user'
20 import { VideoModel } from '../video/video'
21 import { VideoCommentModel } from '../video/video-comment'
22 import { Op } from 'sequelize'
23 import { VideoChannelModel } from '../video/video-channel'
24 import { AccountModel } from './account'
25 import { VideoAbuseModel } from '../video/video-abuse'
26 import { VideoBlacklistModel } from '../video/video-blacklist'
27 import { VideoImportModel } from '../video/video-import'
28 import { ActorModel } from '../activitypub/actor'
29 import { ActorFollowModel } from '../activitypub/actor-follow'
30 import { AvatarModel } from '../avatar/avatar'
31 import { ServerModel } from '../server/server'
37 function buildActorWithAvatarInclude () {
39 attributes: [ 'preferredUsername' ],
40 model: () => ActorModel.unscoped(),
44 attributes: [ 'filename' ],
45 model: () => AvatarModel.unscoped(),
49 attributes: [ 'host' ],
50 model: () => ServerModel.unscoped(),
57 function buildVideoInclude (required: boolean) {
59 attributes: [ 'id', 'uuid', 'name' ],
60 model: () => VideoModel.unscoped(),
65 function buildChannelInclude (required: boolean, withActor = false) {
68 attributes: [ 'id', 'name' ],
69 model: () => VideoChannelModel.unscoped(),
70 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
74 function buildAccountInclude (required: boolean, withActor = false) {
77 attributes: [ 'id', 'name' ],
78 model: () => AccountModel.unscoped(),
79 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
84 [ScopeNames.WITH_ALL]: {
86 Object.assign(buildVideoInclude(false), {
87 include: [ buildChannelInclude(true, true) ]
91 attributes: [ 'id', 'originCommentId' ],
92 model: () => VideoCommentModel.unscoped(),
95 buildAccountInclude(true, true),
96 buildVideoInclude(true)
101 attributes: [ 'id' ],
102 model: () => VideoAbuseModel.unscoped(),
104 include: [ buildVideoInclude(true) ]
108 attributes: [ 'id' ],
109 model: () => VideoBlacklistModel.unscoped(),
111 include: [ buildVideoInclude(true) ]
115 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
116 model: () => VideoImportModel.unscoped(),
118 include: [ buildVideoInclude(false) ]
122 attributes: [ 'id' ],
123 model: () => ActorFollowModel.unscoped(),
127 attributes: [ 'preferredUsername' ],
128 model: () => ActorModel.unscoped(),
133 attributes: [ 'id', 'name' ],
134 model: () => AccountModel.unscoped(),
138 attributes: [ 'filename' ],
139 model: () => AvatarModel.unscoped(),
143 attributes: [ 'host' ],
144 model: () => ServerModel.unscoped(),
150 attributes: [ 'preferredUsername' ],
151 model: () => ActorModel.unscoped(),
153 as: 'ActorFollowing',
155 buildChannelInclude(false),
156 buildAccountInclude(false)
162 buildAccountInclude(false, true)
167 tableName: 'userNotification',
173 fields: [ 'videoId' ],
181 fields: [ 'commentId' ],
189 fields: [ 'videoAbuseId' ],
197 fields: [ 'videoBlacklistId' ],
205 fields: [ 'videoImportId' ],
213 fields: [ 'accountId' ],
221 fields: [ 'actorFollowId' ],
230 export class UserNotificationModel extends Model<UserNotificationModel> {
234 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
236 type: UserNotificationType
240 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
250 @ForeignKey(() => UserModel)
254 @BelongsTo(() => UserModel, {
262 @ForeignKey(() => VideoModel)
266 @BelongsTo(() => VideoModel, {
274 @ForeignKey(() => VideoCommentModel)
278 @BelongsTo(() => VideoCommentModel, {
284 Comment: VideoCommentModel
286 @ForeignKey(() => VideoAbuseModel)
290 @BelongsTo(() => VideoAbuseModel, {
296 VideoAbuse: VideoAbuseModel
298 @ForeignKey(() => VideoBlacklistModel)
300 videoBlacklistId: number
302 @BelongsTo(() => VideoBlacklistModel, {
308 VideoBlacklist: VideoBlacklistModel
310 @ForeignKey(() => VideoImportModel)
312 videoImportId: number
314 @BelongsTo(() => VideoImportModel, {
320 VideoImport: VideoImportModel
322 @ForeignKey(() => AccountModel)
326 @BelongsTo(() => AccountModel, {
332 Account: AccountModel
334 @ForeignKey(() => ActorFollowModel)
336 actorFollowId: number
338 @BelongsTo(() => ActorFollowModel, {
344 ActorFollow: ActorFollowModel
346 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
347 const query: IFindOptions<UserNotificationModel> = {
350 order: getSort(sort),
356 if (unread !== undefined) query.where['read'] = !unread
358 return UserNotificationModel.scope(ScopeNames.WITH_ALL)
359 .findAndCountAll(query)
360 .then(({ rows, count }) => {
368 static markAsRead (userId: number, notificationIds: number[]) {
373 [Op.any]: notificationIds
378 return UserNotificationModel.update({ read: true }, query)
381 static markAllAsRead (userId: number) {
382 const query = { where: { userId } }
384 return UserNotificationModel.update({ read: true }, query)
387 toFormattedJSON (): UserNotification {
388 const video = this.Video
389 ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
392 const videoImport = this.VideoImport ? {
393 id: this.VideoImport.id,
394 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
395 torrentName: this.VideoImport.torrentName,
396 magnetUri: this.VideoImport.magnetUri,
397 targetUrl: this.VideoImport.targetUrl
400 const comment = this.Comment ? {
402 threadId: this.Comment.getThreadId(),
403 account: this.formatActor(this.Comment.Account),
404 video: this.formatVideo(this.Comment.Video)
407 const videoAbuse = this.VideoAbuse ? {
408 id: this.VideoAbuse.id,
409 video: this.formatVideo(this.VideoAbuse.Video)
412 const videoBlacklist = this.VideoBlacklist ? {
413 id: this.VideoBlacklist.id,
414 video: this.formatVideo(this.VideoBlacklist.Video)
417 const account = this.Account ? this.formatActor(this.Account) : undefined
419 const actorFollow = this.ActorFollow ? {
420 id: this.ActorFollow.id,
422 id: this.ActorFollow.ActorFollower.Account.id,
423 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
424 name: this.ActorFollow.ActorFollower.preferredUsername,
425 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined,
426 host: this.ActorFollow.ActorFollower.getHost()
429 type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
430 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
431 name: this.ActorFollow.ActorFollowing.preferredUsername
446 createdAt: this.createdAt.toISOString(),
447 updatedAt: this.updatedAt.toISOString()
451 private formatVideo (video: VideoModel) {
459 private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
460 const avatar = accountOrChannel.Actor.Avatar
461 ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() }
465 id: accountOrChannel.id,
466 displayName: accountOrChannel.getDisplayName(),
467 name: accountOrChannel.Actor.preferredUsername,
468 host: accountOrChannel.Actor.getHost(),