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