1 import * as Bluebird from 'bluebird'
2 import { join } from 'path'
3 import { Transaction } from 'sequelize'
4 import * as url from 'url'
5 import * as uuidv4 from 'uuid/v4'
6 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8 import { getActorUrl } from '../../helpers/activitypub'
9 import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11 import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
12 import { logger } from '../../helpers/logger'
13 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
15 import { getUrlFromWebfinger } from '../../helpers/webfinger'
16 import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
17 import { AccountModel } from '../../models/account/account'
18 import { ActorModel } from '../../models/activitypub/actor'
19 import { AvatarModel } from '../../models/avatar/avatar'
20 import { ServerModel } from '../../models/server/server'
21 import { VideoChannelModel } from '../../models/video/video-channel'
22 import { JobQueue } from '../job-queue'
23 import { getServerActor } from '../../helpers/utils'
25 // Set account keys, this could be long so process after the account creation and do not block the client
26 function setAsyncActorKeys (actor: ActorModel) {
27 return createPrivateAndPublicKeys()
28 .then(({ publicKey, privateKey }) => {
29 actor.set('publicKey', publicKey)
30 actor.set('privateKey', privateKey)
34 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
39 async function getOrCreateActorAndServerAndModel (activityActor: string | ActivityPubActor, recurseIfNeeded = true) {
40 const actorUrl = getActorUrl(activityActor)
42 let actor = await ActorModel.loadByUrl(actorUrl)
44 // We don't have this actor in our database, fetch it on remote
46 const result = await fetchRemoteActor(actorUrl)
47 if (result === undefined) throw new Error('Cannot fetch remote actor.')
49 // Create the attributed to actor
50 // In PeerTube a video channel is owned by an account
51 let ownerActor: ActorModel = undefined
52 if (recurseIfNeeded === true && result.actor.type === 'Group') {
53 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
54 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
57 // Assert we don't recurse another time
58 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
60 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
66 arguments: [ result, ownerActor ],
67 errorMessage: 'Cannot save actor and server with many retries.'
69 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
74 errorMessage: 'Cannot refresh actor if needed with many retries.'
76 return retryTransactionWrapper(refreshActorIfNeeded, options)
79 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
80 return new ActorModel({
89 inboxUrl: url + '/inbox',
90 outboxUrl: url + '/outbox',
91 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
92 followersUrl: url + '/followers',
93 followingUrl: url + '/following'
97 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
98 const followersCount = await fetchActorTotalItems(attributes.followers)
99 const followingCount = await fetchActorTotalItems(attributes.following)
101 actorInstance.set('type', attributes.type)
102 actorInstance.set('uuid', attributes.uuid)
103 actorInstance.set('preferredUsername', attributes.preferredUsername)
104 actorInstance.set('url', attributes.id)
105 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
106 actorInstance.set('followersCount', followersCount)
107 actorInstance.set('followingCount', followingCount)
108 actorInstance.set('inboxUrl', attributes.inbox)
109 actorInstance.set('outboxUrl', attributes.outbox)
110 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
111 actorInstance.set('followersUrl', attributes.followers)
112 actorInstance.set('followingUrl', attributes.following)
115 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
116 if (avatarName !== undefined) {
117 if (actorInstance.avatarId) {
119 await actorInstance.Avatar.destroy({ transaction: t })
121 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
125 const avatar = await AvatarModel.create({
127 }, { transaction: t })
129 actorInstance.set('avatarId', avatar.id)
130 actorInstance.Avatar = avatar
136 async function fetchActorTotalItems (url: string) {
145 const { body } = await doRequest(options)
146 return body.totalItems ? body.totalItems : 0
148 logger.warn('Cannot fetch remote actor count %s.', url, { err })
153 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
155 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
156 isActivityPubUrlValid(actorJSON.icon.url)
158 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
160 const avatarName = uuidv4() + extension
161 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
163 await doRequestAndSaveToFile({
165 uri: actorJSON.icon.url
174 async function addFetchOutboxJob (actor: ActorModel) {
175 // Don't fetch ourselves
176 const serverActor = await getServerActor()
177 if (serverActor.id === actor.id) {
178 logger.error('Cannot fetch our own outbox!')
183 uris: [ actor.outboxUrl ]
186 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
190 getOrCreateActorAndServerAndModel,
193 fetchActorTotalItems,
196 updateActorAvatarInstance,
200 // ---------------------------------------------------------------------------
202 function saveActorAndServerAndModelIfNotExist (
203 result: FetchRemoteActorResult,
204 ownerActor?: ActorModel,
206 ): Bluebird<ActorModel> | Promise<ActorModel> {
207 let actor = result.actor
209 if (t !== undefined) return save(t)
211 return sequelizeTypescript.transaction(t => save(t))
213 async function save (t: Transaction) {
214 const actorHost = url.parse(actor.url).host
216 const serverOptions = {
225 const [ server ] = await ServerModel.findOrCreate(serverOptions)
227 // Save our new account in database
228 actor.set('serverId', server.id)
231 if (result.avatarName) {
232 const avatar = await AvatarModel.create({
233 filename: result.avatarName
234 }, { transaction: t })
235 actor.set('avatarId', avatar.id)
238 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
239 // (which could be false in a retried query)
240 const [ actorCreated ] = await ActorModel.findOrCreate({
241 defaults: actor.toJSON(),
248 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
249 actorCreated.Account = await saveAccount(actorCreated, result, t)
250 actorCreated.Account.Actor = actorCreated
251 } else if (actorCreated.type === 'Group') { // Video channel
252 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
253 actorCreated.VideoChannel.Actor = actorCreated
260 type FetchRemoteActorResult = {
266 attributedTo: ActivityPubAttributedTo[]
268 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
276 logger.info('Fetching remote actor %s.', actorUrl)
278 const requestResult = await doRequest(options)
279 normalizeActor(requestResult.body)
281 const actorJSON: ActivityPubActor = requestResult.body
283 if (isActorObjectValid(actorJSON) === false) {
284 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
288 const followersCount = await fetchActorTotalItems(actorJSON.followers)
289 const followingCount = await fetchActorTotalItems(actorJSON.following)
291 const actor = new ActorModel({
292 type: actorJSON.type,
293 uuid: actorJSON.uuid,
294 preferredUsername: actorJSON.preferredUsername,
296 publicKey: actorJSON.publicKey.publicKeyPem,
298 followersCount: followersCount,
299 followingCount: followingCount,
300 inboxUrl: actorJSON.inbox,
301 outboxUrl: actorJSON.outbox,
302 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
303 followersUrl: actorJSON.followers,
304 followingUrl: actorJSON.following
307 const avatarName = await fetchAvatarIfExists(actorJSON)
309 const name = actorJSON.name || actorJSON.preferredUsername
314 summary: actorJSON.summary,
315 support: actorJSON.support,
316 attributedTo: actorJSON.attributedTo
320 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
321 const [ accountCreated ] = await AccountModel.findOrCreate({
324 description: result.summary,
333 return accountCreated
336 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
337 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
340 description: result.summary,
341 support: result.support,
343 accountId: ownerActor.Account.id
351 return videoChannelCreated
354 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
355 if (!actor.isOutdated()) return actor
358 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
359 const result = await fetchRemoteActor(actorUrl)
360 if (result === undefined) {
361 logger.warn('Cannot fetch remote actor in refresh actor.')
365 return sequelizeTypescript.transaction(async t => {
366 updateInstanceWithAnother(actor, result.actor)
368 if (result.avatarName !== undefined) {
369 await updateActorAvatarInstance(actor, result.avatarName, t)
373 actor.setDataValue('updatedAt', new Date())
374 await actor.save({ transaction: t })
377 await actor.save({ transaction: t })
379 actor.Account.set('name', result.name)
380 actor.Account.set('description', result.summary)
381 await actor.Account.save({ transaction: t })
382 } else if (actor.VideoChannel) {
383 await actor.save({ transaction: t })
385 actor.VideoChannel.set('name', result.name)
386 actor.VideoChannel.set('description', result.summary)
387 actor.VideoChannel.set('support', result.support)
388 await actor.VideoChannel.save({ transaction: t })
394 logger.warn('Cannot refresh actor.', { err })