aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/user
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/user')
-rw-r--r--server/models/user/user-notification-setting.ts221
-rw-r--r--server/models/user/user-notification.ts665
-rw-r--r--server/models/user/user-video-history.ts100
-rw-r--r--server/models/user/user.ts967
4 files changed, 1953 insertions, 0 deletions
diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts
new file mode 100644
index 000000000..138051528
--- /dev/null
+++ b/server/models/user/user-notification-setting.ts
@@ -0,0 +1,221 @@
1import {
2 AfterDestroy,
3 AfterUpdate,
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 Default,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
16import { MNotificationSettingFormattable } from '@server/types/models'
17import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
18import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
19import { throwIfNotValid } from '../utils'
20import { UserModel } from './user'
21
22@Table({
23 tableName: 'userNotificationSetting',
24 indexes: [
25 {
26 fields: [ 'userId' ],
27 unique: true
28 }
29 ]
30})
31export class UserNotificationSettingModel extends Model {
32
33 @AllowNull(false)
34 @Default(null)
35 @Is(
36 'UserNotificationSettingNewVideoFromSubscription',
37 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription')
38 )
39 @Column
40 newVideoFromSubscription: UserNotificationSettingValue
41
42 @AllowNull(false)
43 @Default(null)
44 @Is(
45 'UserNotificationSettingNewCommentOnMyVideo',
46 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo')
47 )
48 @Column
49 newCommentOnMyVideo: UserNotificationSettingValue
50
51 @AllowNull(false)
52 @Default(null)
53 @Is(
54 'UserNotificationSettingAbuseAsModerator',
55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
56 )
57 @Column
58 abuseAsModerator: UserNotificationSettingValue
59
60 @AllowNull(false)
61 @Default(null)
62 @Is(
63 'UserNotificationSettingVideoAutoBlacklistAsModerator',
64 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
65 )
66 @Column
67 videoAutoBlacklistAsModerator: UserNotificationSettingValue
68
69 @AllowNull(false)
70 @Default(null)
71 @Is(
72 'UserNotificationSettingBlacklistOnMyVideo',
73 value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo')
74 )
75 @Column
76 blacklistOnMyVideo: UserNotificationSettingValue
77
78 @AllowNull(false)
79 @Default(null)
80 @Is(
81 'UserNotificationSettingMyVideoPublished',
82 value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished')
83 )
84 @Column
85 myVideoPublished: UserNotificationSettingValue
86
87 @AllowNull(false)
88 @Default(null)
89 @Is(
90 'UserNotificationSettingMyVideoImportFinished',
91 value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished')
92 )
93 @Column
94 myVideoImportFinished: UserNotificationSettingValue
95
96 @AllowNull(false)
97 @Default(null)
98 @Is(
99 'UserNotificationSettingNewUserRegistration',
100 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration')
101 )
102 @Column
103 newUserRegistration: UserNotificationSettingValue
104
105 @AllowNull(false)
106 @Default(null)
107 @Is(
108 'UserNotificationSettingNewInstanceFollower',
109 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newInstanceFollower')
110 )
111 @Column
112 newInstanceFollower: UserNotificationSettingValue
113
114 @AllowNull(false)
115 @Default(null)
116 @Is(
117 'UserNotificationSettingNewInstanceFollower',
118 value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
119 )
120 @Column
121 autoInstanceFollowing: UserNotificationSettingValue
122
123 @AllowNull(false)
124 @Default(null)
125 @Is(
126 'UserNotificationSettingNewFollow',
127 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow')
128 )
129 @Column
130 newFollow: UserNotificationSettingValue
131
132 @AllowNull(false)
133 @Default(null)
134 @Is(
135 'UserNotificationSettingCommentMention',
136 value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention')
137 )
138 @Column
139 commentMention: UserNotificationSettingValue
140
141 @AllowNull(false)
142 @Default(null)
143 @Is(
144 'UserNotificationSettingAbuseStateChange',
145 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange')
146 )
147 @Column
148 abuseStateChange: UserNotificationSettingValue
149
150 @AllowNull(false)
151 @Default(null)
152 @Is(
153 'UserNotificationSettingAbuseNewMessage',
154 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage')
155 )
156 @Column
157 abuseNewMessage: UserNotificationSettingValue
158
159 @AllowNull(false)
160 @Default(null)
161 @Is(
162 'UserNotificationSettingNewPeerTubeVersion',
163 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
164 )
165 @Column
166 newPeerTubeVersion: UserNotificationSettingValue
167
168 @AllowNull(false)
169 @Default(null)
170 @Is(
171 'UserNotificationSettingNewPeerPluginVersion',
172 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
173 )
174 @Column
175 newPluginVersion: UserNotificationSettingValue
176
177 @ForeignKey(() => UserModel)
178 @Column
179 userId: number
180
181 @BelongsTo(() => UserModel, {
182 foreignKey: {
183 allowNull: false
184 },
185 onDelete: 'cascade'
186 })
187 User: UserModel
188
189 @CreatedAt
190 createdAt: Date
191
192 @UpdatedAt
193 updatedAt: Date
194
195 @AfterUpdate
196 @AfterDestroy
197 static removeTokenCache (instance: UserNotificationSettingModel) {
198 return TokensCache.Instance.clearCacheByUserId(instance.userId)
199 }
200
201 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
202 return {
203 newCommentOnMyVideo: this.newCommentOnMyVideo,
204 newVideoFromSubscription: this.newVideoFromSubscription,
205 abuseAsModerator: this.abuseAsModerator,
206 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
207 blacklistOnMyVideo: this.blacklistOnMyVideo,
208 myVideoPublished: this.myVideoPublished,
209 myVideoImportFinished: this.myVideoImportFinished,
210 newUserRegistration: this.newUserRegistration,
211 commentMention: this.commentMention,
212 newFollow: this.newFollow,
213 newInstanceFollower: this.newInstanceFollower,
214 autoInstanceFollowing: this.autoInstanceFollowing,
215 abuseNewMessage: this.abuseNewMessage,
216 abuseStateChange: this.abuseStateChange,
217 newPeerTubeVersion: this.newPeerTubeVersion,
218 newPluginVersion: this.newPluginVersion
219 }
220 }
221}
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts
new file mode 100644
index 000000000..f7f9ac867
--- /dev/null
+++ b/server/models/user/user-notification.ts
@@ -0,0 +1,665 @@
1import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
4import { UserNotification, UserNotificationType } from '../../../shared'
5import { isBooleanValid } from '../../helpers/custom-validators/misc'
6import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
7import { AbuseModel } from '../abuse/abuse'
8import { VideoAbuseModel } from '../abuse/video-abuse'
9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
10import { AccountModel } from '../account/account'
11import { ActorModel } from '../actor/actor'
12import { ActorFollowModel } from '../actor/actor-follow'
13import { ActorImageModel } from '../actor/actor-image'
14import { ApplicationModel } from '../application/application'
15import { PluginModel } from '../server/plugin'
16import { ServerModel } from '../server/server'
17import { getSort, throwIfNotValid } from '../utils'
18import { VideoModel } from '../video/video'
19import { VideoBlacklistModel } from '../video/video-blacklist'
20import { VideoChannelModel } from '../video/video-channel'
21import { VideoCommentModel } from '../video/video-comment'
22import { VideoImportModel } from '../video/video-import'
23import { UserModel } from './user'
24
25enum ScopeNames {
26 WITH_ALL = 'WITH_ALL'
27}
28
29function buildActorWithAvatarInclude () {
30 return {
31 attributes: [ 'preferredUsername' ],
32 model: ActorModel.unscoped(),
33 required: true,
34 include: [
35 {
36 attributes: [ 'filename' ],
37 as: 'Avatar',
38 model: ActorImageModel.unscoped(),
39 required: false
40 },
41 {
42 attributes: [ 'host' ],
43 model: ServerModel.unscoped(),
44 required: false
45 }
46 ]
47 }
48}
49
50function buildVideoInclude (required: boolean) {
51 return {
52 attributes: [ 'id', 'uuid', 'name' ],
53 model: VideoModel.unscoped(),
54 required
55 }
56}
57
58function buildChannelInclude (required: boolean, withActor = false) {
59 return {
60 required,
61 attributes: [ 'id', 'name' ],
62 model: VideoChannelModel.unscoped(),
63 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
64 }
65}
66
67function buildAccountInclude (required: boolean, withActor = false) {
68 return {
69 required,
70 attributes: [ 'id', 'name' ],
71 model: AccountModel.unscoped(),
72 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
73 }
74}
75
76@Scopes(() => ({
77 [ScopeNames.WITH_ALL]: {
78 include: [
79 Object.assign(buildVideoInclude(false), {
80 include: [ buildChannelInclude(true, true) ]
81 }),
82
83 {
84 attributes: [ 'id', 'originCommentId' ],
85 model: VideoCommentModel.unscoped(),
86 required: false,
87 include: [
88 buildAccountInclude(true, true),
89 buildVideoInclude(true)
90 ]
91 },
92
93 {
94 attributes: [ 'id', 'state' ],
95 model: AbuseModel.unscoped(),
96 required: false,
97 include: [
98 {
99 attributes: [ 'id' ],
100 model: VideoAbuseModel.unscoped(),
101 required: false,
102 include: [ buildVideoInclude(false) ]
103 },
104 {
105 attributes: [ 'id' ],
106 model: VideoCommentAbuseModel.unscoped(),
107 required: false,
108 include: [
109 {
110 attributes: [ 'id', 'originCommentId' ],
111 model: VideoCommentModel.unscoped(),
112 required: false,
113 include: [
114 {
115 attributes: [ 'id', 'name', 'uuid' ],
116 model: VideoModel.unscoped(),
117 required: false
118 }
119 ]
120 }
121 ]
122 },
123 {
124 model: AccountModel,
125 as: 'FlaggedAccount',
126 required: false,
127 include: [ buildActorWithAvatarInclude() ]
128 }
129 ]
130 },
131
132 {
133 attributes: [ 'id' ],
134 model: VideoBlacklistModel.unscoped(),
135 required: false,
136 include: [ buildVideoInclude(true) ]
137 },
138
139 {
140 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
141 model: VideoImportModel.unscoped(),
142 required: false,
143 include: [ buildVideoInclude(false) ]
144 },
145
146 {
147 attributes: [ 'id', 'name', 'type', 'latestVersion' ],
148 model: PluginModel.unscoped(),
149 required: false
150 },
151
152 {
153 attributes: [ 'id', 'latestPeerTubeVersion' ],
154 model: ApplicationModel.unscoped(),
155 required: false
156 },
157
158 {
159 attributes: [ 'id', 'state' ],
160 model: ActorFollowModel.unscoped(),
161 required: false,
162 include: [
163 {
164 attributes: [ 'preferredUsername' ],
165 model: ActorModel.unscoped(),
166 required: true,
167 as: 'ActorFollower',
168 include: [
169 {
170 attributes: [ 'id', 'name' ],
171 model: AccountModel.unscoped(),
172 required: true
173 },
174 {
175 attributes: [ 'filename' ],
176 as: 'Avatar',
177 model: ActorImageModel.unscoped(),
178 required: false
179 },
180 {
181 attributes: [ 'host' ],
182 model: ServerModel.unscoped(),
183 required: false
184 }
185 ]
186 },
187 {
188 attributes: [ 'preferredUsername', 'type' ],
189 model: ActorModel.unscoped(),
190 required: true,
191 as: 'ActorFollowing',
192 include: [
193 buildChannelInclude(false),
194 buildAccountInclude(false),
195 {
196 attributes: [ 'host' ],
197 model: ServerModel.unscoped(),
198 required: false
199 }
200 ]
201 }
202 ]
203 },
204
205 buildAccountInclude(false, true)
206 ]
207 }
208}))
209@Table({
210 tableName: 'userNotification',
211 indexes: [
212 {
213 fields: [ 'userId' ]
214 },
215 {
216 fields: [ 'videoId' ],
217 where: {
218 videoId: {
219 [Op.ne]: null
220 }
221 }
222 },
223 {
224 fields: [ 'commentId' ],
225 where: {
226 commentId: {
227 [Op.ne]: null
228 }
229 }
230 },
231 {
232 fields: [ 'abuseId' ],
233 where: {
234 abuseId: {
235 [Op.ne]: null
236 }
237 }
238 },
239 {
240 fields: [ 'videoBlacklistId' ],
241 where: {
242 videoBlacklistId: {
243 [Op.ne]: null
244 }
245 }
246 },
247 {
248 fields: [ 'videoImportId' ],
249 where: {
250 videoImportId: {
251 [Op.ne]: null
252 }
253 }
254 },
255 {
256 fields: [ 'accountId' ],
257 where: {
258 accountId: {
259 [Op.ne]: null
260 }
261 }
262 },
263 {
264 fields: [ 'actorFollowId' ],
265 where: {
266 actorFollowId: {
267 [Op.ne]: null
268 }
269 }
270 },
271 {
272 fields: [ 'pluginId' ],
273 where: {
274 pluginId: {
275 [Op.ne]: null
276 }
277 }
278 },
279 {
280 fields: [ 'applicationId' ],
281 where: {
282 applicationId: {
283 [Op.ne]: null
284 }
285 }
286 }
287 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
288})
289export class UserNotificationModel extends Model {
290
291 @AllowNull(false)
292 @Default(null)
293 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
294 @Column
295 type: UserNotificationType
296
297 @AllowNull(false)
298 @Default(false)
299 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
300 @Column
301 read: boolean
302
303 @CreatedAt
304 createdAt: Date
305
306 @UpdatedAt
307 updatedAt: Date
308
309 @ForeignKey(() => UserModel)
310 @Column
311 userId: number
312
313 @BelongsTo(() => UserModel, {
314 foreignKey: {
315 allowNull: false
316 },
317 onDelete: 'cascade'
318 })
319 User: UserModel
320
321 @ForeignKey(() => VideoModel)
322 @Column
323 videoId: number
324
325 @BelongsTo(() => VideoModel, {
326 foreignKey: {
327 allowNull: true
328 },
329 onDelete: 'cascade'
330 })
331 Video: VideoModel
332
333 @ForeignKey(() => VideoCommentModel)
334 @Column
335 commentId: number
336
337 @BelongsTo(() => VideoCommentModel, {
338 foreignKey: {
339 allowNull: true
340 },
341 onDelete: 'cascade'
342 })
343 Comment: VideoCommentModel
344
345 @ForeignKey(() => AbuseModel)
346 @Column
347 abuseId: number
348
349 @BelongsTo(() => AbuseModel, {
350 foreignKey: {
351 allowNull: true
352 },
353 onDelete: 'cascade'
354 })
355 Abuse: AbuseModel
356
357 @ForeignKey(() => VideoBlacklistModel)
358 @Column
359 videoBlacklistId: number
360
361 @BelongsTo(() => VideoBlacklistModel, {
362 foreignKey: {
363 allowNull: true
364 },
365 onDelete: 'cascade'
366 })
367 VideoBlacklist: VideoBlacklistModel
368
369 @ForeignKey(() => VideoImportModel)
370 @Column
371 videoImportId: number
372
373 @BelongsTo(() => VideoImportModel, {
374 foreignKey: {
375 allowNull: true
376 },
377 onDelete: 'cascade'
378 })
379 VideoImport: VideoImportModel
380
381 @ForeignKey(() => AccountModel)
382 @Column
383 accountId: number
384
385 @BelongsTo(() => AccountModel, {
386 foreignKey: {
387 allowNull: true
388 },
389 onDelete: 'cascade'
390 })
391 Account: AccountModel
392
393 @ForeignKey(() => ActorFollowModel)
394 @Column
395 actorFollowId: number
396
397 @BelongsTo(() => ActorFollowModel, {
398 foreignKey: {
399 allowNull: true
400 },
401 onDelete: 'cascade'
402 })
403 ActorFollow: ActorFollowModel
404
405 @ForeignKey(() => PluginModel)
406 @Column
407 pluginId: number
408
409 @BelongsTo(() => PluginModel, {
410 foreignKey: {
411 allowNull: true
412 },
413 onDelete: 'cascade'
414 })
415 Plugin: PluginModel
416
417 @ForeignKey(() => ApplicationModel)
418 @Column
419 applicationId: number
420
421 @BelongsTo(() => ApplicationModel, {
422 foreignKey: {
423 allowNull: true
424 },
425 onDelete: 'cascade'
426 })
427 Application: ApplicationModel
428
429 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
430 const where = { userId }
431
432 const query: FindOptions = {
433 offset: start,
434 limit: count,
435 order: getSort(sort),
436 where
437 }
438
439 if (unread !== undefined) query.where['read'] = !unread
440
441 return Promise.all([
442 UserNotificationModel.count({ where })
443 .then(count => count || 0),
444
445 count === 0
446 ? []
447 : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query)
448 ]).then(([ total, data ]) => ({ total, data }))
449 }
450
451 static markAsRead (userId: number, notificationIds: number[]) {
452 const query = {
453 where: {
454 userId,
455 id: {
456 [Op.in]: notificationIds
457 }
458 }
459 }
460
461 return UserNotificationModel.update({ read: true }, query)
462 }
463
464 static markAllAsRead (userId: number) {
465 const query = { where: { userId } }
466
467 return UserNotificationModel.update({ read: true }, query)
468 }
469
470 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
471 const id = parseInt(options.id + '', 10)
472
473 function buildAccountWhereQuery (base: string) {
474 const whereSuffix = options.forUserId
475 ? ` AND "userNotification"."userId" = ${options.forUserId}`
476 : ''
477
478 if (options.type === 'account') {
479 return base +
480 ` WHERE "account"."id" = ${id} ${whereSuffix}`
481 }
482
483 return base +
484 ` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
485 }
486
487 const queries = [
488 buildAccountWhereQuery(
489 `SELECT "userNotification"."id" FROM "userNotification" ` +
490 `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` +
491 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
492 ),
493
494 // Remove notifications from muted accounts that followed ours
495 buildAccountWhereQuery(
496 `SELECT "userNotification"."id" FROM "userNotification" ` +
497 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
498 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
499 `INNER JOIN account ON account."actorId" = actor.id `
500 ),
501
502 // Remove notifications from muted accounts that commented something
503 buildAccountWhereQuery(
504 `SELECT "userNotification"."id" FROM "userNotification" ` +
505 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
506 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
507 `INNER JOIN account ON account."actorId" = actor.id `
508 ),
509
510 buildAccountWhereQuery(
511 `SELECT "userNotification"."id" FROM "userNotification" ` +
512 `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` +
513 `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
514 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
515 )
516 ]
517
518 const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
519
520 return UserNotificationModel.sequelize.query(query)
521 }
522
523 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
524 const video = this.Video
525 ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) })
526 : undefined
527
528 const videoImport = this.VideoImport
529 ? {
530 id: this.VideoImport.id,
531 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
532 torrentName: this.VideoImport.torrentName,
533 magnetUri: this.VideoImport.magnetUri,
534 targetUrl: this.VideoImport.targetUrl
535 }
536 : undefined
537
538 const comment = this.Comment
539 ? {
540 id: this.Comment.id,
541 threadId: this.Comment.getThreadId(),
542 account: this.formatActor(this.Comment.Account),
543 video: this.formatVideo(this.Comment.Video)
544 }
545 : undefined
546
547 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
548
549 const videoBlacklist = this.VideoBlacklist
550 ? {
551 id: this.VideoBlacklist.id,
552 video: this.formatVideo(this.VideoBlacklist.Video)
553 }
554 : undefined
555
556 const account = this.Account ? this.formatActor(this.Account) : undefined
557
558 const actorFollowingType = {
559 Application: 'instance' as 'instance',
560 Group: 'channel' as 'channel',
561 Person: 'account' as 'account'
562 }
563 const actorFollow = this.ActorFollow
564 ? {
565 id: this.ActorFollow.id,
566 state: this.ActorFollow.state,
567 follower: {
568 id: this.ActorFollow.ActorFollower.Account.id,
569 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
570 name: this.ActorFollow.ActorFollower.preferredUsername,
571 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined,
572 host: this.ActorFollow.ActorFollower.getHost()
573 },
574 following: {
575 type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
576 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
577 name: this.ActorFollow.ActorFollowing.preferredUsername,
578 host: this.ActorFollow.ActorFollowing.getHost()
579 }
580 }
581 : undefined
582
583 const plugin = this.Plugin
584 ? {
585 name: this.Plugin.name,
586 type: this.Plugin.type,
587 latestVersion: this.Plugin.latestVersion
588 }
589 : undefined
590
591 const peertube = this.Application
592 ? { latestVersion: this.Application.latestPeerTubeVersion }
593 : undefined
594
595 return {
596 id: this.id,
597 type: this.type,
598 read: this.read,
599 video,
600 videoImport,
601 comment,
602 abuse,
603 videoBlacklist,
604 account,
605 actorFollow,
606 plugin,
607 peertube,
608 createdAt: this.createdAt.toISOString(),
609 updatedAt: this.updatedAt.toISOString()
610 }
611 }
612
613 formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
614 return {
615 id: video.id,
616 uuid: video.uuid,
617 name: video.name
618 }
619 }
620
621 formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
622 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
623 ? {
624 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
625
626 video: abuse.VideoCommentAbuse.VideoComment.Video
627 ? {
628 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
629 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
630 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
631 }
632 : undefined
633 }
634 : undefined
635
636 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
637
638 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
639
640 return {
641 id: abuse.id,
642 state: abuse.state,
643 video: videoAbuse,
644 comment: commentAbuse,
645 account: accountAbuse
646 }
647 }
648
649 formatActor (
650 this: UserNotificationModelForApi,
651 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
652 ) {
653 const avatar = accountOrChannel.Actor.Avatar
654 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
655 : undefined
656
657 return {
658 id: accountOrChannel.id,
659 displayName: accountOrChannel.getDisplayName(),
660 name: accountOrChannel.Actor.preferredUsername,
661 host: accountOrChannel.Actor.getHost(),
662 avatar
663 }
664 }
665}
diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts
new file mode 100644
index 000000000..6be1d65ea
--- /dev/null
+++ b/server/models/user/user-video-history.ts
@@ -0,0 +1,100 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { UserModel } from './user'
4import { DestroyOptions, Op, Transaction } from 'sequelize'
5import { MUserAccountId, MUserId } from '@server/types/models'
6
7@Table({
8 tableName: 'userVideoHistory',
9 indexes: [
10 {
11 fields: [ 'userId', 'videoId' ],
12 unique: true
13 },
14 {
15 fields: [ 'userId' ]
16 },
17 {
18 fields: [ 'videoId' ]
19 }
20 ]
21})
22export class UserVideoHistoryModel extends Model {
23 @CreatedAt
24 createdAt: Date
25
26 @UpdatedAt
27 updatedAt: Date
28
29 @AllowNull(false)
30 @IsInt
31 @Column
32 currentTime: number
33
34 @ForeignKey(() => VideoModel)
35 @Column
36 videoId: number
37
38 @BelongsTo(() => VideoModel, {
39 foreignKey: {
40 allowNull: false
41 },
42 onDelete: 'CASCADE'
43 })
44 Video: VideoModel
45
46 @ForeignKey(() => UserModel)
47 @Column
48 userId: number
49
50 @BelongsTo(() => UserModel, {
51 foreignKey: {
52 allowNull: false
53 },
54 onDelete: 'CASCADE'
55 })
56 User: UserModel
57
58 static listForApi (user: MUserAccountId, start: number, count: number, search?: string) {
59 return VideoModel.listForApi({
60 start,
61 count,
62 search,
63 sort: '-"userVideoHistory"."updatedAt"',
64 nsfw: null, // All
65 includeLocalVideos: true,
66 withFiles: false,
67 user,
68 historyOfUser: user
69 })
70 }
71
72 static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
73 const query: DestroyOptions = {
74 where: {
75 userId: user.id
76 },
77 transaction: t
78 }
79
80 if (beforeDate) {
81 query.where['updatedAt'] = {
82 [Op.lt]: beforeDate
83 }
84 }
85
86 return UserVideoHistoryModel.destroy(query)
87 }
88
89 static removeOldHistory (beforeDate: string) {
90 const query: DestroyOptions = {
91 where: {
92 updatedAt: {
93 [Op.lt]: beforeDate
94 }
95 }
96 }
97
98 return UserVideoHistoryModel.destroy(query)
99 }
100}
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
new file mode 100644
index 000000000..8d2564e54
--- /dev/null
+++ b/server/models/user/user.ts
@@ -0,0 +1,967 @@
1import { values } from 'lodash'
2import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize'
3import {
4 AfterDestroy,
5 AfterUpdate,
6 AllowNull,
7 BeforeCreate,
8 BeforeUpdate,
9 Column,
10 CreatedAt,
11 DataType,
12 Default,
13 DefaultScope,
14 HasMany,
15 HasOne,
16 Is,
17 IsEmail,
18 IsUUID,
19 Model,
20 Scopes,
21 Table,
22 UpdatedAt
23} from 'sequelize-typescript'
24import { TokensCache } from '@server/lib/auth/tokens-cache'
25import {
26 MMyUserFormattable,
27 MUser,
28 MUserDefault,
29 MUserFormattable,
30 MUserNotifSettingChannelDefault,
31 MUserWithNotificationSetting,
32 MVideoWithRights
33} from '@server/types/models'
34import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
35import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models'
36import { User, UserRole } from '../../../shared/models/users'
37import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
38import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
39import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
40import {
41 isNoInstanceConfigWarningModal,
42 isNoWelcomeModal,
43 isUserAdminFlagsValid,
44 isUserAutoPlayNextVideoPlaylistValid,
45 isUserAutoPlayNextVideoValid,
46 isUserAutoPlayVideoValid,
47 isUserBlockedReasonValid,
48 isUserBlockedValid,
49 isUserEmailVerifiedValid,
50 isUserNSFWPolicyValid,
51 isUserPasswordValid,
52 isUserRoleValid,
53 isUserUsernameValid,
54 isUserVideoLanguages,
55 isUserVideoQuotaDailyValid,
56 isUserVideoQuotaValid,
57 isUserVideosHistoryEnabledValid,
58 isUserWebTorrentEnabledValid
59} from '../../helpers/custom-validators/users'
60import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
61import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
62import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
63import { AccountModel } from '../account/account'
64import { ActorModel } from '../actor/actor'
65import { ActorFollowModel } from '../actor/actor-follow'
66import { ActorImageModel } from '../actor/actor-image'
67import { OAuthTokenModel } from '../oauth/oauth-token'
68import { getSort, throwIfNotValid } from '../utils'
69import { VideoModel } from '../video/video'
70import { VideoChannelModel } from '../video/video-channel'
71import { VideoImportModel } from '../video/video-import'
72import { VideoLiveModel } from '../video/video-live'
73import { VideoPlaylistModel } from '../video/video-playlist'
74import { UserNotificationSettingModel } from './user-notification-setting'
75
76enum ScopeNames {
77 FOR_ME_API = 'FOR_ME_API',
78 WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
79 WITH_STATS = 'WITH_STATS'
80}
81
82@DefaultScope(() => ({
83 include: [
84 {
85 model: AccountModel,
86 required: true
87 },
88 {
89 model: UserNotificationSettingModel,
90 required: true
91 }
92 ]
93}))
94@Scopes(() => ({
95 [ScopeNames.FOR_ME_API]: {
96 include: [
97 {
98 model: AccountModel,
99 include: [
100 {
101 model: VideoChannelModel.unscoped(),
102 include: [
103 {
104 model: ActorModel,
105 required: true,
106 include: [
107 {
108 model: ActorImageModel,
109 as: 'Banner',
110 required: false
111 }
112 ]
113 }
114 ]
115 },
116 {
117 attributes: [ 'id', 'name', 'type' ],
118 model: VideoPlaylistModel.unscoped(),
119 required: true,
120 where: {
121 type: {
122 [Op.ne]: VideoPlaylistType.REGULAR
123 }
124 }
125 }
126 ]
127 },
128 {
129 model: UserNotificationSettingModel,
130 required: true
131 }
132 ]
133 },
134 [ScopeNames.WITH_VIDEOCHANNELS]: {
135 include: [
136 {
137 model: AccountModel,
138 include: [
139 {
140 model: VideoChannelModel
141 },
142 {
143 attributes: [ 'id', 'name', 'type' ],
144 model: VideoPlaylistModel.unscoped(),
145 required: true,
146 where: {
147 type: {
148 [Op.ne]: VideoPlaylistType.REGULAR
149 }
150 }
151 }
152 ]
153 }
154 ]
155 },
156 [ScopeNames.WITH_STATS]: {
157 attributes: {
158 include: [
159 [
160 literal(
161 '(' +
162 UserModel.generateUserQuotaBaseSQL({
163 withSelect: false,
164 whereUserId: '"UserModel"."id"'
165 }) +
166 ')'
167 ),
168 'videoQuotaUsed'
169 ],
170 [
171 literal(
172 '(' +
173 'SELECT COUNT("video"."id") ' +
174 'FROM "video" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
176 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
177 'WHERE "account"."userId" = "UserModel"."id"' +
178 ')'
179 ),
180 'videosCount'
181 ],
182 [
183 literal(
184 '(' +
185 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
186 'FROM (' +
187 'SELECT COUNT("abuse"."id") AS "abuses", ' +
188 `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
189 'FROM "abuse" ' +
190 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
191 'WHERE "account"."userId" = "UserModel"."id"' +
192 ') t' +
193 ')'
194 ),
195 'abusesCount'
196 ],
197 [
198 literal(
199 '(' +
200 'SELECT COUNT("abuse"."id") ' +
201 'FROM "abuse" ' +
202 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
203 'WHERE "account"."userId" = "UserModel"."id"' +
204 ')'
205 ),
206 'abusesCreatedCount'
207 ],
208 [
209 literal(
210 '(' +
211 'SELECT COUNT("videoComment"."id") ' +
212 'FROM "videoComment" ' +
213 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
214 'WHERE "account"."userId" = "UserModel"."id"' +
215 ')'
216 ),
217 'videoCommentsCount'
218 ]
219 ]
220 }
221 }
222}))
223@Table({
224 tableName: 'user',
225 indexes: [
226 {
227 fields: [ 'username' ],
228 unique: true
229 },
230 {
231 fields: [ 'email' ],
232 unique: true
233 }
234 ]
235})
236export class UserModel extends Model {
237
238 @AllowNull(true)
239 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
240 @Column
241 password: string
242
243 @AllowNull(false)
244 @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
245 @Column
246 username: string
247
248 @AllowNull(false)
249 @IsEmail
250 @Column(DataType.STRING(400))
251 email: string
252
253 @AllowNull(true)
254 @IsEmail
255 @Column(DataType.STRING(400))
256 pendingEmail: string
257
258 @AllowNull(true)
259 @Default(null)
260 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
261 @Column
262 emailVerified: boolean
263
264 @AllowNull(false)
265 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
266 @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
267 nsfwPolicy: NSFWPolicyType
268
269 @AllowNull(false)
270 @Default(true)
271 @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
272 @Column
273 webTorrentEnabled: boolean
274
275 @AllowNull(false)
276 @Default(true)
277 @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
278 @Column
279 videosHistoryEnabled: boolean
280
281 @AllowNull(false)
282 @Default(true)
283 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
284 @Column
285 autoPlayVideo: boolean
286
287 @AllowNull(false)
288 @Default(false)
289 @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
290 @Column
291 autoPlayNextVideo: boolean
292
293 @AllowNull(false)
294 @Default(true)
295 @Is(
296 'UserAutoPlayNextVideoPlaylist',
297 value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')
298 )
299 @Column
300 autoPlayNextVideoPlaylist: boolean
301
302 @AllowNull(true)
303 @Default(null)
304 @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
305 @Column(DataType.ARRAY(DataType.STRING))
306 videoLanguages: string[]
307
308 @AllowNull(false)
309 @Default(UserAdminFlag.NONE)
310 @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
311 @Column
312 adminFlags?: UserAdminFlag
313
314 @AllowNull(false)
315 @Default(false)
316 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
317 @Column
318 blocked: boolean
319
320 @AllowNull(true)
321 @Default(null)
322 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
323 @Column
324 blockedReason: string
325
326 @AllowNull(false)
327 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
328 @Column
329 role: number
330
331 @AllowNull(false)
332 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
333 @Column(DataType.BIGINT)
334 videoQuota: number
335
336 @AllowNull(false)
337 @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
338 @Column(DataType.BIGINT)
339 videoQuotaDaily: number
340
341 @AllowNull(false)
342 @Default(DEFAULT_USER_THEME_NAME)
343 @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
344 @Column
345 theme: string
346
347 @AllowNull(false)
348 @Default(false)
349 @Is(
350 'UserNoInstanceConfigWarningModal',
351 value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
352 )
353 @Column
354 noInstanceConfigWarningModal: boolean
355
356 @AllowNull(false)
357 @Default(false)
358 @Is(
359 'UserNoInstanceConfigWarningModal',
360 value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
361 )
362 @Column
363 noWelcomeModal: boolean
364
365 @AllowNull(true)
366 @Default(null)
367 @Column
368 pluginAuth: string
369
370 @AllowNull(false)
371 @Default(DataType.UUIDV4)
372 @IsUUID(4)
373 @Column(DataType.UUID)
374 feedToken: string
375
376 @AllowNull(true)
377 @Default(null)
378 @Column
379 lastLoginDate: Date
380
381 @CreatedAt
382 createdAt: Date
383
384 @UpdatedAt
385 updatedAt: Date
386
387 @HasOne(() => AccountModel, {
388 foreignKey: 'userId',
389 onDelete: 'cascade',
390 hooks: true
391 })
392 Account: AccountModel
393
394 @HasOne(() => UserNotificationSettingModel, {
395 foreignKey: 'userId',
396 onDelete: 'cascade',
397 hooks: true
398 })
399 NotificationSetting: UserNotificationSettingModel
400
401 @HasMany(() => VideoImportModel, {
402 foreignKey: 'userId',
403 onDelete: 'cascade'
404 })
405 VideoImports: VideoImportModel[]
406
407 @HasMany(() => OAuthTokenModel, {
408 foreignKey: 'userId',
409 onDelete: 'cascade'
410 })
411 OAuthTokens: OAuthTokenModel[]
412
413 @BeforeCreate
414 @BeforeUpdate
415 static cryptPasswordIfNeeded (instance: UserModel) {
416 if (instance.changed('password') && instance.password) {
417 return cryptPassword(instance.password)
418 .then(hash => {
419 instance.password = hash
420 return undefined
421 })
422 }
423 }
424
425 @AfterUpdate
426 @AfterDestroy
427 static removeTokenCache (instance: UserModel) {
428 return TokensCache.Instance.clearCacheByUserId(instance.id)
429 }
430
431 static countTotal () {
432 return this.count()
433 }
434
435 static listForApi (parameters: {
436 start: number
437 count: number
438 sort: string
439 search?: string
440 blocked?: boolean
441 }) {
442 const { start, count, sort, search, blocked } = parameters
443 const where: WhereOptions = {}
444
445 if (search) {
446 Object.assign(where, {
447 [Op.or]: [
448 {
449 email: {
450 [Op.iLike]: '%' + search + '%'
451 }
452 },
453 {
454 username: {
455 [Op.iLike]: '%' + search + '%'
456 }
457 }
458 ]
459 })
460 }
461
462 if (blocked !== undefined) {
463 Object.assign(where, {
464 blocked: blocked
465 })
466 }
467
468 const query: FindOptions = {
469 attributes: {
470 include: [
471 [
472 literal(
473 '(' +
474 UserModel.generateUserQuotaBaseSQL({
475 withSelect: false,
476 whereUserId: '"UserModel"."id"'
477 }) +
478 ')'
479 ),
480 'videoQuotaUsed'
481 ] as any // FIXME: typings
482 ]
483 },
484 offset: start,
485 limit: count,
486 order: getSort(sort),
487 where
488 }
489
490 return UserModel.findAndCountAll(query)
491 .then(({ rows, count }) => {
492 return {
493 data: rows,
494 total: count
495 }
496 })
497 }
498
499 static listWithRight (right: UserRight): Promise<MUserDefault[]> {
500 const roles = Object.keys(USER_ROLE_LABELS)
501 .map(k => parseInt(k, 10) as UserRole)
502 .filter(role => hasUserRight(role, right))
503
504 const query = {
505 where: {
506 role: {
507 [Op.in]: roles
508 }
509 }
510 }
511
512 return UserModel.findAll(query)
513 }
514
515 static listUserSubscribersOf (actorId: number): Promise<MUserWithNotificationSetting[]> {
516 const query = {
517 include: [
518 {
519 model: UserNotificationSettingModel.unscoped(),
520 required: true
521 },
522 {
523 attributes: [ 'userId' ],
524 model: AccountModel.unscoped(),
525 required: true,
526 include: [
527 {
528 attributes: [],
529 model: ActorModel.unscoped(),
530 required: true,
531 where: {
532 serverId: null
533 },
534 include: [
535 {
536 attributes: [],
537 as: 'ActorFollowings',
538 model: ActorFollowModel.unscoped(),
539 required: true,
540 where: {
541 targetActorId: actorId
542 }
543 }
544 ]
545 }
546 ]
547 }
548 ]
549 }
550
551 return UserModel.unscoped().findAll(query)
552 }
553
554 static listByUsernames (usernames: string[]): Promise<MUserDefault[]> {
555 const query = {
556 where: {
557 username: usernames
558 }
559 }
560
561 return UserModel.findAll(query)
562 }
563
564 static loadById (id: number): Promise<MUser> {
565 return UserModel.unscoped().findByPk(id)
566 }
567
568 static loadByIdFull (id: number): Promise<MUserDefault> {
569 return UserModel.findByPk(id)
570 }
571
572 static loadByIdWithChannels (id: number, withStats = false): Promise<MUserDefault> {
573 const scopes = [
574 ScopeNames.WITH_VIDEOCHANNELS
575 ]
576
577 if (withStats) scopes.push(ScopeNames.WITH_STATS)
578
579 return UserModel.scope(scopes).findByPk(id)
580 }
581
582 static loadByUsername (username: string): Promise<MUserDefault> {
583 const query = {
584 where: {
585 username
586 }
587 }
588
589 return UserModel.findOne(query)
590 }
591
592 static loadForMeAPI (id: number): Promise<MUserNotifSettingChannelDefault> {
593 const query = {
594 where: {
595 id
596 }
597 }
598
599 return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
600 }
601
602 static loadByEmail (email: string): Promise<MUserDefault> {
603 const query = {
604 where: {
605 email
606 }
607 }
608
609 return UserModel.findOne(query)
610 }
611
612 static loadByUsernameOrEmail (username: string, email?: string): Promise<MUserDefault> {
613 if (!email) email = username
614
615 const query = {
616 where: {
617 [Op.or]: [
618 where(fn('lower', col('username')), fn('lower', username)),
619
620 { email }
621 ]
622 }
623 }
624
625 return UserModel.findOne(query)
626 }
627
628 static loadByVideoId (videoId: number): Promise<MUserDefault> {
629 const query = {
630 include: [
631 {
632 required: true,
633 attributes: [ 'id' ],
634 model: AccountModel.unscoped(),
635 include: [
636 {
637 required: true,
638 attributes: [ 'id' ],
639 model: VideoChannelModel.unscoped(),
640 include: [
641 {
642 required: true,
643 attributes: [ 'id' ],
644 model: VideoModel.unscoped(),
645 where: {
646 id: videoId
647 }
648 }
649 ]
650 }
651 ]
652 }
653 ]
654 }
655
656 return UserModel.findOne(query)
657 }
658
659 static loadByVideoImportId (videoImportId: number): Promise<MUserDefault> {
660 const query = {
661 include: [
662 {
663 required: true,
664 attributes: [ 'id' ],
665 model: VideoImportModel.unscoped(),
666 where: {
667 id: videoImportId
668 }
669 }
670 ]
671 }
672
673 return UserModel.findOne(query)
674 }
675
676 static loadByChannelActorId (videoChannelActorId: number): Promise<MUserDefault> {
677 const query = {
678 include: [
679 {
680 required: true,
681 attributes: [ 'id' ],
682 model: AccountModel.unscoped(),
683 include: [
684 {
685 required: true,
686 attributes: [ 'id' ],
687 model: VideoChannelModel.unscoped(),
688 where: {
689 actorId: videoChannelActorId
690 }
691 }
692 ]
693 }
694 ]
695 }
696
697 return UserModel.findOne(query)
698 }
699
700 static loadByAccountActorId (accountActorId: number): Promise<MUserDefault> {
701 const query = {
702 include: [
703 {
704 required: true,
705 attributes: [ 'id' ],
706 model: AccountModel.unscoped(),
707 where: {
708 actorId: accountActorId
709 }
710 }
711 ]
712 }
713
714 return UserModel.findOne(query)
715 }
716
717 static loadByLiveId (liveId: number): Promise<MUser> {
718 const query = {
719 include: [
720 {
721 attributes: [ 'id' ],
722 model: AccountModel.unscoped(),
723 required: true,
724 include: [
725 {
726 attributes: [ 'id' ],
727 model: VideoChannelModel.unscoped(),
728 required: true,
729 include: [
730 {
731 attributes: [ 'id' ],
732 model: VideoModel.unscoped(),
733 required: true,
734 include: [
735 {
736 attributes: [],
737 model: VideoLiveModel.unscoped(),
738 required: true,
739 where: {
740 id: liveId
741 }
742 }
743 ]
744 }
745 ]
746 }
747 ]
748 }
749 ]
750 }
751
752 return UserModel.unscoped().findOne(query)
753 }
754
755 static generateUserQuotaBaseSQL (options: {
756 whereUserId: '$userId' | '"UserModel"."id"'
757 withSelect: boolean
758 where?: string
759 }) {
760 const andWhere = options.where
761 ? 'AND ' + options.where
762 : ''
763
764 const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
765 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
766 `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
767
768 const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
769 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
770 videoChannelJoin
771
772 const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
773 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
774 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
775 videoChannelJoin
776
777 return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
778 'FROM (' +
779 `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
780 'GROUP BY "t1"."videoId"' +
781 ') t2'
782 }
783
784 static getTotalRawQuery (query: string, userId: number) {
785 const options = {
786 bind: { userId },
787 type: QueryTypes.SELECT as QueryTypes.SELECT
788 }
789
790 return UserModel.sequelize.query<{ total: string }>(query, options)
791 .then(([ { total } ]) => {
792 if (total === null) return 0
793
794 return parseInt(total, 10)
795 })
796 }
797
798 static async getStats () {
799 function getActiveUsers (days: number) {
800 const query = {
801 where: {
802 [Op.and]: [
803 literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`)
804 ]
805 }
806 }
807
808 return UserModel.count(query)
809 }
810
811 const totalUsers = await UserModel.count()
812 const totalDailyActiveUsers = await getActiveUsers(1)
813 const totalWeeklyActiveUsers = await getActiveUsers(7)
814 const totalMonthlyActiveUsers = await getActiveUsers(30)
815 const totalHalfYearActiveUsers = await getActiveUsers(180)
816
817 return {
818 totalUsers,
819 totalDailyActiveUsers,
820 totalWeeklyActiveUsers,
821 totalMonthlyActiveUsers,
822 totalHalfYearActiveUsers
823 }
824 }
825
826 static autoComplete (search: string) {
827 const query = {
828 where: {
829 username: {
830 [Op.like]: `%${search}%`
831 }
832 },
833 limit: 10
834 }
835
836 return UserModel.findAll(query)
837 .then(u => u.map(u => u.username))
838 }
839
840 canGetVideo (video: MVideoWithRights) {
841 const videoUserId = video.VideoChannel.Account.userId
842
843 if (video.isBlacklisted()) {
844 return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
845 }
846
847 if (video.privacy === VideoPrivacy.PRIVATE) {
848 return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
849 }
850
851 if (video.privacy === VideoPrivacy.INTERNAL) return true
852
853 return false
854 }
855
856 hasRight (right: UserRight) {
857 return hasUserRight(this.role, right)
858 }
859
860 hasAdminFlag (flag: UserAdminFlag) {
861 return this.adminFlags & flag
862 }
863
864 isPasswordMatch (password: string) {
865 return comparePassword(password, this.password)
866 }
867
868 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
869 const videoQuotaUsed = this.get('videoQuotaUsed')
870 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
871 const videosCount = this.get('videosCount')
872 const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
873 const abusesCreatedCount = this.get('abusesCreatedCount')
874 const videoCommentsCount = this.get('videoCommentsCount')
875
876 const json: User = {
877 id: this.id,
878 username: this.username,
879 email: this.email,
880 theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
881
882 pendingEmail: this.pendingEmail,
883 emailVerified: this.emailVerified,
884
885 nsfwPolicy: this.nsfwPolicy,
886 webTorrentEnabled: this.webTorrentEnabled,
887 videosHistoryEnabled: this.videosHistoryEnabled,
888 autoPlayVideo: this.autoPlayVideo,
889 autoPlayNextVideo: this.autoPlayNextVideo,
890 autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
891 videoLanguages: this.videoLanguages,
892
893 role: this.role,
894 roleLabel: USER_ROLE_LABELS[this.role],
895
896 videoQuota: this.videoQuota,
897 videoQuotaDaily: this.videoQuotaDaily,
898 videoQuotaUsed: videoQuotaUsed !== undefined
899 ? parseInt(videoQuotaUsed + '', 10)
900 : undefined,
901 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
902 ? parseInt(videoQuotaUsedDaily + '', 10)
903 : undefined,
904 videosCount: videosCount !== undefined
905 ? parseInt(videosCount + '', 10)
906 : undefined,
907 abusesCount: abusesCount
908 ? parseInt(abusesCount, 10)
909 : undefined,
910 abusesAcceptedCount: abusesAcceptedCount
911 ? parseInt(abusesAcceptedCount, 10)
912 : undefined,
913 abusesCreatedCount: abusesCreatedCount !== undefined
914 ? parseInt(abusesCreatedCount + '', 10)
915 : undefined,
916 videoCommentsCount: videoCommentsCount !== undefined
917 ? parseInt(videoCommentsCount + '', 10)
918 : undefined,
919
920 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
921 noWelcomeModal: this.noWelcomeModal,
922
923 blocked: this.blocked,
924 blockedReason: this.blockedReason,
925
926 account: this.Account.toFormattedJSON(),
927
928 notificationSettings: this.NotificationSetting
929 ? this.NotificationSetting.toFormattedJSON()
930 : undefined,
931
932 videoChannels: [],
933
934 createdAt: this.createdAt,
935
936 pluginAuth: this.pluginAuth,
937
938 lastLoginDate: this.lastLoginDate
939 }
940
941 if (parameters.withAdminFlags) {
942 Object.assign(json, { adminFlags: this.adminFlags })
943 }
944
945 if (Array.isArray(this.Account.VideoChannels) === true) {
946 json.videoChannels = this.Account.VideoChannels
947 .map(c => c.toFormattedJSON())
948 .sort((v1, v2) => {
949 if (v1.createdAt < v2.createdAt) return -1
950 if (v1.createdAt === v2.createdAt) return 0
951
952 return 1
953 })
954 }
955
956 return json
957 }
958
959 toMeFormattedJSON (this: MMyUserFormattable): MyUser {
960 const formatted = this.toFormattedJSON()
961
962 const specialPlaylists = this.Account.VideoPlaylists
963 .map(p => ({ id: p.id, name: p.name, type: p.type }))
964
965 return Object.assign(formatted, { specialPlaylists })
966 }
967}