diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/models/account | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/models/account')
-rw-r--r-- | server/models/account/account-blocklist.ts | 236 | ||||
-rw-r--r-- | server/models/account/account-video-rate.ts | 259 | ||||
-rw-r--r-- | server/models/account/account.ts | 468 | ||||
-rw-r--r-- | server/models/account/actor-custom-page.ts | 69 |
4 files changed, 0 insertions, 1032 deletions
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts deleted file mode 100644 index f6212ff6e..000000000 --- a/server/models/account/account-blocklist.ts +++ /dev/null | |||
@@ -1,236 +0,0 @@ | |||
1 | import { FindOptions, Op, QueryTypes } from 'sequelize' | ||
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { handlesToNameAndHost } from '@server/helpers/actors' | ||
4 | import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models' | ||
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountBlock } from '../../../shared/models' | ||
7 | import { ActorModel } from '../actor/actor' | ||
8 | import { ServerModel } from '../server/server' | ||
9 | import { createSafeIn, getSort, searchAttribute } from '../shared' | ||
10 | import { AccountModel } from './account' | ||
11 | |||
12 | @Table({ | ||
13 | tableName: 'accountBlocklist', | ||
14 | indexes: [ | ||
15 | { | ||
16 | fields: [ 'accountId', 'targetAccountId' ], | ||
17 | unique: true | ||
18 | }, | ||
19 | { | ||
20 | fields: [ 'targetAccountId' ] | ||
21 | } | ||
22 | ] | ||
23 | }) | ||
24 | export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountBlocklistModel>>> { | ||
25 | |||
26 | @CreatedAt | ||
27 | createdAt: Date | ||
28 | |||
29 | @UpdatedAt | ||
30 | updatedAt: Date | ||
31 | |||
32 | @ForeignKey(() => AccountModel) | ||
33 | @Column | ||
34 | accountId: number | ||
35 | |||
36 | @BelongsTo(() => AccountModel, { | ||
37 | foreignKey: { | ||
38 | name: 'accountId', | ||
39 | allowNull: false | ||
40 | }, | ||
41 | as: 'ByAccount', | ||
42 | onDelete: 'CASCADE' | ||
43 | }) | ||
44 | ByAccount: AccountModel | ||
45 | |||
46 | @ForeignKey(() => AccountModel) | ||
47 | @Column | ||
48 | targetAccountId: number | ||
49 | |||
50 | @BelongsTo(() => AccountModel, { | ||
51 | foreignKey: { | ||
52 | name: 'targetAccountId', | ||
53 | allowNull: false | ||
54 | }, | ||
55 | as: 'BlockedAccount', | ||
56 | onDelete: 'CASCADE' | ||
57 | }) | ||
58 | BlockedAccount: AccountModel | ||
59 | |||
60 | static isAccountMutedByAccounts (accountIds: number[], targetAccountId: number) { | ||
61 | const query = { | ||
62 | attributes: [ 'accountId', 'id' ], | ||
63 | where: { | ||
64 | accountId: { | ||
65 | [Op.in]: accountIds | ||
66 | }, | ||
67 | targetAccountId | ||
68 | }, | ||
69 | raw: true | ||
70 | } | ||
71 | |||
72 | return AccountBlocklistModel.unscoped() | ||
73 | .findAll(query) | ||
74 | .then(rows => { | ||
75 | const result: { [accountId: number]: boolean } = {} | ||
76 | |||
77 | for (const accountId of accountIds) { | ||
78 | result[accountId] = !!rows.find(r => r.accountId === accountId) | ||
79 | } | ||
80 | |||
81 | return result | ||
82 | }) | ||
83 | } | ||
84 | |||
85 | static loadByAccountAndTarget (accountId: number, targetAccountId: number): Promise<MAccountBlocklist> { | ||
86 | const query = { | ||
87 | where: { | ||
88 | accountId, | ||
89 | targetAccountId | ||
90 | } | ||
91 | } | ||
92 | |||
93 | return AccountBlocklistModel.findOne(query) | ||
94 | } | ||
95 | |||
96 | static listForApi (parameters: { | ||
97 | start: number | ||
98 | count: number | ||
99 | sort: string | ||
100 | search?: string | ||
101 | accountId: number | ||
102 | }) { | ||
103 | const { start, count, sort, search, accountId } = parameters | ||
104 | |||
105 | const getQuery = (forCount: boolean) => { | ||
106 | const query: FindOptions = { | ||
107 | offset: start, | ||
108 | limit: count, | ||
109 | order: getSort(sort), | ||
110 | where: { accountId } | ||
111 | } | ||
112 | |||
113 | if (search) { | ||
114 | Object.assign(query.where, { | ||
115 | [Op.or]: [ | ||
116 | searchAttribute(search, '$BlockedAccount.name$'), | ||
117 | searchAttribute(search, '$BlockedAccount.Actor.url$') | ||
118 | ] | ||
119 | }) | ||
120 | } | ||
121 | |||
122 | if (forCount !== true) { | ||
123 | query.include = [ | ||
124 | { | ||
125 | model: AccountModel, | ||
126 | required: true, | ||
127 | as: 'ByAccount' | ||
128 | }, | ||
129 | { | ||
130 | model: AccountModel, | ||
131 | required: true, | ||
132 | as: 'BlockedAccount' | ||
133 | } | ||
134 | ] | ||
135 | } else if (search) { // We need some joins when counting with search | ||
136 | query.include = [ | ||
137 | { | ||
138 | model: AccountModel.unscoped(), | ||
139 | required: true, | ||
140 | as: 'BlockedAccount', | ||
141 | include: [ | ||
142 | { | ||
143 | model: ActorModel.unscoped(), | ||
144 | required: true | ||
145 | } | ||
146 | ] | ||
147 | } | ||
148 | ] | ||
149 | } | ||
150 | |||
151 | return query | ||
152 | } | ||
153 | |||
154 | return Promise.all([ | ||
155 | AccountBlocklistModel.count(getQuery(true)), | ||
156 | AccountBlocklistModel.findAll(getQuery(false)) | ||
157 | ]).then(([ total, data ]) => ({ total, data })) | ||
158 | } | ||
159 | |||
160 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { | ||
161 | const query = { | ||
162 | attributes: [ 'id' ], | ||
163 | where: { | ||
164 | accountId: { | ||
165 | [Op.in]: accountIds | ||
166 | } | ||
167 | }, | ||
168 | include: [ | ||
169 | { | ||
170 | attributes: [ 'id' ], | ||
171 | model: AccountModel.unscoped(), | ||
172 | required: true, | ||
173 | as: 'BlockedAccount', | ||
174 | include: [ | ||
175 | { | ||
176 | attributes: [ 'preferredUsername' ], | ||
177 | model: ActorModel.unscoped(), | ||
178 | required: true, | ||
179 | include: [ | ||
180 | { | ||
181 | attributes: [ 'host' ], | ||
182 | model: ServerModel.unscoped(), | ||
183 | required: true | ||
184 | } | ||
185 | ] | ||
186 | } | ||
187 | ] | ||
188 | } | ||
189 | ] | ||
190 | } | ||
191 | |||
192 | return AccountBlocklistModel.findAll(query) | ||
193 | .then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`)) | ||
194 | } | ||
195 | |||
196 | static getBlockStatus (byAccountIds: number[], handles: string[]): Promise<{ name: string, host: string, accountId: number }[]> { | ||
197 | const sanitizedHandles = handlesToNameAndHost(handles) | ||
198 | |||
199 | const localHandles = sanitizedHandles.filter(h => !h.host) | ||
200 | .map(h => h.name) | ||
201 | |||
202 | const remoteHandles = sanitizedHandles.filter(h => !!h.host) | ||
203 | .map(h => ([ h.name, h.host ])) | ||
204 | |||
205 | const handlesWhere: string[] = [] | ||
206 | |||
207 | if (localHandles.length !== 0) { | ||
208 | handlesWhere.push(`("actor"."preferredUsername" IN (:localHandles) AND "server"."id" IS NULL)`) | ||
209 | } | ||
210 | |||
211 | if (remoteHandles.length !== 0) { | ||
212 | handlesWhere.push(`(("actor"."preferredUsername", "server"."host") IN (:remoteHandles))`) | ||
213 | } | ||
214 | |||
215 | const rawQuery = `SELECT "accountBlocklist"."accountId", "actor"."preferredUsername" AS "name", "server"."host" ` + | ||
216 | `FROM "accountBlocklist" ` + | ||
217 | `INNER JOIN "account" ON "account"."id" = "accountBlocklist"."targetAccountId" ` + | ||
218 | `INNER JOIN "actor" ON "actor"."id" = "account"."actorId" ` + | ||
219 | `LEFT JOIN "server" ON "server"."id" = "actor"."serverId" ` + | ||
220 | `WHERE "accountBlocklist"."accountId" IN (${createSafeIn(AccountBlocklistModel.sequelize, byAccountIds)}) ` + | ||
221 | `AND (${handlesWhere.join(' OR ')})` | ||
222 | |||
223 | return AccountBlocklistModel.sequelize.query(rawQuery, { | ||
224 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
225 | replacements: { byAccountIds, localHandles, remoteHandles } | ||
226 | }) | ||
227 | } | ||
228 | |||
229 | toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock { | ||
230 | return { | ||
231 | byAccount: this.ByAccount.toFormattedJSON(), | ||
232 | blockedAccount: this.BlockedAccount.toFormattedJSON(), | ||
233 | createdAt: this.createdAt | ||
234 | } | ||
235 | } | ||
236 | } | ||
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts deleted file mode 100644 index 18ff07d53..000000000 --- a/server/models/account/account-video-rate.ts +++ /dev/null | |||
@@ -1,259 +0,0 @@ | |||
1 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | ||
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { | ||
4 | MAccountVideoRate, | ||
5 | MAccountVideoRateAccountUrl, | ||
6 | MAccountVideoRateAccountVideo, | ||
7 | MAccountVideoRateFormattable | ||
8 | } from '@server/types/models' | ||
9 | import { AccountVideoRate, VideoRateType } from '@shared/models' | ||
10 | import { AttributesOnly } from '@shared/typescript-utils' | ||
11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' | ||
13 | import { ActorModel } from '../actor/actor' | ||
14 | import { getSort, throwIfNotValid } from '../shared' | ||
15 | import { VideoModel } from '../video/video' | ||
16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | ||
17 | import { AccountModel } from './account' | ||
18 | |||
19 | /* | ||
20 | Account rates per video. | ||
21 | */ | ||
22 | @Table({ | ||
23 | tableName: 'accountVideoRate', | ||
24 | indexes: [ | ||
25 | { | ||
26 | fields: [ 'videoId', 'accountId' ], | ||
27 | unique: true | ||
28 | }, | ||
29 | { | ||
30 | fields: [ 'videoId' ] | ||
31 | }, | ||
32 | { | ||
33 | fields: [ 'accountId' ] | ||
34 | }, | ||
35 | { | ||
36 | fields: [ 'videoId', 'type' ] | ||
37 | }, | ||
38 | { | ||
39 | fields: [ 'url' ], | ||
40 | unique: true | ||
41 | } | ||
42 | ] | ||
43 | }) | ||
44 | export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountVideoRateModel>>> { | ||
45 | |||
46 | @AllowNull(false) | ||
47 | @Column(DataType.ENUM(...Object.values(VIDEO_RATE_TYPES))) | ||
48 | type: VideoRateType | ||
49 | |||
50 | @AllowNull(false) | ||
51 | @Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | ||
52 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max)) | ||
53 | url: string | ||
54 | |||
55 | @CreatedAt | ||
56 | createdAt: Date | ||
57 | |||
58 | @UpdatedAt | ||
59 | updatedAt: Date | ||
60 | |||
61 | @ForeignKey(() => VideoModel) | ||
62 | @Column | ||
63 | videoId: number | ||
64 | |||
65 | @BelongsTo(() => VideoModel, { | ||
66 | foreignKey: { | ||
67 | allowNull: false | ||
68 | }, | ||
69 | onDelete: 'CASCADE' | ||
70 | }) | ||
71 | Video: VideoModel | ||
72 | |||
73 | @ForeignKey(() => AccountModel) | ||
74 | @Column | ||
75 | accountId: number | ||
76 | |||
77 | @BelongsTo(() => AccountModel, { | ||
78 | foreignKey: { | ||
79 | allowNull: false | ||
80 | }, | ||
81 | onDelete: 'CASCADE' | ||
82 | }) | ||
83 | Account: AccountModel | ||
84 | |||
85 | static load (accountId: number, videoId: number, transaction?: Transaction): Promise<MAccountVideoRate> { | ||
86 | const options: FindOptions = { | ||
87 | where: { | ||
88 | accountId, | ||
89 | videoId | ||
90 | } | ||
91 | } | ||
92 | if (transaction) options.transaction = transaction | ||
93 | |||
94 | return AccountVideoRateModel.findOne(options) | ||
95 | } | ||
96 | |||
97 | static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Promise<MAccountVideoRate> { | ||
98 | const options: FindOptions = { | ||
99 | where: { | ||
100 | [Op.or]: [ | ||
101 | { | ||
102 | accountId, | ||
103 | videoId | ||
104 | }, | ||
105 | { | ||
106 | url | ||
107 | } | ||
108 | ] | ||
109 | } | ||
110 | } | ||
111 | if (t) options.transaction = t | ||
112 | |||
113 | return AccountVideoRateModel.findOne(options) | ||
114 | } | ||
115 | |||
116 | static listByAccountForApi (options: { | ||
117 | start: number | ||
118 | count: number | ||
119 | sort: string | ||
120 | type?: string | ||
121 | accountId: number | ||
122 | }) { | ||
123 | const getQuery = (forCount: boolean) => { | ||
124 | const query: FindOptions = { | ||
125 | offset: options.start, | ||
126 | limit: options.count, | ||
127 | order: getSort(options.sort), | ||
128 | where: { | ||
129 | accountId: options.accountId | ||
130 | } | ||
131 | } | ||
132 | |||
133 | if (options.type) query.where['type'] = options.type | ||
134 | |||
135 | if (forCount !== true) { | ||
136 | query.include = [ | ||
137 | { | ||
138 | model: VideoModel, | ||
139 | required: true, | ||
140 | include: [ | ||
141 | { | ||
142 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
143 | required: true | ||
144 | } | ||
145 | ] | ||
146 | } | ||
147 | ] | ||
148 | } | ||
149 | |||
150 | return query | ||
151 | } | ||
152 | |||
153 | return Promise.all([ | ||
154 | AccountVideoRateModel.count(getQuery(true)), | ||
155 | AccountVideoRateModel.findAll(getQuery(false)) | ||
156 | ]).then(([ total, data ]) => ({ total, data })) | ||
157 | } | ||
158 | |||
159 | static listRemoteRateUrlsOfLocalVideos () { | ||
160 | const query = `SELECT "accountVideoRate".url FROM "accountVideoRate" ` + | ||
161 | `INNER JOIN account ON account.id = "accountVideoRate"."accountId" ` + | ||
162 | `INNER JOIN actor ON actor.id = account."actorId" AND actor."serverId" IS NOT NULL ` + | ||
163 | `INNER JOIN video ON video.id = "accountVideoRate"."videoId" AND video.remote IS FALSE` | ||
164 | |||
165 | return AccountVideoRateModel.sequelize.query<{ url: string }>(query, { | ||
166 | type: QueryTypes.SELECT, | ||
167 | raw: true | ||
168 | }).then(rows => rows.map(r => r.url)) | ||
169 | } | ||
170 | |||
171 | static loadLocalAndPopulateVideo ( | ||
172 | rateType: VideoRateType, | ||
173 | accountName: string, | ||
174 | videoId: number, | ||
175 | t?: Transaction | ||
176 | ): Promise<MAccountVideoRateAccountVideo> { | ||
177 | const options: FindOptions = { | ||
178 | where: { | ||
179 | videoId, | ||
180 | type: rateType | ||
181 | }, | ||
182 | include: [ | ||
183 | { | ||
184 | model: AccountModel.unscoped(), | ||
185 | required: true, | ||
186 | include: [ | ||
187 | { | ||
188 | attributes: [ 'id', 'url', 'followersUrl', 'preferredUsername' ], | ||
189 | model: ActorModel.unscoped(), | ||
190 | required: true, | ||
191 | where: { | ||
192 | [Op.and]: [ | ||
193 | ActorModel.wherePreferredUsername(accountName), | ||
194 | { serverId: null } | ||
195 | ] | ||
196 | } | ||
197 | } | ||
198 | ] | ||
199 | }, | ||
200 | { | ||
201 | model: VideoModel.unscoped(), | ||
202 | required: true | ||
203 | } | ||
204 | ] | ||
205 | } | ||
206 | if (t) options.transaction = t | ||
207 | |||
208 | return AccountVideoRateModel.findOne(options) | ||
209 | } | ||
210 | |||
211 | static loadByUrl (url: string, transaction: Transaction) { | ||
212 | const options: FindOptions = { | ||
213 | where: { | ||
214 | url | ||
215 | } | ||
216 | } | ||
217 | if (transaction) options.transaction = transaction | ||
218 | |||
219 | return AccountVideoRateModel.findOne(options) | ||
220 | } | ||
221 | |||
222 | static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { | ||
223 | const query = { | ||
224 | offset: start, | ||
225 | limit: count, | ||
226 | where: { | ||
227 | videoId, | ||
228 | type: rateType | ||
229 | }, | ||
230 | transaction: t, | ||
231 | include: [ | ||
232 | { | ||
233 | attributes: [ 'actorId' ], | ||
234 | model: AccountModel.unscoped(), | ||
235 | required: true, | ||
236 | include: [ | ||
237 | { | ||
238 | attributes: [ 'url' ], | ||
239 | model: ActorModel.unscoped(), | ||
240 | required: true | ||
241 | } | ||
242 | ] | ||
243 | } | ||
244 | ] | ||
245 | } | ||
246 | |||
247 | return Promise.all([ | ||
248 | AccountVideoRateModel.count(query), | ||
249 | AccountVideoRateModel.findAll<MAccountVideoRateAccountUrl>(query) | ||
250 | ]).then(([ total, data ]) => ({ total, data })) | ||
251 | } | ||
252 | |||
253 | toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate { | ||
254 | return { | ||
255 | video: this.Video.toFormattedJSON(), | ||
256 | rating: this.type | ||
257 | } | ||
258 | } | ||
259 | } | ||
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 | } | ||
diff --git a/server/models/account/actor-custom-page.ts b/server/models/account/actor-custom-page.ts deleted file mode 100644 index 893023181..000000000 --- a/server/models/account/actor-custom-page.ts +++ /dev/null | |||
@@ -1,69 +0,0 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { CustomPage } from '@shared/models' | ||
3 | import { ActorModel } from '../actor/actor' | ||
4 | import { getServerActor } from '../application/application' | ||
5 | |||
6 | @Table({ | ||
7 | tableName: 'actorCustomPage', | ||
8 | indexes: [ | ||
9 | { | ||
10 | fields: [ 'actorId', 'type' ], | ||
11 | unique: true | ||
12 | } | ||
13 | ] | ||
14 | }) | ||
15 | export class ActorCustomPageModel extends Model { | ||
16 | |||
17 | @AllowNull(true) | ||
18 | @Column(DataType.TEXT) | ||
19 | content: string | ||
20 | |||
21 | @AllowNull(false) | ||
22 | @Column | ||
23 | type: 'homepage' | ||
24 | |||
25 | @CreatedAt | ||
26 | createdAt: Date | ||
27 | |||
28 | @UpdatedAt | ||
29 | updatedAt: Date | ||
30 | |||
31 | @ForeignKey(() => ActorModel) | ||
32 | @Column | ||
33 | actorId: number | ||
34 | |||
35 | @BelongsTo(() => ActorModel, { | ||
36 | foreignKey: { | ||
37 | name: 'actorId', | ||
38 | allowNull: false | ||
39 | }, | ||
40 | onDelete: 'cascade' | ||
41 | }) | ||
42 | Actor: ActorModel | ||
43 | |||
44 | static async updateInstanceHomepage (content: string) { | ||
45 | const serverActor = await getServerActor() | ||
46 | |||
47 | return ActorCustomPageModel.upsert({ | ||
48 | content, | ||
49 | actorId: serverActor.id, | ||
50 | type: 'homepage' | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | static async loadInstanceHomepage () { | ||
55 | const serverActor = await getServerActor() | ||
56 | |||
57 | return ActorCustomPageModel.findOne({ | ||
58 | where: { | ||
59 | actorId: serverActor.id | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | toFormattedJSON (): CustomPage { | ||
65 | return { | ||
66 | content: this.content | ||
67 | } | ||
68 | } | ||
69 | } | ||