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