]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/user/user-notification.ts
Don't inject untrusted input
[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'
d0800f76 16import { throwIfNotValid } from '../utils'
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'
cef534ed 23
cef534ed
C
24@Table({
25 tableName: 'userNotification',
26 indexes: [
27 {
457bb213 28 fields: [ 'userId' ]
cef534ed
C
29 },
30 {
457bb213
C
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 {
d95d1559 47 fields: [ 'abuseId' ],
457bb213 48 where: {
d95d1559 49 abuseId: {
457bb213
C
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 }
32a18cbf
C
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 }
cef534ed 101 }
3acc5084 102 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
cef534ed 103})
16c016e8 104export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
cef534ed
C
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 })
d0800f76 158 VideoComment: VideoCommentModel
cef534ed 159
d95d1559 160 @ForeignKey(() => AbuseModel)
cef534ed 161 @Column
d95d1559 162 abuseId: number
cef534ed 163
d95d1559 164 @BelongsTo(() => AbuseModel, {
cef534ed
C
165 foreignKey: {
166 allowNull: true
167 },
168 onDelete: 'cascade'
169 })
d95d1559 170 Abuse: AbuseModel
cef534ed
C
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
dc133480
C
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
f7cc67b4
C
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
32a18cbf
C
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
dc133480 244 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
119b16e5
C
245 const where = { userId }
246
d0800f76 247 const query = {
248 userId,
249 unread,
cef534ed
C
250 offset: start,
251 limit: count,
d0800f76 252 sort,
156c44c8 253 where
cef534ed
C
254 }
255
dc133480
C
256 if (unread !== undefined) query.where['read'] = !unread
257
119b16e5
C
258 return Promise.all([
259 UserNotificationModel.count({ where })
260 .then(count => count || 0),
261
262 count === 0
d0800f76 263 ? [] as UserNotificationModelForApi[]
156c44c8 264 : new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
119b16e5 265 ]).then(([ total, data ]) => ({ total, data }))
cef534ed
C
266 }
267
268 static markAsRead (userId: number, notificationIds: number[]) {
269 const query = {
270 where: {
271 userId,
272 id: {
0374b6b5 273 [Op.in]: notificationIds
cef534ed
C
274 }
275 }
276 }
277
278 return UserNotificationModel.update({ read: true }, query)
279 }
280
2f1548fd
C
281 static markAllAsRead (userId: number) {
282 const query = { where: { userId } }
283
284 return UserNotificationModel.update({ read: true }, query)
285 }
286
ea3674d0 287 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
4638cd71 288 const id = forceNumber(options.id)
ea3674d0
C
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
453e83ea 340 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
457bb213 341 const video = this.Video
d0800f76 342 ? {
343 ...this.formatVideo(this.Video),
344
345 channel: this.formatActor(this.Video.VideoChannel)
346 }
457bb213 347 : undefined
dc133480 348
ba5a8d89
C
349 const videoImport = this.VideoImport
350 ? {
351 id: this.VideoImport.id,
d0800f76 352 video: this.VideoImport.Video
353 ? this.formatVideo(this.VideoImport.Video)
354 : undefined,
ba5a8d89
C
355 torrentName: this.VideoImport.torrentName,
356 magnetUri: this.VideoImport.magnetUri,
357 targetUrl: this.VideoImport.targetUrl
358 }
359 : undefined
360
d0800f76 361 const comment = this.VideoComment
ba5a8d89 362 ? {
d0800f76 363 id: this.VideoComment.id,
364 threadId: this.VideoComment.getThreadId(),
365 account: this.formatActor(this.VideoComment.Account),
366 video: this.formatVideo(this.VideoComment.Video)
ba5a8d89
C
367 }
368 : undefined
cef534ed 369
d95d1559 370 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
cef534ed 371
ba5a8d89
C
372 const videoBlacklist = this.VideoBlacklist
373 ? {
374 id: this.VideoBlacklist.id,
375 video: this.formatVideo(this.VideoBlacklist.Video)
376 }
377 : undefined
cef534ed 378
457bb213 379 const account = this.Account ? this.formatActor(this.Account) : undefined
f7cc67b4 380
8424c402
C
381 const actorFollowingType = {
382 Application: 'instance' as 'instance',
383 Group: 'channel' as 'channel',
384 Person: 'account' as 'account'
385 }
ba5a8d89
C
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,
d0800f76 394 host: this.ActorFollow.ActorFollower.getHost(),
395
396 ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
ba5a8d89
C
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 }
f7cc67b4 404 }
ba5a8d89 405 : undefined
f7cc67b4 406
32a18cbf
C
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
cef534ed
C
419 return {
420 id: this.id,
421 type: this.type,
422 read: this.read,
423 video,
dc133480 424 videoImport,
cef534ed 425 comment,
d95d1559 426 abuse,
cef534ed 427 videoBlacklist,
f7cc67b4
C
428 account,
429 actorFollow,
32a18cbf
C
430 plugin,
431 peertube,
cef534ed
C
432 createdAt: this.createdAt.toISOString(),
433 updatedAt: this.updatedAt.toISOString()
434 }
435 }
dc133480 436
d0800f76 437 formatVideo (video: UserNotificationIncludes.VideoInclude) {
dc133480
C
438 return {
439 id: video.id,
440 uuid: video.uuid,
29837f88 441 shortUUID: uuidToShort(video.uuid),
dc133480
C
442 name: video.name
443 }
444 }
457bb213 445
d0800f76 446 formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
ba5a8d89
C
447 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
448 ? {
449 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
450
32a18cbf
C
451 video: abuse.VideoCommentAbuse.VideoComment.Video
452 ? {
453 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
454 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
29837f88 455 shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
32a18cbf
C
456 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
457 }
458 : undefined
d95d1559 459 }
ba5a8d89 460 : undefined
d95d1559 461
d0800f76 462 const videoAbuse = abuse.VideoAbuse?.Video
463 ? this.formatVideo(abuse.VideoAbuse.Video)
464 : undefined
d95d1559 465
d0800f76 466 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
467 ? this.formatActor(abuse.FlaggedAccount)
468 : undefined
d95d1559
C
469
470 return {
471 id: abuse.id,
594d3e48 472 state: abuse.state,
d95d1559
C
473 video: videoAbuse,
474 comment: commentAbuse,
475 account: accountAbuse
476 }
477 }
478
453e83ea 479 formatActor (
453e83ea
C
480 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
481 ) {
457bb213
C
482 return {
483 id: accountOrChannel.id,
484 displayName: accountOrChannel.getDisplayName(),
485 name: accountOrChannel.Actor.preferredUsername,
38967f7b 486 host: accountOrChannel.Actor.getHost(),
d0800f76 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
457bb213
C
506 }
507 }
cef534ed 508}