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