1 import { values } from 'lodash'
2 import { literal, Op, Transaction } from 'sequelize'
18 } from 'sequelize-typescript'
19 import { getBiggestActorImage } from '@server/lib/actor-image'
20 import { ModelCache } from '@server/models/model-cache'
21 import { getLowercaseExtension } from '@shared/core-utils'
22 import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
23 import { AttributesOnly } from '@shared/typescript-utils'
24 import { activityPubContextify } from '../../helpers/activitypub'
26 isActorFollowersCountValid,
27 isActorFollowingCountValid,
28 isActorPreferredUsernameValid,
29 isActorPrivateKeyValid,
31 } from '../../helpers/custom-validators/activitypub/actor'
32 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
35 ACTIVITY_PUB_ACTOR_TYPES,
40 } from '../../initializers/constants'
43 MActorAccountChannelId,
50 MActorSummaryFormattable,
53 } from '../../types/models'
54 import { AccountModel } from '../account/account'
55 import { ServerModel } from '../server/server'
56 import { isOutdated, throwIfNotValid } from '../utils'
57 import { VideoModel } from '../video/video'
58 import { VideoChannelModel } from '../video/video-channel'
59 import { ActorFollowModel } from './actor-follow'
60 import { ActorImageModel } from './actor-image'
66 export const unusedActorAttributesForAPI = [
76 @DefaultScope(() => ({
83 model: ActorImageModel,
93 model: AccountModel.unscoped(),
97 model: VideoChannelModel.unscoped(),
111 model: ActorImageModel,
116 model: ActorImageModel,
131 fields: [ 'preferredUsername', 'serverId' ],
140 fields: [ 'preferredUsername' ],
147 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
150 fields: [ 'sharedInboxUrl' ]
153 fields: [ 'serverId' ]
156 fields: [ 'followersUrl' ]
160 export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
163 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
164 type: ActivityPubActorType
167 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
169 preferredUsername: string
172 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
173 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
177 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
178 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
182 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
183 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
187 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
189 followersCount: number
192 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
194 followingCount: number
197 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
198 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
202 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
203 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
207 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
208 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
209 sharedInboxUrl: string
212 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
213 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
217 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
218 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
223 remoteCreatedAt: Date
231 @HasMany(() => ActorImageModel, {
239 type: ActorImageType.AVATAR
242 Avatars: ActorImageModel[]
244 @HasMany(() => ActorImageModel, {
252 type: ActorImageType.BANNER
255 Banners: ActorImageModel[]
257 @HasMany(() => ActorFollowModel, {
262 as: 'ActorFollowings',
265 ActorFollowing: ActorFollowModel[]
267 @HasMany(() => ActorFollowModel, {
269 name: 'targetActorId',
272 as: 'ActorFollowers',
275 ActorFollowers: ActorFollowModel[]
277 @ForeignKey(() => ServerModel)
281 @BelongsTo(() => ServerModel, {
289 @HasOne(() => AccountModel, {
296 Account: AccountModel
298 @HasOne(() => VideoChannelModel, {
305 VideoChannel: VideoChannelModel
307 static load (id: number): Promise<MActor> {
308 return ActorModel.unscoped().findByPk(id)
311 static loadFull (id: number): Promise<MActorFull> {
312 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
315 static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
319 attributes: [ 'id' ],
320 model: AccountModel.unscoped(),
324 attributes: [ 'id' ],
325 model: VideoChannelModel.unscoped(),
329 attributes: [ 'id' ],
330 model: VideoModel.unscoped(),
344 return ActorModel.unscoped().findOne(query)
347 static isActorUrlExist (url: string) {
355 return ActorModel.unscoped().findOne(query)
359 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
363 [Op.in]: followersUrls
369 return ActorModel.scope(ScopeNames.FULL).findAll(query)
372 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
382 return ActorModel.scope(ScopeNames.FULL).findOne(query)
385 return ModelCache.Instance.doCache({
386 cacheType: 'local-actor-name',
387 key: preferredUsername,
388 // The server actor never change, so we can easily cache it
389 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
394 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
397 attributes: [ 'url' ],
405 return ActorModel.unscoped().findOne(query)
408 return ModelCache.Instance.doCache({
409 cacheType: 'local-actor-name',
410 key: preferredUsername,
411 // The server actor never change, so we can easily cache it
412 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
417 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
433 return ActorModel.scope(ScopeNames.FULL).findOne(query)
436 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
444 attributes: [ 'id' ],
445 model: AccountModel.unscoped(),
449 attributes: [ 'id' ],
450 model: VideoChannelModel.unscoped(),
456 return ActorModel.unscoped().findOne(query)
459 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
467 return ActorModel.scope(ScopeNames.FULL).findOne(query)
470 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
471 const sanitizedOfId = parseInt(ofId + '', 10)
472 const where = { id: sanitizedOfId }
474 let columnToUpdate: string
475 let columnOfCount: string
477 if (type === 'followers') {
478 columnToUpdate = 'followersCount'
479 columnOfCount = 'targetActorId'
481 columnToUpdate = 'followingCount'
482 columnOfCount = 'actorId'
485 return ActorModel.update({
486 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
487 }, { where, transaction })
490 static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
494 attributes: [ 'id' ],
495 model: AccountModel.unscoped(),
499 attributes: [ 'id', 'accountId' ],
500 model: VideoChannelModel.unscoped(),
504 attributes: [ 'id', 'channelId' ],
505 model: VideoModel.unscoped(),
518 return ActorModel.unscoped().findOne(query)
521 getSharedInbox (this: MActorWithInboxes) {
522 return this.sharedInboxUrl || this.inboxUrl
525 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
528 name: this.preferredUsername,
529 host: this.getHost(),
530 avatars: (this.Avatars || []).map(a => a.toFormattedJSON()),
532 // TODO: remove, deprecated in 4.2
533 avatar: this.hasImage(ActorImageType.AVATAR)
534 ? this.Avatars[0].toFormattedJSON()
539 toFormattedJSON (this: MActorFormattable) {
541 ...this.toFormattedSummaryJSON(),
544 hostRedundancyAllowed: this.getRedundancyAllowed(),
545 followingCount: this.followingCount,
546 followersCount: this.followersCount,
547 createdAt: this.getCreatedAt(),
549 banners: (this.Banners || []).map(b => b.toFormattedJSON()),
551 // TODO: remove, deprecated in 4.2
552 banner: this.hasImage(ActorImageType.BANNER)
553 ? this.Banners[0].toFormattedJSON()
558 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
559 let icon: ActivityIconObject
560 let icons: ActivityIconObject[]
561 let image: ActivityIconObject
563 if (this.hasImage(ActorImageType.AVATAR)) {
564 icon = getBiggestActorImage(this.Avatars).toActivityPubObject()
565 icons = this.Avatars.map(a => a.toActivityPubObject())
568 if (this.hasImage(ActorImageType.BANNER)) {
569 const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
570 const extension = getLowercaseExtension(banner.filename)
574 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
575 height: banner.height,
577 url: ActorImageModel.getImageUrl(banner)
584 following: this.getFollowingUrl(),
585 followers: this.getFollowersUrl(),
586 playlists: this.getPlaylistsUrl(),
587 inbox: this.inboxUrl,
588 outbox: this.outboxUrl,
589 preferredUsername: this.preferredUsername,
593 sharedInbox: this.sharedInboxUrl
596 id: this.getPublicKeyUrl(),
598 publicKeyPem: this.publicKey
600 published: this.getCreatedAt().toISOString(),
608 return activityPubContextify(json)
611 getFollowerSharedInboxUrls (t: Transaction) {
613 attributes: [ 'sharedInboxUrl' ],
617 model: ActorFollowModel.unscoped(),
619 as: 'ActorFollowing',
622 targetActorId: this.id
629 return ActorModel.findAll(query)
630 .then(accounts => accounts.map(a => a.sharedInboxUrl))
634 return this.url + '/following'
638 return this.url + '/followers'
642 return this.url + '/playlists'
646 return this.url + '#main-key'
650 return this.serverId === null
653 getWebfingerUrl (this: MActorServer) {
654 return 'acct:' + this.preferredUsername + '@' + this.getHost()
658 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
661 getHost (this: MActorHost) {
662 return this.Server ? this.Server.host : WEBSERVER.HOST
665 getRedundancyAllowed () {
666 return this.Server ? this.Server.redundancyAllowed : false
669 hasImage (type: ActorImageType) {
670 const images = type === ActorImageType.AVATAR
674 return Array.isArray(images) && images.length !== 0
678 if (this.isOwned()) return false
680 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
683 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
684 return this.remoteCreatedAt || this.createdAt