]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/user/user-notification.ts
Merge remote-tracking branch 'weblate/develop' 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 { uuidToShort } from '@shared/extra-utils'
6 import { UserNotification, UserNotificationType } from '@shared/models'
7 import { AttributesOnly } from '@shared/typescript-utils'
8 import { isBooleanValid } from '../../helpers/custom-validators/misc'
9 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
10 import { AbuseModel } from '../abuse/abuse'
11 import { AccountModel } from '../account/account'
12 import { ActorFollowModel } from '../actor/actor-follow'
13 import { ApplicationModel } from '../application/application'
14 import { PluginModel } from '../server/plugin'
15 import { throwIfNotValid } from '../utils'
16 import { VideoModel } from '../video/video'
17 import { VideoBlacklistModel } from '../video/video-blacklist'
18 import { VideoCommentModel } from '../video/video-comment'
19 import { VideoImportModel } from '../video/video-import'
20 import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
21 import { UserModel } from './user'
22
23 @Table({
24 tableName: 'userNotification',
25 indexes: [
26 {
27 fields: [ 'userId' ]
28 },
29 {
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 {
46 fields: [ 'abuseId' ],
47 where: {
48 abuseId: {
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 }
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 }
100 }
101 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
102 })
103 export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
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 })
157 VideoComment: VideoCommentModel
158
159 @ForeignKey(() => AbuseModel)
160 @Column
161 abuseId: number
162
163 @BelongsTo(() => AbuseModel, {
164 foreignKey: {
165 allowNull: true
166 },
167 onDelete: 'cascade'
168 })
169 Abuse: AbuseModel
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
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
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
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
243 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
244 const where = { userId }
245
246 const query = {
247 userId,
248 unread,
249 offset: start,
250 limit: count,
251 sort,
252 where
253 }
254
255 if (unread !== undefined) query.where['read'] = !unread
256
257 return Promise.all([
258 UserNotificationModel.count({ where })
259 .then(count => count || 0),
260
261 count === 0
262 ? [] as UserNotificationModelForApi[]
263 : new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
264 ]).then(([ total, data ]) => ({ total, data }))
265 }
266
267 static markAsRead (userId: number, notificationIds: number[]) {
268 const query = {
269 where: {
270 userId,
271 id: {
272 [Op.in]: notificationIds
273 }
274 }
275 }
276
277 return UserNotificationModel.update({ read: true }, query)
278 }
279
280 static markAllAsRead (userId: number) {
281 const query = { where: { userId } }
282
283 return UserNotificationModel.update({ read: true }, query)
284 }
285
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
339 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
340 const video = this.Video
341 ? {
342 ...this.formatVideo(this.Video),
343
344 channel: this.formatActor(this.Video.VideoChannel)
345 }
346 : undefined
347
348 const videoImport = this.VideoImport
349 ? {
350 id: this.VideoImport.id,
351 video: this.VideoImport.Video
352 ? this.formatVideo(this.VideoImport.Video)
353 : undefined,
354 torrentName: this.VideoImport.torrentName,
355 magnetUri: this.VideoImport.magnetUri,
356 targetUrl: this.VideoImport.targetUrl
357 }
358 : undefined
359
360 const comment = this.VideoComment
361 ? {
362 id: this.VideoComment.id,
363 threadId: this.VideoComment.getThreadId(),
364 account: this.formatActor(this.VideoComment.Account),
365 video: this.formatVideo(this.VideoComment.Video)
366 }
367 : undefined
368
369 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
370
371 const videoBlacklist = this.VideoBlacklist
372 ? {
373 id: this.VideoBlacklist.id,
374 video: this.formatVideo(this.VideoBlacklist.Video)
375 }
376 : undefined
377
378 const account = this.Account ? this.formatActor(this.Account) : undefined
379
380 const actorFollowingType = {
381 Application: 'instance' as 'instance',
382 Group: 'channel' as 'channel',
383 Person: 'account' as 'account'
384 }
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,
393 host: this.ActorFollow.ActorFollower.getHost(),
394
395 ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
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 }
403 }
404 : undefined
405
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
418 return {
419 id: this.id,
420 type: this.type,
421 read: this.read,
422 video,
423 videoImport,
424 comment,
425 abuse,
426 videoBlacklist,
427 account,
428 actorFollow,
429 plugin,
430 peertube,
431 createdAt: this.createdAt.toISOString(),
432 updatedAt: this.updatedAt.toISOString()
433 }
434 }
435
436 formatVideo (video: UserNotificationIncludes.VideoInclude) {
437 return {
438 id: video.id,
439 uuid: video.uuid,
440 shortUUID: uuidToShort(video.uuid),
441 name: video.name
442 }
443 }
444
445 formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
446 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
447 ? {
448 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
449
450 video: abuse.VideoCommentAbuse.VideoComment.Video
451 ? {
452 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
453 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
454 shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
455 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
456 }
457 : undefined
458 }
459 : undefined
460
461 const videoAbuse = abuse.VideoAbuse?.Video
462 ? this.formatVideo(abuse.VideoAbuse.Video)
463 : undefined
464
465 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
466 ? this.formatActor(abuse.FlaggedAccount)
467 : undefined
468
469 return {
470 id: abuse.id,
471 state: abuse.state,
472 video: videoAbuse,
473 comment: commentAbuse,
474 account: accountAbuse
475 }
476 }
477
478 formatActor (
479 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
480 ) {
481 return {
482 id: accountOrChannel.id,
483 displayName: accountOrChannel.getDisplayName(),
484 name: accountOrChannel.Actor.preferredUsername,
485 host: accountOrChannel.Actor.getHost(),
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
505 }
506 }
507 }