1 import * as Bluebird from 'bluebird'
2 import { Transaction } from 'sequelize'
3 import * as url from 'url'
4 import * as uuidv4 from 'uuid/v4'
5 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
6 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
8 import { isActorObjectValid, normalizeActor } 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, downloadImage } from '../../helpers/requests'
14 import { getUrlFromWebfinger } from '../../helpers/webfinger'
15 import { AVATARS_SIZE, MIMETYPES, sequelizeTypescript, WEBSERVER } 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 { JobQueue } from '../job-queue'
22 import { getServerActor } from '../../helpers/utils'
23 import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
24 import { CONFIG } from '../../initializers/config'
26 // Set account keys, this could be long so process after the account creation and do not block the client
27 function setAsyncActorKeys (actor: ActorModel) {
28 return createPrivateAndPublicKeys()
29 .then(({ publicKey, privateKey }) => {
30 actor.set('publicKey', publicKey)
31 actor.set('privateKey', privateKey)
35 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
40 async function getOrCreateActorAndServerAndModel (
41 activityActor: string | ActivityPubActor,
42 fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
43 recurseIfNeeded = true,
44 updateCollections = false
46 const actorUrl = getAPId(activityActor)
48 let accountPlaylistsUrl: string
50 let actor = await fetchActorByUrl(actorUrl, fetchType)
51 // Orphan actor (not associated to an account of channel) so recreate it
52 if (actor && (!actor.Account && !actor.VideoChannel)) {
57 // We don't have this actor in our database, fetch it on remote
59 const { result } = await fetchRemoteActor(actorUrl)
60 if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
62 // Create the attributed to actor
63 // In PeerTube a video channel is owned by an account
64 let ownerActor: ActorModel = undefined
65 if (recurseIfNeeded === true && result.actor.type === 'Group') {
66 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
67 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
69 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
70 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
74 // Don't recurse another time
75 const recurseIfNeeded = false
76 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded)
78 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
83 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
85 accountPlaylistsUrl = result.playlists
88 if (actor.Account) actor.Account.Actor = actor
89 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
91 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
92 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
94 if ((created === true || refreshed === true) && updateCollections === true) {
95 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
96 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
99 // We created a new account: fetch the playlists
100 if (created === true && actor.Account && accountPlaylistsUrl) {
101 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
102 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
105 return actorRefreshed
108 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
109 return new ActorModel({
118 inboxUrl: url + '/inbox',
119 outboxUrl: url + '/outbox',
120 sharedInboxUrl: WEBSERVER.URL + '/inbox',
121 followersUrl: url + '/followers',
122 followingUrl: url + '/following'
126 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
127 const followersCount = await fetchActorTotalItems(attributes.followers)
128 const followingCount = await fetchActorTotalItems(attributes.following)
130 actorInstance.set('type', attributes.type)
131 actorInstance.set('uuid', attributes.uuid)
132 actorInstance.set('preferredUsername', attributes.preferredUsername)
133 actorInstance.set('url', attributes.id)
134 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
135 actorInstance.set('followersCount', followersCount)
136 actorInstance.set('followingCount', followingCount)
137 actorInstance.set('inboxUrl', attributes.inbox)
138 actorInstance.set('outboxUrl', attributes.outbox)
139 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
140 actorInstance.set('followersUrl', attributes.followers)
141 actorInstance.set('followingUrl', attributes.following)
144 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
145 if (avatarName !== undefined) {
146 if (actorInstance.avatarId) {
148 await actorInstance.Avatar.destroy({ transaction: t })
150 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
154 const avatar = await AvatarModel.create({
156 }, { transaction: t })
158 actorInstance.set('avatarId', avatar.id)
159 actorInstance.Avatar = avatar
165 async function fetchActorTotalItems (url: string) {
174 const { body } = await doRequest(options)
175 return body.totalItems ? body.totalItems : 0
177 logger.warn('Cannot fetch remote actor count %s.', url, { err })
182 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
184 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
185 isActivityPubUrlValid(actorJSON.icon.url)
187 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
189 const avatarName = uuidv4() + extension
190 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
198 async function addFetchOutboxJob (actor: ActorModel) {
199 // Don't fetch ourselves
200 const serverActor = await getServerActor()
201 if (serverActor.id === actor.id) {
202 logger.error('Cannot fetch our own outbox!')
207 uri: actor.outboxUrl,
208 type: 'activity' as 'activity'
211 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
214 async function refreshActorIfNeeded (
215 actorArg: ActorModel,
216 fetchedType: ActorFetchByUrlType
217 ): Promise<{ actor: ActorModel, refreshed: boolean }> {
218 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
220 // We need more attributes
221 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
226 actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
228 logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
232 const { result, statusCode } = await fetchRemoteActor(actorUrl)
234 if (statusCode === 404) {
235 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
236 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
237 return { actor: undefined, refreshed: false }
240 if (result === undefined) {
241 logger.warn('Cannot fetch remote actor in refresh actor.')
242 return { actor, refreshed: false }
245 return sequelizeTypescript.transaction(async t => {
246 updateInstanceWithAnother(actor, result.actor)
248 if (result.avatarName !== undefined) {
249 await updateActorAvatarInstance(actor, result.avatarName, t)
253 actor.setDataValue('updatedAt', new Date())
254 await actor.save({ transaction: t })
257 actor.Account.set('name', result.name)
258 actor.Account.set('description', result.summary)
260 await actor.Account.save({ transaction: t })
261 } else if (actor.VideoChannel) {
262 actor.VideoChannel.set('name', result.name)
263 actor.VideoChannel.set('description', result.summary)
264 actor.VideoChannel.set('support', result.support)
266 await actor.VideoChannel.save({ transaction: t })
269 return { refreshed: true, actor }
272 logger.warn('Cannot refresh actor.', { err })
273 return { actor, refreshed: false }
278 getOrCreateActorAndServerAndModel,
281 fetchActorTotalItems,
284 refreshActorIfNeeded,
285 updateActorAvatarInstance,
289 // ---------------------------------------------------------------------------
291 function saveActorAndServerAndModelIfNotExist (
292 result: FetchRemoteActorResult,
293 ownerActor?: ActorModel,
295 ): Bluebird<ActorModel> | Promise<ActorModel> {
296 let actor = result.actor
298 if (t !== undefined) return save(t)
300 return sequelizeTypescript.transaction(t => save(t))
302 async function save (t: Transaction) {
303 const actorHost = url.parse(actor.url).host
305 const serverOptions = {
314 const [ server ] = await ServerModel.findOrCreate(serverOptions)
316 // Save our new account in database
317 actor.set('serverId', server.id)
320 if (result.avatarName) {
321 const avatar = await AvatarModel.create({
322 filename: result.avatarName
323 }, { transaction: t })
324 actor.set('avatarId', avatar.id)
327 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
328 // (which could be false in a retried query)
329 const [ actorCreated ] = await ActorModel.findOrCreate({
330 defaults: actor.toJSON(),
337 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
338 actorCreated.Account = await saveAccount(actorCreated, result, t)
339 actorCreated.Account.Actor = actorCreated
340 } else if (actorCreated.type === 'Group') { // Video channel
341 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
342 actorCreated.VideoChannel.Actor = actorCreated
343 actorCreated.VideoChannel.Account = ownerActor.Account
346 actorCreated.Server = server
352 type FetchRemoteActorResult = {
359 attributedTo: ActivityPubAttributedTo[]
361 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
369 logger.info('Fetching remote actor %s.', actorUrl)
371 const requestResult = await doRequest<ActivityPubActor>(options)
372 normalizeActor(requestResult.body)
374 const actorJSON = requestResult.body
375 if (isActorObjectValid(actorJSON) === false) {
376 logger.debug('Remote actor JSON is not valid.', { actorJSON })
377 return { result: undefined, statusCode: requestResult.response.statusCode }
380 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
381 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
382 return { result: undefined, statusCode: requestResult.response.statusCode }
385 const followersCount = await fetchActorTotalItems(actorJSON.followers)
386 const followingCount = await fetchActorTotalItems(actorJSON.following)
388 const actor = new ActorModel({
389 type: actorJSON.type,
390 uuid: actorJSON.uuid,
391 preferredUsername: actorJSON.preferredUsername,
393 publicKey: actorJSON.publicKey.publicKeyPem,
395 followersCount: followersCount,
396 followingCount: followingCount,
397 inboxUrl: actorJSON.inbox,
398 outboxUrl: actorJSON.outbox,
399 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
400 followersUrl: actorJSON.followers,
401 followingUrl: actorJSON.following
404 const avatarName = await fetchAvatarIfExists(actorJSON)
406 const name = actorJSON.name || actorJSON.preferredUsername
408 statusCode: requestResult.response.statusCode,
413 summary: actorJSON.summary,
414 support: actorJSON.support,
415 playlists: actorJSON.playlists,
416 attributedTo: actorJSON.attributedTo
421 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
422 const [ accountCreated ] = await AccountModel.findOrCreate({
425 description: result.summary,
434 return accountCreated
437 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
438 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
441 description: result.summary,
442 support: result.support,
444 accountId: ownerActor.Account.id
452 return videoChannelCreated