]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Try to improve redundancy tests
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / actor.ts
CommitLineData
50d6de9c 1import * as Bluebird from 'bluebird'
c5911fd3 2import { join } from 'path'
50d6de9c
C
3import { Transaction } from 'sequelize'
4import * as url from 'url'
c5911fd3 5import * as uuidv4 from 'uuid/v4'
50d6de9c
C
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
6be84cbc 8import { getActorUrl } from '../../helpers/activitypub'
938d3fa0 9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
c5911fd3 10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
a5625b41 11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
da854ddd
C
12import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
c5911fd3 14import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
a5625b41 15import { getUrlFromWebfinger } from '../../helpers/webfinger'
938d3fa0 16import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
50d6de9c
C
17import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor'
c5911fd3 19import { AvatarModel } from '../../models/avatar/avatar'
50d6de9c
C
20import { ServerModel } from '../../models/server/server'
21import { VideoChannelModel } from '../../models/video/video-channel'
16f29007
C
22import { JobQueue } from '../job-queue'
23import { getServerActor } from '../../helpers/utils'
e587e0ec 24import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
50d6de9c 25
e12a0092 26// Set account keys, this could be long so process after the account creation and do not block the client
50d6de9c
C
27function 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 => {
d5b7d911 35 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
50d6de9c
C
36 return actor
37 })
38}
39
687d638c
C
40async function getOrCreateActorAndServerAndModel (
41 activityActor: string | ActivityPubActor,
e587e0ec 42 fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
687d638c
C
43 recurseIfNeeded = true,
44 updateCollections = false
45) {
6be84cbc 46 const actorUrl = getActorUrl(activityActor)
687d638c 47 let created = false
6be84cbc 48
e587e0ec 49 let actor = await fetchActorByUrl(actorUrl, fetchType)
25e4d6ee 50 // Orphan actor (not associated to an account of channel) so recreate it
6104adc3 51 if (actor && (!actor.Account && !actor.VideoChannel)) {
25e4d6ee
C
52 await actor.destroy()
53 actor = null
54 }
50d6de9c
C
55
56 // We don't have this actor in our database, fetch it on remote
57 if (!actor) {
f5b0af50 58 const { result } = await fetchRemoteActor(actorUrl)
601527d7 59 if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
50d6de9c
C
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
e587e0ec 70 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
50d6de9c
C
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
90d4bb81 77 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
687d638c 78 created = true
50d6de9c
C
79 }
80
d9bdd007
C
81 if (actor.Account) actor.Account.Actor = actor
82 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
83
e587e0ec 84 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
687d638c 85 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
f5b0af50 86
687d638c
C
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
50d6de9c
C
93}
94
c5911fd3
C
95function 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
a5625b41
C
113async 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
131async 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) {
d5b7d911 137 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
a5625b41
C
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
265ba139
C
152async function fetchActorTotalItems (url: string) {
153 const options = {
154 uri: url,
155 method: 'GET',
156 json: true,
157 activityPub: true
158 }
159
265ba139 160 try {
7006bc63
C
161 const { body } = await doRequest(options)
162 return body.totalItems ? body.totalItems : 0
265ba139 163 } catch (err) {
d5b7d911 164 logger.warn('Cannot fetch remote actor count %s.', url, { err })
7006bc63 165 return 0
265ba139 166 }
265ba139
C
167}
168
169async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
170 if (
ac81d1a0 171 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
265ba139
C
172 isActivityPubUrlValid(actorJSON.icon.url)
173 ) {
ac81d1a0 174 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
265ba139
C
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
16f29007
C
190async 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 = {
f6eebcb3
C
199 uri: actor.outboxUrl,
200 type: 'activity' as 'activity'
16f29007
C
201 }
202
203 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
204}
205
c5911fd3
C
206export {
207 getOrCreateActorAndServerAndModel,
208 buildActorInstance,
265ba139
C
209 setAsyncActorKeys,
210 fetchActorTotalItems,
a5625b41
C
211 fetchAvatarIfExists,
212 updateActorInstance,
16f29007
C
213 updateActorAvatarInstance,
214 addFetchOutboxJob
c5911fd3
C
215}
216
217// ---------------------------------------------------------------------------
218
50d6de9c
C
219function 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
c5911fd3
C
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
50d6de9c
C
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)
2c897999
C
257 const [ actorCreated ] = await ActorModel.findOrCreate({
258 defaults: actor.toJSON(),
259 where: {
260 url: actor.url
261 },
262 transaction: t
263 })
50d6de9c
C
264
265 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
2422c46b 266 actorCreated.Account = await saveAccount(actorCreated, result, t)
50d6de9c
C
267 actorCreated.Account.Actor = actorCreated
268 } else if (actorCreated.type === 'Group') { // Video channel
2422c46b 269 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
50d6de9c 270 actorCreated.VideoChannel.Actor = actorCreated
f6eebcb3 271 actorCreated.VideoChannel.Account = ownerActor.Account
50d6de9c
C
272 }
273
274 return actorCreated
275 }
276}
277
278type FetchRemoteActorResult = {
279 actor: ActorModel
e12a0092 280 name: string
50d6de9c 281 summary: string
2422c46b 282 support?: string
c5911fd3 283 avatarName?: string
50d6de9c
C
284 attributedTo: ActivityPubAttributedTo[]
285}
f5b0af50 286async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
50d6de9c
C
287 const options = {
288 uri: actorUrl,
289 method: 'GET',
da854ddd
C
290 json: true,
291 activityPub: true
50d6de9c
C
292 }
293
294 logger.info('Fetching remote actor %s.', actorUrl)
295
da854ddd 296 const requestResult = await doRequest(options)
f47776e2
C
297 normalizeActor(requestResult.body)
298
299 const actorJSON: ActivityPubActor = requestResult.body
50d6de9c 300
265ba139 301 if (isActorObjectValid(actorJSON) === false) {
50d6de9c 302 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
f5b0af50 303 return { result: undefined, statusCode: requestResult.response.statusCode }
50d6de9c
C
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,
e12a0092
C
312 preferredUsername: actorJSON.preferredUsername,
313 url: actorJSON.id,
50d6de9c
C
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
265ba139 325 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 326
e12a0092 327 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c 328 return {
f5b0af50
C
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 }
50d6de9c
C
338 }
339}
340
2c897999
C
341async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
342 const [ accountCreated ] = await AccountModel.findOrCreate({
343 defaults: {
344 name: result.name,
2422c46b 345 description: result.summary,
2c897999
C
346 actorId: actor.id
347 },
348 where: {
349 actorId: actor.id
350 },
351 transaction: t
50d6de9c
C
352 })
353
2c897999 354 return accountCreated
50d6de9c
C
355}
356
357async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
2c897999
C
358 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
359 defaults: {
360 name: result.name,
361 description: result.summary,
2422c46b 362 support: result.support,
2c897999
C
363 actorId: actor.id,
364 accountId: ownerActor.Account.id
365 },
366 where: {
367 actorId: actor.id
368 },
369 transaction: t
50d6de9c
C
370 })
371
2c897999 372 return videoChannelCreated
50d6de9c 373}
a5625b41 374
e587e0ec
C
375async 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)
a5625b41 383
94a5ff8a 384 try {
06a05d5f 385 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
f5b0af50
C
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()
687d638c 391 return { actor: undefined, refreshed: false }
f5b0af50
C
392 }
393
94a5ff8a
C
394 if (result === undefined) {
395 logger.warn('Cannot fetch remote actor in refresh actor.')
687d638c 396 return { actor, refreshed: false }
a5625b41
C
397 }
398
94a5ff8a
C
399 return sequelizeTypescript.transaction(async t => {
400 updateInstanceWithAnother(actor, result.actor)
a5625b41 401
94a5ff8a
C
402 if (result.avatarName !== undefined) {
403 await updateActorAvatarInstance(actor, result.avatarName, t)
404 }
a5625b41 405
94a5ff8a
C
406 // Force update
407 actor.setDataValue('updatedAt', new Date())
a5625b41
C
408 await actor.save({ transaction: t })
409
94a5ff8a 410 if (actor.Account) {
94a5ff8a 411 actor.Account.set('name', result.name)
2422c46b 412 actor.Account.set('description', result.summary)
c48e82b5 413
94a5ff8a
C
414 await actor.Account.save({ transaction: t })
415 } else if (actor.VideoChannel) {
94a5ff8a 416 actor.VideoChannel.set('name', result.name)
2422c46b
C
417 actor.VideoChannel.set('description', result.summary)
418 actor.VideoChannel.set('support', result.support)
c48e82b5 419
94a5ff8a
C
420 await actor.VideoChannel.save({ transaction: t })
421 }
a5625b41 422
687d638c 423 return { refreshed: true, actor }
94a5ff8a
C
424 })
425 } catch (err) {
d5b7d911 426 logger.warn('Cannot refresh actor.', { err })
687d638c 427 return { actor, refreshed: false }
94a5ff8a 428 }
a5625b41 429}