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