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