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