]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/account/account.ts
Set actor preferred name case insensitive
[github/Chocobozzz/PeerTube.git] / server / models / account / account.ts
CommitLineData
b49f22d8 1import { FindOptions, Includeable, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize'
3fd3ab2d 2import {
2422c46b
C
3 AllowNull,
4 BeforeDestroy,
5 BelongsTo,
6 Column,
453e83ea
C
7 CreatedAt,
8 DataType,
2422c46b
C
9 Default,
10 DefaultScope,
11 ForeignKey,
12 HasMany,
13 Is,
09979f89
C
14 Model,
15 Scopes,
2422c46b 16 Table,
3fd3ab2d
C
17 UpdatedAt
18} from 'sequelize-typescript'
8c4bbd94 19import { ModelCache } from '@server/models/shared/model-cache'
6b5f72be 20import { AttributesOnly } from '@shared/typescript-utils'
418d092a 21import { Account, AccountSummary } from '../../../shared/models/actors'
2422c46b 22import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
a59f210f 23import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
c158a5fa 24import { sendDeleteActor } from '../../lib/activitypub/send/send-delete'
a59f210f
C
25import {
26 MAccount,
27 MAccountActor,
28 MAccountAP,
29 MAccountDefault,
30 MAccountFormattable,
31 MAccountSummaryFormattable,
32 MChannelActor
33} from '../../types/models'
7d9ba5c0
C
34import { ActorModel } from '../actor/actor'
35import { ActorFollowModel } from '../actor/actor-follow'
36import { ActorImageModel } from '../actor/actor-image'
3fd3ab2d 37import { ApplicationModel } from '../application/application'
3fd3ab2d 38import { ServerModel } from '../server/server'
a59f210f 39import { ServerBlocklistModel } from '../server/server-blocklist'
8c4bbd94 40import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared'
85c20aae 41import { UserModel } from '../user/user'
a59f210f 42import { VideoModel } from '../video/video'
3fd3ab2d 43import { VideoChannelModel } from '../video/video-channel'
f05a1c30 44import { VideoCommentModel } from '../video/video-comment'
418d092a 45import { VideoPlaylistModel } from '../video/video-playlist'
bfbd9128 46import { AccountBlocklistModel } from './account-blocklist'
418d092a
C
47
48export enum ScopeNames {
49 SUMMARY = 'SUMMARY'
50}
3fd3ab2d 51
bfbd9128 52export type SummaryOptions = {
4f32032f 53 actorRequired?: boolean // Default: true
bfbd9128 54 whereActor?: WhereOptions
fa47956e 55 whereServer?: WhereOptions
bfbd9128 56 withAccountBlockerIds?: number[]
d0800f76 57 forCount?: boolean
bfbd9128
C
58}
59
3acc5084 60@DefaultScope(() => ({
50d6de9c 61 include: [
3fd3ab2d 62 {
3acc5084 63 model: ActorModel, // Default scope includes avatar and server
f37dc0dd 64 required: true
e4f97bab 65 }
e4f97bab 66 ]
3acc5084
C
67}))
68@Scopes(() => ({
a1587156 69 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
bfbd9128
C
70 const serverInclude: IncludeOptions = {
71 attributes: [ 'host' ],
72 model: ServerModel.unscoped(),
fa47956e
C
73 required: !!options.whereServer,
74 where: options.whereServer
bfbd9128
C
75 }
76
d0800f76 77 const actorInclude: Includeable = {
78 attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
79 model: ActorModel.unscoped(),
80 required: options.actorRequired ?? true,
81 where: options.whereActor,
82 include: [ serverInclude ]
83 }
bfbd9128 84
d0800f76 85 if (options.forCount !== true) {
86 actorInclude.include.push({
87 model: ActorImageModel,
88 as: 'Avatars',
89 required: false
90 })
91 }
92
93 const queryInclude: Includeable[] = [
94 actorInclude
b49f22d8
C
95 ]
96
97 const query: FindOptions = {
98 attributes: [ 'id', 'name', 'actorId' ]
418d092a 99 }
bfbd9128
C
100
101 if (options.withAccountBlockerIds) {
b49f22d8 102 queryInclude.push({
bfbd9128
C
103 attributes: [ 'id' ],
104 model: AccountBlocklistModel.unscoped(),
3c10840f 105 as: 'BlockedBy',
bfbd9128
C
106 required: false,
107 where: {
108 accountId: {
109 [Op.in]: options.withAccountBlockerIds
110 }
111 }
112 })
113
114 serverInclude.include = [
115 {
116 attributes: [ 'id' ],
117 model: ServerBlocklistModel.unscoped(),
118 required: false,
119 where: {
120 accountId: {
121 [Op.in]: options.withAccountBlockerIds
122 }
123 }
124 }
125 ]
126 }
127
b49f22d8
C
128 query.include = queryInclude
129
bfbd9128 130 return query
418d092a 131 }
3acc5084 132}))
50d6de9c 133@Table({
8cd72bd3
C
134 tableName: 'account',
135 indexes: [
136 {
137 fields: [ 'actorId' ],
138 unique: true
139 },
140 {
141 fields: [ 'applicationId' ]
142 },
143 {
144 fields: [ 'userId' ]
145 }
146 ]
50d6de9c 147})
16c016e8 148export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
3fd3ab2d 149
50d6de9c 150 @AllowNull(false)
50d6de9c
C
151 @Column
152 name: string
153
2422c46b
C
154 @AllowNull(true)
155 @Default(null)
1735c825 156 @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description', true))
241c3357 157 @Column(DataType.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max))
2422c46b
C
158 description: string
159
3fd3ab2d
C
160 @CreatedAt
161 createdAt: Date
162
163 @UpdatedAt
164 updatedAt: Date
165
fadf619a 166 @ForeignKey(() => ActorModel)
3fd3ab2d 167 @Column
fadf619a 168 actorId: number
e4f97bab 169
fadf619a 170 @BelongsTo(() => ActorModel, {
e4f97bab 171 foreignKey: {
fadf619a 172 allowNull: false
e4f97bab
C
173 },
174 onDelete: 'cascade'
175 })
fadf619a 176 Actor: ActorModel
e4f97bab 177
3fd3ab2d
C
178 @ForeignKey(() => UserModel)
179 @Column
180 userId: number
181
182 @BelongsTo(() => UserModel, {
e4f97bab 183 foreignKey: {
e4f97bab
C
184 allowNull: true
185 },
186 onDelete: 'cascade'
187 })
3fd3ab2d
C
188 User: UserModel
189
190 @ForeignKey(() => ApplicationModel)
191 @Column
192 applicationId: number
e4f97bab 193
3fd3ab2d 194 @BelongsTo(() => ApplicationModel, {
e4f97bab 195 foreignKey: {
e4f97bab
C
196 allowNull: true
197 },
198 onDelete: 'cascade'
199 })
f05a1c30 200 Application: ApplicationModel
e4f97bab 201
3fd3ab2d 202 @HasMany(() => VideoChannelModel, {
e4f97bab 203 foreignKey: {
e4f97bab
C
204 allowNull: false
205 },
206 onDelete: 'cascade',
207 hooks: true
208 })
3fd3ab2d 209 VideoChannels: VideoChannelModel[]
e4f97bab 210
418d092a
C
211 @HasMany(() => VideoPlaylistModel, {
212 foreignKey: {
213 allowNull: false
214 },
215 onDelete: 'cascade',
216 hooks: true
217 })
218 VideoPlaylists: VideoPlaylistModel[]
219
f05a1c30
C
220 @HasMany(() => VideoCommentModel, {
221 foreignKey: {
69222afa 222 allowNull: true
f05a1c30
C
223 },
224 onDelete: 'cascade',
225 hooks: true
226 })
227 VideoComments: VideoCommentModel[]
228
bfbd9128
C
229 @HasMany(() => AccountBlocklistModel, {
230 foreignKey: {
231 name: 'targetAccountId',
232 allowNull: false
233 },
2760b454 234 as: 'BlockedBy',
bfbd9128
C
235 onDelete: 'CASCADE'
236 })
2760b454 237 BlockedBy: AccountBlocklistModel[]
bfbd9128 238
f05a1c30
C
239 @BeforeDestroy
240 static async sendDeleteIfOwned (instance: AccountModel, options) {
241 if (!instance.Actor) {
e6122097 242 instance.Actor = await instance.$get('Actor', { transaction: options.transaction })
f05a1c30
C
243 }
244
44b88f18 245 await ActorFollowModel.removeFollowsOf(instance.Actor.id, options.transaction)
2af337c8 246
c5a893d5 247 if (instance.isOwned()) {
c5a893d5
C
248 return sendDeleteActor(instance.Actor, options.transaction)
249 }
250
251 return undefined
e4f97bab
C
252 }
253
eb66ee88
C
254 // ---------------------------------------------------------------------------
255
256 static getSQLAttributes (tableName: string, aliasPrefix = '') {
257 return buildSQLAttributes({
258 model: this,
259 tableName,
260 aliasPrefix
261 })
262 }
263
264 // ---------------------------------------------------------------------------
265
b49f22d8 266 static load (id: number, transaction?: Transaction): Promise<MAccountDefault> {
9b39106d 267 return AccountModel.findByPk(id, { transaction })
3fd3ab2d 268 }
2295ce6c 269
b49f22d8 270 static loadByNameWithHost (nameWithHost: string): Promise<MAccountDefault> {
92bf2f62
C
271 const [ accountName, host ] = nameWithHost.split('@')
272
6dd9de95 273 if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName)
92bf2f62
C
274
275 return AccountModel.loadByNameAndHost(accountName, host)
276 }
277
b49f22d8 278 static loadLocalByName (name: string): Promise<MAccountDefault> {
0ffd6d32
C
279 const fun = () => {
280 const query = {
281 where: {
282 [Op.or]: [
283 {
284 userId: {
285 [Op.ne]: null
286 }
287 },
288 {
289 applicationId: {
290 [Op.ne]: null
291 }
3fd3ab2d 292 }
0ffd6d32
C
293 ]
294 },
295 include: [
3fd3ab2d 296 {
0ffd6d32
C
297 model: ActorModel,
298 required: true,
85c20aae 299 where: ActorModel.wherePreferredUsername(name)
3fd3ab2d
C
300 }
301 ]
0ffd6d32 302 }
e8cb4409 303
0ffd6d32
C
304 return AccountModel.findOne(query)
305 }
e4a686b4 306
0ffd6d32
C
307 return ModelCache.Instance.doCache({
308 cacheType: 'local-account-name',
309 key: name,
310 fun,
311 // The server actor never change, so we can easily cache it
312 whitelist: () => name === SERVER_ACTOR_NAME
313 })
e8cb4409
C
314 }
315
b49f22d8 316 static loadByNameAndHost (name: string, host: string): Promise<MAccountDefault> {
e8cb4409
C
317 const query = {
318 include: [
319 {
320 model: ActorModel,
321 required: true,
85c20aae 322 where: ActorModel.wherePreferredUsername(name),
e8cb4409
C
323 include: [
324 {
325 model: ServerModel,
326 required: true,
327 where: {
328 host
329 }
330 }
331 ]
332 }
333 ]
3fd3ab2d 334 }
7a7724e6 335
3fd3ab2d
C
336 return AccountModel.findOne(query)
337 }
7a7724e6 338
b49f22d8 339 static loadByUrl (url: string, transaction?: Transaction): Promise<MAccountDefault> {
3fd3ab2d 340 const query = {
fadf619a
C
341 include: [
342 {
343 model: ActorModel,
344 required: true,
345 where: {
346 url
347 }
348 }
349 ],
3fd3ab2d
C
350 transaction
351 }
e4f97bab 352
3fd3ab2d
C
353 return AccountModel.findOne(query)
354 }
e4f97bab 355
265ba139
C
356 static listForApi (start: number, count: number, sort: string) {
357 const query = {
358 offset: start,
359 limit: count,
6ff9c676 360 order: getSort(sort)
265ba139
C
361 }
362
d0800f76 363 return Promise.all([
364 AccountModel.count(),
365 AccountModel.findAll(query)
366 ]).then(([ total, data ]) => ({ total, data }))
265ba139
C
367 }
368
b49f22d8 369 static loadAccountIdFromVideo (videoId: number): Promise<MAccount> {
696d83fd
C
370 const query = {
371 include: [
372 {
373 attributes: [ 'id', 'accountId' ],
374 model: VideoChannelModel.unscoped(),
375 required: true,
376 include: [
377 {
378 attributes: [ 'id', 'channelId' ],
379 model: VideoModel.unscoped(),
380 where: {
381 id: videoId
382 }
383 }
384 ]
385 }
386 ]
387 }
388
389 return AccountModel.findOne(query)
390 }
391
b49f22d8 392 static listLocalsForSitemap (sort: string): Promise<MAccountActor[]> {
2feebf3e
C
393 const query = {
394 attributes: [ ],
395 offset: 0,
396 order: getSort(sort),
397 include: [
398 {
399 attributes: [ 'preferredUsername', 'serverId' ],
400 model: ActorModel.unscoped(),
401 where: {
402 serverId: null
403 }
404 }
405 ]
406 }
407
408 return AccountModel
409 .unscoped()
410 .findAll(query)
411 }
412
d95d1559
C
413 getClientUrl () {
414 return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
415 }
416
1ca9f7c3 417 toFormattedJSON (this: MAccountFormattable): Account {
d0800f76 418 return {
419 ...this.Actor.toFormattedJSON(),
420
3fd3ab2d 421 id: this.id,
244e76a5 422 displayName: this.getDisplayName(),
2422c46b 423 description: this.description,
e024fd6a 424 updatedAt: this.updatedAt,
d0800f76 425 userId: this.userId ?? undefined
3fd3ab2d
C
426 }
427 }
e4f97bab 428
1ca9f7c3
C
429 toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary {
430 const actor = this.Actor.toFormattedSummaryJSON()
418d092a
C
431
432 return {
433 id: this.id,
418d092a 434 displayName: this.getDisplayName(),
d0800f76 435
436 name: actor.name,
418d092a
C
437 url: actor.url,
438 host: actor.host,
d0800f76 439 avatars: actor.avatars,
440
441 // TODO: remove, deprecated in 4.2
418d092a
C
442 avatar: actor.avatar
443 }
444 }
445
3b504f6e
C
446 async toActivityPubObject (this: MAccountAP) {
447 const obj = await this.Actor.toActivityPubObject(this.name)
2422c46b
C
448
449 return Object.assign(obj, {
450 summary: this.description
451 })
e4f97bab
C
452 }
453
3fd3ab2d 454 isOwned () {
fadf619a 455 return this.Actor.isOwned()
3fd3ab2d 456 }
244e76a5 457
744d0eca
C
458 isOutdated () {
459 return this.Actor.isOutdated()
460 }
461
244e76a5
RK
462 getDisplayName () {
463 return this.name
464 }
bfbd9128 465
a59f210f
C
466 getLocalUrl (this: MAccountActor | MChannelActor) {
467 return WEBSERVER.URL + `/accounts/` + this.Actor.preferredUsername
468 }
469
bfbd9128 470 isBlocked () {
2760b454 471 return this.BlockedBy && this.BlockedBy.length !== 0
bfbd9128 472 }
63c93323 473}