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