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