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 const account = await saveAccount(actorCreated, result, t)
229 actorCreated.Account = account
230 actorCreated.Account.Actor = actorCreated
231 } else if (actorCreated.type === 'Group') { // Video channel
232 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
233 actorCreated.VideoChannel = videoChannel
234 actorCreated.VideoChannel.Actor = actorCreated
241 type FetchRemoteActorResult = {
246 attributedTo: ActivityPubAttributedTo[]
248 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
256 logger.info('Fetching remote actor %s.', actorUrl)
258 const requestResult = await doRequest(options)
259 const actorJSON: ActivityPubActor = normalizeActor(requestResult.body)
261 if (isActorObjectValid(actorJSON) === false) {
262 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
266 const followersCount = await fetchActorTotalItems(actorJSON.followers)
267 const followingCount = await fetchActorTotalItems(actorJSON.following)
269 const actor = new ActorModel({
270 type: actorJSON.type,
271 uuid: actorJSON.uuid,
272 preferredUsername: actorJSON.preferredUsername,
274 publicKey: actorJSON.publicKey.publicKeyPem,
276 followersCount: followersCount,
277 followingCount: followingCount,
278 inboxUrl: actorJSON.inbox,
279 outboxUrl: actorJSON.outbox,
280 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
281 followersUrl: actorJSON.followers,
282 followingUrl: actorJSON.following
285 const avatarName = await fetchAvatarIfExists(actorJSON)
287 const name = actorJSON.name || actorJSON.preferredUsername
292 summary: actorJSON.summary,
293 attributedTo: actorJSON.attributedTo
297 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
298 const [ accountCreated ] = await AccountModel.findOrCreate({
309 return accountCreated
312 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
313 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
316 description: result.summary,
318 accountId: ownerActor.Account.id
326 return videoChannelCreated
329 async function refreshActorIfNeeded (actor: ActorModel) {
330 if (!actor.isOutdated()) return actor
333 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
334 const result = await fetchRemoteActor(actorUrl)
335 if (result === undefined) {
336 logger.warn('Cannot fetch remote actor in refresh actor.')
340 return sequelizeTypescript.transaction(async t => {
341 updateInstanceWithAnother(actor, result.actor)
343 if (result.avatarName !== undefined) {
344 await updateActorAvatarInstance(actor, result.avatarName, t)
348 actor.setDataValue('updatedAt', new Date())
349 await actor.save({ transaction: t })
352 await actor.save({ transaction: t })
354 actor.Account.set('name', result.name)
355 await actor.Account.save({ transaction: t })
356 } else if (actor.VideoChannel) {
357 await actor.save({ transaction: t })
359 actor.VideoChannel.set('name', result.name)
360 await actor.VideoChannel.save({ transaction: t })
366 logger.warn('Cannot refresh actor.', err)
371 function normalizeActor (actor: any) {
372 if (actor && actor.url && typeof actor.url === 'string') return actor
374 actor.url = actor.url.href || actor.url.url