]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/user/user-notification.ts
Merge branch 'release/5.0.0' into develop
[github/Chocobozzz/PeerTube.git] / server / models / user / user-notification.ts
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'
24
25 @Table({
26 tableName: 'userNotification',
27 indexes: [
28 {
29 fields: [ 'userId' ]
30 },
31 {
32 fields: [ 'videoId' ],
33 where: {
34 videoId: {
35 [Op.ne]: null
36 }
37 }
38 },
39 {
40 fields: [ 'commentId' ],
41 where: {
42 commentId: {
43 [Op.ne]: null
44 }
45 }
46 },
47 {
48 fields: [ 'abuseId' ],
49 where: {
50 abuseId: {
51 [Op.ne]: null
52 }
53 }
54 },
55 {
56 fields: [ 'videoBlacklistId' ],
57 where: {
58 videoBlacklistId: {
59 [Op.ne]: null
60 }
61 }
62 },
63 {
64 fields: [ 'videoImportId' ],
65 where: {
66 videoImportId: {
67 [Op.ne]: null
68 }
69 }
70 },
71 {
72 fields: [ 'accountId' ],
73 where: {
74 accountId: {
75 [Op.ne]: null
76 }
77 }
78 },
79 {
80 fields: [ 'actorFollowId' ],
81 where: {
82 actorFollowId: {
83 [Op.ne]: null
84 }
85 }
86 },
87 {
88 fields: [ 'pluginId' ],
89 where: {
90 pluginId: {
91 [Op.ne]: null
92 }
93 }
94 },
95 {
96 fields: [ 'applicationId' ],
97 where: {
98 applicationId: {
99 [Op.ne]: null
100 }
101 }
102 },
103 {
104 fields: [ 'userRegistrationId' ],
105 where: {
106 userRegistrationId: {
107 [Op.ne]: null
108 }
109 }
110 }
111 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
112 })
113 export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
114
115 @AllowNull(false)
116 @Default(null)
117 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
118 @Column
119 type: UserNotificationType
120
121 @AllowNull(false)
122 @Default(false)
123 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
124 @Column
125 read: boolean
126
127 @CreatedAt
128 createdAt: Date
129
130 @UpdatedAt
131 updatedAt: Date
132
133 @ForeignKey(() => UserModel)
134 @Column
135 userId: number
136
137 @BelongsTo(() => UserModel, {
138 foreignKey: {
139 allowNull: false
140 },
141 onDelete: 'cascade'
142 })
143 User: UserModel
144
145 @ForeignKey(() => VideoModel)
146 @Column
147 videoId: number
148
149 @BelongsTo(() => VideoModel, {
150 foreignKey: {
151 allowNull: true
152 },
153 onDelete: 'cascade'
154 })
155 Video: VideoModel
156
157 @ForeignKey(() => VideoCommentModel)
158 @Column
159 commentId: number
160
161 @BelongsTo(() => VideoCommentModel, {
162 foreignKey: {
163 allowNull: true
164 },
165 onDelete: 'cascade'
166 })
167 VideoComment: VideoCommentModel
168
169 @ForeignKey(() => AbuseModel)
170 @Column
171 abuseId: number
172
173 @BelongsTo(() => AbuseModel, {
174 foreignKey: {
175 allowNull: true
176 },
177 onDelete: 'cascade'
178 })
179 Abuse: AbuseModel
180
181 @ForeignKey(() => VideoBlacklistModel)
182 @Column
183 videoBlacklistId: number
184
185 @BelongsTo(() => VideoBlacklistModel, {
186 foreignKey: {
187 allowNull: true
188 },
189 onDelete: 'cascade'
190 })
191 VideoBlacklist: VideoBlacklistModel
192
193 @ForeignKey(() => VideoImportModel)
194 @Column
195 videoImportId: number
196
197 @BelongsTo(() => VideoImportModel, {
198 foreignKey: {
199 allowNull: true
200 },
201 onDelete: 'cascade'
202 })
203 VideoImport: VideoImportModel
204
205 @ForeignKey(() => AccountModel)
206 @Column
207 accountId: number
208
209 @BelongsTo(() => AccountModel, {
210 foreignKey: {
211 allowNull: true
212 },
213 onDelete: 'cascade'
214 })
215 Account: AccountModel
216
217 @ForeignKey(() => ActorFollowModel)
218 @Column
219 actorFollowId: number
220
221 @BelongsTo(() => ActorFollowModel, {
222 foreignKey: {
223 allowNull: true
224 },
225 onDelete: 'cascade'
226 })
227 ActorFollow: ActorFollowModel
228
229 @ForeignKey(() => PluginModel)
230 @Column
231 pluginId: number
232
233 @BelongsTo(() => PluginModel, {
234 foreignKey: {
235 allowNull: true
236 },
237 onDelete: 'cascade'
238 })
239 Plugin: PluginModel
240
241 @ForeignKey(() => ApplicationModel)
242 @Column
243 applicationId: number
244
245 @BelongsTo(() => ApplicationModel, {
246 foreignKey: {
247 allowNull: true
248 },
249 onDelete: 'cascade'
250 })
251 Application: ApplicationModel
252
253 @ForeignKey(() => UserRegistrationModel)
254 @Column
255 userRegistrationId: number
256
257 @BelongsTo(() => UserRegistrationModel, {
258 foreignKey: {
259 allowNull: true
260 },
261 onDelete: 'cascade'
262 })
263 UserRegistration: UserRegistrationModel
264
265 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
266 const where = { userId }
267
268 const query = {
269 userId,
270 unread,
271 offset: start,
272 limit: count,
273 sort,
274 where
275 }
276
277 if (unread !== undefined) query.where['read'] = !unread
278
279 return Promise.all([
280 UserNotificationModel.count({ where })
281 .then(count => count || 0),
282
283 count === 0
284 ? [] as UserNotificationModelForApi[]
285 : new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
286 ]).then(([ total, data ]) => ({ total, data }))
287 }
288
289 static markAsRead (userId: number, notificationIds: number[]) {
290 const query = {
291 where: {
292 userId,
293 id: {
294 [Op.in]: notificationIds
295 }
296 }
297 }
298
299 return UserNotificationModel.update({ read: true }, query)
300 }
301
302 static markAllAsRead (userId: number) {
303 const query = { where: { userId } }
304
305 return UserNotificationModel.update({ read: true }, query)
306 }
307
308 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
309 const id = forceNumber(options.id)
310
311 function buildAccountWhereQuery (base: string) {
312 const whereSuffix = options.forUserId
313 ? ` AND "userNotification"."userId" = ${options.forUserId}`
314 : ''
315
316 if (options.type === 'account') {
317 return base +
318 ` WHERE "account"."id" = ${id} ${whereSuffix}`
319 }
320
321 return base +
322 ` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
323 }
324
325 const queries = [
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" `
330 ),
331
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 `
338 ),
339
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 `
346 ),
347
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" `
353 )
354 ]
355
356 const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
357
358 return UserNotificationModel.sequelize.query(query)
359 }
360
361 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
362 const video = this.Video
363 ? {
364 ...this.formatVideo(this.Video),
365
366 channel: this.formatActor(this.Video.VideoChannel)
367 }
368 : undefined
369
370 const videoImport = this.VideoImport
371 ? {
372 id: this.VideoImport.id,
373 video: this.VideoImport.Video
374 ? this.formatVideo(this.VideoImport.Video)
375 : undefined,
376 torrentName: this.VideoImport.torrentName,
377 magnetUri: this.VideoImport.magnetUri,
378 targetUrl: this.VideoImport.targetUrl
379 }
380 : undefined
381
382 const comment = this.VideoComment
383 ? {
384 id: this.VideoComment.id,
385 threadId: this.VideoComment.getThreadId(),
386 account: this.formatActor(this.VideoComment.Account),
387 video: this.formatVideo(this.VideoComment.Video)
388 }
389 : undefined
390
391 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
392
393 const videoBlacklist = this.VideoBlacklist
394 ? {
395 id: this.VideoBlacklist.id,
396 video: this.formatVideo(this.VideoBlacklist.Video)
397 }
398 : undefined
399
400 const account = this.Account ? this.formatActor(this.Account) : undefined
401
402 const actorFollowingType = {
403 Application: 'instance' as 'instance',
404 Group: 'channel' as 'channel',
405 Person: 'account' as 'account'
406 }
407 const actorFollow = this.ActorFollow
408 ? {
409 id: this.ActorFollow.id,
410 state: this.ActorFollow.state,
411 follower: {
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(),
416
417 ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
418 },
419 following: {
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()
424 }
425 }
426 : undefined
427
428 const plugin = this.Plugin
429 ? {
430 name: this.Plugin.name,
431 type: this.Plugin.type,
432 latestVersion: this.Plugin.latestVersion
433 }
434 : undefined
435
436 const peertube = this.Application
437 ? { latestVersion: this.Application.latestPeerTubeVersion }
438 : undefined
439
440 const registration = this.UserRegistration
441 ? { id: this.UserRegistration.id, username: this.UserRegistration.username }
442 : undefined
443
444 return {
445 id: this.id,
446 type: this.type,
447 read: this.read,
448 video,
449 videoImport,
450 comment,
451 abuse,
452 videoBlacklist,
453 account,
454 actorFollow,
455 plugin,
456 peertube,
457 registration,
458 createdAt: this.createdAt.toISOString(),
459 updatedAt: this.updatedAt.toISOString()
460 }
461 }
462
463 formatVideo (video: UserNotificationIncludes.VideoInclude) {
464 return {
465 id: video.id,
466 uuid: video.uuid,
467 shortUUID: uuidToShort(video.uuid),
468 name: video.name
469 }
470 }
471
472 formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
473 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
474 ? {
475 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
476
477 video: abuse.VideoCommentAbuse.VideoComment.Video
478 ? {
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
483 }
484 : undefined
485 }
486 : undefined
487
488 const videoAbuse = abuse.VideoAbuse?.Video
489 ? this.formatVideo(abuse.VideoAbuse.Video)
490 : undefined
491
492 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
493 ? this.formatActor(abuse.FlaggedAccount)
494 : undefined
495
496 return {
497 id: abuse.id,
498 state: abuse.state,
499 video: videoAbuse,
500 comment: commentAbuse,
501 account: accountAbuse
502 }
503 }
504
505 formatActor (
506 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
507 ) {
508 return {
509 id: accountOrChannel.id,
510 displayName: accountOrChannel.getDisplayName(),
511 name: accountOrChannel.Actor.preferredUsername,
512 host: accountOrChannel.Actor.getHost(),
513
514 ...this.formatAvatars(accountOrChannel.Actor.Avatars)
515 }
516 }
517
518 formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) {
519 if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] }
520
521 return {
522 avatar: this.formatAvatar(getBiggestActorImage(avatars)),
523
524 avatars: avatars.map(a => this.formatAvatar(a))
525 }
526 }
527
528 formatAvatar (a: UserNotificationIncludes.ActorImageInclude) {
529 return {
530 path: a.getStaticPath(),
531 width: a.width
532 }
533 }
534 }