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 } 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 { IMAGE_MIMETYPE_EXT, CONFIG, sequelizeTypescript, CONSTRAINTS_FIELDS } 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 { truncate } from 'lodash'
24 // Set account keys, this could be long so process after the account creation and do not block the client
25 function setAsyncActorKeys (actor: ActorModel) {
26 return createPrivateAndPublicKeys()
27 .then(({ publicKey, privateKey }) => {
28 actor.set('publicKey', publicKey)
29 actor.set('privateKey', privateKey)
33 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
38 async function getOrCreateActorAndServerAndModel (activityActor: string | ActivityPubActor, recurseIfNeeded = true) {
39 const actorUrl = getActorUrl(activityActor)
41 let actor = await ActorModel.loadByUrl(actorUrl)
43 // We don't have this actor in our database, fetch it on remote
45 const result = await fetchRemoteActor(actorUrl)
46 if (result === undefined) throw new Error('Cannot fetch remote actor.')
48 // Create the attributed to actor
49 // In PeerTube a video channel is owned by an account
50 let ownerActor: ActorModel = undefined
51 if (recurseIfNeeded === true && result.actor.type === 'Group') {
52 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
53 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
56 // Assert we don't recurse another time
57 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
59 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
65 arguments: [ result, ownerActor ],
66 errorMessage: 'Cannot save actor and server with many retries.'
68 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
73 errorMessage: 'Cannot refresh actor if needed with many retries.'
75 return retryTransactionWrapper(refreshActorIfNeeded, options)
78 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
79 return new ActorModel({
88 inboxUrl: url + '/inbox',
89 outboxUrl: url + '/outbox',
90 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
91 followersUrl: url + '/followers',
92 followingUrl: url + '/following'
96 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
97 const followersCount = await fetchActorTotalItems(attributes.followers)
98 const followingCount = await fetchActorTotalItems(attributes.following)
100 actorInstance.set('type', attributes.type)
101 actorInstance.set('uuid', attributes.uuid)
102 actorInstance.set('preferredUsername', attributes.preferredUsername)
103 actorInstance.set('url', attributes.id)
104 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
105 actorInstance.set('followersCount', followersCount)
106 actorInstance.set('followingCount', followingCount)
107 actorInstance.set('inboxUrl', attributes.inbox)
108 actorInstance.set('outboxUrl', attributes.outbox)
109 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
110 actorInstance.set('followersUrl', attributes.followers)
111 actorInstance.set('followingUrl', attributes.following)
114 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
115 if (avatarName !== undefined) {
116 if (actorInstance.avatarId) {
118 await actorInstance.Avatar.destroy({ transaction: t })
120 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
124 const avatar = await AvatarModel.create({
126 }, { transaction: t })
128 actorInstance.set('avatarId', avatar.id)
129 actorInstance.Avatar = avatar
135 async function fetchActorTotalItems (url: string) {
144 const { body } = await doRequest(options)
145 return body.totalItems ? body.totalItems : 0
147 logger.warn('Cannot fetch remote actor count %s.', url, { err })
152 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
154 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
155 isActivityPubUrlValid(actorJSON.icon.url)
157 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
159 const avatarName = uuidv4() + extension
160 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
162 await doRequestAndSaveToFile({
164 uri: actorJSON.icon.url
173 function normalizeActor (actor: any) {
176 if (!actor.url || typeof actor.url !== 'string') {
177 actor.url = actor.url.href || actor.url.url
180 if (actor.summary && typeof actor.summary === 'string') {
181 actor.summary = truncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max })
183 if (actor.summary.length < CONSTRAINTS_FIELDS.USERS.DESCRIPTION.min) {
192 getOrCreateActorAndServerAndModel,
195 fetchActorTotalItems,
198 updateActorAvatarInstance,
202 // ---------------------------------------------------------------------------
204 function saveActorAndServerAndModelIfNotExist (
205 result: FetchRemoteActorResult,
206 ownerActor?: ActorModel,
208 ): Bluebird<ActorModel> | Promise<ActorModel> {
209 let actor = result.actor
211 if (t !== undefined) return save(t)
213 return sequelizeTypescript.transaction(t => save(t))
215 async function save (t: Transaction) {
216 const actorHost = url.parse(actor.url).host
218 const serverOptions = {
227 const [ server ] = await ServerModel.findOrCreate(serverOptions)
229 // Save our new account in database
230 actor.set('serverId', server.id)
233 if (result.avatarName) {
234 const avatar = await AvatarModel.create({
235 filename: result.avatarName
236 }, { transaction: t })
237 actor.set('avatarId', avatar.id)
240 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
241 // (which could be false in a retried query)
242 const [ actorCreated ] = await ActorModel.findOrCreate({
243 defaults: actor.toJSON(),
250 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
251 actorCreated.Account = await saveAccount(actorCreated, result, t)
252 actorCreated.Account.Actor = actorCreated
253 } else if (actorCreated.type === 'Group') { // Video channel
254 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
255 actorCreated.VideoChannel.Actor = actorCreated
262 type FetchRemoteActorResult = {
268 attributedTo: ActivityPubAttributedTo[]
270 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
278 logger.info('Fetching remote actor %s.', actorUrl)
280 const requestResult = await doRequest(options)
281 normalizeActor(requestResult.body)
283 const actorJSON: ActivityPubActor = requestResult.body
285 if (isActorObjectValid(actorJSON) === false) {
286 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
290 const followersCount = await fetchActorTotalItems(actorJSON.followers)
291 const followingCount = await fetchActorTotalItems(actorJSON.following)
293 const actor = new ActorModel({
294 type: actorJSON.type,
295 uuid: actorJSON.uuid,
296 preferredUsername: actorJSON.preferredUsername,
298 publicKey: actorJSON.publicKey.publicKeyPem,
300 followersCount: followersCount,
301 followingCount: followingCount,
302 inboxUrl: actorJSON.inbox,
303 outboxUrl: actorJSON.outbox,
304 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
305 followersUrl: actorJSON.followers,
306 followingUrl: actorJSON.following
309 const avatarName = await fetchAvatarIfExists(actorJSON)
311 const name = actorJSON.name || actorJSON.preferredUsername
316 summary: actorJSON.summary,
317 support: actorJSON.support,
318 attributedTo: actorJSON.attributedTo
322 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
323 const [ accountCreated ] = await AccountModel.findOrCreate({
326 description: result.summary,
335 return accountCreated
338 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
339 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
342 description: result.summary,
343 support: result.support,
345 accountId: ownerActor.Account.id
353 return videoChannelCreated
356 async function refreshActorIfNeeded (actor: ActorModel) {
357 if (!actor.isOutdated()) return actor
360 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
361 const result = await fetchRemoteActor(actorUrl)
362 if (result === undefined) {
363 logger.warn('Cannot fetch remote actor in refresh actor.')
367 return sequelizeTypescript.transaction(async t => {
368 updateInstanceWithAnother(actor, result.actor)
370 if (result.avatarName !== undefined) {
371 await updateActorAvatarInstance(actor, result.avatarName, t)
375 actor.setDataValue('updatedAt', new Date())
376 await actor.save({ transaction: t })
379 await actor.save({ transaction: t })
381 actor.Account.set('name', result.name)
382 actor.Account.set('description', result.summary)
383 await actor.Account.save({ transaction: t })
384 } else if (actor.VideoChannel) {
385 await actor.save({ transaction: t })
387 actor.VideoChannel.set('name', result.name)
388 actor.VideoChannel.set('description', result.summary)
389 actor.VideoChannel.set('support', result.support)
390 await actor.VideoChannel.save({ transaction: t })
396 logger.warn('Cannot refresh actor.', { err })