Fix video announces processing
[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   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' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
151     isActivityPubUrlValid(actorJSON.icon.url)
152   ) {
153     const extension = AVATAR_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       const account = await saveAccount(actorCreated, result, t)
229       actorCreated.Account = account
230       actorCreated.Account.Actor = actorCreated
231     } else if (actorCreated.type === 'Group') { // Video channel
232       const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
233       actorCreated.VideoChannel = videoChannel
234       actorCreated.VideoChannel.Actor = actorCreated
235     }
236
237     return actorCreated
238   }
239 }
240
241 type FetchRemoteActorResult = {
242   actor: ActorModel
243   name: string
244   summary: string
245   avatarName?: string
246   attributedTo: ActivityPubAttributedTo[]
247 }
248 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
249   const options = {
250     uri: actorUrl,
251     method: 'GET',
252     json: true,
253     activityPub: true
254   }
255
256   logger.info('Fetching remote actor %s.', actorUrl)
257
258   const requestResult = await doRequest(options)
259   const actorJSON: ActivityPubActor = normalizeActor(requestResult.body)
260
261   if (isActorObjectValid(actorJSON) === false) {
262     logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
263     return undefined
264   }
265
266   const followersCount = await fetchActorTotalItems(actorJSON.followers)
267   const followingCount = await fetchActorTotalItems(actorJSON.following)
268
269   const actor = new ActorModel({
270     type: actorJSON.type,
271     uuid: actorJSON.uuid,
272     preferredUsername: actorJSON.preferredUsername,
273     url: actorJSON.id,
274     publicKey: actorJSON.publicKey.publicKeyPem,
275     privateKey: null,
276     followersCount: followersCount,
277     followingCount: followingCount,
278     inboxUrl: actorJSON.inbox,
279     outboxUrl: actorJSON.outbox,
280     sharedInboxUrl: actorJSON.endpoints.sharedInbox,
281     followersUrl: actorJSON.followers,
282     followingUrl: actorJSON.following
283   })
284
285   const avatarName = await fetchAvatarIfExists(actorJSON)
286
287   const name = actorJSON.name || actorJSON.preferredUsername
288   return {
289     actor,
290     name,
291     avatarName,
292     summary: actorJSON.summary,
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       actorId: actor.id
302     },
303     where: {
304       actorId: actor.id
305     },
306     transaction: t
307   })
308
309   return accountCreated
310 }
311
312 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
313   const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
314     defaults: {
315       name: result.name,
316       description: result.summary,
317       actorId: actor.id,
318       accountId: ownerActor.Account.id
319     },
320     where: {
321       actorId: actor.id
322     },
323     transaction: t
324   })
325
326   return videoChannelCreated
327 }
328
329 async function refreshActorIfNeeded (actor: ActorModel) {
330   if (!actor.isOutdated()) return actor
331
332   try {
333     const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
334     const result = await fetchRemoteActor(actorUrl)
335     if (result === undefined) {
336       logger.warn('Cannot fetch remote actor in refresh actor.')
337       return actor
338     }
339
340     return sequelizeTypescript.transaction(async t => {
341       updateInstanceWithAnother(actor, result.actor)
342
343       if (result.avatarName !== undefined) {
344         await updateActorAvatarInstance(actor, result.avatarName, t)
345       }
346
347       // Force update
348       actor.setDataValue('updatedAt', new Date())
349       await actor.save({ transaction: t })
350
351       if (actor.Account) {
352         await actor.save({ transaction: t })
353
354         actor.Account.set('name', result.name)
355         await actor.Account.save({ transaction: t })
356       } else if (actor.VideoChannel) {
357         await actor.save({ transaction: t })
358
359         actor.VideoChannel.set('name', result.name)
360         await actor.VideoChannel.save({ transaction: t })
361       }
362
363       return actor
364     })
365   } catch (err) {
366     logger.warn('Cannot refresh actor.', err)
367     return actor
368   }
369 }
370
371 function normalizeActor (actor: any) {
372   if (actor && actor.url && typeof actor.url === 'string') return actor
373
374   actor.url = actor.url.href || actor.url.url
375   return actor
376 }