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