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