import { Op, Transaction } from 'sequelize' import { sequelizeTypescript } from '@server/initializers/database' import { AccountModel } from '@server/models/account/account' import { ActorModel } from '@server/models/actor/actor' import { ServerModel } from '@server/models/server/server' import { VideoChannelModel } from '@server/models/video/video-channel' import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' import { ActivityPubActor, ActorImageType } from '@shared/models' import { updateActorImages } from '../image' import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes' import { fetchActorFollowsCount } from './url-to-object' export class APActorCreator { constructor ( private readonly actorObject: ActivityPubActor, private readonly ownerActor?: MActorFullActor ) { } async create (): Promise { const { followersCount, followingCount } = await fetchActorFollowsCount(this.actorObject) const actorInstance = new ActorModel(getActorAttributesFromObject(this.actorObject, followersCount, followingCount)) return sequelizeTypescript.transaction(async t => { const server = await this.setServer(actorInstance, t) const { actorCreated, created } = await this.saveActor(actorInstance, t) await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t) await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t) await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance actorCreated.Account = await this.saveAccount(actorCreated, t) as MAccountDefault actorCreated.Account.Actor = actorCreated } if (actorCreated.type === 'Group') { // Video channel const channel = await this.saveVideoChannel(actorCreated, t) actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: this.ownerActor.Account }) } actorCreated.Server = server return actorCreated }) } private async setServer (actor: MActor, t: Transaction) { const actorHost = new URL(actor.url).host const serverOptions = { where: { host: actorHost }, defaults: { host: actorHost }, transaction: t } const [ server ] = await ServerModel.findOrCreate(serverOptions) // Save our new account in database actor.serverId = server.id return server as MServer } private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { const imagesInfo = getImagesInfoFromObject(this.actorObject, type) if (imagesInfo.length === 0) return return updateActorImages(actor as MActorImages, type, imagesInfo, t) } private async saveActor (actor: MActor, t: Transaction) { // Force the actor creation using findOrCreate() instead of save() // Sometimes Sequelize skips the save() when it thinks the instance already exists // (which could be false in a retried query) const [ actorCreated, created ] = await ActorModel.findOrCreate({ defaults: actor.toJSON(), where: { [Op.or]: [ { url: actor.url }, { serverId: actor.serverId, preferredUsername: actor.preferredUsername } ] }, transaction: t }) return { actorCreated, created } } private async tryToFixActorUrlIfNeeded (actorCreated: MActor, newActor: MActor, created: boolean, t: Transaction) { // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards if (created !== true && actorCreated.url !== newActor.url) { // Only fix http://example.com/account/djidane to https://example.com/account/djidane if (actorCreated.url.replace(/^http:\/\//, '') !== newActor.url.replace(/^https:\/\//, '')) { throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${newActor.url}`) } actorCreated.url = newActor.url await actorCreated.save({ transaction: t }) } } private async saveAccount (actor: MActorId, t: Transaction) { const [ accountCreated ] = await AccountModel.findOrCreate({ defaults: { name: getActorDisplayNameFromObject(this.actorObject), description: this.actorObject.summary, actorId: actor.id }, where: { actorId: actor.id }, transaction: t }) return accountCreated as MAccount } private async saveVideoChannel (actor: MActorId, t: Transaction) { const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({ defaults: { name: getActorDisplayNameFromObject(this.actorObject), description: this.actorObject.summary, support: this.actorObject.support, actorId: actor.id, accountId: this.ownerActor.Account.id }, where: { actorId: actor.id }, transaction: t }) return videoChannelCreated as MChannel } }