]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Fix new Angular 7 issues
[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'
5c6d985f 8import { checkUrlsSameHost, 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
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
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
182
183 await doRequestAndSaveToFile({
184 method: 'GET',
185 uri: actorJSON.icon.url
186 }, destPath)
187
188 return avatarName
189 }
190
191 return undefined
192}
193
16f29007
C
194async function addFetchOutboxJob (actor: ActorModel) {
195 // Don't fetch ourselves
196 const serverActor = await getServerActor()
197 if (serverActor.id === actor.id) {
198 logger.error('Cannot fetch our own outbox!')
199 return undefined
200 }
201
202 const payload = {
f6eebcb3
C
203 uri: actor.outboxUrl,
204 type: 'activity' as 'activity'
16f29007
C
205 }
206
207 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
208}
209
c5911fd3
C
210export {
211 getOrCreateActorAndServerAndModel,
212 buildActorInstance,
265ba139
C
213 setAsyncActorKeys,
214 fetchActorTotalItems,
a5625b41
C
215 fetchAvatarIfExists,
216 updateActorInstance,
16f29007
C
217 updateActorAvatarInstance,
218 addFetchOutboxJob
c5911fd3
C
219}
220
221// ---------------------------------------------------------------------------
222
50d6de9c
C
223function saveActorAndServerAndModelIfNotExist (
224 result: FetchRemoteActorResult,
225 ownerActor?: ActorModel,
226 t?: Transaction
227): Bluebird<ActorModel> | Promise<ActorModel> {
228 let actor = result.actor
229
230 if (t !== undefined) return save(t)
231
232 return sequelizeTypescript.transaction(t => save(t))
233
234 async function save (t: Transaction) {
235 const actorHost = url.parse(actor.url).host
236
237 const serverOptions = {
238 where: {
239 host: actorHost
240 },
241 defaults: {
242 host: actorHost
243 },
244 transaction: t
245 }
246 const [ server ] = await ServerModel.findOrCreate(serverOptions)
247
248 // Save our new account in database
249 actor.set('serverId', server.id)
250
c5911fd3
C
251 // Avatar?
252 if (result.avatarName) {
253 const avatar = await AvatarModel.create({
254 filename: result.avatarName
255 }, { transaction: t })
256 actor.set('avatarId', avatar.id)
257 }
258
50d6de9c
C
259 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
260 // (which could be false in a retried query)
2c897999
C
261 const [ actorCreated ] = await ActorModel.findOrCreate({
262 defaults: actor.toJSON(),
263 where: {
264 url: actor.url
265 },
266 transaction: t
267 })
50d6de9c
C
268
269 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
2422c46b 270 actorCreated.Account = await saveAccount(actorCreated, result, t)
50d6de9c
C
271 actorCreated.Account.Actor = actorCreated
272 } else if (actorCreated.type === 'Group') { // Video channel
2422c46b 273 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
50d6de9c 274 actorCreated.VideoChannel.Actor = actorCreated
f6eebcb3 275 actorCreated.VideoChannel.Account = ownerActor.Account
50d6de9c
C
276 }
277
278 return actorCreated
279 }
280}
281
282type FetchRemoteActorResult = {
283 actor: ActorModel
e12a0092 284 name: string
50d6de9c 285 summary: string
2422c46b 286 support?: string
c5911fd3 287 avatarName?: string
50d6de9c
C
288 attributedTo: ActivityPubAttributedTo[]
289}
f5b0af50 290async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
50d6de9c
C
291 const options = {
292 uri: actorUrl,
293 method: 'GET',
da854ddd
C
294 json: true,
295 activityPub: true
50d6de9c
C
296 }
297
298 logger.info('Fetching remote actor %s.', actorUrl)
299
da854ddd 300 const requestResult = await doRequest(options)
f47776e2
C
301 normalizeActor(requestResult.body)
302
303 const actorJSON: ActivityPubActor = requestResult.body
265ba139 304 if (isActorObjectValid(actorJSON) === false) {
50d6de9c 305 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
f5b0af50 306 return { result: undefined, statusCode: requestResult.response.statusCode }
50d6de9c
C
307 }
308
5c6d985f
C
309 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
310 throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
311 }
312
50d6de9c
C
313 const followersCount = await fetchActorTotalItems(actorJSON.followers)
314 const followingCount = await fetchActorTotalItems(actorJSON.following)
315
316 const actor = new ActorModel({
317 type: actorJSON.type,
318 uuid: actorJSON.uuid,
e12a0092
C
319 preferredUsername: actorJSON.preferredUsername,
320 url: actorJSON.id,
50d6de9c
C
321 publicKey: actorJSON.publicKey.publicKeyPem,
322 privateKey: null,
323 followersCount: followersCount,
324 followingCount: followingCount,
325 inboxUrl: actorJSON.inbox,
326 outboxUrl: actorJSON.outbox,
327 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
328 followersUrl: actorJSON.followers,
329 followingUrl: actorJSON.following
330 })
331
265ba139 332 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 333
e12a0092 334 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c 335 return {
f5b0af50
C
336 statusCode: requestResult.response.statusCode,
337 result: {
338 actor,
339 name,
340 avatarName,
341 summary: actorJSON.summary,
342 support: actorJSON.support,
343 attributedTo: actorJSON.attributedTo
344 }
50d6de9c
C
345 }
346}
347
2c897999
C
348async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
349 const [ accountCreated ] = await AccountModel.findOrCreate({
350 defaults: {
351 name: result.name,
2422c46b 352 description: result.summary,
2c897999
C
353 actorId: actor.id
354 },
355 where: {
356 actorId: actor.id
357 },
358 transaction: t
50d6de9c
C
359 })
360
2c897999 361 return accountCreated
50d6de9c
C
362}
363
364async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
2c897999
C
365 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
366 defaults: {
367 name: result.name,
368 description: result.summary,
2422c46b 369 support: result.support,
2c897999
C
370 actorId: actor.id,
371 accountId: ownerActor.Account.id
372 },
373 where: {
374 actorId: actor.id
375 },
376 transaction: t
50d6de9c
C
377 })
378
2c897999 379 return videoChannelCreated
50d6de9c 380}
a5625b41 381
e587e0ec
C
382async function refreshActorIfNeeded (
383 actorArg: ActorModel,
384 fetchedType: ActorFetchByUrlType
385): Promise<{ actor: ActorModel, refreshed: boolean }> {
386 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
387
388 // We need more attributes
389 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
a5625b41 390
94a5ff8a 391 try {
06a05d5f 392 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
f5b0af50
C
393 const { result, statusCode } = await fetchRemoteActor(actorUrl)
394
395 if (statusCode === 404) {
396 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
397 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
687d638c 398 return { actor: undefined, refreshed: false }
f5b0af50
C
399 }
400
94a5ff8a
C
401 if (result === undefined) {
402 logger.warn('Cannot fetch remote actor in refresh actor.')
687d638c 403 return { actor, refreshed: false }
a5625b41
C
404 }
405
94a5ff8a
C
406 return sequelizeTypescript.transaction(async t => {
407 updateInstanceWithAnother(actor, result.actor)
a5625b41 408
94a5ff8a
C
409 if (result.avatarName !== undefined) {
410 await updateActorAvatarInstance(actor, result.avatarName, t)
411 }
a5625b41 412
94a5ff8a
C
413 // Force update
414 actor.setDataValue('updatedAt', new Date())
a5625b41
C
415 await actor.save({ transaction: t })
416
94a5ff8a 417 if (actor.Account) {
94a5ff8a 418 actor.Account.set('name', result.name)
2422c46b 419 actor.Account.set('description', result.summary)
c48e82b5 420
94a5ff8a
C
421 await actor.Account.save({ transaction: t })
422 } else if (actor.VideoChannel) {
94a5ff8a 423 actor.VideoChannel.set('name', result.name)
2422c46b
C
424 actor.VideoChannel.set('description', result.summary)
425 actor.VideoChannel.set('support', result.support)
c48e82b5 426
94a5ff8a
C
427 await actor.VideoChannel.save({ transaction: t })
428 }
a5625b41 429
687d638c 430 return { refreshed: true, actor }
94a5ff8a
C
431 })
432 } catch (err) {
d5b7d911 433 logger.warn('Cannot refresh actor.', { err })
687d638c 434 return { actor, refreshed: false }
94a5ff8a 435 }
a5625b41 436}