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