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'
23 // Set account keys, this could be long so process after the account creation and do not block the client
24 function setAsyncActorKeys (actor: ActorModel) {
25 return createPrivateAndPublicKeys()
26 .then(({ publicKey, privateKey }) => {
27 actor.set('publicKey', publicKey)
28 actor.set('privateKey', privateKey)
32 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
37 async function getOrCreateActorAndServerAndModel (activityActor: string | ActivityPubActor, recurseIfNeeded = true) {
38 const actorUrl = getActorUrl(activityActor)
40 let actor = await ActorModel.loadByUrl(actorUrl)
42 // We don't have this actor in our database, fetch it on remote
44 const result = await fetchRemoteActor(actorUrl)
45 if (result === undefined) throw new Error('Cannot fetch remote actor.')
47 // Create the attributed to actor
48 // In PeerTube a video channel is owned by an account
49 let ownerActor: ActorModel = undefined
50 if (recurseIfNeeded === true && result.actor.type === 'Group') {
51 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
52 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
55 // Assert we don't recurse another time
56 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
58 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
64 arguments: [ result, ownerActor ],
65 errorMessage: 'Cannot save actor and server with many retries.'
67 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
72 errorMessage: 'Cannot refresh actor if needed with many retries.'
74 return retryTransactionWrapper(refreshActorIfNeeded, options)
77 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
78 return new ActorModel({
87 inboxUrl: url + '/inbox',
88 outboxUrl: url + '/outbox',
89 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
90 followersUrl: url + '/followers',
91 followingUrl: url + '/following'
95 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
96 const followersCount = await fetchActorTotalItems(attributes.followers)
97 const followingCount = await fetchActorTotalItems(attributes.following)
99 actorInstance.set('type', attributes.type)
100 actorInstance.set('uuid', attributes.uuid)
101 actorInstance.set('preferredUsername', attributes.preferredUsername)
102 actorInstance.set('url', attributes.id)
103 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
104 actorInstance.set('followersCount', followersCount)
105 actorInstance.set('followingCount', followingCount)
106 actorInstance.set('inboxUrl', attributes.inbox)
107 actorInstance.set('outboxUrl', attributes.outbox)
108 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
109 actorInstance.set('followersUrl', attributes.followers)
110 actorInstance.set('followingUrl', attributes.following)
113 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
114 if (avatarName !== undefined) {
115 if (actorInstance.avatarId) {
117 await actorInstance.Avatar.destroy({ transaction: t })
119 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
123 const avatar = await AvatarModel.create({
125 }, { transaction: t })
127 actorInstance.set('avatarId', avatar.id)
128 actorInstance.Avatar = avatar
134 async function fetchActorTotalItems (url: string) {
143 const { body } = await doRequest(options)
144 return body.totalItems ? body.totalItems : 0
146 logger.warn('Cannot fetch remote actor count %s.', url, { err })
151 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
153 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
154 isActivityPubUrlValid(actorJSON.icon.url)
156 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
158 const avatarName = uuidv4() + extension
159 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
161 await doRequestAndSaveToFile({
163 uri: actorJSON.icon.url
173 getOrCreateActorAndServerAndModel,
176 fetchActorTotalItems,
179 updateActorAvatarInstance
182 // ---------------------------------------------------------------------------
184 function saveActorAndServerAndModelIfNotExist (
185 result: FetchRemoteActorResult,
186 ownerActor?: ActorModel,
188 ): Bluebird<ActorModel> | Promise<ActorModel> {
189 let actor = result.actor
191 if (t !== undefined) return save(t)
193 return sequelizeTypescript.transaction(t => save(t))
195 async function save (t: Transaction) {
196 const actorHost = url.parse(actor.url).host
198 const serverOptions = {
207 const [ server ] = await ServerModel.findOrCreate(serverOptions)
209 // Save our new account in database
210 actor.set('serverId', server.id)
213 if (result.avatarName) {
214 const avatar = await AvatarModel.create({
215 filename: result.avatarName
216 }, { transaction: t })
217 actor.set('avatarId', avatar.id)
220 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
221 // (which could be false in a retried query)
222 const [ actorCreated ] = await ActorModel.findOrCreate({
223 defaults: actor.toJSON(),
230 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
231 actorCreated.Account = await saveAccount(actorCreated, result, t)
232 actorCreated.Account.Actor = actorCreated
233 } else if (actorCreated.type === 'Group') { // Video channel
234 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
235 actorCreated.VideoChannel.Actor = actorCreated
242 type FetchRemoteActorResult = {
248 attributedTo: ActivityPubAttributedTo[]
250 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
258 logger.info('Fetching remote actor %s.', actorUrl)
260 const requestResult = await doRequest(options)
261 normalizeActor(requestResult.body)
263 const actorJSON: ActivityPubActor = requestResult.body
265 if (isActorObjectValid(actorJSON) === false) {
266 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
270 const followersCount = await fetchActorTotalItems(actorJSON.followers)
271 const followingCount = await fetchActorTotalItems(actorJSON.following)
273 const actor = new ActorModel({
274 type: actorJSON.type,
275 uuid: actorJSON.uuid,
276 preferredUsername: actorJSON.preferredUsername,
278 publicKey: actorJSON.publicKey.publicKeyPem,
280 followersCount: followersCount,
281 followingCount: followingCount,
282 inboxUrl: actorJSON.inbox,
283 outboxUrl: actorJSON.outbox,
284 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
285 followersUrl: actorJSON.followers,
286 followingUrl: actorJSON.following
289 const avatarName = await fetchAvatarIfExists(actorJSON)
291 const name = actorJSON.name || actorJSON.preferredUsername
296 summary: actorJSON.summary,
297 support: actorJSON.support,
298 attributedTo: actorJSON.attributedTo
302 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
303 const [ accountCreated ] = await AccountModel.findOrCreate({
306 description: result.summary,
315 return accountCreated
318 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
319 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
322 description: result.summary,
323 support: result.support,
325 accountId: ownerActor.Account.id
333 return videoChannelCreated
336 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
337 if (!actor.isOutdated()) return actor
340 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
341 const result = await fetchRemoteActor(actorUrl)
342 if (result === undefined) {
343 logger.warn('Cannot fetch remote actor in refresh actor.')
347 return sequelizeTypescript.transaction(async t => {
348 updateInstanceWithAnother(actor, result.actor)
350 if (result.avatarName !== undefined) {
351 await updateActorAvatarInstance(actor, result.avatarName, t)
355 actor.setDataValue('updatedAt', new Date())
356 await actor.save({ transaction: t })
359 await actor.save({ transaction: t })
361 actor.Account.set('name', result.name)
362 actor.Account.set('description', result.summary)
363 await actor.Account.save({ transaction: t })
364 } else if (actor.VideoChannel) {
365 await actor.save({ transaction: t })
367 actor.VideoChannel.set('name', result.name)
368 actor.VideoChannel.set('description', result.summary)
369 actor.VideoChannel.set('support', result.support)
370 await actor.VideoChannel.save({ transaction: t })
376 logger.warn('Cannot refresh actor.', { err })