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 { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
9 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10 import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11 import { logger } from '../../helpers/logger'
12 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
14 import { getUrlFromWebfinger } from '../../helpers/webfinger'
15 import { IMAGE_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
16 import { AccountModel } from '../../models/account/account'
17 import { ActorModel } from '../../models/activitypub/actor'
18 import { AvatarModel } from '../../models/avatar/avatar'
19 import { ServerModel } from '../../models/server/server'
20 import { VideoChannelModel } from '../../models/video/video-channel'
22 // Set account keys, this could be long so process after the account creation and do not block the client
23 function setAsyncActorKeys (actor: ActorModel) {
24 return createPrivateAndPublicKeys()
25 .then(({ publicKey, privateKey }) => {
26 actor.set('publicKey', publicKey)
27 actor.set('privateKey', privateKey)
31 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
36 async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
37 let actor = await ActorModel.loadByUrl(actorUrl)
39 // We don't have this actor in our database, fetch it on remote
41 const result = await fetchRemoteActor(actorUrl)
42 if (result === undefined) throw new Error('Cannot fetch remote actor.')
44 // Create the attributed to actor
45 // In PeerTube a video channel is owned by an account
46 let ownerActor: ActorModel = undefined
47 if (recurseIfNeeded === true && result.actor.type === 'Group') {
48 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
49 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
52 // Assert we don't recurse another time
53 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
55 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
61 arguments: [ result, ownerActor ],
62 errorMessage: 'Cannot save actor and server with many retries.'
64 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
69 errorMessage: 'Cannot refresh actor if needed with many retries.'
71 return retryTransactionWrapper(refreshActorIfNeeded, options)
74 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
75 return new ActorModel({
84 inboxUrl: url + '/inbox',
85 outboxUrl: url + '/outbox',
86 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
87 followersUrl: url + '/followers',
88 followingUrl: url + '/following'
92 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
93 const followersCount = await fetchActorTotalItems(attributes.followers)
94 const followingCount = await fetchActorTotalItems(attributes.following)
96 actorInstance.set('type', attributes.type)
97 actorInstance.set('uuid', attributes.uuid)
98 actorInstance.set('preferredUsername', attributes.preferredUsername)
99 actorInstance.set('url', attributes.id)
100 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
101 actorInstance.set('followersCount', followersCount)
102 actorInstance.set('followingCount', followingCount)
103 actorInstance.set('inboxUrl', attributes.inbox)
104 actorInstance.set('outboxUrl', attributes.outbox)
105 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
106 actorInstance.set('followersUrl', attributes.followers)
107 actorInstance.set('followingUrl', attributes.following)
110 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
111 if (avatarName !== undefined) {
112 if (actorInstance.avatarId) {
114 await actorInstance.Avatar.destroy({ transaction: t })
116 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, err)
120 const avatar = await AvatarModel.create({
122 }, { transaction: t })
124 actorInstance.set('avatarId', avatar.id)
125 actorInstance.Avatar = avatar
131 async function fetchActorTotalItems (url: string) {
140 const { body } = await doRequest(options)
141 return body.totalItems ? body.totalItems : 0
143 logger.warn('Cannot fetch remote actor count %s.', url, err)
148 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
150 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
151 isActivityPubUrlValid(actorJSON.icon.url)
153 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
155 const avatarName = uuidv4() + extension
156 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
158 await doRequestAndSaveToFile({
160 uri: actorJSON.icon.url
170 getOrCreateActorAndServerAndModel,
173 fetchActorTotalItems,
176 updateActorAvatarInstance
179 // ---------------------------------------------------------------------------
181 function saveActorAndServerAndModelIfNotExist (
182 result: FetchRemoteActorResult,
183 ownerActor?: ActorModel,
185 ): Bluebird<ActorModel> | Promise<ActorModel> {
186 let actor = result.actor
188 if (t !== undefined) return save(t)
190 return sequelizeTypescript.transaction(t => save(t))
192 async function save (t: Transaction) {
193 const actorHost = url.parse(actor.url).host
195 const serverOptions = {
204 const [ server ] = await ServerModel.findOrCreate(serverOptions)
206 // Save our new account in database
207 actor.set('serverId', server.id)
210 if (result.avatarName) {
211 const avatar = await AvatarModel.create({
212 filename: result.avatarName
213 }, { transaction: t })
214 actor.set('avatarId', avatar.id)
217 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
218 // (which could be false in a retried query)
219 const [ actorCreated ] = await ActorModel.findOrCreate({
220 defaults: actor.toJSON(),
227 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
228 actorCreated.Account = await saveAccount(actorCreated, result, t)
229 actorCreated.Account.Actor = actorCreated
230 } else if (actorCreated.type === 'Group') { // Video channel
231 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
232 actorCreated.VideoChannel.Actor = actorCreated
239 type FetchRemoteActorResult = {
245 attributedTo: ActivityPubAttributedTo[]
247 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
255 logger.info('Fetching remote actor %s.', actorUrl)
257 const requestResult = await doRequest(options)
258 const actorJSON: ActivityPubActor = normalizeActor(requestResult.body)
260 if (isActorObjectValid(actorJSON) === false) {
261 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
265 const followersCount = await fetchActorTotalItems(actorJSON.followers)
266 const followingCount = await fetchActorTotalItems(actorJSON.following)
268 const actor = new ActorModel({
269 type: actorJSON.type,
270 uuid: actorJSON.uuid,
271 preferredUsername: actorJSON.preferredUsername,
273 publicKey: actorJSON.publicKey.publicKeyPem,
275 followersCount: followersCount,
276 followingCount: followingCount,
277 inboxUrl: actorJSON.inbox,
278 outboxUrl: actorJSON.outbox,
279 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
280 followersUrl: actorJSON.followers,
281 followingUrl: actorJSON.following
284 const avatarName = await fetchAvatarIfExists(actorJSON)
286 const name = actorJSON.name || actorJSON.preferredUsername
291 summary: actorJSON.summary,
292 support: actorJSON.support,
293 attributedTo: actorJSON.attributedTo
297 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
298 const [ accountCreated ] = await AccountModel.findOrCreate({
301 description: result.summary,
310 return accountCreated
313 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
314 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
317 description: result.summary,
318 support: result.support,
320 accountId: ownerActor.Account.id
328 return videoChannelCreated
331 async function refreshActorIfNeeded (actor: ActorModel) {
332 if (!actor.isOutdated()) return actor
335 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
336 const result = await fetchRemoteActor(actorUrl)
337 if (result === undefined) {
338 logger.warn('Cannot fetch remote actor in refresh actor.')
342 return sequelizeTypescript.transaction(async t => {
343 updateInstanceWithAnother(actor, result.actor)
345 if (result.avatarName !== undefined) {
346 await updateActorAvatarInstance(actor, result.avatarName, t)
350 actor.setDataValue('updatedAt', new Date())
351 await actor.save({ transaction: t })
354 await actor.save({ transaction: t })
356 actor.Account.set('name', result.name)
357 actor.Account.set('description', result.summary)
358 await actor.Account.save({ transaction: t })
359 } else if (actor.VideoChannel) {
360 await actor.save({ transaction: t })
362 actor.VideoChannel.set('name', result.name)
363 actor.VideoChannel.set('description', result.summary)
364 actor.VideoChannel.set('support', result.support)
365 await actor.VideoChannel.save({ transaction: t })
371 logger.warn('Cannot refresh actor.', err)
376 function normalizeActor (actor: any) {
377 if (actor && actor.url && typeof actor.url === 'string') return actor
379 actor.url = actor.url.href || actor.url.url