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'
24 function buildActorWithAvatarInclude () {
26 attributes: [ 'preferredUsername' ],
27 model: ActorModel.unscoped(),
31 attributes: [ 'filename' ],
32 model: AvatarModel.unscoped(),
36 attributes: [ 'host' ],
37 model: ServerModel.unscoped(),
44 function buildVideoInclude (required: boolean) {
46 attributes: [ 'id', 'uuid', 'name' ],
47 model: VideoModel.unscoped(),
52 function buildChannelInclude (required: boolean, withActor = false) {
55 attributes: [ 'id', 'name' ],
56 model: VideoChannelModel.unscoped(),
57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
61 function buildAccountInclude (required: boolean, withActor = false) {
64 attributes: [ 'id', 'name' ],
65 model: AccountModel.unscoped(),
66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
71 [ScopeNames.WITH_ALL]: {
73 Object.assign(buildVideoInclude(false), {
74 include: [ buildChannelInclude(true, true) ]
78 attributes: [ 'id', 'originCommentId' ],
79 model: VideoCommentModel.unscoped(),
82 buildAccountInclude(true, true),
83 buildVideoInclude(true)
89 model: VideoAbuseModel.unscoped(),
91 include: [ buildVideoInclude(true) ]
96 model: VideoBlacklistModel.unscoped(),
98 include: [ buildVideoInclude(true) ]
102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
103 model: VideoImportModel.unscoped(),
105 include: [ buildVideoInclude(false) ]
109 attributes: [ 'id', 'state' ],
110 model: ActorFollowModel.unscoped(),
114 attributes: [ 'preferredUsername' ],
115 model: ActorModel.unscoped(),
120 attributes: [ 'id', 'name' ],
121 model: AccountModel.unscoped(),
125 attributes: [ 'filename' ],
126 model: AvatarModel.unscoped(),
130 attributes: [ 'host' ],
131 model: ServerModel.unscoped(),
137 attributes: [ 'preferredUsername' ],
138 model: ActorModel.unscoped(),
140 as: 'ActorFollowing',
142 buildChannelInclude(false),
143 buildAccountInclude(false)
149 buildAccountInclude(false, true)
154 tableName: 'userNotification',
160 fields: [ 'videoId' ],
168 fields: [ 'commentId' ],
176 fields: [ 'videoAbuseId' ],
184 fields: [ 'videoBlacklistId' ],
192 fields: [ 'videoImportId' ],
200 fields: [ 'accountId' ],
208 fields: [ 'actorFollowId' ],
215 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
217 export class UserNotificationModel extends Model<UserNotificationModel> {
221 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
223 type: UserNotificationType
227 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
237 @ForeignKey(() => UserModel)
241 @BelongsTo(() => UserModel, {
249 @ForeignKey(() => VideoModel)
253 @BelongsTo(() => VideoModel, {
261 @ForeignKey(() => VideoCommentModel)
265 @BelongsTo(() => VideoCommentModel, {
271 Comment: VideoCommentModel
273 @ForeignKey(() => VideoAbuseModel)
277 @BelongsTo(() => VideoAbuseModel, {
283 VideoAbuse: VideoAbuseModel
285 @ForeignKey(() => VideoBlacklistModel)
287 videoBlacklistId: number
289 @BelongsTo(() => VideoBlacklistModel, {
295 VideoBlacklist: VideoBlacklistModel
297 @ForeignKey(() => VideoImportModel)
299 videoImportId: number
301 @BelongsTo(() => VideoImportModel, {
307 VideoImport: VideoImportModel
309 @ForeignKey(() => AccountModel)
313 @BelongsTo(() => AccountModel, {
319 Account: AccountModel
321 @ForeignKey(() => ActorFollowModel)
323 actorFollowId: number
325 @BelongsTo(() => ActorFollowModel, {
331 ActorFollow: ActorFollowModel
333 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
334 const query: FindOptions = {
337 order: getSort(sort),
343 if (unread !== undefined) query.where['read'] = !unread
345 return UserNotificationModel.scope(ScopeNames.WITH_ALL)
346 .findAndCountAll(query)
347 .then(({ rows, count }) => {
355 static markAsRead (userId: number, notificationIds: number[]) {
360 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
365 return UserNotificationModel.update({ read: true }, query)
368 static markAllAsRead (userId: number) {
369 const query = { where: { userId } }
371 return UserNotificationModel.update({ read: true }, query)
374 toFormattedJSON (): UserNotification {
375 const video = this.Video
376 ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
379 const videoImport = this.VideoImport ? {
380 id: this.VideoImport.id,
381 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
382 torrentName: this.VideoImport.torrentName,
383 magnetUri: this.VideoImport.magnetUri,
384 targetUrl: this.VideoImport.targetUrl
387 const comment = this.Comment ? {
389 threadId: this.Comment.getThreadId(),
390 account: this.formatActor(this.Comment.Account),
391 video: this.formatVideo(this.Comment.Video)
394 const videoAbuse = this.VideoAbuse ? {
395 id: this.VideoAbuse.id,
396 video: this.formatVideo(this.VideoAbuse.Video)
399 const videoBlacklist = this.VideoBlacklist ? {
400 id: this.VideoBlacklist.id,
401 video: this.formatVideo(this.VideoBlacklist.Video)
404 const account = this.Account ? this.formatActor(this.Account) : undefined
406 const actorFollow = this.ActorFollow ? {
407 id: this.ActorFollow.id,
408 state: this.ActorFollow.state,
410 id: this.ActorFollow.ActorFollower.Account.id,
411 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
412 name: this.ActorFollow.ActorFollower.preferredUsername,
413 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined,
414 host: this.ActorFollow.ActorFollower.getHost()
417 type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
418 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
419 name: this.ActorFollow.ActorFollowing.preferredUsername
434 createdAt: this.createdAt.toISOString(),
435 updatedAt: this.updatedAt.toISOString()
439 private formatVideo (video: VideoModel) {
447 private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
448 const avatar = accountOrChannel.Actor.Avatar
449 ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() }
453 id: accountOrChannel.id,
454 displayName: accountOrChannel.getDisplayName(),
455 name: accountOrChannel.Actor.preferredUsername,
456 host: accountOrChannel.Actor.getHost(),