]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/account/user-notification.ts
9b13a83763eb572f688665429c639b1d2003abbb
[github/Chocobozzz/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' ],
139 model: ActorModel.unscoped(),
140 required: true,
141 as: 'ActorFollowing',
142 include: [
143 buildChannelInclude(false),
144 buildAccountInclude(false)
145 ]
146 }
147 ]
148 },
149
150 buildAccountInclude(false, true)
151 ]
152 }
153 }))
154 @Table({
155 tableName: 'userNotification',
156 indexes: [
157 {
158 fields: [ 'userId' ]
159 },
160 {
161 fields: [ 'videoId' ],
162 where: {
163 videoId: {
164 [Op.ne]: null
165 }
166 }
167 },
168 {
169 fields: [ 'commentId' ],
170 where: {
171 commentId: {
172 [Op.ne]: null
173 }
174 }
175 },
176 {
177 fields: [ 'videoAbuseId' ],
178 where: {
179 videoAbuseId: {
180 [Op.ne]: null
181 }
182 }
183 },
184 {
185 fields: [ 'videoBlacklistId' ],
186 where: {
187 videoBlacklistId: {
188 [Op.ne]: null
189 }
190 }
191 },
192 {
193 fields: [ 'videoImportId' ],
194 where: {
195 videoImportId: {
196 [Op.ne]: null
197 }
198 }
199 },
200 {
201 fields: [ 'accountId' ],
202 where: {
203 accountId: {
204 [Op.ne]: null
205 }
206 }
207 },
208 {
209 fields: [ 'actorFollowId' ],
210 where: {
211 actorFollowId: {
212 [Op.ne]: null
213 }
214 }
215 }
216 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
217 })
218 export class UserNotificationModel extends Model<UserNotificationModel> {
219
220 @AllowNull(false)
221 @Default(null)
222 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
223 @Column
224 type: UserNotificationType
225
226 @AllowNull(false)
227 @Default(false)
228 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
229 @Column
230 read: boolean
231
232 @CreatedAt
233 createdAt: Date
234
235 @UpdatedAt
236 updatedAt: Date
237
238 @ForeignKey(() => UserModel)
239 @Column
240 userId: number
241
242 @BelongsTo(() => UserModel, {
243 foreignKey: {
244 allowNull: false
245 },
246 onDelete: 'cascade'
247 })
248 User: UserModel
249
250 @ForeignKey(() => VideoModel)
251 @Column
252 videoId: number
253
254 @BelongsTo(() => VideoModel, {
255 foreignKey: {
256 allowNull: true
257 },
258 onDelete: 'cascade'
259 })
260 Video: VideoModel
261
262 @ForeignKey(() => VideoCommentModel)
263 @Column
264 commentId: number
265
266 @BelongsTo(() => VideoCommentModel, {
267 foreignKey: {
268 allowNull: true
269 },
270 onDelete: 'cascade'
271 })
272 Comment: VideoCommentModel
273
274 @ForeignKey(() => VideoAbuseModel)
275 @Column
276 videoAbuseId: number
277
278 @BelongsTo(() => VideoAbuseModel, {
279 foreignKey: {
280 allowNull: true
281 },
282 onDelete: 'cascade'
283 })
284 VideoAbuse: VideoAbuseModel
285
286 @ForeignKey(() => VideoBlacklistModel)
287 @Column
288 videoBlacklistId: number
289
290 @BelongsTo(() => VideoBlacklistModel, {
291 foreignKey: {
292 allowNull: true
293 },
294 onDelete: 'cascade'
295 })
296 VideoBlacklist: VideoBlacklistModel
297
298 @ForeignKey(() => VideoImportModel)
299 @Column
300 videoImportId: number
301
302 @BelongsTo(() => VideoImportModel, {
303 foreignKey: {
304 allowNull: true
305 },
306 onDelete: 'cascade'
307 })
308 VideoImport: VideoImportModel
309
310 @ForeignKey(() => AccountModel)
311 @Column
312 accountId: number
313
314 @BelongsTo(() => AccountModel, {
315 foreignKey: {
316 allowNull: true
317 },
318 onDelete: 'cascade'
319 })
320 Account: AccountModel
321
322 @ForeignKey(() => ActorFollowModel)
323 @Column
324 actorFollowId: number
325
326 @BelongsTo(() => ActorFollowModel, {
327 foreignKey: {
328 allowNull: true
329 },
330 onDelete: 'cascade'
331 })
332 ActorFollow: ActorFollowModel
333
334 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
335 const query: FindOptions = {
336 offset: start,
337 limit: count,
338 order: getSort(sort),
339 where: {
340 userId
341 }
342 }
343
344 if (unread !== undefined) query.where['read'] = !unread
345
346 return UserNotificationModel.scope(ScopeNames.WITH_ALL)
347 .findAndCountAll(query)
348 .then(({ rows, count }) => {
349 return {
350 data: rows,
351 total: count
352 }
353 })
354 }
355
356 static markAsRead (userId: number, notificationIds: number[]) {
357 const query = {
358 where: {
359 userId,
360 id: {
361 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
362 }
363 }
364 }
365
366 return UserNotificationModel.update({ read: true }, query)
367 }
368
369 static markAllAsRead (userId: number) {
370 const query = { where: { userId } }
371
372 return UserNotificationModel.update({ read: true }, query)
373 }
374
375 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
376 const video = this.Video
377 ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
378 : undefined
379
380 const videoImport = this.VideoImport ? {
381 id: this.VideoImport.id,
382 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
383 torrentName: this.VideoImport.torrentName,
384 magnetUri: this.VideoImport.magnetUri,
385 targetUrl: this.VideoImport.targetUrl
386 } : undefined
387
388 const comment = this.Comment ? {
389 id: this.Comment.id,
390 threadId: this.Comment.getThreadId(),
391 account: this.formatActor(this.Comment.Account),
392 video: this.formatVideo(this.Comment.Video)
393 } : undefined
394
395 const videoAbuse = this.VideoAbuse ? {
396 id: this.VideoAbuse.id,
397 video: this.formatVideo(this.VideoAbuse.Video)
398 } : undefined
399
400 const videoBlacklist = this.VideoBlacklist ? {
401 id: this.VideoBlacklist.id,
402 video: this.formatVideo(this.VideoBlacklist.Video)
403 } : undefined
404
405 const account = this.Account ? this.formatActor(this.Account) : undefined
406
407 const actorFollow = this.ActorFollow ? {
408 id: this.ActorFollow.id,
409 state: this.ActorFollow.state,
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.getStaticPath() } : undefined,
415 host: this.ActorFollow.ActorFollower.getHost()
416 },
417 following: {
418 type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
419 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
420 name: this.ActorFollow.ActorFollowing.preferredUsername
421 }
422 } : undefined
423
424 return {
425 id: this.id,
426 type: this.type,
427 read: this.read,
428 video,
429 videoImport,
430 comment,
431 videoAbuse,
432 videoBlacklist,
433 account,
434 actorFollow,
435 createdAt: this.createdAt.toISOString(),
436 updatedAt: this.updatedAt.toISOString()
437 }
438 }
439
440 formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
441 return {
442 id: video.id,
443 uuid: video.uuid,
444 name: video.name
445 }
446 }
447
448 formatActor (
449 this: UserNotificationModelForApi,
450 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
451 ) {
452 const avatar = accountOrChannel.Actor.Avatar
453 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
454 : undefined
455
456 return {
457 id: accountOrChannel.id,
458 displayName: accountOrChannel.getDisplayName(),
459 name: accountOrChannel.Actor.preferredUsername,
460 host: accountOrChannel.Actor.getHost(),
461 avatar
462 }
463 }
464 }