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