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