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