1 import { literal, Op, QueryTypes, Transaction } from 'sequelize'
17 } from 'sequelize-typescript'
18 import { activityPubContextify } from '@server/lib/activitypub/context'
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'
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,
51 MActorSummaryFormattable,
54 } from '../../types/models'
55 import { AccountModel } from '../account/account'
56 import { getServerActor } from '../application/application'
57 import { ServerModel } from '../server/server'
58 import { isOutdated, throwIfNotValid } from '../utils'
59 import { VideoModel } from '../video/video'
60 import { VideoChannelModel } from '../video/video-channel'
61 import { ActorFollowModel } from './actor-follow'
62 import { ActorImageModel } from './actor-image'
68 export const unusedActorAttributesForAPI = [
78 @DefaultScope(() => ({
85 model: ActorImageModel,
95 model: AccountModel.unscoped(),
99 model: VideoChannelModel.unscoped(),
113 model: ActorImageModel,
118 model: ActorImageModel,
133 fields: [ 'preferredUsername', 'serverId' ],
142 fields: [ 'preferredUsername' ],
149 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
152 fields: [ 'sharedInboxUrl' ]
155 fields: [ 'serverId' ]
158 fields: [ 'followersUrl' ]
162 export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
165 @Column(DataType.ENUM(...Object.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 @HasMany(() => ActorImageModel, {
241 type: ActorImageType.AVATAR
244 Avatars: ActorImageModel[]
246 @HasMany(() => ActorImageModel, {
254 type: ActorImageType.BANNER
257 Banners: ActorImageModel[]
259 @HasMany(() => ActorFollowModel, {
264 as: 'ActorFollowings',
267 ActorFollowing: ActorFollowModel[]
269 @HasMany(() => ActorFollowModel, {
271 name: 'targetActorId',
274 as: 'ActorFollowers',
277 ActorFollowers: ActorFollowModel[]
279 @ForeignKey(() => ServerModel)
283 @BelongsTo(() => ServerModel, {
291 @HasOne(() => AccountModel, {
298 Account: AccountModel
300 @HasOne(() => VideoChannelModel, {
307 VideoChannel: VideoChannelModel
309 static async load (id: number): Promise<MActor> {
310 const actorServer = await getServerActor()
311 if (id === actorServer.id) return actorServer
313 return ActorModel.unscoped().findByPk(id)
316 static loadFull (id: number): Promise<MActorFull> {
317 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
320 static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
321 const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
323 `INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
324 `INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
325 `INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
328 type: QueryTypes.SELECT as QueryTypes.SELECT,
329 replacements: { videoId },
334 return ActorModel.sequelize.query<MActorId & MActorFollowersUrl>(query, options)
337 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
341 [Op.in]: followersUrls
347 return ActorModel.scope(ScopeNames.FULL).findAll(query)
350 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
360 return ActorModel.scope(ScopeNames.FULL).findOne(query)
363 return ModelCache.Instance.doCache({
364 cacheType: 'local-actor-name',
365 key: preferredUsername,
366 // The server actor never change, so we can easily cache it
367 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
372 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
375 attributes: [ 'url' ],
383 return ActorModel.unscoped().findOne(query)
386 return ModelCache.Instance.doCache({
387 cacheType: 'local-actor-name',
388 key: preferredUsername,
389 // The server actor never change, so we can easily cache it
390 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
395 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
411 return ActorModel.scope(ScopeNames.FULL).findOne(query)
414 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
422 attributes: [ 'id' ],
423 model: AccountModel.unscoped(),
427 attributes: [ 'id' ],
428 model: VideoChannelModel.unscoped(),
434 return ActorModel.unscoped().findOne(query)
437 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
445 return ActorModel.scope(ScopeNames.FULL).findOne(query)
448 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
449 const sanitizedOfId = parseInt(ofId + '', 10)
450 const where = { id: sanitizedOfId }
452 let columnToUpdate: string
453 let columnOfCount: string
455 if (type === 'followers') {
456 columnToUpdate = 'followersCount'
457 columnOfCount = 'targetActorId'
459 columnToUpdate = 'followingCount'
460 columnOfCount = 'actorId'
463 return ActorModel.update({
464 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId} AND "state" = 'accepted')`)
465 }, { where, transaction })
468 static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
472 attributes: [ 'id' ],
473 model: AccountModel.unscoped(),
477 attributes: [ 'id', 'accountId' ],
478 model: VideoChannelModel.unscoped(),
482 attributes: [ 'id', 'channelId' ],
483 model: VideoModel.unscoped(),
496 return ActorModel.unscoped().findOne(query)
499 getSharedInbox (this: MActorWithInboxes) {
500 return this.sharedInboxUrl || this.inboxUrl
503 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
506 name: this.preferredUsername,
507 host: this.getHost(),
508 avatars: (this.Avatars || []).map(a => a.toFormattedJSON()),
510 // TODO: remove, deprecated in 4.2
511 avatar: this.hasImage(ActorImageType.AVATAR)
512 ? this.Avatars[0].toFormattedJSON()
517 toFormattedJSON (this: MActorFormattable) {
519 ...this.toFormattedSummaryJSON(),
522 hostRedundancyAllowed: this.getRedundancyAllowed(),
523 followingCount: this.followingCount,
524 followersCount: this.followersCount,
525 createdAt: this.getCreatedAt(),
527 banners: (this.Banners || []).map(b => b.toFormattedJSON()),
529 // TODO: remove, deprecated in 4.2
530 banner: this.hasImage(ActorImageType.BANNER)
531 ? this.Banners[0].toFormattedJSON()
536 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
537 let icon: ActivityIconObject
538 let icons: ActivityIconObject[]
539 let image: ActivityIconObject
541 if (this.hasImage(ActorImageType.AVATAR)) {
542 icon = getBiggestActorImage(this.Avatars).toActivityPubObject()
543 icons = this.Avatars.map(a => a.toActivityPubObject())
546 if (this.hasImage(ActorImageType.BANNER)) {
547 const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
548 const extension = getLowercaseExtension(banner.filename)
552 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
553 height: banner.height,
555 url: ActorImageModel.getImageUrl(banner)
562 following: this.getFollowingUrl(),
563 followers: this.getFollowersUrl(),
564 playlists: this.getPlaylistsUrl(),
565 inbox: this.inboxUrl,
566 outbox: this.outboxUrl,
567 preferredUsername: this.preferredUsername,
571 sharedInbox: this.sharedInboxUrl
574 id: this.getPublicKeyUrl(),
576 publicKeyPem: this.publicKey
578 published: this.getCreatedAt().toISOString(),
586 return activityPubContextify(json, 'Actor')
589 getFollowerSharedInboxUrls (t: Transaction) {
591 attributes: [ 'sharedInboxUrl' ],
595 model: ActorFollowModel.unscoped(),
597 as: 'ActorFollowing',
600 targetActorId: this.id
607 return ActorModel.findAll(query)
608 .then(accounts => accounts.map(a => a.sharedInboxUrl))
612 return this.url + '/following'
616 return this.url + '/followers'
620 return this.url + '/playlists'
624 return this.url + '#main-key'
628 return this.serverId === null
631 getWebfingerUrl (this: MActorServer) {
632 return 'acct:' + this.preferredUsername + '@' + this.getHost()
636 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
639 getHost (this: MActorHost) {
640 return this.Server ? this.Server.host : WEBSERVER.HOST
643 getRedundancyAllowed () {
644 return this.Server ? this.Server.redundancyAllowed : false
647 hasImage (type: ActorImageType) {
648 const images = type === ActorImageType.AVATAR
652 return Array.isArray(images) && images.length !== 0
656 if (this.isOwned()) return false
658 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
661 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
662 return this.remoteCreatedAt || this.createdAt