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