]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/account/user-notification.ts
Refactor how we use icons
[github/Chocobozzz/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 }