Update server dependencies
[oweals/peertube.git] / server / lib / activitypub / actor.ts
1 import * as Bluebird from 'bluebird'
2 import { Transaction } from 'sequelize'
3 import { URL } from 'url'
4 import { v4 as uuidv4 } from 'uuid'
5 import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
8 import { sanitizeAndCheckActorObject } 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 } from '../../helpers/requests'
14 import { getUrlFromWebfinger } from '../../helpers/webfinger'
15 import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
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 { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
23 import { sequelizeTypescript } from '../../initializers/database'
24 import {
25   MAccount,
26   MAccountDefault,
27   MActor,
28   MActorAccountChannelId,
29   MActorAccountChannelIdActor,
30   MActorAccountId,
31   MActorDefault,
32   MActorFull,
33   MActorFullActor,
34   MActorId,
35   MChannel
36 } from '../../typings/models'
37 import { extname } from 'path'
38 import { getServerActor } from '@server/models/application/application'
39
40 // Set account keys, this could be long so process after the account creation and do not block the client
41 function setAsyncActorKeys <T extends MActor> (actor: T) {
42   return createPrivateAndPublicKeys()
43     .then(({ publicKey, privateKey }) => {
44       actor.publicKey = publicKey
45       actor.privateKey = privateKey
46       return actor.save()
47     })
48     .catch(err => {
49       logger.error('Cannot set public/private keys of actor %d.', actor.url, { err })
50       return actor
51     })
52 }
53
54 function getOrCreateActorAndServerAndModel (
55   activityActor: string | ActivityPubActor,
56   fetchType: 'all',
57   recurseIfNeeded?: boolean,
58   updateCollections?: boolean
59 ): Promise<MActorFullActor>
60
61 function getOrCreateActorAndServerAndModel (
62   activityActor: string | ActivityPubActor,
63   fetchType?: 'association-ids',
64   recurseIfNeeded?: boolean,
65   updateCollections?: boolean
66 ): Promise<MActorAccountChannelId>
67
68 async function getOrCreateActorAndServerAndModel (
69   activityActor: string | ActivityPubActor,
70   fetchType: ActorFetchByUrlType = 'association-ids',
71   recurseIfNeeded = true,
72   updateCollections = false
73 ): Promise<MActorFullActor | MActorAccountChannelId> {
74   const actorUrl = getAPId(activityActor)
75   let created = false
76   let accountPlaylistsUrl: string
77
78   let actor = await fetchActorByUrl(actorUrl, fetchType)
79   // Orphan actor (not associated to an account of channel) so recreate it
80   if (actor && (!actor.Account && !actor.VideoChannel)) {
81     await actor.destroy()
82     actor = null
83   }
84
85   // We don't have this actor in our database, fetch it on remote
86   if (!actor) {
87     const { result } = await fetchRemoteActor(actorUrl)
88     if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
89
90     // Create the attributed to actor
91     // In PeerTube a video channel is owned by an account
92     let ownerActor: MActorFullActor
93     if (recurseIfNeeded === true && result.actor.type === 'Group') {
94       const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
95       if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
96
97       if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
98         throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
99       }
100
101       try {
102         // Don't recurse another time
103         const recurseIfNeeded = false
104         ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded)
105       } catch (err) {
106         logger.error('Cannot get or create account attributed to video channel ' + actor.url)
107         throw new Error(err)
108       }
109     }
110
111     actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
112     created = true
113     accountPlaylistsUrl = result.playlists
114   }
115
116   if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor
117   if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
118
119   const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
120   if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
121
122   if ((created === true || refreshed === true) && updateCollections === true) {
123     const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
124     await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
125   }
126
127   // We created a new account: fetch the playlists
128   if (created === true && actor.Account && accountPlaylistsUrl) {
129     const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
130     await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
131   }
132
133   return actorRefreshed
134 }
135
136 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
137   return new ActorModel({
138     type,
139     url,
140     preferredUsername,
141     uuid,
142     publicKey: null,
143     privateKey: null,
144     followersCount: 0,
145     followingCount: 0,
146     inboxUrl: url + '/inbox',
147     outboxUrl: url + '/outbox',
148     sharedInboxUrl: WEBSERVER.URL + '/inbox',
149     followersUrl: url + '/followers',
150     followingUrl: url + '/following'
151   }) as MActor
152 }
153
154 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
155   const followersCount = await fetchActorTotalItems(attributes.followers)
156   const followingCount = await fetchActorTotalItems(attributes.following)
157
158   actorInstance.type = attributes.type
159   actorInstance.preferredUsername = attributes.preferredUsername
160   actorInstance.url = attributes.id
161   actorInstance.publicKey = attributes.publicKey.publicKeyPem
162   actorInstance.followersCount = followersCount
163   actorInstance.followingCount = followingCount
164   actorInstance.inboxUrl = attributes.inbox
165   actorInstance.outboxUrl = attributes.outbox
166   actorInstance.followersUrl = attributes.followers
167   actorInstance.followingUrl = attributes.following
168
169   if (attributes.endpoints?.sharedInbox) {
170     actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox
171   }
172 }
173
174 type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string }
175 async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) {
176   if (!info.name) return actor
177
178   if (actor.Avatar) {
179     // Don't update the avatar if the file URL did not change
180     if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor
181
182     try {
183       await actor.Avatar.destroy({ transaction: t })
184     } catch (err) {
185       logger.error('Cannot remove old avatar of actor %s.', actor.url, { err })
186     }
187   }
188
189   const avatar = await AvatarModel.create({
190     filename: info.name,
191     onDisk: info.onDisk,
192     fileUrl: info.fileUrl
193   }, { transaction: t })
194
195   actor.avatarId = avatar.id
196   actor.Avatar = avatar
197
198   return actor
199 }
200
201 async function fetchActorTotalItems (url: string) {
202   const options = {
203     uri: url,
204     method: 'GET',
205     json: true,
206     activityPub: true
207   }
208
209   try {
210     const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options)
211     return body.totalItems ? body.totalItems : 0
212   } catch (err) {
213     logger.warn('Cannot fetch remote actor count %s.', url, { err })
214     return 0
215   }
216 }
217
218 function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
219   const mimetypes = MIMETYPES.IMAGE
220   const icon = actorJSON.icon
221
222   if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
223
224   let extension: string
225
226   if (icon.mediaType) {
227     extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
228   } else {
229     const tmp = extname(icon.url)
230
231     if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
232   }
233
234   if (!extension) return undefined
235
236   return {
237     name: uuidv4() + extension,
238     fileUrl: icon.url
239   }
240 }
241
242 async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
243   // Don't fetch ourselves
244   const serverActor = await getServerActor()
245   if (serverActor.id === actor.id) {
246     logger.error('Cannot fetch our own outbox!')
247     return undefined
248   }
249
250   const payload = {
251     uri: actor.outboxUrl,
252     type: 'activity' as 'activity'
253   }
254
255   return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
256 }
257
258 async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (
259   actorArg: T,
260   fetchedType: ActorFetchByUrlType
261 ): Promise<{ actor: T | MActorFull, refreshed: boolean }> {
262   if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
263
264   // We need more attributes
265   const actor = fetchedType === 'all'
266     ? actorArg as MActorFull
267     : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
268
269   try {
270     let actorUrl: string
271     try {
272       actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
273     } catch (err) {
274       logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
275       actorUrl = actor.url
276     }
277
278     const { result, statusCode } = await fetchRemoteActor(actorUrl)
279
280     if (statusCode === 404) {
281       logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
282       actor.Account
283         ? await actor.Account.destroy()
284         : await actor.VideoChannel.destroy()
285
286       return { actor: undefined, refreshed: false }
287     }
288
289     if (result === undefined) {
290       logger.warn('Cannot fetch remote actor in refresh actor.')
291       return { actor, refreshed: false }
292     }
293
294     return sequelizeTypescript.transaction(async t => {
295       updateInstanceWithAnother(actor, result.actor)
296
297       if (result.avatar !== undefined) {
298         const avatarInfo = {
299           name: result.avatar.name,
300           fileUrl: result.avatar.fileUrl,
301           onDisk: false
302         }
303
304         await updateActorAvatarInstance(actor, avatarInfo, t)
305       }
306
307       // Force update
308       actor.setDataValue('updatedAt', new Date())
309       await actor.save({ transaction: t })
310
311       if (actor.Account) {
312         actor.Account.name = result.name
313         actor.Account.description = result.summary
314
315         await actor.Account.save({ transaction: t })
316       } else if (actor.VideoChannel) {
317         actor.VideoChannel.name = result.name
318         actor.VideoChannel.description = result.summary
319         actor.VideoChannel.support = result.support
320
321         await actor.VideoChannel.save({ transaction: t })
322       }
323
324       return { refreshed: true, actor }
325     })
326   } catch (err) {
327     logger.warn('Cannot refresh actor %s.', actor.url, { err })
328     return { actor, refreshed: false }
329   }
330 }
331
332 export {
333   getOrCreateActorAndServerAndModel,
334   buildActorInstance,
335   setAsyncActorKeys,
336   fetchActorTotalItems,
337   getAvatarInfoIfExists,
338   updateActorInstance,
339   refreshActorIfNeeded,
340   updateActorAvatarInstance,
341   addFetchOutboxJob
342 }
343
344 // ---------------------------------------------------------------------------
345
346 function saveActorAndServerAndModelIfNotExist (
347   result: FetchRemoteActorResult,
348   ownerActor?: MActorFullActor,
349   t?: Transaction
350 ): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
351   const actor = result.actor
352
353   if (t !== undefined) return save(t)
354
355   return sequelizeTypescript.transaction(t => save(t))
356
357   async function save (t: Transaction) {
358     const actorHost = new URL(actor.url).host
359
360     const serverOptions = {
361       where: {
362         host: actorHost
363       },
364       defaults: {
365         host: actorHost
366       },
367       transaction: t
368     }
369     const [ server ] = await ServerModel.findOrCreate(serverOptions)
370
371     // Save our new account in database
372     actor.serverId = server.id
373
374     // Avatar?
375     if (result.avatar) {
376       const avatar = await AvatarModel.create({
377         filename: result.avatar.name,
378         fileUrl: result.avatar.fileUrl,
379         onDisk: false
380       }, { transaction: t })
381
382       actor.avatarId = avatar.id
383     }
384
385     // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
386     // (which could be false in a retried query)
387     const [ actorCreated ] = await ActorModel.findOrCreate<MActorFullActor>({
388       defaults: actor.toJSON(),
389       where: {
390         url: actor.url
391       },
392       transaction: t
393     })
394
395     if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
396       actorCreated.Account = await saveAccount(actorCreated, result, t) as MAccountDefault
397       actorCreated.Account.Actor = actorCreated
398     } else if (actorCreated.type === 'Group') { // Video channel
399       const channel = await saveVideoChannel(actorCreated, result, ownerActor, t)
400       actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: ownerActor.Account })
401     }
402
403     actorCreated.Server = server
404
405     return actorCreated
406   }
407 }
408
409 type FetchRemoteActorResult = {
410   actor: MActor
411   name: string
412   summary: string
413   support?: string
414   playlists?: string
415   avatar?: {
416     name: string
417     fileUrl: string
418   }
419   attributedTo: ActivityPubAttributedTo[]
420 }
421 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
422   const options = {
423     uri: actorUrl,
424     method: 'GET',
425     json: true,
426     activityPub: true
427   }
428
429   logger.info('Fetching remote actor %s.', actorUrl)
430
431   const requestResult = await doRequest<ActivityPubActor>(options)
432   const actorJSON = requestResult.body
433
434   if (sanitizeAndCheckActorObject(actorJSON) === false) {
435     logger.debug('Remote actor JSON is not valid.', { actorJSON })
436     return { result: undefined, statusCode: requestResult.response.statusCode }
437   }
438
439   if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
440     logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
441     return { result: undefined, statusCode: requestResult.response.statusCode }
442   }
443
444   const followersCount = await fetchActorTotalItems(actorJSON.followers)
445   const followingCount = await fetchActorTotalItems(actorJSON.following)
446
447   const actor = new ActorModel({
448     type: actorJSON.type,
449     preferredUsername: actorJSON.preferredUsername,
450     url: actorJSON.id,
451     publicKey: actorJSON.publicKey.publicKeyPem,
452     privateKey: null,
453     followersCount: followersCount,
454     followingCount: followingCount,
455     inboxUrl: actorJSON.inbox,
456     outboxUrl: actorJSON.outbox,
457     followersUrl: actorJSON.followers,
458     followingUrl: actorJSON.following,
459
460     sharedInboxUrl: actorJSON.endpoints?.sharedInbox
461       ? actorJSON.endpoints.sharedInbox
462       : null
463   })
464
465   const avatarInfo = await getAvatarInfoIfExists(actorJSON)
466
467   const name = actorJSON.name || actorJSON.preferredUsername
468   return {
469     statusCode: requestResult.response.statusCode,
470     result: {
471       actor,
472       name,
473       avatar: avatarInfo,
474       summary: actorJSON.summary,
475       support: actorJSON.support,
476       playlists: actorJSON.playlists,
477       attributedTo: actorJSON.attributedTo
478     }
479   }
480 }
481
482 async function saveAccount (actor: MActorId, result: FetchRemoteActorResult, t: Transaction) {
483   const [ accountCreated ] = await AccountModel.findOrCreate({
484     defaults: {
485       name: result.name,
486       description: result.summary,
487       actorId: actor.id
488     },
489     where: {
490       actorId: actor.id
491     },
492     transaction: t
493   })
494
495   return accountCreated as MAccount
496 }
497
498 async function saveVideoChannel (actor: MActorId, result: FetchRemoteActorResult, ownerActor: MActorAccountId, t: Transaction) {
499   const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
500     defaults: {
501       name: result.name,
502       description: result.summary,
503       support: result.support,
504       actorId: actor.id,
505       accountId: ownerActor.Account.id
506     },
507     where: {
508       actorId: actor.id
509     },
510     transaction: t
511   })
512
513   return videoChannelCreated as MChannel
514 }