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