1 import { values } from 'lodash'
2 import { literal, Op, Transaction } from 'sequelize'
18 } from 'sequelize-typescript'
19 import { getLowercaseExtension } from '@server/helpers/core-utils'
20 import { ModelCache } from '@server/models/model-cache'
21 import { AttributesOnly } from '@shared/core-utils'
22 import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
23 import { ActorImage } from '../../../shared/models/actors/actor-image.model'
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: [ 'avatarId' ]
159 fields: [ 'followersUrl' ]
163 export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
166 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
167 type: ActivityPubActorType
170 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
172 preferredUsername: string
175 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
176 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
180 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
181 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
185 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
186 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
190 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
192 followersCount: number
195 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
197 followingCount: number
200 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
201 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
205 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
206 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
210 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
211 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
212 sharedInboxUrl: string
215 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
216 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
220 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
221 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
226 remoteCreatedAt: Date
234 @ForeignKey(() => ActorImageModel)
238 @ForeignKey(() => ActorImageModel)
242 @BelongsTo(() => ActorImageModel, {
248 onDelete: 'set null',
251 Avatar: ActorImageModel
253 @BelongsTo(() => ActorImageModel, {
259 onDelete: 'set null',
262 Banner: ActorImageModel
264 @HasMany(() => ActorFollowModel, {
269 as: 'ActorFollowings',
272 ActorFollowing: ActorFollowModel[]
274 @HasMany(() => ActorFollowModel, {
276 name: 'targetActorId',
279 as: 'ActorFollowers',
282 ActorFollowers: ActorFollowModel[]
284 @ForeignKey(() => ServerModel)
288 @BelongsTo(() => ServerModel, {
296 @HasOne(() => AccountModel, {
303 Account: AccountModel
305 @HasOne(() => VideoChannelModel, {
312 VideoChannel: VideoChannelModel
314 static load (id: number): Promise<MActor> {
315 return ActorModel.unscoped().findByPk(id)
318 static loadFull (id: number): Promise<MActorFull> {
319 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
322 static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
326 attributes: [ 'id' ],
327 model: AccountModel.unscoped(),
331 attributes: [ 'id' ],
332 model: VideoChannelModel.unscoped(),
336 attributes: [ 'id' ],
337 model: VideoModel.unscoped(),
351 return ActorModel.unscoped().findOne(query)
354 static isActorUrlExist (url: string) {
362 return ActorModel.unscoped().findOne(query)
366 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
370 [Op.in]: followersUrls
376 return ActorModel.scope(ScopeNames.FULL).findAll(query)
379 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
389 return ActorModel.scope(ScopeNames.FULL)
393 return ModelCache.Instance.doCache({
394 cacheType: 'local-actor-name',
395 key: preferredUsername,
396 // The server actor never change, so we can easily cache it
397 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
402 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
405 attributes: [ 'url' ],
413 return ActorModel.unscoped()
417 return ModelCache.Instance.doCache({
418 cacheType: 'local-actor-name',
419 key: preferredUsername,
420 // The server actor never change, so we can easily cache it
421 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
426 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
442 return ActorModel.scope(ScopeNames.FULL).findOne(query)
445 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
453 attributes: [ 'id' ],
454 model: AccountModel.unscoped(),
458 attributes: [ 'id' ],
459 model: VideoChannelModel.unscoped(),
465 return ActorModel.unscoped().findOne(query)
468 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
476 return ActorModel.scope(ScopeNames.FULL).findOne(query)
479 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
480 const sanitizedOfId = parseInt(ofId + '', 10)
481 const where = { id: sanitizedOfId }
483 let columnToUpdate: string
484 let columnOfCount: string
486 if (type === 'followers') {
487 columnToUpdate = 'followersCount'
488 columnOfCount = 'targetActorId'
490 columnToUpdate = 'followingCount'
491 columnOfCount = 'actorId'
494 return ActorModel.update({
495 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
496 }, { where, transaction })
499 static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
503 attributes: [ 'id' ],
504 model: AccountModel.unscoped(),
508 attributes: [ 'id', 'accountId' ],
509 model: VideoChannelModel.unscoped(),
513 attributes: [ 'id', 'channelId' ],
514 model: VideoModel.unscoped(),
527 return ActorModel.unscoped().findOne(query)
530 getSharedInbox (this: MActorWithInboxes) {
531 return this.sharedInboxUrl || this.inboxUrl
534 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
535 let avatar: ActorImage = null
537 avatar = this.Avatar.toFormattedJSON()
542 name: this.preferredUsername,
543 host: this.getHost(),
548 toFormattedJSON (this: MActorFormattable) {
549 const base = this.toFormattedSummaryJSON()
551 let banner: ActorImage = null
553 banner = this.Banner.toFormattedJSON()
556 return Object.assign(base, {
558 hostRedundancyAllowed: this.getRedundancyAllowed(),
559 followingCount: this.followingCount,
560 followersCount: this.followersCount,
562 createdAt: this.getCreatedAt()
566 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
567 let icon: ActivityIconObject
568 let image: ActivityIconObject
571 const extension = getLowercaseExtension(this.Avatar.filename)
575 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
576 height: this.Avatar.height,
577 width: this.Avatar.width,
578 url: this.getAvatarUrl()
583 const banner = (this as MActorAPChannel).Banner
584 const extension = getLowercaseExtension(banner.filename)
588 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
589 height: banner.height,
591 url: this.getBannerUrl()
598 following: this.getFollowingUrl(),
599 followers: this.getFollowersUrl(),
600 playlists: this.getPlaylistsUrl(),
601 inbox: this.inboxUrl,
602 outbox: this.outboxUrl,
603 preferredUsername: this.preferredUsername,
607 sharedInbox: this.sharedInboxUrl
610 id: this.getPublicKeyUrl(),
612 publicKeyPem: this.publicKey
614 published: this.getCreatedAt().toISOString(),
619 return activityPubContextify(json)
622 getFollowerSharedInboxUrls (t: Transaction) {
624 attributes: [ 'sharedInboxUrl' ],
628 model: ActorFollowModel.unscoped(),
630 as: 'ActorFollowing',
633 targetActorId: this.id
640 return ActorModel.findAll(query)
641 .then(accounts => accounts.map(a => a.sharedInboxUrl))
645 return this.url + '/following'
649 return this.url + '/followers'
653 return this.url + '/playlists'
657 return this.url + '#main-key'
661 return this.serverId === null
664 getWebfingerUrl (this: MActorServer) {
665 return 'acct:' + this.preferredUsername + '@' + this.getHost()
669 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
672 getHost (this: MActorHost) {
673 return this.Server ? this.Server.host : WEBSERVER.HOST
676 getRedundancyAllowed () {
677 return this.Server ? this.Server.redundancyAllowed : false
681 if (!this.avatarId) return undefined
683 return WEBSERVER.URL + this.Avatar.getStaticPath()
687 if (!this.bannerId) return undefined
689 return WEBSERVER.URL + this.Banner.getStaticPath()
693 if (this.isOwned()) return false
695 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
698 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
699 return this.remoteCreatedAt || this.createdAt