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