]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/account/user-notification.ts
Cleaner warning of IP address leaking on embedded videos (#2034)
[github/Chocobozzz/PeerTube.git] / server / models / account / user-notification.ts
1 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2 import { UserNotification, UserNotificationType } from '../../../shared'
3 import { getSort, throwIfNotValid } from '../utils'
4 import { isBooleanValid } from '../../helpers/custom-validators/misc'
5 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
6 import { UserModel } from './user'
7 import { VideoModel } from '../video/video'
8 import { VideoCommentModel } from '../video/video-comment'
9 import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10 import { VideoChannelModel } from '../video/video-channel'
11 import { AccountModel } from './account'
12 import { VideoAbuseModel } from '../video/video-abuse'
13 import { VideoBlacklistModel } from '../video/video-blacklist'
14 import { VideoImportModel } from '../video/video-import'
15 import { ActorModel } from '../activitypub/actor'
16 import { ActorFollowModel } from '../activitypub/actor-follow'
17 import { AvatarModel } from '../avatar/avatar'
18 import { ServerModel } from '../server/server'
19
20 enum ScopeNames {
21 WITH_ALL = 'WITH_ALL'
22 }
23
24 function buildActorWithAvatarInclude () {
25 return {
26 attributes: [ 'preferredUsername' ],
27 model: ActorModel.unscoped(),
28 required: true,
29 include: [
30 {
31 attributes: [ 'filename' ],
32 model: AvatarModel.unscoped(),
33 required: false
34 },
35 {
36 attributes: [ 'host' ],
37 model: ServerModel.unscoped(),
38 required: false
39 }
40 ]
41 }
42 }
43
44 function buildVideoInclude (required: boolean) {
45 return {
46 attributes: [ 'id', 'uuid', 'name' ],
47 model: VideoModel.unscoped(),
48 required
49 }
50 }
51
52 function buildChannelInclude (required: boolean, withActor = false) {
53 return {
54 required,
55 attributes: [ 'id', 'name' ],
56 model: VideoChannelModel.unscoped(),
57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
58 }
59 }
60
61 function buildAccountInclude (required: boolean, withActor = false) {
62 return {
63 required,
64 attributes: [ 'id', 'name' ],
65 model: AccountModel.unscoped(),
66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
67 }
68 }
69
70 @Scopes(() => ({
71 [ScopeNames.WITH_ALL]: {
72 include: [
73 Object.assign(buildVideoInclude(false), {
74 include: [ buildChannelInclude(true, true) ]
75 }),
76
77 {
78 attributes: [ 'id', 'originCommentId' ],
79 model: VideoCommentModel.unscoped(),
80 required: false,
81 include: [
82 buildAccountInclude(true, true),
83 buildVideoInclude(true)
84 ]
85 },
86
87 {
88 attributes: [ 'id' ],
89 model: VideoAbuseModel.unscoped(),
90 required: false,
91 include: [ buildVideoInclude(true) ]
92 },
93
94 {
95 attributes: [ 'id' ],
96 model: VideoBlacklistModel.unscoped(),
97 required: false,
98 include: [ buildVideoInclude(true) ]
99 },
100
101 {
102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
103 model: VideoImportModel.unscoped(),
104 required: false,
105 include: [ buildVideoInclude(false) ]
106 },
107
108 {
109 attributes: [ 'id', 'state' ],
110 model: ActorFollowModel.unscoped(),
111 required: false,
112 include: [
113 {
114 attributes: [ 'preferredUsername' ],
115 model: ActorModel.unscoped(),
116 required: true,
117 as: 'ActorFollower',
118 include: [
119 {
120 attributes: [ 'id', 'name' ],
121 model: AccountModel.unscoped(),
122 required: true
123 },
124 {
125 attributes: [ 'filename' ],
126 model: AvatarModel.unscoped(),
127 required: false
128 },
129 {
130 attributes: [ 'host' ],
131 model: ServerModel.unscoped(),
132 required: false
133 }
134 ]
135 },
136 {
137 attributes: [ 'preferredUsername' ],
138 model: ActorModel.unscoped(),
139 required: true,
140 as: 'ActorFollowing',
141 include: [
142 buildChannelInclude(false),
143 buildAccountInclude(false)
144 ]
145 }
146 ]
147 },
148
149 buildAccountInclude(false, true)
150 ]
151 }
152 }))
153 @Table({
154 tableName: 'userNotification',
155 indexes: [
156 {
157 fields: [ 'userId' ]
158 },
159 {
160 fields: [ 'videoId' ],
161 where: {
162 videoId: {
163 [Op.ne]: null
164 }
165 }
166 },
167 {
168 fields: [ 'commentId' ],
169 where: {
170 commentId: {
171 [Op.ne]: null
172 }
173 }
174 },
175 {
176 fields: [ 'videoAbuseId' ],
177 where: {
178 videoAbuseId: {
179 [Op.ne]: null
180 }
181 }
182 },
183 {
184 fields: [ 'videoBlacklistId' ],
185 where: {
186 videoBlacklistId: {
187 [Op.ne]: null
188 }
189 }
190 },
191 {
192 fields: [ 'videoImportId' ],
193 where: {
194 videoImportId: {
195 [Op.ne]: null
196 }
197 }
198 },
199 {
200 fields: [ 'accountId' ],
201 where: {
202 accountId: {
203 [Op.ne]: null
204 }
205 }
206 },
207 {
208 fields: [ 'actorFollowId' ],
209 where: {
210 actorFollowId: {
211 [Op.ne]: null
212 }
213 }
214 }
215 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
216 })
217 export class UserNotificationModel extends Model<UserNotificationModel> {
218
219 @AllowNull(false)
220 @Default(null)
221 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
222 @Column
223 type: UserNotificationType
224
225 @AllowNull(false)
226 @Default(false)
227 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
228 @Column
229 read: boolean
230
231 @CreatedAt
232 createdAt: Date
233
234 @UpdatedAt
235 updatedAt: Date
236
237 @ForeignKey(() => UserModel)
238 @Column
239 userId: number
240
241 @BelongsTo(() => UserModel, {
242 foreignKey: {
243 allowNull: false
244 },
245 onDelete: 'cascade'
246 })
247 User: UserModel
248
249 @ForeignKey(() => VideoModel)
250 @Column
251 videoId: number
252
253 @BelongsTo(() => VideoModel, {
254 foreignKey: {
255 allowNull: true
256 },
257 onDelete: 'cascade'
258 })
259 Video: VideoModel
260
261 @ForeignKey(() => VideoCommentModel)
262 @Column
263 commentId: number
264
265 @BelongsTo(() => VideoCommentModel, {
266 foreignKey: {
267 allowNull: true
268 },
269 onDelete: 'cascade'
270 })
271 Comment: VideoCommentModel
272
273 @ForeignKey(() => VideoAbuseModel)
274 @Column
275 videoAbuseId: number
276
277 @BelongsTo(() => VideoAbuseModel, {
278 foreignKey: {
279 allowNull: true
280 },
281 onDelete: 'cascade'
282 })
283 VideoAbuse: VideoAbuseModel
284
285 @ForeignKey(() => VideoBlacklistModel)
286 @Column
287 videoBlacklistId: number
288
289 @BelongsTo(() => VideoBlacklistModel, {
290 foreignKey: {
291 allowNull: true
292 },
293 onDelete: 'cascade'
294 })
295 VideoBlacklist: VideoBlacklistModel
296
297 @ForeignKey(() => VideoImportModel)
298 @Column
299 videoImportId: number
300
301 @BelongsTo(() => VideoImportModel, {
302 foreignKey: {
303 allowNull: true
304 },
305 onDelete: 'cascade'
306 })
307 VideoImport: VideoImportModel
308
309 @ForeignKey(() => AccountModel)
310 @Column
311 accountId: number
312
313 @BelongsTo(() => AccountModel, {
314 foreignKey: {
315 allowNull: true
316 },
317 onDelete: 'cascade'
318 })
319 Account: AccountModel
320
321 @ForeignKey(() => ActorFollowModel)
322 @Column
323 actorFollowId: number
324
325 @BelongsTo(() => ActorFollowModel, {
326 foreignKey: {
327 allowNull: true
328 },
329 onDelete: 'cascade'
330 })
331 ActorFollow: ActorFollowModel
332
333 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
334 const query: FindOptions = {
335 offset: start,
336 limit: count,
337 order: getSort(sort),
338 where: {
339 userId
340 }
341 }
342
343 if (unread !== undefined) query.where['read'] = !unread
344
345 return UserNotificationModel.scope(ScopeNames.WITH_ALL)
346 .findAndCountAll(query)
347 .then(({ rows, count }) => {
348 return {
349 data: rows,
350 total: count
351 }
352 })
353 }
354
355 static markAsRead (userId: number, notificationIds: number[]) {
356 const query = {
357 where: {
358 userId,
359 id: {
360 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
361 }
362 }
363 }
364
365 return UserNotificationModel.update({ read: true }, query)
366 }
367
368 static markAllAsRead (userId: number) {
369 const query = { where: { userId } }
370
371 return UserNotificationModel.update({ read: true }, query)
372 }
373
374 toFormattedJSON (): UserNotification {
375 const video = this.Video
376 ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
377 : undefined
378
379 const videoImport = this.VideoImport ? {
380 id: this.VideoImport.id,
381 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
382 torrentName: this.VideoImport.torrentName,
383 magnetUri: this.VideoImport.magnetUri,
384 targetUrl: this.VideoImport.targetUrl
385 } : undefined
386
387 const comment = this.Comment ? {
388 id: this.Comment.id,
389 threadId: this.Comment.getThreadId(),
390 account: this.formatActor(this.Comment.Account),
391 video: this.formatVideo(this.Comment.Video)
392 } : undefined
393
394 const videoAbuse = this.VideoAbuse ? {
395 id: this.VideoAbuse.id,
396 video: this.formatVideo(this.VideoAbuse.Video)
397 } : undefined
398
399 const videoBlacklist = this.VideoBlacklist ? {
400 id: this.VideoBlacklist.id,
401 video: this.formatVideo(this.VideoBlacklist.Video)
402 } : undefined
403
404 const account = this.Account ? this.formatActor(this.Account) : undefined
405
406 const actorFollow = this.ActorFollow ? {
407 id: this.ActorFollow.id,
408 state: this.ActorFollow.state,
409 follower: {
410 id: this.ActorFollow.ActorFollower.Account.id,
411 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
412 name: this.ActorFollow.ActorFollower.preferredUsername,
413 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
414 host: this.ActorFollow.ActorFollower.getHost()
415 },
416 following: {
417 type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
418 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
419 name: this.ActorFollow.ActorFollowing.preferredUsername
420 }
421 } : undefined
422
423 return {
424 id: this.id,
425 type: this.type,
426 read: this.read,
427 video,
428 videoImport,
429 comment,
430 videoAbuse,
431 videoBlacklist,
432 account,
433 actorFollow,
434 createdAt: this.createdAt.toISOString(),
435 updatedAt: this.updatedAt.toISOString()
436 }
437 }
438
439 private formatVideo (video: VideoModel) {
440 return {
441 id: video.id,
442 uuid: video.uuid,
443 name: video.name
444 }
445 }
446
447 private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
448 const avatar = accountOrChannel.Actor.Avatar
449 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
450 : undefined
451
452 return {
453 id: accountOrChannel.id,
454 displayName: accountOrChannel.getDisplayName(),
455 name: accountOrChannel.Actor.preferredUsername,
456 host: accountOrChannel.Actor.getHost(),
457 avatar
458 }
459 }
460 }