1 import * as Bluebird from 'bluebird'
2 import { Transaction } from 'sequelize'
3 import * as url from 'url'
4 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
5 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
6 import { createPrivateAndPublicKeys, doRequest, logger, retryTransactionWrapper } from '../../helpers'
7 import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub'
8 import { ACTIVITY_PUB, CONFIG, sequelizeTypescript } from '../../initializers'
9 import { AccountModel } from '../../models/account/account'
10 import { ActorModel } from '../../models/activitypub/actor'
11 import { ServerModel } from '../../models/server/server'
12 import { VideoChannelModel } from '../../models/video/video-channel'
14 // Set account keys, this could be long so process after the account creation and do not block the client
15 function setAsyncActorKeys (actor: ActorModel) {
16 return createPrivateAndPublicKeys()
17 .then(({ publicKey, privateKey }) => {
18 actor.set('publicKey', publicKey)
19 actor.set('privateKey', privateKey)
23 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
28 async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
29 let actor = await ActorModel.loadByUrl(actorUrl)
31 // We don't have this actor in our database, fetch it on remote
33 const result = await fetchRemoteActor(actorUrl)
34 if (result === undefined) throw new Error('Cannot fetch remote actor.')
36 // Create the attributed to actor
37 // In PeerTube a video channel is owned by an account
38 let ownerActor: ActorModel = undefined
39 if (recurseIfNeeded === true && result.actor.type === 'Group') {
40 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
41 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
44 // Assert we don't recurse another time
45 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
47 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
53 arguments: [ result, ownerActor ],
54 errorMessage: 'Cannot save actor and server with many retries.'
56 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
62 function saveActorAndServerAndModelIfNotExist (
63 result: FetchRemoteActorResult,
64 ownerActor?: ActorModel,
66 ): Bluebird<ActorModel> | Promise<ActorModel> {
67 let actor = result.actor
69 if (t !== undefined) return save(t)
71 return sequelizeTypescript.transaction(t => save(t))
73 async function save (t: Transaction) {
74 const actorHost = url.parse(actor.url).host
76 const serverOptions = {
85 const [ server ] = await ServerModel.findOrCreate(serverOptions)
87 // Save our new account in database
88 actor.set('serverId', server.id)
90 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
91 // (which could be false in a retried query)
92 const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
94 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
95 const account = await saveAccount(actorCreated, result, t)
96 actorCreated.Account = account
97 actorCreated.Account.Actor = actorCreated
98 } else if (actorCreated.type === 'Group') { // Video channel
99 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
100 actorCreated.VideoChannel = videoChannel
101 actorCreated.VideoChannel.Actor = actorCreated
108 type FetchRemoteActorResult = {
110 preferredUsername: string
112 attributedTo: ActivityPubAttributedTo[]
114 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
119 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
123 logger.info('Fetching remote actor %s.', actorUrl)
127 requestResult = await doRequest(options)
129 logger.warn('Cannot fetch remote actor %s.', actorUrl, err)
133 const actorJSON: ActivityPubActor = JSON.parse(requestResult.body)
134 if (isRemoteActorValid(actorJSON) === false) {
135 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
139 const followersCount = await fetchActorTotalItems(actorJSON.followers)
140 const followingCount = await fetchActorTotalItems(actorJSON.following)
142 const actor = new ActorModel({
143 type: actorJSON.type,
144 uuid: actorJSON.uuid,
145 name: actorJSON.name,
147 publicKey: actorJSON.publicKey.publicKeyPem,
149 followersCount: followersCount,
150 followingCount: followingCount,
151 inboxUrl: actorJSON.inbox,
152 outboxUrl: actorJSON.outbox,
153 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
154 followersUrl: actorJSON.followers,
155 followingUrl: actorJSON.following
160 preferredUsername: actorJSON.preferredUsername,
161 summary: actorJSON.summary,
162 attributedTo: actorJSON.attributedTo
166 function buildActorInstance (type: ActivityPubActorType, url: string, name: string, uuid?: string) {
167 return new ActorModel({
176 inboxUrl: url + '/inbox',
177 outboxUrl: url + '/outbox',
178 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
179 followersUrl: url + '/followers',
180 followingUrl: url + '/following'
185 getOrCreateActorAndServerAndModel,
186 saveActorAndServerAndModelIfNotExist,
192 // ---------------------------------------------------------------------------
194 async function fetchActorTotalItems (url: string) {
202 requestResult = await doRequest(options)
204 logger.warn('Cannot fetch remote actor count %s.', url, err)
208 return requestResult.totalItems ? requestResult.totalItems : 0
211 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
212 const account = new AccountModel({
213 name: result.preferredUsername,
217 return account.save({ transaction: t })
220 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
221 const videoChannel = new VideoChannelModel({
222 name: result.preferredUsername,
223 description: result.summary,
225 accountId: ownerActor.Account.id
228 return videoChannel.save({ transaction: t })