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