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