AP mimeType -> mediaType
[oweals/peertube.git] / server / lib / activitypub / actor.ts
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, normalizeActor } 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 { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } 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 { JobQueue } from '../job-queue'
23 import { getServerActor } from '../../helpers/utils'
24 import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
25
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)
32       return actor.save()
33     })
34     .catch(err => {
35       logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
36       return actor
37     })
38 }
39
40 async function getOrCreateActorAndServerAndModel (
41   activityActor: string | ActivityPubActor,
42   fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
43   recurseIfNeeded = true,
44   updateCollections = false
45 ) {
46   const actorUrl = getActorUrl(activityActor)
47   let created = false
48
49   let actor = await fetchActorByUrl(actorUrl, fetchType)
50   // Orphan actor (not associated to an account of channel) so recreate it
51   if (actor && (!actor.Account && !actor.VideoChannel)) {
52     await actor.destroy()
53     actor = null
54   }
55
56   // We don't have this actor in our database, fetch it on remote
57   if (!actor) {
58     const { result } = await fetchRemoteActor(actorUrl)
59     if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
60
61     // Create the attributed to actor
62     // In PeerTube a video channel is owned by an account
63     let ownerActor: ActorModel = undefined
64     if (recurseIfNeeded === true && result.actor.type === 'Group') {
65       const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
66       if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
67
68       try {
69         // Assert we don't recurse another time
70         ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
71       } catch (err) {
72         logger.error('Cannot get or create account attributed to video channel ' + actor.url)
73         throw new Error(err)
74       }
75     }
76
77     actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
78     created = true
79   }
80
81   if (actor.Account) actor.Account.Actor = actor
82   if (actor.VideoChannel) actor.VideoChannel.Actor = actor
83
84   const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
85   if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
86
87   if ((created === true || refreshed === true) && updateCollections === true) {
88     const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
89     await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
90   }
91
92   return actorRefreshed
93 }
94
95 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
96   return new ActorModel({
97     type,
98     url,
99     preferredUsername,
100     uuid,
101     publicKey: null,
102     privateKey: null,
103     followersCount: 0,
104     followingCount: 0,
105     inboxUrl: url + '/inbox',
106     outboxUrl: url + '/outbox',
107     sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
108     followersUrl: url + '/followers',
109     followingUrl: url + '/following'
110   })
111 }
112
113 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
114   const followersCount = await fetchActorTotalItems(attributes.followers)
115   const followingCount = await fetchActorTotalItems(attributes.following)
116
117   actorInstance.set('type', attributes.type)
118   actorInstance.set('uuid', attributes.uuid)
119   actorInstance.set('preferredUsername', attributes.preferredUsername)
120   actorInstance.set('url', attributes.id)
121   actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
122   actorInstance.set('followersCount', followersCount)
123   actorInstance.set('followingCount', followingCount)
124   actorInstance.set('inboxUrl', attributes.inbox)
125   actorInstance.set('outboxUrl', attributes.outbox)
126   actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
127   actorInstance.set('followersUrl', attributes.followers)
128   actorInstance.set('followingUrl', attributes.following)
129 }
130
131 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
132   if (avatarName !== undefined) {
133     if (actorInstance.avatarId) {
134       try {
135         await actorInstance.Avatar.destroy({ transaction: t })
136       } catch (err) {
137         logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
138       }
139     }
140
141     const avatar = await AvatarModel.create({
142       filename: avatarName
143     }, { transaction: t })
144
145     actorInstance.set('avatarId', avatar.id)
146     actorInstance.Avatar = avatar
147   }
148
149   return actorInstance
150 }
151
152 async function fetchActorTotalItems (url: string) {
153   const options = {
154     uri: url,
155     method: 'GET',
156     json: true,
157     activityPub: true
158   }
159
160   try {
161     const { body } = await doRequest(options)
162     return body.totalItems ? body.totalItems : 0
163   } catch (err) {
164     logger.warn('Cannot fetch remote actor count %s.', url, { err })
165     return 0
166   }
167 }
168
169 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
170   if (
171     actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
172     isActivityPubUrlValid(actorJSON.icon.url)
173   ) {
174     const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
175
176     const avatarName = uuidv4() + extension
177     const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
178
179     await doRequestAndSaveToFile({
180       method: 'GET',
181       uri: actorJSON.icon.url
182     }, destPath)
183
184     return avatarName
185   }
186
187   return undefined
188 }
189
190 async function addFetchOutboxJob (actor: ActorModel) {
191   // Don't fetch ourselves
192   const serverActor = await getServerActor()
193   if (serverActor.id === actor.id) {
194     logger.error('Cannot fetch our own outbox!')
195     return undefined
196   }
197
198   const payload = {
199     uri: actor.outboxUrl,
200     type: 'activity' as 'activity'
201   }
202
203   return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
204 }
205
206 export {
207   getOrCreateActorAndServerAndModel,
208   buildActorInstance,
209   setAsyncActorKeys,
210   fetchActorTotalItems,
211   fetchAvatarIfExists,
212   updateActorInstance,
213   updateActorAvatarInstance,
214   addFetchOutboxJob
215 }
216
217 // ---------------------------------------------------------------------------
218
219 function saveActorAndServerAndModelIfNotExist (
220   result: FetchRemoteActorResult,
221   ownerActor?: ActorModel,
222   t?: Transaction
223 ): Bluebird<ActorModel> | Promise<ActorModel> {
224   let actor = result.actor
225
226   if (t !== undefined) return save(t)
227
228   return sequelizeTypescript.transaction(t => save(t))
229
230   async function save (t: Transaction) {
231     const actorHost = url.parse(actor.url).host
232
233     const serverOptions = {
234       where: {
235         host: actorHost
236       },
237       defaults: {
238         host: actorHost
239       },
240       transaction: t
241     }
242     const [ server ] = await ServerModel.findOrCreate(serverOptions)
243
244     // Save our new account in database
245     actor.set('serverId', server.id)
246
247     // Avatar?
248     if (result.avatarName) {
249       const avatar = await AvatarModel.create({
250         filename: result.avatarName
251       }, { transaction: t })
252       actor.set('avatarId', avatar.id)
253     }
254
255     // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
256     // (which could be false in a retried query)
257     const [ actorCreated ] = await ActorModel.findOrCreate({
258       defaults: actor.toJSON(),
259       where: {
260         url: actor.url
261       },
262       transaction: t
263     })
264
265     if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
266       actorCreated.Account = await saveAccount(actorCreated, result, t)
267       actorCreated.Account.Actor = actorCreated
268     } else if (actorCreated.type === 'Group') { // Video channel
269       actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
270       actorCreated.VideoChannel.Actor = actorCreated
271       actorCreated.VideoChannel.Account = ownerActor.Account
272     }
273
274     return actorCreated
275   }
276 }
277
278 type FetchRemoteActorResult = {
279   actor: ActorModel
280   name: string
281   summary: string
282   support?: string
283   avatarName?: string
284   attributedTo: ActivityPubAttributedTo[]
285 }
286 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
287   const options = {
288     uri: actorUrl,
289     method: 'GET',
290     json: true,
291     activityPub: true
292   }
293
294   logger.info('Fetching remote actor %s.', actorUrl)
295
296   const requestResult = await doRequest(options)
297   normalizeActor(requestResult.body)
298
299   const actorJSON: ActivityPubActor = requestResult.body
300
301   if (isActorObjectValid(actorJSON) === false) {
302     logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
303     return { result: undefined, statusCode: requestResult.response.statusCode }
304   }
305
306   const followersCount = await fetchActorTotalItems(actorJSON.followers)
307   const followingCount = await fetchActorTotalItems(actorJSON.following)
308
309   const actor = new ActorModel({
310     type: actorJSON.type,
311     uuid: actorJSON.uuid,
312     preferredUsername: actorJSON.preferredUsername,
313     url: actorJSON.id,
314     publicKey: actorJSON.publicKey.publicKeyPem,
315     privateKey: null,
316     followersCount: followersCount,
317     followingCount: followingCount,
318     inboxUrl: actorJSON.inbox,
319     outboxUrl: actorJSON.outbox,
320     sharedInboxUrl: actorJSON.endpoints.sharedInbox,
321     followersUrl: actorJSON.followers,
322     followingUrl: actorJSON.following
323   })
324
325   const avatarName = await fetchAvatarIfExists(actorJSON)
326
327   const name = actorJSON.name || actorJSON.preferredUsername
328   return {
329     statusCode: requestResult.response.statusCode,
330     result: {
331       actor,
332       name,
333       avatarName,
334       summary: actorJSON.summary,
335       support: actorJSON.support,
336       attributedTo: actorJSON.attributedTo
337     }
338   }
339 }
340
341 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
342   const [ accountCreated ] = await AccountModel.findOrCreate({
343     defaults: {
344       name: result.name,
345       description: result.summary,
346       actorId: actor.id
347     },
348     where: {
349       actorId: actor.id
350     },
351     transaction: t
352   })
353
354   return accountCreated
355 }
356
357 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
358   const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
359     defaults: {
360       name: result.name,
361       description: result.summary,
362       support: result.support,
363       actorId: actor.id,
364       accountId: ownerActor.Account.id
365     },
366     where: {
367       actorId: actor.id
368     },
369     transaction: t
370   })
371
372   return videoChannelCreated
373 }
374
375 async function refreshActorIfNeeded (
376   actorArg: ActorModel,
377   fetchedType: ActorFetchByUrlType
378 ): Promise<{ actor: ActorModel, refreshed: boolean }> {
379   if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
380
381   // We need more attributes
382   const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
383
384   try {
385     const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
386     const { result, statusCode } = await fetchRemoteActor(actorUrl)
387
388     if (statusCode === 404) {
389       logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
390       actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
391       return { actor: undefined, refreshed: false }
392     }
393
394     if (result === undefined) {
395       logger.warn('Cannot fetch remote actor in refresh actor.')
396       return { actor, refreshed: false }
397     }
398
399     return sequelizeTypescript.transaction(async t => {
400       updateInstanceWithAnother(actor, result.actor)
401
402       if (result.avatarName !== undefined) {
403         await updateActorAvatarInstance(actor, result.avatarName, t)
404       }
405
406       // Force update
407       actor.setDataValue('updatedAt', new Date())
408       await actor.save({ transaction: t })
409
410       if (actor.Account) {
411         actor.Account.set('name', result.name)
412         actor.Account.set('description', result.summary)
413
414         await actor.Account.save({ transaction: t })
415       } else if (actor.VideoChannel) {
416         actor.VideoChannel.set('name', result.name)
417         actor.VideoChannel.set('description', result.summary)
418         actor.VideoChannel.set('support', result.support)
419
420         await actor.VideoChannel.save({ transaction: t })
421       }
422
423       return { refreshed: true, actor }
424     })
425   } catch (err) {
426     logger.warn('Cannot refresh actor.', { err })
427     return { actor, refreshed: false }
428   }
429 }