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'
32 import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
35 MActorAccountChannelId,
41 MActorSummaryFormattable,
44 } from '../../types/models'
45 import { AccountModel } from '../account/account'
46 import { ActorImageModel } from '../account/actor-image'
47 import { ServerModel } from '../server/server'
48 import { isOutdated, throwIfNotValid } from '../utils'
49 import { VideoModel } from '../video/video'
50 import { VideoChannelModel } from '../video/video-channel'
51 import { ActorFollowModel } from './actor-follow'
57 export const unusedActorAttributesForAPI = [
69 @DefaultScope(() => ({
76 model: ActorImageModel,
86 model: AccountModel.unscoped(),
90 model: VideoChannelModel.unscoped(),
104 model: ActorImageModel,
119 fields: [ 'preferredUsername', 'serverId' ],
128 fields: [ 'preferredUsername' ],
135 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
138 fields: [ 'sharedInboxUrl' ]
141 fields: [ 'serverId' ]
144 fields: [ 'avatarId' ]
147 fields: [ 'followersUrl' ]
151 export class ActorModel extends Model {
154 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
155 type: ActivityPubActorType
158 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
160 preferredUsername: string
163 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
164 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
168 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
169 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
173 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
174 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
178 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
180 followersCount: number
183 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
185 followingCount: number
188 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
189 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
193 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
194 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
198 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
199 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
200 sharedInboxUrl: string
203 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
204 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
208 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
209 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
218 @ForeignKey(() => ActorImageModel)
222 @ForeignKey(() => ActorImageModel)
226 @BelongsTo(() => ActorImageModel, {
232 onDelete: 'set null',
235 Avatar: ActorImageModel
237 @BelongsTo(() => ActorImageModel, {
243 onDelete: 'set null',
246 Banner: ActorImageModel
248 @HasMany(() => ActorFollowModel, {
253 as: 'ActorFollowings',
256 ActorFollowing: ActorFollowModel[]
258 @HasMany(() => ActorFollowModel, {
260 name: 'targetActorId',
263 as: 'ActorFollowers',
266 ActorFollowers: ActorFollowModel[]
268 @ForeignKey(() => ServerModel)
272 @BelongsTo(() => ServerModel, {
280 @HasOne(() => AccountModel, {
287 Account: AccountModel
289 @HasOne(() => VideoChannelModel, {
296 VideoChannel: VideoChannelModel
298 static load (id: number): Promise<MActor> {
299 return ActorModel.unscoped().findByPk(id)
302 static loadFull (id: number): Promise<MActorFull> {
303 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
306 static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
310 attributes: [ 'id' ],
311 model: AccountModel.unscoped(),
315 attributes: [ 'id' ],
316 model: VideoChannelModel.unscoped(),
320 attributes: [ 'id' ],
321 model: VideoModel.unscoped(),
335 return ActorModel.unscoped().findOne(query)
338 static isActorUrlExist (url: string) {
346 return ActorModel.unscoped().findOne(query)
350 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
354 [Op.in]: followersUrls
360 return ActorModel.scope(ScopeNames.FULL).findAll(query)
363 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
373 return ActorModel.scope(ScopeNames.FULL)
377 return ModelCache.Instance.doCache({
378 cacheType: 'local-actor-name',
379 key: preferredUsername,
380 // The server actor never change, so we can easily cache it
381 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
386 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
389 attributes: [ 'url' ],
397 return ActorModel.unscoped()
401 return ModelCache.Instance.doCache({
402 cacheType: 'local-actor-name',
403 key: preferredUsername,
404 // The server actor never change, so we can easily cache it
405 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
410 static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
426 return ActorModel.scope(ScopeNames.FULL).findOne(query)
429 static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
437 attributes: [ 'id' ],
438 model: AccountModel.unscoped(),
442 attributes: [ 'id' ],
443 model: VideoChannelModel.unscoped(),
449 return ActorModel.unscoped().findOne(query)
452 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
460 return ActorModel.scope(ScopeNames.FULL).findOne(query)
463 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
464 const sanitizedOfId = parseInt(ofId + '', 10)
465 const where = { id: sanitizedOfId }
467 let columnToUpdate: string
468 let columnOfCount: string
470 if (type === 'followers') {
471 columnToUpdate = 'followersCount'
472 columnOfCount = 'targetActorId'
474 columnToUpdate = 'followingCount'
475 columnOfCount = 'actorId'
478 return ActorModel.update({
479 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
480 }, { where, transaction })
483 static loadAccountActorByVideoId (videoId: number): Promise<MActor> {
487 attributes: [ 'id' ],
488 model: AccountModel.unscoped(),
492 attributes: [ 'id', 'accountId' ],
493 model: VideoChannelModel.unscoped(),
497 attributes: [ 'id', 'channelId' ],
498 model: VideoModel.unscoped(),
510 return ActorModel.unscoped().findOne(query)
513 getSharedInbox (this: MActorWithInboxes) {
514 return this.sharedInboxUrl || this.inboxUrl
517 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
518 let avatar: ActorImage = null
520 avatar = this.Avatar.toFormattedJSON()
525 name: this.preferredUsername,
526 host: this.getHost(),
531 toFormattedJSON (this: MActorFormattable) {
532 const base = this.toFormattedSummaryJSON()
534 return Object.assign(base, {
536 hostRedundancyAllowed: this.getRedundancyAllowed(),
537 followingCount: this.followingCount,
538 followersCount: this.followersCount,
539 createdAt: this.createdAt,
540 updatedAt: this.updatedAt
544 toActivityPubObject (this: MActorAP, name: string) {
545 let icon: ActivityIconObject
548 const extension = extname(this.Avatar.filename)
552 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
553 url: this.getAvatarUrl()
560 following: this.getFollowingUrl(),
561 followers: this.getFollowersUrl(),
562 playlists: this.getPlaylistsUrl(),
563 inbox: this.inboxUrl,
564 outbox: this.outboxUrl,
565 preferredUsername: this.preferredUsername,
569 sharedInbox: this.sharedInboxUrl
572 id: this.getPublicKeyUrl(),
574 publicKeyPem: this.publicKey
579 return activityPubContextify(json)
582 getFollowerSharedInboxUrls (t: Transaction) {
584 attributes: [ 'sharedInboxUrl' ],
588 model: ActorFollowModel.unscoped(),
590 as: 'ActorFollowing',
593 targetActorId: this.id
600 return ActorModel.findAll(query)
601 .then(accounts => accounts.map(a => a.sharedInboxUrl))
605 return this.url + '/following'
609 return this.url + '/followers'
613 return this.url + '/playlists'
617 return this.url + '#main-key'
621 return this.serverId === null
624 getWebfingerUrl (this: MActorServer) {
625 return 'acct:' + this.preferredUsername + '@' + this.getHost()
629 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
632 getHost (this: MActorHost) {
633 return this.Server ? this.Server.host : WEBSERVER.HOST
636 getRedundancyAllowed () {
637 return this.Server ? this.Server.redundancyAllowed : false
641 if (!this.avatarId) return undefined
643 return WEBSERVER.URL + this.Avatar.getStaticPath()
647 if (this.isOwned()) return false
649 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)