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 (
40 activityActor: string | ActivityPubActor,
41 recurseIfNeeded = true,
42 updateCollections = false
44 const actorUrl = getActorUrl(activityActor)
47 let actor = await ActorModel.loadByUrl(actorUrl)
48 // Orphan actor (not associated to an account of channel) so recreate it
49 if (actor && (!actor.Account && !actor.VideoChannel)) {
54 // We don't have this actor in our database, fetch it on remote
56 const { result } = await fetchRemoteActor(actorUrl)
57 if (result === undefined) throw new Error('Cannot fetch remote actor.')
59 // Create the attributed to actor
60 // In PeerTube a video channel is owned by an account
61 let ownerActor: ActorModel = undefined
62 if (recurseIfNeeded === true && result.actor.type === 'Group') {
63 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
64 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
67 // Assert we don't recurse another time
68 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
70 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
75 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
79 if (actor.Account) actor.Account.Actor = actor
80 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
82 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor)
83 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
85 if ((created === true || refreshed === true) && updateCollections === true) {
86 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
87 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
93 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
94 return new ActorModel({
103 inboxUrl: url + '/inbox',
104 outboxUrl: url + '/outbox',
105 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
106 followersUrl: url + '/followers',
107 followingUrl: url + '/following'
111 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
112 const followersCount = await fetchActorTotalItems(attributes.followers)
113 const followingCount = await fetchActorTotalItems(attributes.following)
115 actorInstance.set('type', attributes.type)
116 actorInstance.set('uuid', attributes.uuid)
117 actorInstance.set('preferredUsername', attributes.preferredUsername)
118 actorInstance.set('url', attributes.id)
119 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
120 actorInstance.set('followersCount', followersCount)
121 actorInstance.set('followingCount', followingCount)
122 actorInstance.set('inboxUrl', attributes.inbox)
123 actorInstance.set('outboxUrl', attributes.outbox)
124 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
125 actorInstance.set('followersUrl', attributes.followers)
126 actorInstance.set('followingUrl', attributes.following)
129 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
130 if (avatarName !== undefined) {
131 if (actorInstance.avatarId) {
133 await actorInstance.Avatar.destroy({ transaction: t })
135 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
139 const avatar = await AvatarModel.create({
141 }, { transaction: t })
143 actorInstance.set('avatarId', avatar.id)
144 actorInstance.Avatar = avatar
150 async function fetchActorTotalItems (url: string) {
159 const { body } = await doRequest(options)
160 return body.totalItems ? body.totalItems : 0
162 logger.warn('Cannot fetch remote actor count %s.', url, { err })
167 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
169 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
170 isActivityPubUrlValid(actorJSON.icon.url)
172 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
174 const avatarName = uuidv4() + extension
175 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
177 await doRequestAndSaveToFile({
179 uri: actorJSON.icon.url
188 async function addFetchOutboxJob (actor: ActorModel) {
189 // Don't fetch ourselves
190 const serverActor = await getServerActor()
191 if (serverActor.id === actor.id) {
192 logger.error('Cannot fetch our own outbox!')
197 uri: actor.outboxUrl,
198 type: 'activity' as 'activity'
201 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
205 getOrCreateActorAndServerAndModel,
208 fetchActorTotalItems,
211 updateActorAvatarInstance,
215 // ---------------------------------------------------------------------------
217 function saveActorAndServerAndModelIfNotExist (
218 result: FetchRemoteActorResult,
219 ownerActor?: ActorModel,
221 ): Bluebird<ActorModel> | Promise<ActorModel> {
222 let actor = result.actor
224 if (t !== undefined) return save(t)
226 return sequelizeTypescript.transaction(t => save(t))
228 async function save (t: Transaction) {
229 const actorHost = url.parse(actor.url).host
231 const serverOptions = {
240 const [ server ] = await ServerModel.findOrCreate(serverOptions)
242 // Save our new account in database
243 actor.set('serverId', server.id)
246 if (result.avatarName) {
247 const avatar = await AvatarModel.create({
248 filename: result.avatarName
249 }, { transaction: t })
250 actor.set('avatarId', avatar.id)
253 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
254 // (which could be false in a retried query)
255 const [ actorCreated ] = await ActorModel.findOrCreate({
256 defaults: actor.toJSON(),
263 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
264 actorCreated.Account = await saveAccount(actorCreated, result, t)
265 actorCreated.Account.Actor = actorCreated
266 } else if (actorCreated.type === 'Group') { // Video channel
267 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
268 actorCreated.VideoChannel.Actor = actorCreated
269 actorCreated.VideoChannel.Account = ownerActor.Account
276 type FetchRemoteActorResult = {
282 attributedTo: ActivityPubAttributedTo[]
284 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
292 logger.info('Fetching remote actor %s.', actorUrl)
294 const requestResult = await doRequest(options)
295 normalizeActor(requestResult.body)
297 const actorJSON: ActivityPubActor = requestResult.body
299 if (isActorObjectValid(actorJSON) === false) {
300 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
301 return { result: undefined, statusCode: requestResult.response.statusCode }
304 const followersCount = await fetchActorTotalItems(actorJSON.followers)
305 const followingCount = await fetchActorTotalItems(actorJSON.following)
307 const actor = new ActorModel({
308 type: actorJSON.type,
309 uuid: actorJSON.uuid,
310 preferredUsername: actorJSON.preferredUsername,
312 publicKey: actorJSON.publicKey.publicKeyPem,
314 followersCount: followersCount,
315 followingCount: followingCount,
316 inboxUrl: actorJSON.inbox,
317 outboxUrl: actorJSON.outbox,
318 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
319 followersUrl: actorJSON.followers,
320 followingUrl: actorJSON.following
323 const avatarName = await fetchAvatarIfExists(actorJSON)
325 const name = actorJSON.name || actorJSON.preferredUsername
327 statusCode: requestResult.response.statusCode,
332 summary: actorJSON.summary,
333 support: actorJSON.support,
334 attributedTo: actorJSON.attributedTo
339 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
340 const [ accountCreated ] = await AccountModel.findOrCreate({
343 description: result.summary,
352 return accountCreated
355 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
356 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
359 description: result.summary,
360 support: result.support,
362 accountId: ownerActor.Account.id
370 return videoChannelCreated
373 async function refreshActorIfNeeded (actor: ActorModel): Promise<{ actor: ActorModel, refreshed: boolean }> {
374 if (!actor.isOutdated()) return { actor, refreshed: false }
377 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
378 const { result, statusCode } = await fetchRemoteActor(actorUrl)
380 if (statusCode === 404) {
381 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
382 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
383 return { actor: undefined, refreshed: false }
386 if (result === undefined) {
387 logger.warn('Cannot fetch remote actor in refresh actor.')
388 return { actor, refreshed: false }
391 return sequelizeTypescript.transaction(async t => {
392 updateInstanceWithAnother(actor, result.actor)
394 if (result.avatarName !== undefined) {
395 await updateActorAvatarInstance(actor, result.avatarName, t)
399 actor.setDataValue('updatedAt', new Date())
400 await actor.save({ transaction: t })
403 await actor.save({ transaction: t })
405 actor.Account.set('name', result.name)
406 actor.Account.set('description', result.summary)
407 await actor.Account.save({ transaction: t })
408 } else if (actor.VideoChannel) {
409 await actor.save({ transaction: t })
411 actor.VideoChannel.set('name', result.name)
412 actor.VideoChannel.set('description', result.summary)
413 actor.VideoChannel.set('support', result.support)
414 await actor.VideoChannel.save({ transaction: t })
417 return { refreshed: true, actor }
420 logger.warn('Cannot refresh actor.', { err })
421 return { actor, refreshed: false }