1 import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
2 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3 import { getBiggestActorImage } from '@server/lib/actor-image'
4 import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
5 import { forceNumber } from '@shared/core-utils'
6 import { uuidToShort } from '@shared/extra-utils'
7 import { UserNotification, UserNotificationType } from '@shared/models'
8 import { AttributesOnly } from '@shared/typescript-utils'
9 import { isBooleanValid } from '../../helpers/custom-validators/misc'
10 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
11 import { AbuseModel } from '../abuse/abuse'
12 import { AccountModel } from '../account/account'
13 import { ActorFollowModel } from '../actor/actor-follow'
14 import { ApplicationModel } from '../application/application'
15 import { PluginModel } from '../server/plugin'
16 import { throwIfNotValid } from '../shared'
17 import { VideoModel } from '../video/video'
18 import { VideoBlacklistModel } from '../video/video-blacklist'
19 import { VideoCommentModel } from '../video/video-comment'
20 import { VideoImportModel } from '../video/video-import'
21 import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
22 import { UserModel } from './user'
23 import { UserRegistrationModel } from './user-registration'
26 tableName: 'userNotification',
32 fields: [ 'videoId' ],
40 fields: [ 'commentId' ],
48 fields: [ 'abuseId' ],
56 fields: [ 'videoBlacklistId' ],
64 fields: [ 'videoImportId' ],
72 fields: [ 'accountId' ],
80 fields: [ 'actorFollowId' ],
88 fields: [ 'pluginId' ],
96 fields: [ 'applicationId' ],
104 fields: [ 'userRegistrationId' ],
106 userRegistrationId: {
111 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
113 export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
117 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
119 type: UserNotificationType
123 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
133 @ForeignKey(() => UserModel)
137 @BelongsTo(() => UserModel, {
145 @ForeignKey(() => VideoModel)
149 @BelongsTo(() => VideoModel, {
157 @ForeignKey(() => VideoCommentModel)
161 @BelongsTo(() => VideoCommentModel, {
167 VideoComment: VideoCommentModel
169 @ForeignKey(() => AbuseModel)
173 @BelongsTo(() => AbuseModel, {
181 @ForeignKey(() => VideoBlacklistModel)
183 videoBlacklistId: number
185 @BelongsTo(() => VideoBlacklistModel, {
191 VideoBlacklist: VideoBlacklistModel
193 @ForeignKey(() => VideoImportModel)
195 videoImportId: number
197 @BelongsTo(() => VideoImportModel, {
203 VideoImport: VideoImportModel
205 @ForeignKey(() => AccountModel)
209 @BelongsTo(() => AccountModel, {
215 Account: AccountModel
217 @ForeignKey(() => ActorFollowModel)
219 actorFollowId: number
221 @BelongsTo(() => ActorFollowModel, {
227 ActorFollow: ActorFollowModel
229 @ForeignKey(() => PluginModel)
233 @BelongsTo(() => PluginModel, {
241 @ForeignKey(() => ApplicationModel)
243 applicationId: number
245 @BelongsTo(() => ApplicationModel, {
251 Application: ApplicationModel
253 @ForeignKey(() => UserRegistrationModel)
255 userRegistrationId: number
257 @BelongsTo(() => UserRegistrationModel, {
263 UserRegistration: UserRegistrationModel
265 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
266 const where = { userId }
277 if (unread !== undefined) query.where['read'] = !unread
280 UserNotificationModel.count({ where })
281 .then(count => count || 0),
284 ? [] as UserNotificationModelForApi[]
285 : new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
286 ]).then(([ total, data ]) => ({ total, data }))
289 static markAsRead (userId: number, notificationIds: number[]) {
294 [Op.in]: notificationIds
299 return UserNotificationModel.update({ read: true }, query)
302 static markAllAsRead (userId: number) {
303 const query = { where: { userId } }
305 return UserNotificationModel.update({ read: true }, query)
308 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
309 const id = forceNumber(options.id)
311 function buildAccountWhereQuery (base: string) {
312 const whereSuffix = options.forUserId
313 ? ` AND "userNotification"."userId" = ${options.forUserId}`
316 if (options.type === 'account') {
318 ` WHERE "account"."id" = ${id} ${whereSuffix}`
322 ` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
326 buildAccountWhereQuery(
327 `SELECT "userNotification"."id" FROM "userNotification" ` +
328 `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` +
329 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
332 // Remove notifications from muted accounts that followed ours
333 buildAccountWhereQuery(
334 `SELECT "userNotification"."id" FROM "userNotification" ` +
335 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
336 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
337 `INNER JOIN account ON account."actorId" = actor.id `
340 // Remove notifications from muted accounts that commented something
341 buildAccountWhereQuery(
342 `SELECT "userNotification"."id" FROM "userNotification" ` +
343 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
344 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
345 `INNER JOIN account ON account."actorId" = actor.id `
348 buildAccountWhereQuery(
349 `SELECT "userNotification"."id" FROM "userNotification" ` +
350 `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` +
351 `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
352 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
356 const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
358 return UserNotificationModel.sequelize.query(query)
361 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
362 const video = this.Video
364 ...this.formatVideo(this.Video),
366 channel: this.formatActor(this.Video.VideoChannel)
370 const videoImport = this.VideoImport
372 id: this.VideoImport.id,
373 video: this.VideoImport.Video
374 ? this.formatVideo(this.VideoImport.Video)
376 torrentName: this.VideoImport.torrentName,
377 magnetUri: this.VideoImport.magnetUri,
378 targetUrl: this.VideoImport.targetUrl
382 const comment = this.VideoComment
384 id: this.VideoComment.id,
385 threadId: this.VideoComment.getThreadId(),
386 account: this.formatActor(this.VideoComment.Account),
387 video: this.formatVideo(this.VideoComment.Video)
391 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
393 const videoBlacklist = this.VideoBlacklist
395 id: this.VideoBlacklist.id,
396 video: this.formatVideo(this.VideoBlacklist.Video)
400 const account = this.Account ? this.formatActor(this.Account) : undefined
402 const actorFollowingType = {
403 Application: 'instance' as 'instance',
404 Group: 'channel' as 'channel',
405 Person: 'account' as 'account'
407 const actorFollow = this.ActorFollow
409 id: this.ActorFollow.id,
410 state: this.ActorFollow.state,
412 id: this.ActorFollow.ActorFollower.Account.id,
413 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
414 name: this.ActorFollow.ActorFollower.preferredUsername,
415 host: this.ActorFollow.ActorFollower.getHost(),
417 ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
420 type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
421 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
422 name: this.ActorFollow.ActorFollowing.preferredUsername,
423 host: this.ActorFollow.ActorFollowing.getHost()
428 const plugin = this.Plugin
430 name: this.Plugin.name,
431 type: this.Plugin.type,
432 latestVersion: this.Plugin.latestVersion
436 const peertube = this.Application
437 ? { latestVersion: this.Application.latestPeerTubeVersion }
440 const registration = this.UserRegistration
441 ? { id: this.UserRegistration.id, username: this.UserRegistration.username }
458 createdAt: this.createdAt.toISOString(),
459 updatedAt: this.updatedAt.toISOString()
463 formatVideo (video: UserNotificationIncludes.VideoInclude) {
467 shortUUID: uuidToShort(video.uuid),
472 formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
473 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
475 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
477 video: abuse.VideoCommentAbuse.VideoComment.Video
479 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
480 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
481 shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
482 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
488 const videoAbuse = abuse.VideoAbuse?.Video
489 ? this.formatVideo(abuse.VideoAbuse.Video)
492 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
493 ? this.formatActor(abuse.FlaggedAccount)
500 comment: commentAbuse,
501 account: accountAbuse
506 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
509 id: accountOrChannel.id,
510 displayName: accountOrChannel.getDisplayName(),
511 name: accountOrChannel.Actor.preferredUsername,
512 host: accountOrChannel.Actor.getHost(),
514 ...this.formatAvatars(accountOrChannel.Actor.Avatars)
518 formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) {
519 if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] }
522 avatar: this.formatAvatar(getBiggestActorImage(avatars)),
524 avatars: avatars.map(a => this.formatAvatar(a))
528 formatAvatar (a: UserNotificationIncludes.ActorImageInclude) {
530 path: a.getStaticPath(),