f802658cfe9d3b6c3509427964940f8c0b365deb
[oweals/peertube.git] / server / lib / activitypub / actor.ts
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 { 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 { getServerActor } from '../../helpers/utils'
23 import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
24 import { sequelizeTypescript } from '../../initializers/database'
25 import {
26   MAccount,
27   MAccountDefault,
28   MActor,
29   MActorAccountChannelId,
30   MActorAccountChannelIdActor,
31   MActorAccountId,
32   MActorDefault,
33   MActorFull,
34   MActorFullActor,
35   MActorId,
36   MChannel,
37   MChannelAccountDefault
38 } from '../../typings/models'
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 ' + actorRefreshed.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.createJob({ 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.createJob({ 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 && 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 filename did not change
180     if (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(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 async function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
219   if (
220     actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
221     isActivityPubUrlValid(actorJSON.icon.url)
222   ) {
223     const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
224
225     return {
226       name: uuidv4() + extension,
227       fileUrl: actorJSON.icon.url
228     }
229   }
230
231   return undefined
232 }
233
234 async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
235   // Don't fetch ourselves
236   const serverActor = await getServerActor()
237   if (serverActor.id === actor.id) {
238     logger.error('Cannot fetch our own outbox!')
239     return undefined
240   }
241
242   const payload = {
243     uri: actor.outboxUrl,
244     type: 'activity' as 'activity'
245   }
246
247   return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
248 }
249
250 async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (
251   actorArg: T,
252   fetchedType: ActorFetchByUrlType
253 ): Promise<{ actor: T | MActorFull, refreshed: boolean }> {
254   if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
255
256   // We need more attributes
257   const actor = fetchedType === 'all'
258     ? actorArg as MActorFull
259     : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
260
261   try {
262     let actorUrl: string
263     try {
264       actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
265     } catch (err) {
266       logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
267       actorUrl = actor.url
268     }
269
270     const { result, statusCode } = await fetchRemoteActor(actorUrl)
271
272     if (statusCode === 404) {
273       logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
274       actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
275       return { actor: undefined, refreshed: false }
276     }
277
278     if (result === undefined) {
279       logger.warn('Cannot fetch remote actor in refresh actor.')
280       return { actor, refreshed: false }
281     }
282
283     return sequelizeTypescript.transaction(async t => {
284       updateInstanceWithAnother(actor, result.actor)
285
286       if (result.avatar !== undefined) {
287         const avatarInfo = {
288           name: result.avatar.name,
289           fileUrl: result.avatar.fileUrl,
290           onDisk: false
291         }
292
293         await updateActorAvatarInstance(actor, avatarInfo, t)
294       }
295
296       // Force update
297       actor.setDataValue('updatedAt', new Date())
298       await actor.save({ transaction: t })
299
300       if (actor.Account) {
301         actor.Account.name = result.name
302         actor.Account.description = result.summary
303
304         await actor.Account.save({ transaction: t })
305       } else if (actor.VideoChannel) {
306         actor.VideoChannel.name = result.name
307         actor.VideoChannel.description = result.summary
308         actor.VideoChannel.support = result.support
309
310         await actor.VideoChannel.save({ transaction: t })
311       }
312
313       return { refreshed: true, actor }
314     })
315   } catch (err) {
316     logger.warn('Cannot refresh actor %s.', actor.url, { err })
317     return { actor, refreshed: false }
318   }
319 }
320
321 export {
322   getOrCreateActorAndServerAndModel,
323   buildActorInstance,
324   setAsyncActorKeys,
325   fetchActorTotalItems,
326   getAvatarInfoIfExists,
327   updateActorInstance,
328   refreshActorIfNeeded,
329   updateActorAvatarInstance,
330   addFetchOutboxJob
331 }
332
333 // ---------------------------------------------------------------------------
334
335 function saveActorAndServerAndModelIfNotExist (
336   result: FetchRemoteActorResult,
337   ownerActor?: MActorFullActor,
338   t?: Transaction
339 ): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
340   let actor = result.actor
341
342   if (t !== undefined) return save(t)
343
344   return sequelizeTypescript.transaction(t => save(t))
345
346   async function save (t: Transaction) {
347     const actorHost = url.parse(actor.url).host
348
349     const serverOptions = {
350       where: {
351         host: actorHost
352       },
353       defaults: {
354         host: actorHost
355       },
356       transaction: t
357     }
358     const [ server ] = await ServerModel.findOrCreate(serverOptions)
359
360     // Save our new account in database
361     actor.serverId = server.id
362
363     // Avatar?
364     if (result.avatar) {
365       const avatar = await AvatarModel.create({
366         filename: result.avatar.name,
367         fileUrl: result.avatar.fileUrl,
368         onDisk: false
369       }, { transaction: t })
370
371       actor.avatarId = avatar.id
372     }
373
374     // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
375     // (which could be false in a retried query)
376     const [ actorCreated ] = await ActorModel.findOrCreate<MActorFullActor>({
377       defaults: actor.toJSON(),
378       where: {
379         url: actor.url
380       },
381       transaction: t
382     })
383
384     if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
385       actorCreated.Account = await saveAccount(actorCreated, result, t) as MAccountDefault
386       actorCreated.Account.Actor = actorCreated
387     } else if (actorCreated.type === 'Group') { // Video channel
388       const channel = await saveVideoChannel(actorCreated, result, ownerActor, t)
389       actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: ownerActor.Account })
390     }
391
392     actorCreated.Server = server
393
394     return actorCreated
395   }
396 }
397
398 type FetchRemoteActorResult = {
399   actor: MActor
400   name: string
401   summary: string
402   support?: string
403   playlists?: string
404   avatar?: {
405     name: string,
406     fileUrl: string
407   }
408   attributedTo: ActivityPubAttributedTo[]
409 }
410 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
411   const options = {
412     uri: actorUrl,
413     method: 'GET',
414     json: true,
415     activityPub: true
416   }
417
418   logger.info('Fetching remote actor %s.', actorUrl)
419
420   const requestResult = await doRequest<ActivityPubActor>(options)
421   const actorJSON = requestResult.body
422
423   if (sanitizeAndCheckActorObject(actorJSON) === false) {
424     logger.debug('Remote actor JSON is not valid.', { actorJSON })
425     return { result: undefined, statusCode: requestResult.response.statusCode }
426   }
427
428   if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
429     logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
430     return { result: undefined, statusCode: requestResult.response.statusCode }
431   }
432
433   const followersCount = await fetchActorTotalItems(actorJSON.followers)
434   const followingCount = await fetchActorTotalItems(actorJSON.following)
435
436   const actor = new ActorModel({
437     type: actorJSON.type,
438     preferredUsername: actorJSON.preferredUsername,
439     url: actorJSON.id,
440     publicKey: actorJSON.publicKey.publicKeyPem,
441     privateKey: null,
442     followersCount: followersCount,
443     followingCount: followingCount,
444     inboxUrl: actorJSON.inbox,
445     outboxUrl: actorJSON.outbox,
446     followersUrl: actorJSON.followers,
447     followingUrl: actorJSON.following,
448
449     sharedInboxUrl: actorJSON.endpoints && actorJSON.endpoints.sharedInbox
450       ? actorJSON.endpoints.sharedInbox
451       : null
452   })
453
454   const avatarInfo = await getAvatarInfoIfExists(actorJSON)
455
456   const name = actorJSON.name || actorJSON.preferredUsername
457   return {
458     statusCode: requestResult.response.statusCode,
459     result: {
460       actor,
461       name,
462       avatar: avatarInfo,
463       summary: actorJSON.summary,
464       support: actorJSON.support,
465       playlists: actorJSON.playlists,
466       attributedTo: actorJSON.attributedTo
467     }
468   }
469 }
470
471 async function saveAccount (actor: MActorId, result: FetchRemoteActorResult, t: Transaction) {
472   const [ accountCreated ] = await AccountModel.findOrCreate({
473     defaults: {
474       name: result.name,
475       description: result.summary,
476       actorId: actor.id
477     },
478     where: {
479       actorId: actor.id
480     },
481     transaction: t
482   })
483
484   return accountCreated as MAccount
485 }
486
487 async function saveVideoChannel (actor: MActorId, result: FetchRemoteActorResult, ownerActor: MActorAccountId, t: Transaction) {
488   const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
489     defaults: {
490       name: result.name,
491       description: result.summary,
492       support: result.support,
493       actorId: actor.id,
494       accountId: ownerActor.Account.id
495     },
496     where: {
497       actorId: actor.id
498     },
499     transaction: t
500   })
501
502   return videoChannelCreated as MChannel
503 }