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