1 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2 import { UserNotification, UserNotificationType } from '../../../shared'
3 import { getSort, throwIfNotValid } from '../utils'
4 import { isBooleanValid } from '../../helpers/custom-validators/misc'
5 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
6 import { UserModel } from './user'
7 import { VideoModel } from '../video/video'
8 import { VideoCommentModel } from '../video/video-comment'
9 import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10 import { VideoChannelModel } from '../video/video-channel'
11 import { AccountModel } from './account'
12 import { VideoAbuseModel } from '../video/video-abuse'
13 import { VideoBlacklistModel } from '../video/video-blacklist'
14 import { VideoImportModel } from '../video/video-import'
15 import { ActorModel } from '../activitypub/actor'
16 import { ActorFollowModel } from '../activitypub/actor-follow'
17 import { AvatarModel } from '../avatar/avatar'
18 import { ServerModel } from '../server/server'
19 import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/typings/models/user'
25 function buildActorWithAvatarInclude () {
27 attributes: [ 'preferredUsername' ],
28 model: ActorModel.unscoped(),
32 attributes: [ 'filename' ],
33 model: AvatarModel.unscoped(),
37 attributes: [ 'host' ],
38 model: ServerModel.unscoped(),
45 function buildVideoInclude (required: boolean) {
47 attributes: [ 'id', 'uuid', 'name' ],
48 model: VideoModel.unscoped(),
53 function buildChannelInclude (required: boolean, withActor = false) {
56 attributes: [ 'id', 'name' ],
57 model: VideoChannelModel.unscoped(),
58 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
62 function buildAccountInclude (required: boolean, withActor = false) {
65 attributes: [ 'id', 'name' ],
66 model: AccountModel.unscoped(),
67 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
72 [ScopeNames.WITH_ALL]: {
74 Object.assign(buildVideoInclude(false), {
75 include: [ buildChannelInclude(true, true) ]
79 attributes: [ 'id', 'originCommentId' ],
80 model: VideoCommentModel.unscoped(),
83 buildAccountInclude(true, true),
84 buildVideoInclude(true)
90 model: VideoAbuseModel.unscoped(),
92 include: [ buildVideoInclude(true) ]
97 model: VideoBlacklistModel.unscoped(),
99 include: [ buildVideoInclude(true) ]
103 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
104 model: VideoImportModel.unscoped(),
106 include: [ buildVideoInclude(false) ]
110 attributes: [ 'id', 'state' ],
111 model: ActorFollowModel.unscoped(),
115 attributes: [ 'preferredUsername' ],
116 model: ActorModel.unscoped(),
121 attributes: [ 'id', 'name' ],
122 model: AccountModel.unscoped(),
126 attributes: [ 'filename' ],
127 model: AvatarModel.unscoped(),
131 attributes: [ 'host' ],
132 model: ServerModel.unscoped(),
138 attributes: [ 'preferredUsername', 'type' ],
139 model: ActorModel.unscoped(),
141 as: 'ActorFollowing',
143 buildChannelInclude(false),
144 buildAccountInclude(false),
146 attributes: [ 'host' ],
147 model: ServerModel.unscoped(),
155 buildAccountInclude(false, true)
160 tableName: 'userNotification',
166 fields: [ 'videoId' ],
174 fields: [ 'commentId' ],
182 fields: [ 'videoAbuseId' ],
190 fields: [ 'videoBlacklistId' ],
198 fields: [ 'videoImportId' ],
206 fields: [ 'accountId' ],
214 fields: [ 'actorFollowId' ],
221 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
223 export class UserNotificationModel extends Model<UserNotificationModel> {
227 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
229 type: UserNotificationType
233 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
243 @ForeignKey(() => UserModel)
247 @BelongsTo(() => UserModel, {
255 @ForeignKey(() => VideoModel)
259 @BelongsTo(() => VideoModel, {
267 @ForeignKey(() => VideoCommentModel)
271 @BelongsTo(() => VideoCommentModel, {
277 Comment: VideoCommentModel
279 @ForeignKey(() => VideoAbuseModel)
283 @BelongsTo(() => VideoAbuseModel, {
289 VideoAbuse: VideoAbuseModel
291 @ForeignKey(() => VideoBlacklistModel)
293 videoBlacklistId: number
295 @BelongsTo(() => VideoBlacklistModel, {
301 VideoBlacklist: VideoBlacklistModel
303 @ForeignKey(() => VideoImportModel)
305 videoImportId: number
307 @BelongsTo(() => VideoImportModel, {
313 VideoImport: VideoImportModel
315 @ForeignKey(() => AccountModel)
319 @BelongsTo(() => AccountModel, {
325 Account: AccountModel
327 @ForeignKey(() => ActorFollowModel)
329 actorFollowId: number
331 @BelongsTo(() => ActorFollowModel, {
337 ActorFollow: ActorFollowModel
339 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
340 const where = { userId }
342 const query: FindOptions = {
345 order: getSort(sort),
349 if (unread !== undefined) query.where['read'] = !unread
352 UserNotificationModel.count({ where })
353 .then(count => count || 0),
357 : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query)
358 ]).then(([ total, data ]) => ({ total, data }))
361 static markAsRead (userId: number, notificationIds: number[]) {
366 [Op.in]: notificationIds
371 return UserNotificationModel.update({ read: true }, query)
374 static markAllAsRead (userId: number) {
375 const query = { where: { userId } }
377 return UserNotificationModel.update({ read: true }, query)
380 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
381 const video = this.Video
382 ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) })
385 const videoImport = this.VideoImport ? {
386 id: this.VideoImport.id,
387 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
388 torrentName: this.VideoImport.torrentName,
389 magnetUri: this.VideoImport.magnetUri,
390 targetUrl: this.VideoImport.targetUrl
393 const comment = this.Comment ? {
395 threadId: this.Comment.getThreadId(),
396 account: this.formatActor(this.Comment.Account),
397 video: this.formatVideo(this.Comment.Video)
400 const videoAbuse = this.VideoAbuse ? {
401 id: this.VideoAbuse.id,
402 video: this.formatVideo(this.VideoAbuse.Video)
405 const videoBlacklist = this.VideoBlacklist ? {
406 id: this.VideoBlacklist.id,
407 video: this.formatVideo(this.VideoBlacklist.Video)
410 const account = this.Account ? this.formatActor(this.Account) : undefined
412 const actorFollowingType = {
413 Application: 'instance' as 'instance',
414 Group: 'channel' as 'channel',
415 Person: 'account' as 'account'
417 const actorFollow = this.ActorFollow ? {
418 id: this.ActorFollow.id,
419 state: this.ActorFollow.state,
421 id: this.ActorFollow.ActorFollower.Account.id,
422 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
423 name: this.ActorFollow.ActorFollower.preferredUsername,
424 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
425 host: this.ActorFollow.ActorFollower.getHost()
428 type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
429 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
430 name: this.ActorFollow.ActorFollowing.preferredUsername,
431 host: this.ActorFollow.ActorFollowing.getHost()
446 createdAt: this.createdAt.toISOString(),
447 updatedAt: this.updatedAt.toISOString()
451 formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
460 this: UserNotificationModelForApi,
461 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
463 const avatar = accountOrChannel.Actor.Avatar
464 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
468 id: accountOrChannel.id,
469 displayName: accountOrChannel.getDisplayName(),
470 name: accountOrChannel.Actor.preferredUsername,
471 host: accountOrChannel.Actor.getHost(),