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 return retryTransactionWrapper(refreshActorIfNeeded, actor)
76 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
77 return new ActorModel({
86 inboxUrl: url + '/inbox',
87 outboxUrl: url + '/outbox',
88 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
89 followersUrl: url + '/followers',
90 followingUrl: url + '/following'
94 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
95 const followersCount = await fetchActorTotalItems(attributes.followers)
96 const followingCount = await fetchActorTotalItems(attributes.following)
98 actorInstance.set('type', attributes.type)
99 actorInstance.set('uuid', attributes.uuid)
100 actorInstance.set('preferredUsername', attributes.preferredUsername)
101 actorInstance.set('url', attributes.id)
102 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
103 actorInstance.set('followersCount', followersCount)
104 actorInstance.set('followingCount', followingCount)
105 actorInstance.set('inboxUrl', attributes.inbox)
106 actorInstance.set('outboxUrl', attributes.outbox)
107 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
108 actorInstance.set('followersUrl', attributes.followers)
109 actorInstance.set('followingUrl', attributes.following)
112 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
113 if (avatarName !== undefined) {
114 if (actorInstance.avatarId) {
116 await actorInstance.Avatar.destroy({ transaction: t })
118 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
122 const avatar = await AvatarModel.create({
124 }, { transaction: t })
126 actorInstance.set('avatarId', avatar.id)
127 actorInstance.Avatar = avatar
133 async function fetchActorTotalItems (url: string) {
142 const { body } = await doRequest(options)
143 return body.totalItems ? body.totalItems : 0
145 logger.warn('Cannot fetch remote actor count %s.', url, { err })
150 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
152 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
153 isActivityPubUrlValid(actorJSON.icon.url)
155 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
157 const avatarName = uuidv4() + extension
158 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
160 await doRequestAndSaveToFile({
162 uri: actorJSON.icon.url
171 async function addFetchOutboxJob (actor: ActorModel) {
172 // Don't fetch ourselves
173 const serverActor = await getServerActor()
174 if (serverActor.id === actor.id) {
175 logger.error('Cannot fetch our own outbox!')
180 uris: [ actor.outboxUrl ]
183 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
187 getOrCreateActorAndServerAndModel,
190 fetchActorTotalItems,
193 updateActorAvatarInstance,
197 // ---------------------------------------------------------------------------
199 function saveActorAndServerAndModelIfNotExist (
200 result: FetchRemoteActorResult,
201 ownerActor?: ActorModel,
203 ): Bluebird<ActorModel> | Promise<ActorModel> {
204 let actor = result.actor
206 if (t !== undefined) return save(t)
208 return sequelizeTypescript.transaction(t => save(t))
210 async function save (t: Transaction) {
211 const actorHost = url.parse(actor.url).host
213 const serverOptions = {
222 const [ server ] = await ServerModel.findOrCreate(serverOptions)
224 // Save our new account in database
225 actor.set('serverId', server.id)
228 if (result.avatarName) {
229 const avatar = await AvatarModel.create({
230 filename: result.avatarName
231 }, { transaction: t })
232 actor.set('avatarId', avatar.id)
235 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
236 // (which could be false in a retried query)
237 const [ actorCreated ] = await ActorModel.findOrCreate({
238 defaults: actor.toJSON(),
245 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
246 actorCreated.Account = await saveAccount(actorCreated, result, t)
247 actorCreated.Account.Actor = actorCreated
248 } else if (actorCreated.type === 'Group') { // Video channel
249 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
250 actorCreated.VideoChannel.Actor = actorCreated
257 type FetchRemoteActorResult = {
263 attributedTo: ActivityPubAttributedTo[]
265 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
273 logger.info('Fetching remote actor %s.', actorUrl)
275 const requestResult = await doRequest(options)
276 normalizeActor(requestResult.body)
278 const actorJSON: ActivityPubActor = requestResult.body
280 if (isActorObjectValid(actorJSON) === false) {
281 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
285 const followersCount = await fetchActorTotalItems(actorJSON.followers)
286 const followingCount = await fetchActorTotalItems(actorJSON.following)
288 const actor = new ActorModel({
289 type: actorJSON.type,
290 uuid: actorJSON.uuid,
291 preferredUsername: actorJSON.preferredUsername,
293 publicKey: actorJSON.publicKey.publicKeyPem,
295 followersCount: followersCount,
296 followingCount: followingCount,
297 inboxUrl: actorJSON.inbox,
298 outboxUrl: actorJSON.outbox,
299 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
300 followersUrl: actorJSON.followers,
301 followingUrl: actorJSON.following
304 const avatarName = await fetchAvatarIfExists(actorJSON)
306 const name = actorJSON.name || actorJSON.preferredUsername
311 summary: actorJSON.summary,
312 support: actorJSON.support,
313 attributedTo: actorJSON.attributedTo
317 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
318 const [ accountCreated ] = await AccountModel.findOrCreate({
321 description: result.summary,
330 return accountCreated
333 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
334 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
337 description: result.summary,
338 support: result.support,
340 accountId: ownerActor.Account.id
348 return videoChannelCreated
351 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
352 if (!actor.isOutdated()) return actor
355 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
356 const result = await fetchRemoteActor(actorUrl)
357 if (result === undefined) {
358 logger.warn('Cannot fetch remote actor in refresh actor.')
362 return sequelizeTypescript.transaction(async t => {
363 updateInstanceWithAnother(actor, result.actor)
365 if (result.avatarName !== undefined) {
366 await updateActorAvatarInstance(actor, result.avatarName, t)
370 actor.setDataValue('updatedAt', new Date())
371 await actor.save({ transaction: t })
374 await actor.save({ transaction: t })
376 actor.Account.set('name', result.name)
377 actor.Account.set('description', result.summary)
378 await actor.Account.save({ transaction: t })
379 } else if (actor.VideoChannel) {
380 await actor.save({ transaction: t })
382 actor.VideoChannel.set('name', result.name)
383 actor.VideoChannel.set('description', result.summary)
384 actor.VideoChannel.set('support', result.support)
385 await actor.VideoChannel.save({ transaction: t })
391 logger.warn('Cannot refresh actor.', { err })