1 import { values } from 'lodash'
2 import { extname } from 'path'
3 import { literal, Op, Transaction } from 'sequelize'
19 } from 'sequelize-typescript'
20 import { ModelCache } from '@server/models/model-cache'
21 import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
22 import { ActorImage } from '../../../shared/models/actors/actor-image.model'
23 import { activityPubContextify } from '../../helpers/activitypub'
25 isActorFollowersCountValid,
26 isActorFollowingCountValid,
27 isActorPreferredUsernameValid,
28 isActorPrivateKeyValid,
30 } from '../../helpers/custom-validators/activitypub/actor'
31 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
34 ACTIVITY_PUB_ACTOR_TYPES,
39 } from '../../initializers/constants'
42 MActorAccountChannelId,
49 MActorSummaryFormattable,
52 } from '../../types/models'
53 import { AccountModel } from '../account/account'
54 import { ServerModel } from '../server/server'
55 import { isOutdated, throwIfNotValid } from '../utils'
56 import { VideoModel } from '../video/video'
57 import { VideoChannelModel } from '../video/video-channel'
58 import { ActorFollowModel } from './actor-follow'
59 import { ActorImageModel } from './actor-image'
65 export const unusedActorAttributesForAPI = [
75 @DefaultScope(() => ({
82 model: ActorImageModel,
92 model: AccountModel.unscoped(),
96 model: VideoChannelModel.unscoped(),
110 model: ActorImageModel,
115 model: ActorImageModel,
130 fields: [ 'preferredUsername', 'serverId' ],
139 fields: [ 'preferredUsername' ],
146 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
149 fields: [ 'sharedInboxUrl' ]
152 fields: [ 'serverId' ]
155 fields: [ 'avatarId' ]
158 fields: [ 'followersUrl' ]
162 export class ActorModel extends Model {
165 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
166 type: ActivityPubActorType
169 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
171 preferredUsername: string
174 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
175 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
179 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
180 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
184 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
185 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
189 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
191 followersCount: number
194 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
196 followingCount: number
199 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
200 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
204 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
205 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
209 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
210 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
211 sharedInboxUrl: string
214 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
215 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
219 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
220 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
225 remoteCreatedAt: Date
233 @ForeignKey(() => ActorImageModel)
237 @ForeignKey(() => ActorImageModel)
241 @BelongsTo(() => ActorImageModel, {
247 onDelete: 'set null',
250 Avatar: ActorImageModel
252 @BelongsTo(() => ActorImageModel, {
258 onDelete: 'set null',
261 Banner: ActorImageModel
263 @HasMany(() => ActorFollowModel, {
268 as: 'ActorFollowings',
271 ActorFollowing: ActorFollowModel[]
273 @HasMany(() => ActorFollowModel, {
275 name: 'targetActorId',
278 as: 'ActorFollowers',
281 ActorFollowers: ActorFollowModel[]
283 @ForeignKey(() => ServerModel)
287 @BelongsTo(() => ServerModel, {
295 @HasOne(() => AccountModel, {
302 Account: AccountModel
304 @HasOne(() => VideoChannelModel, {
311 VideoChannel: VideoChannelModel
313 static load (id: number): Promise<MActor> {
314 return ActorModel.unscoped().findByPk(id)
317 static loadFull (id: number): Promise<MActorFull> {
318 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
321 static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
325 attributes: [ 'id' ],
326 model: AccountModel.unscoped(),
330 attributes: [ 'id' ],
331 model: VideoChannelModel.unscoped(),
335 attributes: [ 'id' ],
336 model: VideoModel.unscoped(),
350 return ActorModel.unscoped().findOne(query)
353 static isActorUrlExist (url: string) {
361 return ActorModel.unscoped().findOne(query)
365 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
369 [Op.in]: followersUrls
375 return ActorModel.scope(ScopeNames.FULL).findAll(query)
378 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
388 return ActorModel.scope(ScopeNames.FULL)
392 return ModelCache.Instance.doCache({
393 cacheType: 'local-actor-name',
394 key: preferredUsername,
395 // The server actor never change, so we can easily cache it
396 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
401 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
404 attributes: [ 'url' ],
412 return ActorModel.unscoped()
416 return ModelCache.Instance.doCache({
417 cacheType: 'local-actor-name',
418 key: preferredUsername,
419 // The server actor never change, so we can easily cache it
420 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
425 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
441 return ActorModel.scope(ScopeNames.FULL).findOne(query)
444 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
452 attributes: [ 'id' ],
453 model: AccountModel.unscoped(),
457 attributes: [ 'id' ],
458 model: VideoChannelModel.unscoped(),
464 return ActorModel.unscoped().findOne(query)
467 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
475 return ActorModel.scope(ScopeNames.FULL).findOne(query)
478 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
479 const sanitizedOfId = parseInt(ofId + '', 10)
480 const where = { id: sanitizedOfId }
482 let columnToUpdate: string
483 let columnOfCount: string
485 if (type === 'followers') {
486 columnToUpdate = 'followersCount'
487 columnOfCount = 'targetActorId'
489 columnToUpdate = 'followingCount'
490 columnOfCount = 'actorId'
493 return ActorModel.update({
494 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
495 }, { where, transaction })
498 static loadAccountActorByVideoId (videoId: number): Promise<MActor> {
502 attributes: [ 'id' ],
503 model: AccountModel.unscoped(),
507 attributes: [ 'id', 'accountId' ],
508 model: VideoChannelModel.unscoped(),
512 attributes: [ 'id', 'channelId' ],
513 model: VideoModel.unscoped(),
525 return ActorModel.unscoped().findOne(query)
528 getSharedInbox (this: MActorWithInboxes) {
529 return this.sharedInboxUrl || this.inboxUrl
532 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
533 let avatar: ActorImage = null
535 avatar = this.Avatar.toFormattedJSON()
540 name: this.preferredUsername,
541 host: this.getHost(),
546 toFormattedJSON (this: MActorFormattable) {
547 const base = this.toFormattedSummaryJSON()
549 let banner: ActorImage = null
551 banner = this.Banner.toFormattedJSON()
554 return Object.assign(base, {
556 hostRedundancyAllowed: this.getRedundancyAllowed(),
557 followingCount: this.followingCount,
558 followersCount: this.followersCount,
560 createdAt: this.getCreatedAt()
564 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
565 let icon: ActivityIconObject
566 let image: ActivityIconObject
569 const extension = extname(this.Avatar.filename)
573 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
574 height: this.Avatar.height,
575 width: this.Avatar.width,
576 url: this.getAvatarUrl()
581 const banner = (this as MActorAPChannel).Banner
582 const extension = extname(banner.filename)
586 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
587 height: banner.height,
589 url: this.getBannerUrl()
596 following: this.getFollowingUrl(),
597 followers: this.getFollowersUrl(),
598 playlists: this.getPlaylistsUrl(),
599 inbox: this.inboxUrl,
600 outbox: this.outboxUrl,
601 preferredUsername: this.preferredUsername,
605 sharedInbox: this.sharedInboxUrl
608 id: this.getPublicKeyUrl(),
610 publicKeyPem: this.publicKey
612 published: this.getCreatedAt().toISOString(),
617 return activityPubContextify(json)
620 getFollowerSharedInboxUrls (t: Transaction) {
622 attributes: [ 'sharedInboxUrl' ],
626 model: ActorFollowModel.unscoped(),
628 as: 'ActorFollowing',
631 targetActorId: this.id
638 return ActorModel.findAll(query)
639 .then(accounts => accounts.map(a => a.sharedInboxUrl))
643 return this.url + '/following'
647 return this.url + '/followers'
651 return this.url + '/playlists'
655 return this.url + '#main-key'
659 return this.serverId === null
662 getWebfingerUrl (this: MActorServer) {
663 return 'acct:' + this.preferredUsername + '@' + this.getHost()
667 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
670 getHost (this: MActorHost) {
671 return this.Server ? this.Server.host : WEBSERVER.HOST
674 getRedundancyAllowed () {
675 return this.Server ? this.Server.redundancyAllowed : false
679 if (!this.avatarId) return undefined
681 return WEBSERVER.URL + this.Avatar.getStaticPath()
685 if (!this.bannerId) return undefined
687 return WEBSERVER.URL + this.Banner.getStaticPath()
691 if (this.isOwned()) return false
693 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
696 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
697 return this.remoteCreatedAt || this.createdAt