aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/account
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/models/account
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-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.ts236
-rw-r--r--server/models/account/account-video-rate.ts259
-rw-r--r--server/models/account/account.ts468
-rw-r--r--server/models/account/actor-custom-page.ts69
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 @@
1import { FindOptions, Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { handlesToNameAndHost } from '@server/helpers/actors'
4import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models'
5import { AttributesOnly } from '@shared/typescript-utils'
6import { AccountBlock } from '../../../shared/models'
7import { ActorModel } from '../actor/actor'
8import { ServerModel } from '../server/server'
9import { createSafeIn, getSort, searchAttribute } from '../shared'
10import { 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})
24export 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 @@
1import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import {
4 MAccountVideoRate,
5 MAccountVideoRateAccountUrl,
6 MAccountVideoRateAccountVideo,
7 MAccountVideoRateFormattable
8} from '@server/types/models'
9import { AccountVideoRate, VideoRateType } from '@shared/models'
10import { AttributesOnly } from '@shared/typescript-utils'
11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
12import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
13import { ActorModel } from '../actor/actor'
14import { getSort, throwIfNotValid } from '../shared'
15import { VideoModel } from '../video/video'
16import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
17import { 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})
44export 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 @@
1import { FindOptions, Includeable, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize'
2import {
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'
19import { ModelCache } from '@server/models/shared/model-cache'
20import { AttributesOnly } from '@shared/typescript-utils'
21import { Account, AccountSummary } from '../../../shared/models/actors'
22import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
23import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
24import { sendDeleteActor } from '../../lib/activitypub/send/send-delete'
25import {
26 MAccount,
27 MAccountActor,
28 MAccountAP,
29 MAccountDefault,
30 MAccountFormattable,
31 MAccountHost,
32 MAccountSummaryFormattable,
33 MChannelHost
34} from '../../types/models'
35import { ActorModel } from '../actor/actor'
36import { ActorFollowModel } from '../actor/actor-follow'
37import { ActorImageModel } from '../actor/actor-image'
38import { ApplicationModel } from '../application/application'
39import { ServerModel } from '../server/server'
40import { ServerBlocklistModel } from '../server/server-blocklist'
41import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared'
42import { UserModel } from '../user/user'
43import { VideoModel } from '../video/video'
44import { VideoChannelModel } from '../video/video-channel'
45import { VideoCommentModel } from '../video/video-comment'
46import { VideoPlaylistModel } from '../video/video-playlist'
47import { AccountBlocklistModel } from './account-blocklist'
48
49export enum ScopeNames {
50 SUMMARY = 'SUMMARY'
51}
52
53export 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})
149export 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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { CustomPage } from '@shared/models'
3import { ActorModel } from '../actor/actor'
4import { getServerActor } from '../application/application'
5
6@Table({
7 tableName: 'actorCustomPage',
8 indexes: [
9 {
10 fields: [ 'actorId', 'type' ],
11 unique: true
12 }
13 ]
14})
15export 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}