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 { checkUrlsSameHost, 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'
24 import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
26 // Set account keys, this could be long so process after the account creation and do not block the client
27 function setAsyncActorKeys (actor: ActorModel) {
28 return createPrivateAndPublicKeys()
29 .then(({ publicKey, privateKey }) => {
30 actor.set('publicKey', publicKey)
31 actor.set('privateKey', privateKey)
35 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
40 async function getOrCreateActorAndServerAndModel (
41 activityActor: string | ActivityPubActor,
42 fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
43 recurseIfNeeded = true,
44 updateCollections = false
46 const actorUrl = getActorUrl(activityActor)
49 let actor = await fetchActorByUrl(actorUrl, fetchType)
50 // Orphan actor (not associated to an account of channel) so recreate it
51 if (actor && (!actor.Account && !actor.VideoChannel)) {
56 // We don't have this actor in our database, fetch it on remote
58 const { result } = await fetchRemoteActor(actorUrl)
59 if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
61 // Create the attributed to actor
62 // In PeerTube a video channel is owned by an account
63 let ownerActor: ActorModel = undefined
64 if (recurseIfNeeded === true && result.actor.type === 'Group') {
65 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
66 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
68 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
69 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
73 // Don't recurse another time
74 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
76 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
81 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
85 if (actor.Account) actor.Account.Actor = actor
86 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
88 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
89 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
91 if ((created === true || refreshed === true) && updateCollections === true) {
92 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
93 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
99 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
100 return new ActorModel({
109 inboxUrl: url + '/inbox',
110 outboxUrl: url + '/outbox',
111 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
112 followersUrl: url + '/followers',
113 followingUrl: url + '/following'
117 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
118 const followersCount = await fetchActorTotalItems(attributes.followers)
119 const followingCount = await fetchActorTotalItems(attributes.following)
121 actorInstance.set('type', attributes.type)
122 actorInstance.set('uuid', attributes.uuid)
123 actorInstance.set('preferredUsername', attributes.preferredUsername)
124 actorInstance.set('url', attributes.id)
125 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
126 actorInstance.set('followersCount', followersCount)
127 actorInstance.set('followingCount', followingCount)
128 actorInstance.set('inboxUrl', attributes.inbox)
129 actorInstance.set('outboxUrl', attributes.outbox)
130 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
131 actorInstance.set('followersUrl', attributes.followers)
132 actorInstance.set('followingUrl', attributes.following)
135 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
136 if (avatarName !== undefined) {
137 if (actorInstance.avatarId) {
139 await actorInstance.Avatar.destroy({ transaction: t })
141 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
145 const avatar = await AvatarModel.create({
147 }, { transaction: t })
149 actorInstance.set('avatarId', avatar.id)
150 actorInstance.Avatar = avatar
156 async function fetchActorTotalItems (url: string) {
165 const { body } = await doRequest(options)
166 return body.totalItems ? body.totalItems : 0
168 logger.warn('Cannot fetch remote actor count %s.', url, { err })
173 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
175 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
176 isActivityPubUrlValid(actorJSON.icon.url)
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
180 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
183 await doRequestAndSaveToFile({
185 uri: actorJSON.icon.url
194 async function addFetchOutboxJob (actor: ActorModel) {
195 // Don't fetch ourselves
196 const serverActor = await getServerActor()
197 if (serverActor.id === actor.id) {
198 logger.error('Cannot fetch our own outbox!')
203 uri: actor.outboxUrl,
204 type: 'activity' as 'activity'
207 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
211 getOrCreateActorAndServerAndModel,
214 fetchActorTotalItems,
217 updateActorAvatarInstance,
221 // ---------------------------------------------------------------------------
223 function saveActorAndServerAndModelIfNotExist (
224 result: FetchRemoteActorResult,
225 ownerActor?: ActorModel,
227 ): Bluebird<ActorModel> | Promise<ActorModel> {
228 let actor = result.actor
230 if (t !== undefined) return save(t)
232 return sequelizeTypescript.transaction(t => save(t))
234 async function save (t: Transaction) {
235 const actorHost = url.parse(actor.url).host
237 const serverOptions = {
246 const [ server ] = await ServerModel.findOrCreate(serverOptions)
248 // Save our new account in database
249 actor.set('serverId', server.id)
252 if (result.avatarName) {
253 const avatar = await AvatarModel.create({
254 filename: result.avatarName
255 }, { transaction: t })
256 actor.set('avatarId', avatar.id)
259 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
260 // (which could be false in a retried query)
261 const [ actorCreated ] = await ActorModel.findOrCreate({
262 defaults: actor.toJSON(),
269 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
270 actorCreated.Account = await saveAccount(actorCreated, result, t)
271 actorCreated.Account.Actor = actorCreated
272 } else if (actorCreated.type === 'Group') { // Video channel
273 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
274 actorCreated.VideoChannel.Actor = actorCreated
275 actorCreated.VideoChannel.Account = ownerActor.Account
282 type FetchRemoteActorResult = {
288 attributedTo: ActivityPubAttributedTo[]
290 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
298 logger.info('Fetching remote actor %s.', actorUrl)
300 const requestResult = await doRequest(options)
301 normalizeActor(requestResult.body)
303 const actorJSON: ActivityPubActor = requestResult.body
304 if (isActorObjectValid(actorJSON) === false) {
305 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
306 return { result: undefined, statusCode: requestResult.response.statusCode }
309 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
310 throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
313 const followersCount = await fetchActorTotalItems(actorJSON.followers)
314 const followingCount = await fetchActorTotalItems(actorJSON.following)
316 const actor = new ActorModel({
317 type: actorJSON.type,
318 uuid: actorJSON.uuid,
319 preferredUsername: actorJSON.preferredUsername,
321 publicKey: actorJSON.publicKey.publicKeyPem,
323 followersCount: followersCount,
324 followingCount: followingCount,
325 inboxUrl: actorJSON.inbox,
326 outboxUrl: actorJSON.outbox,
327 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
328 followersUrl: actorJSON.followers,
329 followingUrl: actorJSON.following
332 const avatarName = await fetchAvatarIfExists(actorJSON)
334 const name = actorJSON.name || actorJSON.preferredUsername
336 statusCode: requestResult.response.statusCode,
341 summary: actorJSON.summary,
342 support: actorJSON.support,
343 attributedTo: actorJSON.attributedTo
348 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
349 const [ accountCreated ] = await AccountModel.findOrCreate({
352 description: result.summary,
361 return accountCreated
364 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
365 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
368 description: result.summary,
369 support: result.support,
371 accountId: ownerActor.Account.id
379 return videoChannelCreated
382 async function refreshActorIfNeeded (
383 actorArg: ActorModel,
384 fetchedType: ActorFetchByUrlType
385 ): Promise<{ actor: ActorModel, refreshed: boolean }> {
386 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
388 // We need more attributes
389 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
392 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
393 const { result, statusCode } = await fetchRemoteActor(actorUrl)
395 if (statusCode === 404) {
396 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
397 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
398 return { actor: undefined, refreshed: false }
401 if (result === undefined) {
402 logger.warn('Cannot fetch remote actor in refresh actor.')
403 return { actor, refreshed: false }
406 return sequelizeTypescript.transaction(async t => {
407 updateInstanceWithAnother(actor, result.actor)
409 if (result.avatarName !== undefined) {
410 await updateActorAvatarInstance(actor, result.avatarName, t)
414 actor.setDataValue('updatedAt', new Date())
415 await actor.save({ transaction: t })
418 actor.Account.set('name', result.name)
419 actor.Account.set('description', result.summary)
421 await actor.Account.save({ transaction: t })
422 } else if (actor.VideoChannel) {
423 actor.VideoChannel.set('name', result.name)
424 actor.VideoChannel.set('description', result.summary)
425 actor.VideoChannel.set('support', result.support)
427 await actor.VideoChannel.save({ transaction: t })
430 return { refreshed: true, actor }
433 logger.warn('Cannot refresh actor.', { err })
434 return { actor, refreshed: false }