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