]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Refresh orphan actors
[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
C
43 // Orphan actor (not associated to an account of channel) so recreate it
44 if (!actor.Account && !actor.VideoChannel) {
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 = {
180 uris: [ actor.outboxUrl ]
181 }
182
183 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
184}
185
c5911fd3
C
186export {
187 getOrCreateActorAndServerAndModel,
188 buildActorInstance,
265ba139
C
189 setAsyncActorKeys,
190 fetchActorTotalItems,
a5625b41
C
191 fetchAvatarIfExists,
192 updateActorInstance,
16f29007
C
193 updateActorAvatarInstance,
194 addFetchOutboxJob
c5911fd3
C
195}
196
197// ---------------------------------------------------------------------------
198
50d6de9c
C
199function saveActorAndServerAndModelIfNotExist (
200 result: FetchRemoteActorResult,
201 ownerActor?: ActorModel,
202 t?: Transaction
203): Bluebird<ActorModel> | Promise<ActorModel> {
204 let actor = result.actor
205
206 if (t !== undefined) return save(t)
207
208 return sequelizeTypescript.transaction(t => save(t))
209
210 async function save (t: Transaction) {
211 const actorHost = url.parse(actor.url).host
212
213 const serverOptions = {
214 where: {
215 host: actorHost
216 },
217 defaults: {
218 host: actorHost
219 },
220 transaction: t
221 }
222 const [ server ] = await ServerModel.findOrCreate(serverOptions)
223
224 // Save our new account in database
225 actor.set('serverId', server.id)
226
c5911fd3
C
227 // Avatar?
228 if (result.avatarName) {
229 const avatar = await AvatarModel.create({
230 filename: result.avatarName
231 }, { transaction: t })
232 actor.set('avatarId', avatar.id)
233 }
234
50d6de9c
C
235 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
236 // (which could be false in a retried query)
2c897999
C
237 const [ actorCreated ] = await ActorModel.findOrCreate({
238 defaults: actor.toJSON(),
239 where: {
240 url: actor.url
241 },
242 transaction: t
243 })
50d6de9c
C
244
245 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
2422c46b 246 actorCreated.Account = await saveAccount(actorCreated, result, t)
50d6de9c
C
247 actorCreated.Account.Actor = actorCreated
248 } else if (actorCreated.type === 'Group') { // Video channel
2422c46b 249 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
50d6de9c
C
250 actorCreated.VideoChannel.Actor = actorCreated
251 }
252
253 return actorCreated
254 }
255}
256
257type FetchRemoteActorResult = {
258 actor: ActorModel
e12a0092 259 name: string
50d6de9c 260 summary: string
2422c46b 261 support?: string
c5911fd3 262 avatarName?: string
50d6de9c
C
263 attributedTo: ActivityPubAttributedTo[]
264}
265async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
266 const options = {
267 uri: actorUrl,
268 method: 'GET',
da854ddd
C
269 json: true,
270 activityPub: true
50d6de9c
C
271 }
272
273 logger.info('Fetching remote actor %s.', actorUrl)
274
da854ddd 275 const requestResult = await doRequest(options)
f47776e2
C
276 normalizeActor(requestResult.body)
277
278 const actorJSON: ActivityPubActor = requestResult.body
50d6de9c 279
265ba139 280 if (isActorObjectValid(actorJSON) === false) {
50d6de9c
C
281 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
282 return undefined
283 }
284
285 const followersCount = await fetchActorTotalItems(actorJSON.followers)
286 const followingCount = await fetchActorTotalItems(actorJSON.following)
287
288 const actor = new ActorModel({
289 type: actorJSON.type,
290 uuid: actorJSON.uuid,
e12a0092
C
291 preferredUsername: actorJSON.preferredUsername,
292 url: actorJSON.id,
50d6de9c
C
293 publicKey: actorJSON.publicKey.publicKeyPem,
294 privateKey: null,
295 followersCount: followersCount,
296 followingCount: followingCount,
297 inboxUrl: actorJSON.inbox,
298 outboxUrl: actorJSON.outbox,
299 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
300 followersUrl: actorJSON.followers,
301 followingUrl: actorJSON.following
302 })
303
265ba139 304 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 305
e12a0092 306 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c
C
307 return {
308 actor,
e12a0092 309 name,
c5911fd3 310 avatarName,
50d6de9c 311 summary: actorJSON.summary,
2422c46b 312 support: actorJSON.support,
50d6de9c
C
313 attributedTo: actorJSON.attributedTo
314 }
315}
316
2c897999
C
317async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
318 const [ accountCreated ] = await AccountModel.findOrCreate({
319 defaults: {
320 name: result.name,
2422c46b 321 description: result.summary,
2c897999
C
322 actorId: actor.id
323 },
324 where: {
325 actorId: actor.id
326 },
327 transaction: t
50d6de9c
C
328 })
329
2c897999 330 return accountCreated
50d6de9c
C
331}
332
333async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
2c897999
C
334 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
335 defaults: {
336 name: result.name,
337 description: result.summary,
2422c46b 338 support: result.support,
2c897999
C
339 actorId: actor.id,
340 accountId: ownerActor.Account.id
341 },
342 where: {
343 actorId: actor.id
344 },
345 transaction: t
50d6de9c
C
346 })
347
2c897999 348 return videoChannelCreated
50d6de9c 349}
a5625b41 350
0f320037 351async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
a5625b41
C
352 if (!actor.isOutdated()) return actor
353
94a5ff8a
C
354 try {
355 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
356 const result = await fetchRemoteActor(actorUrl)
357 if (result === undefined) {
358 logger.warn('Cannot fetch remote actor in refresh actor.')
359 return actor
a5625b41
C
360 }
361
94a5ff8a
C
362 return sequelizeTypescript.transaction(async t => {
363 updateInstanceWithAnother(actor, result.actor)
a5625b41 364
94a5ff8a
C
365 if (result.avatarName !== undefined) {
366 await updateActorAvatarInstance(actor, result.avatarName, t)
367 }
a5625b41 368
94a5ff8a
C
369 // Force update
370 actor.setDataValue('updatedAt', new Date())
a5625b41
C
371 await actor.save({ transaction: t })
372
94a5ff8a
C
373 if (actor.Account) {
374 await actor.save({ transaction: t })
375
376 actor.Account.set('name', result.name)
2422c46b 377 actor.Account.set('description', result.summary)
94a5ff8a
C
378 await actor.Account.save({ transaction: t })
379 } else if (actor.VideoChannel) {
380 await actor.save({ transaction: t })
381
382 actor.VideoChannel.set('name', result.name)
2422c46b
C
383 actor.VideoChannel.set('description', result.summary)
384 actor.VideoChannel.set('support', result.support)
94a5ff8a
C
385 await actor.VideoChannel.save({ transaction: t })
386 }
a5625b41 387
94a5ff8a
C
388 return actor
389 })
390 } catch (err) {
d5b7d911 391 logger.warn('Cannot refresh actor.', { err })
a5625b41 392 return actor
94a5ff8a 393 }
a5625b41 394}