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, CONSTRAINTS_FIELDS } 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'
21 import { truncate } from 'lodash'
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 (actorUrl: string, recurseIfNeeded = true) {
38 let actor = await ActorModel.loadByUrl(actorUrl)
40 // We don't have this actor in our database, fetch it on remote
42 const result = await fetchRemoteActor(actorUrl)
43 if (result === undefined) throw new Error('Cannot fetch remote actor.')
45 // Create the attributed to actor
46 // In PeerTube a video channel is owned by an account
47 let ownerActor: ActorModel = undefined
48 if (recurseIfNeeded === true && result.actor.type === 'Group') {
49 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
50 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
53 // Assert we don't recurse another time
54 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
56 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
62 arguments: [ result, ownerActor ],
63 errorMessage: 'Cannot save actor and server with many retries.'
65 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
70 errorMessage: 'Cannot refresh actor if needed with many retries.'
72 return retryTransactionWrapper(refreshActorIfNeeded, options)
75 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
76 return new ActorModel({
85 inboxUrl: url + '/inbox',
86 outboxUrl: url + '/outbox',
87 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
88 followersUrl: url + '/followers',
89 followingUrl: url + '/following'
93 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
94 const followersCount = await fetchActorTotalItems(attributes.followers)
95 const followingCount = await fetchActorTotalItems(attributes.following)
97 actorInstance.set('type', attributes.type)
98 actorInstance.set('uuid', attributes.uuid)
99 actorInstance.set('preferredUsername', attributes.preferredUsername)
100 actorInstance.set('url', attributes.id)
101 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
102 actorInstance.set('followersCount', followersCount)
103 actorInstance.set('followingCount', followingCount)
104 actorInstance.set('inboxUrl', attributes.inbox)
105 actorInstance.set('outboxUrl', attributes.outbox)
106 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
107 actorInstance.set('followersUrl', attributes.followers)
108 actorInstance.set('followingUrl', attributes.following)
111 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
112 if (avatarName !== undefined) {
113 if (actorInstance.avatarId) {
115 await actorInstance.Avatar.destroy({ transaction: t })
117 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, err)
121 const avatar = await AvatarModel.create({
123 }, { transaction: t })
125 actorInstance.set('avatarId', avatar.id)
126 actorInstance.Avatar = avatar
132 async function fetchActorTotalItems (url: string) {
141 const { body } = await doRequest(options)
142 return body.totalItems ? body.totalItems : 0
144 logger.warn('Cannot fetch remote actor count %s.', url, err)
149 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
151 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
152 isActivityPubUrlValid(actorJSON.icon.url)
154 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
156 const avatarName = uuidv4() + extension
157 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
159 await doRequestAndSaveToFile({
161 uri: actorJSON.icon.url
170 function normalizeActor (actor: any) {
173 if (!actor.url || typeof actor.url !== 'string') {
174 actor.url = actor.url.href || actor.url.url
177 if (actor.summary && typeof actor.summary === 'string') {
178 actor.summary = truncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max })
180 if (actor.summary.length < CONSTRAINTS_FIELDS.USERS.DESCRIPTION.min) {
189 getOrCreateActorAndServerAndModel,
192 fetchActorTotalItems,
195 updateActorAvatarInstance,
199 // ---------------------------------------------------------------------------
201 function saveActorAndServerAndModelIfNotExist (
202 result: FetchRemoteActorResult,
203 ownerActor?: ActorModel,
205 ): Bluebird<ActorModel> | Promise<ActorModel> {
206 let actor = result.actor
208 if (t !== undefined) return save(t)
210 return sequelizeTypescript.transaction(t => save(t))
212 async function save (t: Transaction) {
213 const actorHost = url.parse(actor.url).host
215 const serverOptions = {
224 const [ server ] = await ServerModel.findOrCreate(serverOptions)
226 // Save our new account in database
227 actor.set('serverId', server.id)
230 if (result.avatarName) {
231 const avatar = await AvatarModel.create({
232 filename: result.avatarName
233 }, { transaction: t })
234 actor.set('avatarId', avatar.id)
237 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
238 // (which could be false in a retried query)
239 const [ actorCreated ] = await ActorModel.findOrCreate({
240 defaults: actor.toJSON(),
247 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
248 actorCreated.Account = await saveAccount(actorCreated, result, t)
249 actorCreated.Account.Actor = actorCreated
250 } else if (actorCreated.type === 'Group') { // Video channel
251 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
252 actorCreated.VideoChannel.Actor = actorCreated
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) {
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)