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 uri: actor.outboxUrl,
181 type: 'activity' as 'activity'
184 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
188 getOrCreateActorAndServerAndModel,
191 fetchActorTotalItems,
194 updateActorAvatarInstance,
198 // ---------------------------------------------------------------------------
200 function saveActorAndServerAndModelIfNotExist (
201 result: FetchRemoteActorResult,
202 ownerActor?: ActorModel,
204 ): Bluebird<ActorModel> | Promise<ActorModel> {
205 let actor = result.actor
207 if (t !== undefined) return save(t)
209 return sequelizeTypescript.transaction(t => save(t))
211 async function save (t: Transaction) {
212 const actorHost = url.parse(actor.url).host
214 const serverOptions = {
223 const [ server ] = await ServerModel.findOrCreate(serverOptions)
225 // Save our new account in database
226 actor.set('serverId', server.id)
229 if (result.avatarName) {
230 const avatar = await AvatarModel.create({
231 filename: result.avatarName
232 }, { transaction: t })
233 actor.set('avatarId', avatar.id)
236 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
237 // (which could be false in a retried query)
238 const [ actorCreated ] = await ActorModel.findOrCreate({
239 defaults: actor.toJSON(),
246 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
247 actorCreated.Account = await saveAccount(actorCreated, result, t)
248 actorCreated.Account.Actor = actorCreated
249 } else if (actorCreated.type === 'Group') { // Video channel
250 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
251 actorCreated.VideoChannel.Actor = actorCreated
252 actorCreated.VideoChannel.Account = ownerActor.Account
259 type FetchRemoteActorResult = {
265 attributedTo: ActivityPubAttributedTo[]
267 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
275 logger.info('Fetching remote actor %s.', actorUrl)
277 const requestResult = await doRequest(options)
278 normalizeActor(requestResult.body)
280 const actorJSON: ActivityPubActor = requestResult.body
282 if (isActorObjectValid(actorJSON) === false) {
283 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
287 const followersCount = await fetchActorTotalItems(actorJSON.followers)
288 const followingCount = await fetchActorTotalItems(actorJSON.following)
290 const actor = new ActorModel({
291 type: actorJSON.type,
292 uuid: actorJSON.uuid,
293 preferredUsername: actorJSON.preferredUsername,
295 publicKey: actorJSON.publicKey.publicKeyPem,
297 followersCount: followersCount,
298 followingCount: followingCount,
299 inboxUrl: actorJSON.inbox,
300 outboxUrl: actorJSON.outbox,
301 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
302 followersUrl: actorJSON.followers,
303 followingUrl: actorJSON.following
306 const avatarName = await fetchAvatarIfExists(actorJSON)
308 const name = actorJSON.name || actorJSON.preferredUsername
313 summary: actorJSON.summary,
314 support: actorJSON.support,
315 attributedTo: actorJSON.attributedTo
319 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
320 const [ accountCreated ] = await AccountModel.findOrCreate({
323 description: result.summary,
332 return accountCreated
335 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
336 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
339 description: result.summary,
340 support: result.support,
342 accountId: ownerActor.Account.id
350 return videoChannelCreated
353 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
354 if (!actor.isOutdated()) return actor
357 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
358 const result = await fetchRemoteActor(actorUrl)
359 if (result === undefined) {
360 logger.warn('Cannot fetch remote actor in refresh actor.')
364 return sequelizeTypescript.transaction(async t => {
365 updateInstanceWithAnother(actor, result.actor)
367 if (result.avatarName !== undefined) {
368 await updateActorAvatarInstance(actor, result.avatarName, t)
372 actor.setDataValue('updatedAt', new Date())
373 await actor.save({ transaction: t })
376 await actor.save({ transaction: t })
378 actor.Account.set('name', result.name)
379 actor.Account.set('description', result.summary)
380 await actor.Account.save({ transaction: t })
381 } else if (actor.VideoChannel) {
382 await actor.save({ transaction: t })
384 actor.VideoChannel.set('name', result.name)
385 actor.VideoChannel.set('description', result.summary)
386 actor.VideoChannel.set('support', result.support)
387 await actor.VideoChannel.save({ transaction: t })
393 logger.warn('Cannot refresh actor.', { err })