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