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