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