08388f2687e7aa537144c7fd39c7a65da161bdc5
[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, Op } 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     ] as any // FIXME: sequelize typings
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 any // FIXME: sequelize typings
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.any]: notificationIds
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.getWebserverPath() } : 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.getWebserverPath() }
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 }