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