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)
44 // We don't have this actor in our database, fetch it on remote
46 const result = await fetchRemoteActor(actorUrl)
47 if (result === undefined) throw new Error('Cannot fetch remote actor.')
49 // Create the attributed to actor
50 // In PeerTube a video channel is owned by an account
51 let ownerActor: ActorModel = undefined
52 if (recurseIfNeeded === true && result.actor.type === 'Group') {
53 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
54 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
57 // Assert we don't recurse another time
58 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
60 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
65 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
68 return retryTransactionWrapper(refreshActorIfNeeded, actor)
71 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
72 return new ActorModel({
81 inboxUrl: url + '/inbox',
82 outboxUrl: url + '/outbox',
83 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
84 followersUrl: url + '/followers',
85 followingUrl: url + '/following'
89 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
90 const followersCount = await fetchActorTotalItems(attributes.followers)
91 const followingCount = await fetchActorTotalItems(attributes.following)
93 actorInstance.set('type', attributes.type)
94 actorInstance.set('uuid', attributes.uuid)
95 actorInstance.set('preferredUsername', attributes.preferredUsername)
96 actorInstance.set('url', attributes.id)
97 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
98 actorInstance.set('followersCount', followersCount)
99 actorInstance.set('followingCount', followingCount)
100 actorInstance.set('inboxUrl', attributes.inbox)
101 actorInstance.set('outboxUrl', attributes.outbox)
102 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
103 actorInstance.set('followersUrl', attributes.followers)
104 actorInstance.set('followingUrl', attributes.following)
107 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
108 if (avatarName !== undefined) {
109 if (actorInstance.avatarId) {
111 await actorInstance.Avatar.destroy({ transaction: t })
113 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
117 const avatar = await AvatarModel.create({
119 }, { transaction: t })
121 actorInstance.set('avatarId', avatar.id)
122 actorInstance.Avatar = avatar
128 async function fetchActorTotalItems (url: string) {
137 const { body } = await doRequest(options)
138 return body.totalItems ? body.totalItems : 0
140 logger.warn('Cannot fetch remote actor count %s.', url, { err })
145 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
147 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
148 isActivityPubUrlValid(actorJSON.icon.url)
150 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
152 const avatarName = uuidv4() + extension
153 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
155 await doRequestAndSaveToFile({
157 uri: actorJSON.icon.url
166 async function addFetchOutboxJob (actor: ActorModel) {
167 // Don't fetch ourselves
168 const serverActor = await getServerActor()
169 if (serverActor.id === actor.id) {
170 logger.error('Cannot fetch our own outbox!')
175 uris: [ actor.outboxUrl ]
178 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
182 getOrCreateActorAndServerAndModel,
185 fetchActorTotalItems,
188 updateActorAvatarInstance,
192 // ---------------------------------------------------------------------------
194 function saveActorAndServerAndModelIfNotExist (
195 result: FetchRemoteActorResult,
196 ownerActor?: ActorModel,
198 ): Bluebird<ActorModel> | Promise<ActorModel> {
199 let actor = result.actor
201 if (t !== undefined) return save(t)
203 return sequelizeTypescript.transaction(t => save(t))
205 async function save (t: Transaction) {
206 const actorHost = url.parse(actor.url).host
208 const serverOptions = {
217 const [ server ] = await ServerModel.findOrCreate(serverOptions)
219 // Save our new account in database
220 actor.set('serverId', server.id)
223 if (result.avatarName) {
224 const avatar = await AvatarModel.create({
225 filename: result.avatarName
226 }, { transaction: t })
227 actor.set('avatarId', avatar.id)
230 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
231 // (which could be false in a retried query)
232 const [ actorCreated ] = await ActorModel.findOrCreate({
233 defaults: actor.toJSON(),
240 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
241 actorCreated.Account = await saveAccount(actorCreated, result, t)
242 actorCreated.Account.Actor = actorCreated
243 } else if (actorCreated.type === 'Group') { // Video channel
244 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
245 actorCreated.VideoChannel.Actor = actorCreated
252 type FetchRemoteActorResult = {
258 attributedTo: ActivityPubAttributedTo[]
260 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
268 logger.info('Fetching remote actor %s.', actorUrl)
270 const requestResult = await doRequest(options)
271 normalizeActor(requestResult.body)
273 const actorJSON: ActivityPubActor = requestResult.body
275 if (isActorObjectValid(actorJSON) === false) {
276 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
280 const followersCount = await fetchActorTotalItems(actorJSON.followers)
281 const followingCount = await fetchActorTotalItems(actorJSON.following)
283 const actor = new ActorModel({
284 type: actorJSON.type,
285 uuid: actorJSON.uuid,
286 preferredUsername: actorJSON.preferredUsername,
288 publicKey: actorJSON.publicKey.publicKeyPem,
290 followersCount: followersCount,
291 followingCount: followingCount,
292 inboxUrl: actorJSON.inbox,
293 outboxUrl: actorJSON.outbox,
294 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
295 followersUrl: actorJSON.followers,
296 followingUrl: actorJSON.following
299 const avatarName = await fetchAvatarIfExists(actorJSON)
301 const name = actorJSON.name || actorJSON.preferredUsername
306 summary: actorJSON.summary,
307 support: actorJSON.support,
308 attributedTo: actorJSON.attributedTo
312 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
313 const [ accountCreated ] = await AccountModel.findOrCreate({
316 description: result.summary,
325 return accountCreated
328 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
329 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
332 description: result.summary,
333 support: result.support,
335 accountId: ownerActor.Account.id
343 return videoChannelCreated
346 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
347 if (!actor.isOutdated()) return actor
350 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
351 const result = await fetchRemoteActor(actorUrl)
352 if (result === undefined) {
353 logger.warn('Cannot fetch remote actor in refresh actor.')
357 return sequelizeTypescript.transaction(async t => {
358 updateInstanceWithAnother(actor, result.actor)
360 if (result.avatarName !== undefined) {
361 await updateActorAvatarInstance(actor, result.avatarName, t)
365 actor.setDataValue('updatedAt', new Date())
366 await actor.save({ transaction: t })
369 await actor.save({ transaction: t })
371 actor.Account.set('name', result.name)
372 actor.Account.set('description', result.summary)
373 await actor.Account.save({ transaction: t })
374 } else if (actor.VideoChannel) {
375 await actor.save({ transaction: t })
377 actor.VideoChannel.set('name', result.name)
378 actor.VideoChannel.set('description', result.summary)
379 actor.VideoChannel.set('support', result.support)
380 await actor.VideoChannel.save({ transaction: t })
386 logger.warn('Cannot refresh actor.', { err })