]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/actor/actor.ts
Add Podcast RSS feeds (#5487)
[github/Chocobozzz/PeerTube.git] / server / models / actor / actor.ts
CommitLineData
85c20aae 1import { col, fn, literal, Op, QueryTypes, Transaction, where } from 'sequelize'
fadf619a 2import {
2422c46b
C
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
2422c46b
C
8 DefaultScope,
9 ForeignKey,
10 HasMany,
11 HasOne,
12 Is,
2422c46b
C
13 Model,
14 Scopes,
15 Table,
16 UpdatedAt
fadf619a 17} from 'sequelize-typescript'
7e98a7df 18import { activityPubContextify } from '@server/lib/activitypub/context'
d0800f76 19import { getBiggestActorImage } from '@server/lib/actor-image'
8c4bbd94 20import { ModelCache } from '@server/models/shared/model-cache'
4638cd71 21import { forceNumber, getLowercaseExtension } from '@shared/core-utils'
d0800f76 22import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
6b5f72be 23import { AttributesOnly } from '@shared/typescript-utils'
fadf619a 24import {
2422c46b
C
25 isActorFollowersCountValid,
26 isActorFollowingCountValid,
27 isActorPreferredUsernameValid,
28 isActorPrivateKeyValid,
da854ddd
C
29 isActorPublicKeyValid
30} from '../../helpers/custom-validators/activitypub/actor'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
2cb03dc1
C
32import {
33 ACTIVITY_PUB,
34 ACTIVITY_PUB_ACTOR_TYPES,
35 CONSTRAINTS_FIELDS,
36 MIMETYPES,
37 SERVER_ACTOR_NAME,
38 WEBSERVER
39} from '../../initializers/constants'
1ca9f7c3
C
40import {
41 MActor,
42 MActorAccountChannelId,
2cb03dc1
C
43 MActorAPAccount,
44 MActorAPChannel,
3396e653 45 MActorFollowersUrl,
1ca9f7c3 46 MActorFormattable,
b5fecbf4
C
47 MActorFull,
48 MActorHost,
cb0eda56 49 MActorHostOnly,
3396e653 50 MActorId,
b49f22d8
C
51 MActorSummaryFormattable,
52 MActorUrl,
47581df0 53 MActorWithInboxes
26d6bf65 54} from '../../types/models'
b49f22d8 55import { AccountModel } from '../account/account'
3396e653 56import { getServerActor } from '../application/application'
b49f22d8 57import { ServerModel } from '../server/server'
8c4bbd94 58import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../shared'
b49f22d8
C
59import { VideoModel } from '../video/video'
60import { VideoChannelModel } from '../video/video-channel'
61import { ActorFollowModel } from './actor-follow'
7d9ba5c0 62import { ActorImageModel } from './actor-image'
fadf619a 63
50d6de9c
C
64enum ScopeNames {
65 FULL = 'FULL'
66}
67
eb66ee88 68export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = [
f37dc0dd
C
69 'publicKey',
70 'privateKey',
71 'inboxUrl',
72 'outboxUrl',
73 'sharedInboxUrl',
74 'followersUrl',
a66c2e32 75 'followingUrl'
f37dc0dd
C
76]
77
3acc5084 78@DefaultScope(() => ({
ce33ee01
C
79 include: [
80 {
3acc5084 81 model: ServerModel,
ce33ee01 82 required: false
c5911fd3
C
83 },
84 {
f4796856 85 model: ActorImageModel,
d0800f76 86 as: 'Avatars',
c5911fd3 87 required: false
ce33ee01
C
88 }
89 ]
3acc5084
C
90}))
91@Scopes(() => ({
50d6de9c
C
92 [ScopeNames.FULL]: {
93 include: [
94 {
3acc5084 95 model: AccountModel.unscoped(),
50d6de9c
C
96 required: false
97 },
98 {
3acc5084 99 model: VideoChannelModel.unscoped(),
c48e82b5
C
100 required: false,
101 include: [
102 {
3acc5084 103 model: AccountModel,
c48e82b5
C
104 required: true
105 }
106 ]
ce33ee01
C
107 },
108 {
3acc5084 109 model: ServerModel,
ce33ee01 110 required: false
c5911fd3
C
111 },
112 {
f4796856 113 model: ActorImageModel,
d0800f76 114 as: 'Avatars',
c5911fd3 115 required: false
2cb03dc1
C
116 },
117 {
118 model: ActorImageModel,
d0800f76 119 as: 'Banners',
2cb03dc1 120 required: false
50d6de9c 121 }
3acc5084 122 ]
50d6de9c 123 }
3acc5084 124}))
fadf619a 125@Table({
50d6de9c
C
126 tableName: 'actor',
127 indexes: [
2ccaeeb3 128 {
8cd72bd3
C
129 fields: [ 'url' ],
130 unique: true
2ccaeeb3 131 },
50d6de9c 132 {
85c20aae
C
133 fields: [ fn('lower', col('preferredUsername')), 'serverId' ],
134 name: 'actor_preferred_username_lower_server_id',
77e08517
C
135 unique: true,
136 where: {
137 serverId: {
138 [Op.ne]: null
139 }
140 }
141 },
0f06c4de 142 {
85c20aae
C
143 fields: [ fn('lower', col('preferredUsername')) ],
144 name: 'actor_preferred_username_lower',
0f06c4de
C
145 unique: true,
146 where: {
147 serverId: null
148 }
149 },
6502c3d4
C
150 {
151 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
57c36b27 152 },
a3d1026b
C
153 {
154 fields: [ 'sharedInboxUrl' ]
155 },
57c36b27
C
156 {
157 fields: [ 'serverId' ]
158 },
8cd72bd3
C
159 {
160 fields: [ 'followersUrl' ]
50d6de9c
C
161 }
162 ]
fadf619a 163})
16c016e8 164export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
fadf619a 165
50d6de9c 166 @AllowNull(false)
690bb8f9 167 @Column(DataType.ENUM(...Object.values(ACTIVITY_PUB_ACTOR_TYPES)))
50d6de9c
C
168 type: ActivityPubActorType
169
fadf619a 170 @AllowNull(false)
e12a0092 171 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
fadf619a 172 @Column
e12a0092 173 preferredUsername: string
fadf619a
C
174
175 @AllowNull(false)
176 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
01de67b9 177 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
178 url: string
179
180 @AllowNull(true)
1735c825 181 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
01de67b9 182 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
fadf619a
C
183 publicKey: string
184
185 @AllowNull(true)
1735c825 186 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
01de67b9 187 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
fadf619a
C
188 privateKey: string
189
190 @AllowNull(false)
191 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
192 @Column
193 followersCount: number
194
195 @AllowNull(false)
196 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
197 @Column
198 followingCount: number
199
200 @AllowNull(false)
201 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
01de67b9 202 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
203 inboxUrl: string
204
0b5c385b
C
205 @AllowNull(true)
206 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
01de67b9 207 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
208 outboxUrl: string
209
47581df0
C
210 @AllowNull(true)
211 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
01de67b9 212 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
213 sharedInboxUrl: string
214
0b5c385b
C
215 @AllowNull(true)
216 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
01de67b9 217 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
218 followersUrl: string
219
0b5c385b
C
220 @AllowNull(true)
221 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
01de67b9 222 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
223 followingUrl: string
224
a66c2e32
C
225 @AllowNull(true)
226 @Column
227 remoteCreatedAt: Date
228
fadf619a
C
229 @CreatedAt
230 createdAt: Date
231
232 @UpdatedAt
233 updatedAt: Date
234
d0800f76 235 @HasMany(() => ActorImageModel, {
236 as: 'Avatars',
237 onDelete: 'cascade',
238 hooks: true,
f4796856 239 foreignKey: {
d0800f76 240 allowNull: false
f4796856 241 },
d0800f76 242 scope: {
243 type: ActorImageType.AVATAR
244 }
f4796856 245 })
d0800f76 246 Avatars: ActorImageModel[]
f4796856 247
d0800f76 248 @HasMany(() => ActorImageModel, {
249 as: 'Banners',
250 onDelete: 'cascade',
251 hooks: true,
fadf619a 252 foreignKey: {
d0800f76 253 allowNull: false
fadf619a 254 },
d0800f76 255 scope: {
256 type: ActorImageType.BANNER
257 }
fadf619a 258 })
d0800f76 259 Banners: ActorImageModel[]
fadf619a 260
50d6de9c 261 @HasMany(() => ActorFollowModel, {
fadf619a 262 foreignKey: {
50d6de9c 263 name: 'actorId',
fadf619a
C
264 allowNull: false
265 },
cef534ed 266 as: 'ActorFollowings',
fadf619a
C
267 onDelete: 'cascade'
268 })
54e74059 269 ActorFollowing: ActorFollowModel[]
fadf619a 270
50d6de9c 271 @HasMany(() => ActorFollowModel, {
fadf619a 272 foreignKey: {
50d6de9c 273 name: 'targetActorId',
fadf619a
C
274 allowNull: false
275 },
54e74059 276 as: 'ActorFollowers',
fadf619a
C
277 onDelete: 'cascade'
278 })
54e74059 279 ActorFollowers: ActorFollowModel[]
fadf619a
C
280
281 @ForeignKey(() => ServerModel)
282 @Column
283 serverId: number
284
285 @BelongsTo(() => ServerModel, {
286 foreignKey: {
287 allowNull: true
288 },
289 onDelete: 'cascade'
290 })
291 Server: ServerModel
292
50d6de9c
C
293 @HasOne(() => AccountModel, {
294 foreignKey: {
c5a893d5
C
295 allowNull: true
296 },
297 onDelete: 'cascade',
298 hooks: true
50d6de9c
C
299 })
300 Account: AccountModel
301
302 @HasOne(() => VideoChannelModel, {
303 foreignKey: {
c5a893d5
C
304 allowNull: true
305 },
306 onDelete: 'cascade',
307 hooks: true
50d6de9c
C
308 })
309 VideoChannel: VideoChannelModel
310
eb66ee88
C
311 // ---------------------------------------------------------------------------
312
313 static getSQLAttributes (tableName: string, aliasPrefix = '') {
314 return buildSQLAttributes({
315 model: this,
316 tableName,
317 aliasPrefix
318 })
319 }
320
321 static getSQLAPIAttributes (tableName: string, aliasPrefix = '') {
322 return buildSQLAttributes({
323 model: this,
324 tableName,
325 aliasPrefix,
326 excludeAttributes: unusedActorAttributesForAPI
327 })
328 }
329
330 // ---------------------------------------------------------------------------
331
85c20aae
C
332 static wherePreferredUsername (preferredUsername: string, colName = 'preferredUsername') {
333 return where(fn('lower', col(colName)), preferredUsername.toLowerCase())
334 }
335
336 // ---------------------------------------------------------------------------
337
3396e653
C
338 static async load (id: number): Promise<MActor> {
339 const actorServer = await getServerActor()
340 if (id === actorServer.id) return actorServer
341
9b39106d 342 return ActorModel.unscoped().findByPk(id)
50d6de9c
C
343 }
344
b49f22d8 345 static loadFull (id: number): Promise<MActorFull> {
453e83ea
C
346 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
347 }
348
3396e653
C
349 static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
350 const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
351 `FROM "actor" ` +
352 `INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
353 `INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
354 `INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
355
356 const options = {
357 type: QueryTypes.SELECT as QueryTypes.SELECT,
358 replacements: { videoId },
359 plain: true as true,
e5565833
C
360 transaction
361 }
362
3396e653 363 return ActorModel.sequelize.query<MActorId & MActorFollowersUrl>(query, options)
d4defe07
C
364 }
365
b49f22d8 366 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
fadf619a
C
367 const query = {
368 where: {
369 followersUrl: {
a1587156 370 [Op.in]: followersUrls
fadf619a
C
371 }
372 },
373 transaction
374 }
375
50d6de9c
C
376 return ActorModel.scope(ScopeNames.FULL).findAll(query)
377 }
378
b49f22d8 379 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
0ffd6d32
C
380 const fun = () => {
381 const query = {
382 where: {
85c20aae 383 [Op.and]: [
65b2ec67 384 this.wherePreferredUsername(preferredUsername, '"ActorModel"."preferredUsername"'),
85c20aae
C
385 {
386 serverId: null
387 }
388 ]
0ffd6d32
C
389 },
390 transaction
391 }
e4a686b4 392
d0800f76 393 return ActorModel.scope(ScopeNames.FULL).findOne(query)
50d6de9c
C
394 }
395
0ffd6d32
C
396 return ModelCache.Instance.doCache({
397 cacheType: 'local-actor-name',
398 key: preferredUsername,
399 // The server actor never change, so we can easily cache it
400 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
401 fun
402 })
0374b6b5
C
403 }
404
b49f22d8 405 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
0ffd6d32
C
406 const fun = () => {
407 const query = {
408 attributes: [ 'url' ],
409 where: {
85c20aae
C
410 [Op.and]: [
411 this.wherePreferredUsername(preferredUsername),
412 {
413 serverId: null
414 }
415 ]
0ffd6d32
C
416 },
417 transaction
418 }
0374b6b5 419
d0800f76 420 return ActorModel.unscoped().findOne(query)
0374b6b5
C
421 }
422
0ffd6d32 423 return ModelCache.Instance.doCache({
85c20aae 424 cacheType: 'local-actor-url',
0ffd6d32
C
425 key: preferredUsername,
426 // The server actor never change, so we can easily cache it
427 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
428 fun
429 })
50d6de9c
C
430 }
431
b49f22d8 432 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
50d6de9c 433 const query = {
65b2ec67 434 where: this.wherePreferredUsername(preferredUsername, '"ActorModel"."preferredUsername"'),
50d6de9c
C
435 include: [
436 {
437 model: ServerModel,
438 required: true,
439 where: {
440 host
441 }
442 }
443 ]
444 }
445
446 return ActorModel.scope(ScopeNames.FULL).findOne(query)
447 }
448
b49f22d8 449 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
e587e0ec
C
450 const query = {
451 where: {
452 url
453 },
454 transaction,
455 include: [
456 {
457 attributes: [ 'id' ],
458 model: AccountModel.unscoped(),
459 required: false
460 },
461 {
462 attributes: [ 'id' ],
463 model: VideoChannelModel.unscoped(),
464 required: false
465 }
466 ]
467 }
468
469 return ActorModel.unscoped().findOne(query)
470 }
471
b49f22d8 472 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
50d6de9c
C
473 const query = {
474 where: {
475 url
476 },
477 transaction
478 }
479
480 return ActorModel.scope(ScopeNames.FULL).findOne(query)
fadf619a
C
481 }
482
e6122097 483 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
4638cd71 484 const sanitizedOfId = forceNumber(ofId)
e6122097
C
485 const where = { id: sanitizedOfId }
486
487 let columnToUpdate: string
488 let columnOfCount: string
489
490 if (type === 'followers') {
491 columnToUpdate = 'followersCount'
492 columnOfCount = 'targetActorId'
493 } else {
494 columnToUpdate = 'followingCount'
495 columnOfCount = 'actorId'
496 }
497
498 return ActorModel.update({
927fa4b1 499 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId} AND "state" = 'accepted')`)
e6122097 500 }, { where, transaction })
32b2b43c
C
501 }
502
06c27593 503 static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
2c8776fc
C
504 const query = {
505 include: [
506 {
507 attributes: [ 'id' ],
508 model: AccountModel.unscoped(),
509 required: true,
510 include: [
511 {
512 attributes: [ 'id', 'accountId' ],
513 model: VideoChannelModel.unscoped(),
514 required: true,
515 include: [
516 {
517 attributes: [ 'id', 'channelId' ],
518 model: VideoModel.unscoped(),
519 where: {
520 id: videoId
521 }
522 }
523 ]
524 }
525 ]
526 }
06c27593
C
527 ],
528 transaction
2c8776fc
C
529 }
530
531 return ActorModel.unscoped().findOne(query)
532 }
533
47581df0
C
534 getSharedInbox (this: MActorWithInboxes) {
535 return this.sharedInboxUrl || this.inboxUrl
536 }
537
1ca9f7c3 538 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
fadf619a 539 return {
4cb6d457 540 url: this.url,
60650c77 541 name: this.preferredUsername,
e12a0092 542 host: this.getHost(),
d0800f76 543 avatars: (this.Avatars || []).map(a => a.toFormattedJSON()),
544
545 // TODO: remove, deprecated in 4.2
546 avatar: this.hasImage(ActorImageType.AVATAR)
547 ? this.Avatars[0].toFormattedJSON()
548 : undefined
1ca9f7c3
C
549 }
550 }
551
552 toFormattedJSON (this: MActorFormattable) {
d0800f76 553 return {
554 ...this.toFormattedSummaryJSON(),
2cb03dc1 555
1ca9f7c3 556 id: this.id,
c48e82b5 557 hostRedundancyAllowed: this.getRedundancyAllowed(),
fadf619a
C
558 followingCount: this.followingCount,
559 followersCount: this.followersCount,
d0800f76 560 createdAt: this.getCreatedAt(),
561
562 banners: (this.Banners || []).map(b => b.toFormattedJSON()),
563
564 // TODO: remove, deprecated in 4.2
565 banner: this.hasImage(ActorImageType.BANNER)
566 ? this.Banners[0].toFormattedJSON()
567 : undefined
568 }
fadf619a
C
569 }
570
2cb03dc1 571 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
a1587156 572 let icon: ActivityIconObject
d0800f76 573 let icons: ActivityIconObject[]
2cb03dc1 574 let image: ActivityIconObject
a1587156 575
d0800f76 576 if (this.hasImage(ActorImageType.AVATAR)) {
577 icon = getBiggestActorImage(this.Avatars).toActivityPubObject()
578 icons = this.Avatars.map(a => a.toActivityPubObject())
c5911fd3
C
579 }
580
d0800f76 581 if (this.hasImage(ActorImageType.BANNER)) {
582 const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
ea54cd04 583 const extension = getLowercaseExtension(banner.filename)
2cb03dc1
C
584
585 image = {
586 type: 'Image',
587 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
84531547
C
588 height: banner.height,
589 width: banner.width,
d0800f76 590 url: ActorImageModel.getImageUrl(banner)
2cb03dc1
C
591 }
592 }
593
fadf619a 594 const json = {
8424c402 595 type: this.type,
fadf619a
C
596 id: this.url,
597 following: this.getFollowingUrl(),
598 followers: this.getFollowersUrl(),
418d092a 599 playlists: this.getPlaylistsUrl(),
fadf619a
C
600 inbox: this.inboxUrl,
601 outbox: this.outboxUrl,
e12a0092 602 preferredUsername: this.preferredUsername,
fadf619a 603 url: this.url,
e12a0092 604 name,
fadf619a
C
605 endpoints: {
606 sharedInbox: this.sharedInboxUrl
607 },
fadf619a
C
608 publicKey: {
609 id: this.getPublicKeyUrl(),
610 owner: this.url,
611 publicKeyPem: this.publicKey
c5911fd3 612 },
a66c2e32 613 published: this.getCreatedAt().toISOString(),
d0800f76 614
2cb03dc1 615 icon,
d0800f76 616 icons,
617
2cb03dc1 618 image
fadf619a
C
619 }
620
a219c910 621 return activityPubContextify(json, 'Actor')
fadf619a
C
622 }
623
941c5eac 624 getFollowerSharedInboxUrls (t: Transaction) {
fadf619a
C
625 const query = {
626 attributes: [ 'sharedInboxUrl' ],
627 include: [
628 {
54e74059
C
629 attribute: [],
630 model: ActorFollowModel.unscoped(),
fadf619a 631 required: true,
d6e99e53 632 as: 'ActorFollowing',
fadf619a 633 where: {
54e74059 634 state: 'accepted',
50d6de9c 635 targetActorId: this.id
fadf619a
C
636 }
637 }
638 ],
639 transaction: t
640 }
641
642 return ActorModel.findAll(query)
643 .then(accounts => accounts.map(a => a.sharedInboxUrl))
644 }
645
646 getFollowingUrl () {
647 return this.url + '/following'
648 }
649
650 getFollowersUrl () {
651 return this.url + '/followers'
652 }
653
418d092a
C
654 getPlaylistsUrl () {
655 return this.url + '/playlists'
656 }
657
fadf619a
C
658 getPublicKeyUrl () {
659 return this.url + '#main-key'
660 }
661
662 isOwned () {
663 return this.serverId === null
664 }
e12a0092 665
cb0eda56 666 getWebfingerUrl (this: MActorHost) {
e12a0092
C
667 return 'acct:' + this.preferredUsername + '@' + this.getHost()
668 }
669
cb0eda56 670 getIdentifier (this: MActorHost) {
80e36cd9
AB
671 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
672 }
673
cb0eda56 674 getHost (this: MActorHostOnly) {
6dd9de95 675 return this.Server ? this.Server.host : WEBSERVER.HOST
e12a0092 676 }
c5911fd3 677
b5fecbf4 678 getRedundancyAllowed () {
c48e82b5
C
679 return this.Server ? this.Server.redundancyAllowed : false
680 }
681
d0800f76 682 hasImage (type: ActorImageType) {
683 const images = type === ActorImageType.AVATAR
684 ? this.Avatars
685 : this.Banners
2cb03dc1 686
d0800f76 687 return Array.isArray(images) && images.length !== 0
2cb03dc1
C
688 }
689
a5625b41
C
690 isOutdated () {
691 if (this.isOwned()) return false
692
9f79ade6 693 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
a5625b41 694 }
a66c2e32
C
695
696 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
697 return this.remoteCreatedAt || this.createdAt
698 }
fadf619a 699}