1 import { values } from 'lodash'
2 import { literal, Op, QueryTypes, Transaction } from 'sequelize'
18 } from 'sequelize-typescript'
19 import { activityPubContextify } from '@server/lib/activitypub/context'
20 import { getBiggestActorImage } from '@server/lib/actor-image'
21 import { ModelCache } from '@server/models/model-cache'
22 import { getLowercaseExtension } from '@shared/core-utils'
23 import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
24 import { AttributesOnly } from '@shared/typescript-utils'
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,
52 MActorSummaryFormattable,
55 } from '../../types/models'
56 import { AccountModel } from '../account/account'
57 import { getServerActor } from '../application/application'
58 import { ServerModel } from '../server/server'
59 import { isOutdated, throwIfNotValid } from '../utils'
60 import { VideoModel } from '../video/video'
61 import { VideoChannelModel } from '../video/video-channel'
62 import { ActorFollowModel } from './actor-follow'
63 import { ActorImageModel } from './actor-image'
69 export const unusedActorAttributesForAPI = [
79 @DefaultScope(() => ({
86 model: ActorImageModel,
96 model: AccountModel.unscoped(),
100 model: VideoChannelModel.unscoped(),
114 model: ActorImageModel,
119 model: ActorImageModel,
134 fields: [ 'preferredUsername', 'serverId' ],
143 fields: [ 'preferredUsername' ],
150 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
153 fields: [ 'sharedInboxUrl' ]
156 fields: [ 'serverId' ]
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 @HasMany(() => ActorImageModel, {
242 type: ActorImageType.AVATAR
245 Avatars: ActorImageModel[]
247 @HasMany(() => ActorImageModel, {
255 type: ActorImageType.BANNER
258 Banners: ActorImageModel[]
260 @HasMany(() => ActorFollowModel, {
265 as: 'ActorFollowings',
268 ActorFollowing: ActorFollowModel[]
270 @HasMany(() => ActorFollowModel, {
272 name: 'targetActorId',
275 as: 'ActorFollowers',
278 ActorFollowers: ActorFollowModel[]
280 @ForeignKey(() => ServerModel)
284 @BelongsTo(() => ServerModel, {
292 @HasOne(() => AccountModel, {
299 Account: AccountModel
301 @HasOne(() => VideoChannelModel, {
308 VideoChannel: VideoChannelModel
310 static async load (id: number): Promise<MActor> {
311 const actorServer = await getServerActor()
312 if (id === actorServer.id) return actorServer
314 return ActorModel.unscoped().findByPk(id)
317 static loadFull (id: number): Promise<MActorFull> {
318 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
321 static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
322 const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
324 `INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
325 `INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
326 `INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
329 type: QueryTypes.SELECT as QueryTypes.SELECT,
330 replacements: { videoId },
335 return ActorModel.sequelize.query<MActorId & MActorFollowersUrl>(query, options)
338 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
342 [Op.in]: followersUrls
348 return ActorModel.scope(ScopeNames.FULL).findAll(query)
351 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
361 return ActorModel.scope(ScopeNames.FULL).findOne(query)
364 return ModelCache.Instance.doCache({
365 cacheType: 'local-actor-name',
366 key: preferredUsername,
367 // The server actor never change, so we can easily cache it
368 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
373 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
376 attributes: [ 'url' ],
384 return ActorModel.unscoped().findOne(query)
387 return ModelCache.Instance.doCache({
388 cacheType: 'local-actor-name',
389 key: preferredUsername,
390 // The server actor never change, so we can easily cache it
391 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
396 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
412 return ActorModel.scope(ScopeNames.FULL).findOne(query)
415 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
423 attributes: [ 'id' ],
424 model: AccountModel.unscoped(),
428 attributes: [ 'id' ],
429 model: VideoChannelModel.unscoped(),
435 return ActorModel.unscoped().findOne(query)
438 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
446 return ActorModel.scope(ScopeNames.FULL).findOne(query)
449 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
450 const sanitizedOfId = parseInt(ofId + '', 10)
451 const where = { id: sanitizedOfId }
453 let columnToUpdate: string
454 let columnOfCount: string
456 if (type === 'followers') {
457 columnToUpdate = 'followersCount'
458 columnOfCount = 'targetActorId'
460 columnToUpdate = 'followingCount'
461 columnOfCount = 'actorId'
464 return ActorModel.update({
465 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId} AND "state" = 'accepted')`)
466 }, { where, transaction })
469 static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
473 attributes: [ 'id' ],
474 model: AccountModel.unscoped(),
478 attributes: [ 'id', 'accountId' ],
479 model: VideoChannelModel.unscoped(),
483 attributes: [ 'id', 'channelId' ],
484 model: VideoModel.unscoped(),
497 return ActorModel.unscoped().findOne(query)
500 getSharedInbox (this: MActorWithInboxes) {
501 return this.sharedInboxUrl || this.inboxUrl
504 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
507 name: this.preferredUsername,
508 host: this.getHost(),
509 avatars: (this.Avatars || []).map(a => a.toFormattedJSON()),
511 // TODO: remove, deprecated in 4.2
512 avatar: this.hasImage(ActorImageType.AVATAR)
513 ? this.Avatars[0].toFormattedJSON()
518 toFormattedJSON (this: MActorFormattable) {
520 ...this.toFormattedSummaryJSON(),
523 hostRedundancyAllowed: this.getRedundancyAllowed(),
524 followingCount: this.followingCount,
525 followersCount: this.followersCount,
526 createdAt: this.getCreatedAt(),
528 banners: (this.Banners || []).map(b => b.toFormattedJSON()),
530 // TODO: remove, deprecated in 4.2
531 banner: this.hasImage(ActorImageType.BANNER)
532 ? this.Banners[0].toFormattedJSON()
537 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
538 let icon: ActivityIconObject
539 let icons: ActivityIconObject[]
540 let image: ActivityIconObject
542 if (this.hasImage(ActorImageType.AVATAR)) {
543 icon = getBiggestActorImage(this.Avatars).toActivityPubObject()
544 icons = this.Avatars.map(a => a.toActivityPubObject())
547 if (this.hasImage(ActorImageType.BANNER)) {
548 const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
549 const extension = getLowercaseExtension(banner.filename)
553 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
554 height: banner.height,
556 url: ActorImageModel.getImageUrl(banner)
563 following: this.getFollowingUrl(),
564 followers: this.getFollowersUrl(),
565 playlists: this.getPlaylistsUrl(),
566 inbox: this.inboxUrl,
567 outbox: this.outboxUrl,
568 preferredUsername: this.preferredUsername,
572 sharedInbox: this.sharedInboxUrl
575 id: this.getPublicKeyUrl(),
577 publicKeyPem: this.publicKey
579 published: this.getCreatedAt().toISOString(),
587 return activityPubContextify(json, 'Actor')
590 getFollowerSharedInboxUrls (t: Transaction) {
592 attributes: [ 'sharedInboxUrl' ],
596 model: ActorFollowModel.unscoped(),
598 as: 'ActorFollowing',
601 targetActorId: this.id
608 return ActorModel.findAll(query)
609 .then(accounts => accounts.map(a => a.sharedInboxUrl))
613 return this.url + '/following'
617 return this.url + '/followers'
621 return this.url + '/playlists'
625 return this.url + '#main-key'
629 return this.serverId === null
632 getWebfingerUrl (this: MActorServer) {
633 return 'acct:' + this.preferredUsername + '@' + this.getHost()
637 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
640 getHost (this: MActorHost) {
641 return this.Server ? this.Server.host : WEBSERVER.HOST
644 getRedundancyAllowed () {
645 return this.Server ? this.Server.redundancyAllowed : false
648 hasImage (type: ActorImageType) {
649 const images = type === ActorImageType.AVATAR
653 return Array.isArray(images) && images.length !== 0
657 if (this.isOwned()) return false
659 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
662 getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) {
663 return this.remoteCreatedAt || this.createdAt