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