]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/lib/activitypub/actor.ts
Merge branch 'hotfix/docker' into develop
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / actor.ts
... / ...
CommitLineData
1import * as Bluebird from 'bluebird'
2import { join } from 'path'
3import { Transaction } from 'sequelize'
4import * as url from 'url'
5import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub'
9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14import { doRequest, downloadImage } from '../../helpers/requests'
15import { getUrlFromWebfinger } from '../../helpers/webfinger'
16import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
17import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor'
19import { AvatarModel } from '../../models/avatar/avatar'
20import { ServerModel } from '../../models/server/server'
21import { VideoChannelModel } from '../../models/video/video-channel'
22import { JobQueue } from '../job-queue'
23import { getServerActor } from '../../helpers/utils'
24import { 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
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 => {
35 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
36 return actor
37 })
38}
39
40async function getOrCreateActorAndServerAndModel (
41 activityActor: string | ActivityPubActor,
42 fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
43 recurseIfNeeded = true,
44 updateCollections = false
45) {
46 const actorUrl = getAPUrl(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 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
72 try {
73 // Don't recurse another time
74 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
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
81 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
82 created = true
83 }
84
85 if (actor.Account) actor.Account.Actor = actor
86 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
87
88 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
89 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
90
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
97}
98
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
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) {
141 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
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
156async function fetchActorTotalItems (url: string) {
157 const options = {
158 uri: url,
159 method: 'GET',
160 json: true,
161 activityPub: true
162 }
163
164 try {
165 const { body } = await doRequest(options)
166 return body.totalItems ? body.totalItems : 0
167 } catch (err) {
168 logger.warn('Cannot fetch remote actor count %s.', url, { err })
169 return 0
170 }
171}
172
173async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
174 if (
175 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
176 isActivityPubUrlValid(actorJSON.icon.url)
177 ) {
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
179
180 const avatarName = uuidv4() + extension
181 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
182
183 return avatarName
184 }
185
186 return undefined
187}
188
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 = {
198 uri: actor.outboxUrl,
199 type: 'activity' as 'activity'
200 }
201
202 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
203}
204
205export {
206 getOrCreateActorAndServerAndModel,
207 buildActorInstance,
208 setAsyncActorKeys,
209 fetchActorTotalItems,
210 fetchAvatarIfExists,
211 updateActorInstance,
212 updateActorAvatarInstance,
213 addFetchOutboxJob
214}
215
216// ---------------------------------------------------------------------------
217
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
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
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)
256 const [ actorCreated ] = await ActorModel.findOrCreate({
257 defaults: actor.toJSON(),
258 where: {
259 url: actor.url
260 },
261 transaction: t
262 })
263
264 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
265 actorCreated.Account = await saveAccount(actorCreated, result, t)
266 actorCreated.Account.Actor = actorCreated
267 } else if (actorCreated.type === 'Group') { // Video channel
268 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
269 actorCreated.VideoChannel.Actor = actorCreated
270 actorCreated.VideoChannel.Account = ownerActor.Account
271 }
272
273 return actorCreated
274 }
275}
276
277type FetchRemoteActorResult = {
278 actor: ActorModel
279 name: string
280 summary: string
281 support?: string
282 avatarName?: string
283 attributedTo: ActivityPubAttributedTo[]
284}
285async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
286 const options = {
287 uri: actorUrl,
288 method: 'GET',
289 json: true,
290 activityPub: true
291 }
292
293 logger.info('Fetching remote actor %s.', actorUrl)
294
295 const requestResult = await doRequest(options)
296 normalizeActor(requestResult.body)
297
298 const actorJSON: ActivityPubActor = requestResult.body
299 if (isActorObjectValid(actorJSON) === false) {
300 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
301 return { result: undefined, statusCode: requestResult.response.statusCode }
302 }
303
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
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,
314 preferredUsername: actorJSON.preferredUsername,
315 url: actorJSON.id,
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
327 const avatarName = await fetchAvatarIfExists(actorJSON)
328
329 const name = actorJSON.name || actorJSON.preferredUsername
330 return {
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 }
340 }
341}
342
343async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
344 const [ accountCreated ] = await AccountModel.findOrCreate({
345 defaults: {
346 name: result.name,
347 description: result.summary,
348 actorId: actor.id
349 },
350 where: {
351 actorId: actor.id
352 },
353 transaction: t
354 })
355
356 return accountCreated
357}
358
359async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
360 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
361 defaults: {
362 name: result.name,
363 description: result.summary,
364 support: result.support,
365 actorId: actor.id,
366 accountId: ownerActor.Account.id
367 },
368 where: {
369 actorId: actor.id
370 },
371 transaction: t
372 })
373
374 return videoChannelCreated
375}
376
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)
385
386 try {
387 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
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()
393 return { actor: undefined, refreshed: false }
394 }
395
396 if (result === undefined) {
397 logger.warn('Cannot fetch remote actor in refresh actor.')
398 return { actor, refreshed: false }
399 }
400
401 return sequelizeTypescript.transaction(async t => {
402 updateInstanceWithAnother(actor, result.actor)
403
404 if (result.avatarName !== undefined) {
405 await updateActorAvatarInstance(actor, result.avatarName, t)
406 }
407
408 // Force update
409 actor.setDataValue('updatedAt', new Date())
410 await actor.save({ transaction: t })
411
412 if (actor.Account) {
413 actor.Account.set('name', result.name)
414 actor.Account.set('description', result.summary)
415
416 await actor.Account.save({ transaction: t })
417 } else if (actor.VideoChannel) {
418 actor.VideoChannel.set('name', result.name)
419 actor.VideoChannel.set('description', result.summary)
420 actor.VideoChannel.set('support', result.support)
421
422 await actor.VideoChannel.save({ transaction: t })
423 }
424
425 return { refreshed: true, actor }
426 })
427 } catch (err) {
428 logger.warn('Cannot refresh actor.', { err })
429 return { actor, refreshed: false }
430 }
431}