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)
43 // Orphan actor (not associated to an account of channel) so recreate it
44 if (actor && (!actor.Account && !actor.VideoChannel)) {
49 // We don't have this actor in our database, fetch it on remote
51 const { result } = await fetchRemoteActor(actorUrl)
52 if (result === undefined) throw new Error('Cannot fetch remote actor.')
54 // Create the attributed to actor
55 // In PeerTube a video channel is owned by an account
56 let ownerActor: ActorModel = undefined
57 if (recurseIfNeeded === true && result.actor.type === 'Group') {
58 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
59 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
62 // Assert we don't recurse another time
63 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
65 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
70 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
73 if (actor.Account) actor.Account.Actor = actor
74 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
76 actor = await retryTransactionWrapper(refreshActorIfNeeded, actor)
77 if (!actor) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
82 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
83 return new ActorModel({
92 inboxUrl: url + '/inbox',
93 outboxUrl: url + '/outbox',
94 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
95 followersUrl: url + '/followers',
96 followingUrl: url + '/following'
100 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
101 const followersCount = await fetchActorTotalItems(attributes.followers)
102 const followingCount = await fetchActorTotalItems(attributes.following)
104 actorInstance.set('type', attributes.type)
105 actorInstance.set('uuid', attributes.uuid)
106 actorInstance.set('preferredUsername', attributes.preferredUsername)
107 actorInstance.set('url', attributes.id)
108 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
109 actorInstance.set('followersCount', followersCount)
110 actorInstance.set('followingCount', followingCount)
111 actorInstance.set('inboxUrl', attributes.inbox)
112 actorInstance.set('outboxUrl', attributes.outbox)
113 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
114 actorInstance.set('followersUrl', attributes.followers)
115 actorInstance.set('followingUrl', attributes.following)
118 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
119 if (avatarName !== undefined) {
120 if (actorInstance.avatarId) {
122 await actorInstance.Avatar.destroy({ transaction: t })
124 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
128 const avatar = await AvatarModel.create({
130 }, { transaction: t })
132 actorInstance.set('avatarId', avatar.id)
133 actorInstance.Avatar = avatar
139 async function fetchActorTotalItems (url: string) {
148 const { body } = await doRequest(options)
149 return body.totalItems ? body.totalItems : 0
151 logger.warn('Cannot fetch remote actor count %s.', url, { err })
156 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
158 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
159 isActivityPubUrlValid(actorJSON.icon.url)
161 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
163 const avatarName = uuidv4() + extension
164 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
166 await doRequestAndSaveToFile({
168 uri: actorJSON.icon.url
177 async function addFetchOutboxJob (actor: ActorModel) {
178 // Don't fetch ourselves
179 const serverActor = await getServerActor()
180 if (serverActor.id === actor.id) {
181 logger.error('Cannot fetch our own outbox!')
186 uri: actor.outboxUrl,
187 type: 'activity' as 'activity'
190 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
194 getOrCreateActorAndServerAndModel,
197 fetchActorTotalItems,
200 updateActorAvatarInstance,
204 // ---------------------------------------------------------------------------
206 function saveActorAndServerAndModelIfNotExist (
207 result: FetchRemoteActorResult,
208 ownerActor?: ActorModel,
210 ): Bluebird<ActorModel> | Promise<ActorModel> {
211 let actor = result.actor
213 if (t !== undefined) return save(t)
215 return sequelizeTypescript.transaction(t => save(t))
217 async function save (t: Transaction) {
218 const actorHost = url.parse(actor.url).host
220 const serverOptions = {
229 const [ server ] = await ServerModel.findOrCreate(serverOptions)
231 // Save our new account in database
232 actor.set('serverId', server.id)
235 if (result.avatarName) {
236 const avatar = await AvatarModel.create({
237 filename: result.avatarName
238 }, { transaction: t })
239 actor.set('avatarId', avatar.id)
242 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
243 // (which could be false in a retried query)
244 const [ actorCreated ] = await ActorModel.findOrCreate({
245 defaults: actor.toJSON(),
252 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
253 actorCreated.Account = await saveAccount(actorCreated, result, t)
254 actorCreated.Account.Actor = actorCreated
255 } else if (actorCreated.type === 'Group') { // Video channel
256 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
257 actorCreated.VideoChannel.Actor = actorCreated
258 actorCreated.VideoChannel.Account = ownerActor.Account
265 type FetchRemoteActorResult = {
271 attributedTo: ActivityPubAttributedTo[]
273 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
281 logger.info('Fetching remote actor %s.', actorUrl)
283 const requestResult = await doRequest(options)
284 normalizeActor(requestResult.body)
286 const actorJSON: ActivityPubActor = requestResult.body
288 if (isActorObjectValid(actorJSON) === false) {
289 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
290 return { result: undefined, statusCode: requestResult.response.statusCode }
293 const followersCount = await fetchActorTotalItems(actorJSON.followers)
294 const followingCount = await fetchActorTotalItems(actorJSON.following)
296 const actor = new ActorModel({
297 type: actorJSON.type,
298 uuid: actorJSON.uuid,
299 preferredUsername: actorJSON.preferredUsername,
301 publicKey: actorJSON.publicKey.publicKeyPem,
303 followersCount: followersCount,
304 followingCount: followingCount,
305 inboxUrl: actorJSON.inbox,
306 outboxUrl: actorJSON.outbox,
307 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
308 followersUrl: actorJSON.followers,
309 followingUrl: actorJSON.following
312 const avatarName = await fetchAvatarIfExists(actorJSON)
314 const name = actorJSON.name || actorJSON.preferredUsername
316 statusCode: requestResult.response.statusCode,
321 summary: actorJSON.summary,
322 support: actorJSON.support,
323 attributedTo: actorJSON.attributedTo
328 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
329 const [ accountCreated ] = await AccountModel.findOrCreate({
332 description: result.summary,
341 return accountCreated
344 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
345 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
348 description: result.summary,
349 support: result.support,
351 accountId: ownerActor.Account.id
359 return videoChannelCreated
362 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
363 if (!actor.isOutdated()) return actor
366 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
367 const { result, statusCode } = await fetchRemoteActor(actorUrl)
369 if (statusCode === 404) {
370 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
371 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
375 if (result === undefined) {
376 logger.warn('Cannot fetch remote actor in refresh actor.')
380 return sequelizeTypescript.transaction(async t => {
381 updateInstanceWithAnother(actor, result.actor)
383 if (result.avatarName !== undefined) {
384 await updateActorAvatarInstance(actor, result.avatarName, t)
388 actor.setDataValue('updatedAt', new Date())
389 await actor.save({ transaction: t })
392 await actor.save({ transaction: t })
394 actor.Account.set('name', result.name)
395 actor.Account.set('description', result.summary)
396 await actor.Account.save({ transaction: t })
397 } else if (actor.VideoChannel) {
398 await actor.save({ transaction: t })
400 actor.VideoChannel.set('name', result.name)
401 actor.VideoChannel.set('description', result.summary)
402 actor.VideoChannel.set('support', result.support)
403 await actor.VideoChannel.save({ transaction: t })
409 logger.warn('Cannot refresh actor.', { err })