]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/user/user-notification.ts
Don't fail remote transcoding on retry
[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'
4638cd71 5import { forceNumber } from '@shared/core-utils'
0628157f 6import { uuidToShort } from '@shared/extra-utils'
d17c7b4e 7import { UserNotification, UserNotificationType } from '@shared/models'
6b5f72be 8import { AttributesOnly } from '@shared/typescript-utils'
cef534ed
C
9import { isBooleanValid } from '../../helpers/custom-validators/misc'
10import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
d95d1559 11import { AbuseModel } from '../abuse/abuse'
7d9ba5c0 12import { AccountModel } from '../account/account'
7d9ba5c0 13import { ActorFollowModel } from '../actor/actor-follow'
32a18cbf 14import { ApplicationModel } from '../application/application'
32a18cbf 15import { PluginModel } from '../server/plugin'
8c4bbd94 16import { throwIfNotValid } from '../shared'
d95d1559
C
17import { VideoModel } from '../video/video'
18import { VideoBlacklistModel } from '../video/video-blacklist'
d95d1559
C
19import { VideoCommentModel } from '../video/video-comment'
20import { VideoImportModel } from '../video/video-import'
d0800f76 21import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder'
d95d1559 22import { UserModel } from './user'
e364e31e 23import { UserRegistrationModel } from './user-registration'
cef534ed 24
cef534ed
C
25@Table({
26 tableName: 'userNotification',
27 indexes: [
28 {
457bb213 29 fields: [ 'userId' ]
cef534ed
C
30 },
31 {
457bb213
C
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 {
d95d1559 48 fields: [ 'abuseId' ],
457bb213 49 where: {
d95d1559 50 abuseId: {
457bb213
C
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 }
32a18cbf
C
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 }
e364e31e
C
102 },
103 {
104 fields: [ 'userRegistrationId' ],
105 where: {
106 userRegistrationId: {
107 [Op.ne]: null
108 }
109 }
cef534ed 110 }
3acc5084 111 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
cef534ed 112})
16c016e8 113export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
cef534ed
C
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 })
d0800f76 167 VideoComment: VideoCommentModel
cef534ed 168
d95d1559 169 @ForeignKey(() => AbuseModel)
cef534ed 170 @Column
d95d1559 171 abuseId: number
cef534ed 172
d95d1559 173 @BelongsTo(() => AbuseModel, {
cef534ed
C
174 foreignKey: {
175 allowNull: true
176 },
177 onDelete: 'cascade'
178 })
d95d1559 179 Abuse: AbuseModel
cef534ed
C
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
dc133480
C
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
f7cc67b4
C
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
32a18cbf
C
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
e364e31e
C
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
dc133480 265 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
119b16e5
C
266 const where = { userId }
267
d0800f76 268 const query = {
269 userId,
270 unread,
cef534ed
C
271 offset: start,
272 limit: count,
d0800f76 273 sort,
156c44c8 274 where
cef534ed
C
275 }
276
dc133480
C
277 if (unread !== undefined) query.where['read'] = !unread
278
119b16e5
C
279 return Promise.all([
280 UserNotificationModel.count({ where })
281 .then(count => count || 0),
282
283 count === 0
d0800f76 284 ? [] as UserNotificationModelForApi[]
156c44c8 285 : new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
119b16e5 286 ]).then(([ total, data ]) => ({ total, data }))
cef534ed
C
287 }
288
289 static markAsRead (userId: number, notificationIds: number[]) {
290 const query = {
291 where: {
292 userId,
293 id: {
0374b6b5 294 [Op.in]: notificationIds
cef534ed
C
295 }
296 }
297 }
298
299 return UserNotificationModel.update({ read: true }, query)
300 }
301
2f1548fd
C
302 static markAllAsRead (userId: number) {
303 const query = { where: { userId } }
304
305 return UserNotificationModel.update({ read: true }, query)
306 }
307
ea3674d0 308 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
4638cd71 309 const id = forceNumber(options.id)
ea3674d0
C
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
453e83ea 361 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
457bb213 362 const video = this.Video
d0800f76 363 ? {
364 ...this.formatVideo(this.Video),
365
366 channel: this.formatActor(this.Video.VideoChannel)
367 }
457bb213 368 : undefined
dc133480 369
ba5a8d89
C
370 const videoImport = this.VideoImport
371 ? {
372 id: this.VideoImport.id,
d0800f76 373 video: this.VideoImport.Video
374 ? this.formatVideo(this.VideoImport.Video)
375 : undefined,
ba5a8d89
C
376 torrentName: this.VideoImport.torrentName,
377 magnetUri: this.VideoImport.magnetUri,
378 targetUrl: this.VideoImport.targetUrl
379 }
380 : undefined
381
d0800f76 382 const comment = this.VideoComment
ba5a8d89 383 ? {
d0800f76 384 id: this.VideoComment.id,
385 threadId: this.VideoComment.getThreadId(),
386 account: this.formatActor(this.VideoComment.Account),
387 video: this.formatVideo(this.VideoComment.Video)
ba5a8d89
C
388 }
389 : undefined
cef534ed 390
d95d1559 391 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
cef534ed 392
ba5a8d89
C
393 const videoBlacklist = this.VideoBlacklist
394 ? {
395 id: this.VideoBlacklist.id,
396 video: this.formatVideo(this.VideoBlacklist.Video)
397 }
398 : undefined
cef534ed 399
457bb213 400 const account = this.Account ? this.formatActor(this.Account) : undefined
f7cc67b4 401
8424c402
C
402 const actorFollowingType = {
403 Application: 'instance' as 'instance',
404 Group: 'channel' as 'channel',
405 Person: 'account' as 'account'
406 }
ba5a8d89
C
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,
d0800f76 415 host: this.ActorFollow.ActorFollower.getHost(),
416
417 ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
ba5a8d89
C
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 }
f7cc67b4 425 }
ba5a8d89 426 : undefined
f7cc67b4 427
32a18cbf
C
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
e364e31e
C
440 const registration = this.UserRegistration
441 ? { id: this.UserRegistration.id, username: this.UserRegistration.username }
442 : undefined
443
cef534ed
C
444 return {
445 id: this.id,
446 type: this.type,
447 read: this.read,
448 video,
dc133480 449 videoImport,
cef534ed 450 comment,
d95d1559 451 abuse,
cef534ed 452 videoBlacklist,
f7cc67b4
C
453 account,
454 actorFollow,
32a18cbf
C
455 plugin,
456 peertube,
e364e31e 457 registration,
cef534ed
C
458 createdAt: this.createdAt.toISOString(),
459 updatedAt: this.updatedAt.toISOString()
460 }
461 }
dc133480 462
d0800f76 463 formatVideo (video: UserNotificationIncludes.VideoInclude) {
dc133480
C
464 return {
465 id: video.id,
466 uuid: video.uuid,
29837f88 467 shortUUID: uuidToShort(video.uuid),
dc133480
C
468 name: video.name
469 }
470 }
457bb213 471
d0800f76 472 formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
ba5a8d89
C
473 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
474 ? {
475 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
476
32a18cbf
C
477 video: abuse.VideoCommentAbuse.VideoComment.Video
478 ? {
479 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
480 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
29837f88 481 shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
32a18cbf
C
482 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
483 }
484 : undefined
d95d1559 485 }
ba5a8d89 486 : undefined
d95d1559 487
d0800f76 488 const videoAbuse = abuse.VideoAbuse?.Video
489 ? this.formatVideo(abuse.VideoAbuse.Video)
490 : undefined
d95d1559 491
d0800f76 492 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
493 ? this.formatActor(abuse.FlaggedAccount)
494 : undefined
d95d1559
C
495
496 return {
497 id: abuse.id,
594d3e48 498 state: abuse.state,
d95d1559
C
499 video: videoAbuse,
500 comment: commentAbuse,
501 account: accountAbuse
502 }
503 }
504
453e83ea 505 formatActor (
453e83ea
C
506 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
507 ) {
457bb213
C
508 return {
509 id: accountOrChannel.id,
510 displayName: accountOrChannel.getDisplayName(),
511 name: accountOrChannel.Actor.preferredUsername,
38967f7b 512 host: accountOrChannel.Actor.getHost(),
d0800f76 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
457bb213
C
532 }
533 }
cef534ed 534}