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