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