]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/user/user-notification.ts
Merge branch 'release/3.2.0' into develop
[github/Chocobozzz/PeerTube.git] / server / models / user / user-notification.ts
CommitLineData
d95d1559 1import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
1735c825 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
d95d1559 3import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
16c016e8 4import { AttributesOnly } from '@shared/core-utils'
cef534ed 5import { UserNotification, UserNotificationType } from '../../../shared'
cef534ed
C
6import { isBooleanValid } from '../../helpers/custom-validators/misc'
7import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
d95d1559
C
8import { AbuseModel } from '../abuse/abuse'
9import { VideoAbuseModel } from '../abuse/video-abuse'
10import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
7d9ba5c0
C
11import { AccountModel } from '../account/account'
12import { ActorModel } from '../actor/actor'
13import { ActorFollowModel } from '../actor/actor-follow'
14import { ActorImageModel } from '../actor/actor-image'
32a18cbf 15import { ApplicationModel } from '../application/application'
32a18cbf 16import { PluginModel } from '../server/plugin'
38967f7b 17import { ServerModel } from '../server/server'
d95d1559
C
18import { getSort, throwIfNotValid } from '../utils'
19import { VideoModel } from '../video/video'
20import { VideoBlacklistModel } from '../video/video-blacklist'
21import { VideoChannelModel } from '../video/video-channel'
22import { VideoCommentModel } from '../video/video-comment'
23import { VideoImportModel } from '../video/video-import'
d95d1559 24import { UserModel } from './user'
cef534ed
C
25
26enum ScopeNames {
27 WITH_ALL = 'WITH_ALL'
28}
29
457bb213
C
30function buildActorWithAvatarInclude () {
31 return {
32 attributes: [ 'preferredUsername' ],
3acc5084 33 model: ActorModel.unscoped(),
457bb213
C
34 required: true,
35 include: [
36 {
37 attributes: [ 'filename' ],
f4796856
C
38 as: 'Avatar',
39 model: ActorImageModel.unscoped(),
457bb213 40 required: false
38967f7b
C
41 },
42 {
43 attributes: [ 'host' ],
3acc5084 44 model: ServerModel.unscoped(),
38967f7b 45 required: false
457bb213
C
46 }
47 ]
48 }
49}
50
dc133480
C
51function buildVideoInclude (required: boolean) {
52 return {
53 attributes: [ 'id', 'uuid', 'name' ],
3acc5084 54 model: VideoModel.unscoped(),
dc133480
C
55 required
56 }
57}
58
457bb213 59function buildChannelInclude (required: boolean, withActor = false) {
dc133480 60 return {
f7cc67b4 61 required,
dc133480 62 attributes: [ 'id', 'name' ],
3acc5084 63 model: VideoChannelModel.unscoped(),
457bb213 64 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
dc133480
C
65 }
66}
67
457bb213 68function buildAccountInclude (required: boolean, withActor = false) {
dc133480 69 return {
f7cc67b4 70 required,
dc133480 71 attributes: [ 'id', 'name' ],
3acc5084 72 model: AccountModel.unscoped(),
457bb213 73 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
dc133480
C
74 }
75}
76
3acc5084 77@Scopes(() => ({
cef534ed
C
78 [ScopeNames.WITH_ALL]: {
79 include: [
dc133480 80 Object.assign(buildVideoInclude(false), {
457bb213 81 include: [ buildChannelInclude(true, true) ]
dc133480 82 }),
457bb213 83
cef534ed 84 {
dc133480 85 attributes: [ 'id', 'originCommentId' ],
3acc5084 86 model: VideoCommentModel.unscoped(),
cef534ed
C
87 required: false,
88 include: [
457bb213 89 buildAccountInclude(true, true),
dc133480 90 buildVideoInclude(true)
cef534ed
C
91 ]
92 },
457bb213 93
cef534ed 94 {
594d3e48 95 attributes: [ 'id', 'state' ],
d95d1559 96 model: AbuseModel.unscoped(),
cef534ed 97 required: false,
d95d1559
C
98 include: [
99 {
100 attributes: [ 'id' ],
101 model: VideoAbuseModel.unscoped(),
102 required: false,
32a18cbf 103 include: [ buildVideoInclude(false) ]
d95d1559
C
104 },
105 {
106 attributes: [ 'id' ],
107 model: VideoCommentAbuseModel.unscoped(),
108 required: false,
109 include: [
110 {
111 attributes: [ 'id', 'originCommentId' ],
ea3674d0 112 model: VideoCommentModel.unscoped(),
32a18cbf 113 required: false,
d95d1559
C
114 include: [
115 {
310b5219 116 attributes: [ 'id', 'name', 'uuid' ],
d95d1559 117 model: VideoModel.unscoped(),
32a18cbf 118 required: false
d95d1559
C
119 }
120 ]
121 }
122 ]
123 },
124 {
125 model: AccountModel,
126 as: 'FlaggedAccount',
32a18cbf 127 required: false,
d95d1559
C
128 include: [ buildActorWithAvatarInclude() ]
129 }
130 ]
cef534ed 131 },
457bb213 132
cef534ed
C
133 {
134 attributes: [ 'id' ],
3acc5084 135 model: VideoBlacklistModel.unscoped(),
cef534ed 136 required: false,
dc133480
C
137 include: [ buildVideoInclude(true) ]
138 },
457bb213 139
dc133480
C
140 {
141 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
3acc5084 142 model: VideoImportModel.unscoped(),
dc133480
C
143 required: false,
144 include: [ buildVideoInclude(false) ]
f7cc67b4 145 },
457bb213 146
32a18cbf
C
147 {
148 attributes: [ 'id', 'name', 'type', 'latestVersion' ],
149 model: PluginModel.unscoped(),
150 required: false
151 },
152
153 {
154 attributes: [ 'id', 'latestPeerTubeVersion' ],
155 model: ApplicationModel.unscoped(),
156 required: false
157 },
158
f7cc67b4 159 {
8ce1ba6e 160 attributes: [ 'id', 'state' ],
3acc5084 161 model: ActorFollowModel.unscoped(),
f7cc67b4
C
162 required: false,
163 include: [
164 {
165 attributes: [ 'preferredUsername' ],
3acc5084 166 model: ActorModel.unscoped(),
f7cc67b4
C
167 required: true,
168 as: 'ActorFollower',
457bb213
C
169 include: [
170 {
171 attributes: [ 'id', 'name' ],
3acc5084 172 model: AccountModel.unscoped(),
457bb213
C
173 required: true
174 },
175 {
176 attributes: [ 'filename' ],
f4796856
C
177 as: 'Avatar',
178 model: ActorImageModel.unscoped(),
457bb213 179 required: false
38967f7b
C
180 },
181 {
182 attributes: [ 'host' ],
3acc5084 183 model: ServerModel.unscoped(),
38967f7b 184 required: false
457bb213
C
185 }
186 ]
f7cc67b4
C
187 },
188 {
8424c402 189 attributes: [ 'preferredUsername', 'type' ],
3acc5084 190 model: ActorModel.unscoped(),
f7cc67b4
C
191 required: true,
192 as: 'ActorFollowing',
193 include: [
194 buildChannelInclude(false),
8424c402
C
195 buildAccountInclude(false),
196 {
197 attributes: [ 'host' ],
198 model: ServerModel.unscoped(),
199 required: false
200 }
f7cc67b4
C
201 ]
202 }
203 ]
457bb213
C
204 },
205
206 buildAccountInclude(false, true)
3acc5084 207 ]
cef534ed 208 }
3acc5084 209}))
cef534ed
C
210@Table({
211 tableName: 'userNotification',
212 indexes: [
213 {
457bb213 214 fields: [ 'userId' ]
cef534ed
C
215 },
216 {
457bb213
C
217 fields: [ 'videoId' ],
218 where: {
219 videoId: {
220 [Op.ne]: null
221 }
222 }
223 },
224 {
225 fields: [ 'commentId' ],
226 where: {
227 commentId: {
228 [Op.ne]: null
229 }
230 }
231 },
232 {
d95d1559 233 fields: [ 'abuseId' ],
457bb213 234 where: {
d95d1559 235 abuseId: {
457bb213
C
236 [Op.ne]: null
237 }
238 }
239 },
240 {
241 fields: [ 'videoBlacklistId' ],
242 where: {
243 videoBlacklistId: {
244 [Op.ne]: null
245 }
246 }
247 },
248 {
249 fields: [ 'videoImportId' ],
250 where: {
251 videoImportId: {
252 [Op.ne]: null
253 }
254 }
255 },
256 {
257 fields: [ 'accountId' ],
258 where: {
259 accountId: {
260 [Op.ne]: null
261 }
262 }
263 },
264 {
265 fields: [ 'actorFollowId' ],
266 where: {
267 actorFollowId: {
268 [Op.ne]: null
269 }
270 }
32a18cbf
C
271 },
272 {
273 fields: [ 'pluginId' ],
274 where: {
275 pluginId: {
276 [Op.ne]: null
277 }
278 }
279 },
280 {
281 fields: [ 'applicationId' ],
282 where: {
283 applicationId: {
284 [Op.ne]: null
285 }
286 }
cef534ed 287 }
3acc5084 288 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
cef534ed 289})
16c016e8 290export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
cef534ed
C
291
292 @AllowNull(false)
293 @Default(null)
294 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
295 @Column
296 type: UserNotificationType
297
298 @AllowNull(false)
299 @Default(false)
300 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
301 @Column
302 read: boolean
303
304 @CreatedAt
305 createdAt: Date
306
307 @UpdatedAt
308 updatedAt: Date
309
310 @ForeignKey(() => UserModel)
311 @Column
312 userId: number
313
314 @BelongsTo(() => UserModel, {
315 foreignKey: {
316 allowNull: false
317 },
318 onDelete: 'cascade'
319 })
320 User: UserModel
321
322 @ForeignKey(() => VideoModel)
323 @Column
324 videoId: number
325
326 @BelongsTo(() => VideoModel, {
327 foreignKey: {
328 allowNull: true
329 },
330 onDelete: 'cascade'
331 })
332 Video: VideoModel
333
334 @ForeignKey(() => VideoCommentModel)
335 @Column
336 commentId: number
337
338 @BelongsTo(() => VideoCommentModel, {
339 foreignKey: {
340 allowNull: true
341 },
342 onDelete: 'cascade'
343 })
344 Comment: VideoCommentModel
345
d95d1559 346 @ForeignKey(() => AbuseModel)
cef534ed 347 @Column
d95d1559 348 abuseId: number
cef534ed 349
d95d1559 350 @BelongsTo(() => AbuseModel, {
cef534ed
C
351 foreignKey: {
352 allowNull: true
353 },
354 onDelete: 'cascade'
355 })
d95d1559 356 Abuse: AbuseModel
cef534ed
C
357
358 @ForeignKey(() => VideoBlacklistModel)
359 @Column
360 videoBlacklistId: number
361
362 @BelongsTo(() => VideoBlacklistModel, {
363 foreignKey: {
364 allowNull: true
365 },
366 onDelete: 'cascade'
367 })
368 VideoBlacklist: VideoBlacklistModel
369
dc133480
C
370 @ForeignKey(() => VideoImportModel)
371 @Column
372 videoImportId: number
373
374 @BelongsTo(() => VideoImportModel, {
375 foreignKey: {
376 allowNull: true
377 },
378 onDelete: 'cascade'
379 })
380 VideoImport: VideoImportModel
381
f7cc67b4
C
382 @ForeignKey(() => AccountModel)
383 @Column
384 accountId: number
385
386 @BelongsTo(() => AccountModel, {
387 foreignKey: {
388 allowNull: true
389 },
390 onDelete: 'cascade'
391 })
392 Account: AccountModel
393
394 @ForeignKey(() => ActorFollowModel)
395 @Column
396 actorFollowId: number
397
398 @BelongsTo(() => ActorFollowModel, {
399 foreignKey: {
400 allowNull: true
401 },
402 onDelete: 'cascade'
403 })
404 ActorFollow: ActorFollowModel
405
32a18cbf
C
406 @ForeignKey(() => PluginModel)
407 @Column
408 pluginId: number
409
410 @BelongsTo(() => PluginModel, {
411 foreignKey: {
412 allowNull: true
413 },
414 onDelete: 'cascade'
415 })
416 Plugin: PluginModel
417
418 @ForeignKey(() => ApplicationModel)
419 @Column
420 applicationId: number
421
422 @BelongsTo(() => ApplicationModel, {
423 foreignKey: {
424 allowNull: true
425 },
426 onDelete: 'cascade'
427 })
428 Application: ApplicationModel
429
dc133480 430 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
119b16e5
C
431 const where = { userId }
432
1735c825 433 const query: FindOptions = {
cef534ed
C
434 offset: start,
435 limit: count,
436 order: getSort(sort),
119b16e5 437 where
cef534ed
C
438 }
439
dc133480
C
440 if (unread !== undefined) query.where['read'] = !unread
441
119b16e5
C
442 return Promise.all([
443 UserNotificationModel.count({ where })
444 .then(count => count || 0),
445
446 count === 0
447 ? []
448 : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query)
449 ]).then(([ total, data ]) => ({ total, data }))
cef534ed
C
450 }
451
452 static markAsRead (userId: number, notificationIds: number[]) {
453 const query = {
454 where: {
455 userId,
456 id: {
0374b6b5 457 [Op.in]: notificationIds
cef534ed
C
458 }
459 }
460 }
461
462 return UserNotificationModel.update({ read: true }, query)
463 }
464
2f1548fd
C
465 static markAllAsRead (userId: number) {
466 const query = { where: { userId } }
467
468 return UserNotificationModel.update({ read: true }, query)
469 }
470
ea3674d0
C
471 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
472 const id = parseInt(options.id + '', 10)
473
474 function buildAccountWhereQuery (base: string) {
475 const whereSuffix = options.forUserId
476 ? ` AND "userNotification"."userId" = ${options.forUserId}`
477 : ''
478
479 if (options.type === 'account') {
480 return base +
481 ` WHERE "account"."id" = ${id} ${whereSuffix}`
482 }
483
484 return base +
485 ` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
486 }
487
488 const queries = [
489 buildAccountWhereQuery(
490 `SELECT "userNotification"."id" FROM "userNotification" ` +
491 `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` +
492 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
493 ),
494
495 // Remove notifications from muted accounts that followed ours
496 buildAccountWhereQuery(
497 `SELECT "userNotification"."id" FROM "userNotification" ` +
498 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
499 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
500 `INNER JOIN account ON account."actorId" = actor.id `
501 ),
502
503 // Remove notifications from muted accounts that commented something
504 buildAccountWhereQuery(
505 `SELECT "userNotification"."id" FROM "userNotification" ` +
506 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
507 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
508 `INNER JOIN account ON account."actorId" = actor.id `
509 ),
510
511 buildAccountWhereQuery(
512 `SELECT "userNotification"."id" FROM "userNotification" ` +
513 `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` +
514 `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
515 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
516 )
517 ]
518
519 const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
520
521 return UserNotificationModel.sequelize.query(query)
522 }
523
453e83ea 524 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
457bb213 525 const video = this.Video
a1587156 526 ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) })
457bb213 527 : undefined
dc133480 528
ba5a8d89
C
529 const videoImport = this.VideoImport
530 ? {
531 id: this.VideoImport.id,
532 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
533 torrentName: this.VideoImport.torrentName,
534 magnetUri: this.VideoImport.magnetUri,
535 targetUrl: this.VideoImport.targetUrl
536 }
537 : undefined
538
539 const comment = this.Comment
540 ? {
541 id: this.Comment.id,
542 threadId: this.Comment.getThreadId(),
543 account: this.formatActor(this.Comment.Account),
544 video: this.formatVideo(this.Comment.Video)
545 }
546 : undefined
cef534ed 547
d95d1559 548 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
cef534ed 549
ba5a8d89
C
550 const videoBlacklist = this.VideoBlacklist
551 ? {
552 id: this.VideoBlacklist.id,
553 video: this.formatVideo(this.VideoBlacklist.Video)
554 }
555 : undefined
cef534ed 556
457bb213 557 const account = this.Account ? this.formatActor(this.Account) : undefined
f7cc67b4 558
8424c402
C
559 const actorFollowingType = {
560 Application: 'instance' as 'instance',
561 Group: 'channel' as 'channel',
562 Person: 'account' as 'account'
563 }
ba5a8d89
C
564 const actorFollow = this.ActorFollow
565 ? {
566 id: this.ActorFollow.id,
567 state: this.ActorFollow.state,
568 follower: {
569 id: this.ActorFollow.ActorFollower.Account.id,
570 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
571 name: this.ActorFollow.ActorFollower.preferredUsername,
572 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
573 host: this.ActorFollow.ActorFollower.getHost()
574 },
575 following: {
576 type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
577 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
578 name: this.ActorFollow.ActorFollowing.preferredUsername,
579 host: this.ActorFollow.ActorFollowing.getHost()
580 }
f7cc67b4 581 }
ba5a8d89 582 : undefined
f7cc67b4 583
32a18cbf
C
584 const plugin = this.Plugin
585 ? {
586 name: this.Plugin.name,
587 type: this.Plugin.type,
588 latestVersion: this.Plugin.latestVersion
589 }
590 : undefined
591
592 const peertube = this.Application
593 ? { latestVersion: this.Application.latestPeerTubeVersion }
594 : undefined
595
cef534ed
C
596 return {
597 id: this.id,
598 type: this.type,
599 read: this.read,
600 video,
dc133480 601 videoImport,
cef534ed 602 comment,
d95d1559 603 abuse,
cef534ed 604 videoBlacklist,
f7cc67b4
C
605 account,
606 actorFollow,
32a18cbf
C
607 plugin,
608 peertube,
cef534ed
C
609 createdAt: this.createdAt.toISOString(),
610 updatedAt: this.updatedAt.toISOString()
611 }
612 }
dc133480 613
453e83ea 614 formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
dc133480
C
615 return {
616 id: video.id,
617 uuid: video.uuid,
618 name: video.name
619 }
620 }
457bb213 621
d95d1559 622 formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
ba5a8d89
C
623 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
624 ? {
625 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
626
32a18cbf
C
627 video: abuse.VideoCommentAbuse.VideoComment.Video
628 ? {
629 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
630 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
631 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
632 }
633 : undefined
d95d1559 634 }
ba5a8d89 635 : undefined
d95d1559
C
636
637 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
638
32a18cbf 639 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
d95d1559
C
640
641 return {
642 id: abuse.id,
594d3e48 643 state: abuse.state,
d95d1559
C
644 video: videoAbuse,
645 comment: commentAbuse,
646 account: accountAbuse
647 }
648 }
649
453e83ea
C
650 formatActor (
651 this: UserNotificationModelForApi,
652 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
653 ) {
457bb213 654 const avatar = accountOrChannel.Actor.Avatar
557b13ae 655 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
457bb213
C
656 : undefined
657
658 return {
659 id: accountOrChannel.id,
660 displayName: accountOrChannel.getDisplayName(),
661 name: accountOrChannel.Actor.preferredUsername,
38967f7b 662 host: accountOrChannel.Actor.getHost(),
457bb213
C
663 avatar
664 }
665 }
cef534ed 666}