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