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