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