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