]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/activitypub/actor.ts
Refresh orphan actors
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / actor.ts
1 import * as Bluebird from 'bluebird'
2 import { join } from 'path'
3 import { Transaction } from 'sequelize'
4 import * as url from 'url'
5 import * as uuidv4 from 'uuid/v4'
6 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8 import { getActorUrl } from '../../helpers/activitypub'
9 import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11 import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
12 import { logger } from '../../helpers/logger'
13 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
15 import { getUrlFromWebfinger } from '../../helpers/webfinger'
16 import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
17 import { AccountModel } from '../../models/account/account'
18 import { ActorModel } from '../../models/activitypub/actor'
19 import { AvatarModel } from '../../models/avatar/avatar'
20 import { ServerModel } from '../../models/server/server'
21 import { VideoChannelModel } from '../../models/video/video-channel'
22 import { JobQueue } from '../job-queue'
23 import { getServerActor } from '../../helpers/utils'
24
25 // Set account keys, this could be long so process after the account creation and do not block the client
26 function 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 => {
34 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
35 return actor
36 })
37 }
38
39 async function getOrCreateActorAndServerAndModel (activityActor: string | ActivityPubActor, recurseIfNeeded = true) {
40 const actorUrl = getActorUrl(activityActor)
41
42 let actor = await ActorModel.loadByUrl(actorUrl)
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 }
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
70 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
71 }
72
73 return retryTransactionWrapper(refreshActorIfNeeded, actor)
74 }
75
76 function 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
94 async 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
112 async 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) {
118 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
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
133 async function fetchActorTotalItems (url: string) {
134 const options = {
135 uri: url,
136 method: 'GET',
137 json: true,
138 activityPub: true
139 }
140
141 try {
142 const { body } = await doRequest(options)
143 return body.totalItems ? body.totalItems : 0
144 } catch (err) {
145 logger.warn('Cannot fetch remote actor count %s.', url, { err })
146 return 0
147 }
148 }
149
150 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
151 if (
152 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
153 isActivityPubUrlValid(actorJSON.icon.url)
154 ) {
155 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
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
171 async 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
186 export {
187 getOrCreateActorAndServerAndModel,
188 buildActorInstance,
189 setAsyncActorKeys,
190 fetchActorTotalItems,
191 fetchAvatarIfExists,
192 updateActorInstance,
193 updateActorAvatarInstance,
194 addFetchOutboxJob
195 }
196
197 // ---------------------------------------------------------------------------
198
199 function 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
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
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)
237 const [ actorCreated ] = await ActorModel.findOrCreate({
238 defaults: actor.toJSON(),
239 where: {
240 url: actor.url
241 },
242 transaction: t
243 })
244
245 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
246 actorCreated.Account = await saveAccount(actorCreated, result, t)
247 actorCreated.Account.Actor = actorCreated
248 } else if (actorCreated.type === 'Group') { // Video channel
249 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
250 actorCreated.VideoChannel.Actor = actorCreated
251 }
252
253 return actorCreated
254 }
255 }
256
257 type FetchRemoteActorResult = {
258 actor: ActorModel
259 name: string
260 summary: string
261 support?: string
262 avatarName?: string
263 attributedTo: ActivityPubAttributedTo[]
264 }
265 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
266 const options = {
267 uri: actorUrl,
268 method: 'GET',
269 json: true,
270 activityPub: true
271 }
272
273 logger.info('Fetching remote actor %s.', actorUrl)
274
275 const requestResult = await doRequest(options)
276 normalizeActor(requestResult.body)
277
278 const actorJSON: ActivityPubActor = requestResult.body
279
280 if (isActorObjectValid(actorJSON) === false) {
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,
291 preferredUsername: actorJSON.preferredUsername,
292 url: actorJSON.id,
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
304 const avatarName = await fetchAvatarIfExists(actorJSON)
305
306 const name = actorJSON.name || actorJSON.preferredUsername
307 return {
308 actor,
309 name,
310 avatarName,
311 summary: actorJSON.summary,
312 support: actorJSON.support,
313 attributedTo: actorJSON.attributedTo
314 }
315 }
316
317 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
318 const [ accountCreated ] = await AccountModel.findOrCreate({
319 defaults: {
320 name: result.name,
321 description: result.summary,
322 actorId: actor.id
323 },
324 where: {
325 actorId: actor.id
326 },
327 transaction: t
328 })
329
330 return accountCreated
331 }
332
333 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
334 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
335 defaults: {
336 name: result.name,
337 description: result.summary,
338 support: result.support,
339 actorId: actor.id,
340 accountId: ownerActor.Account.id
341 },
342 where: {
343 actorId: actor.id
344 },
345 transaction: t
346 })
347
348 return videoChannelCreated
349 }
350
351 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
352 if (!actor.isOutdated()) return actor
353
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
360 }
361
362 return sequelizeTypescript.transaction(async t => {
363 updateInstanceWithAnother(actor, result.actor)
364
365 if (result.avatarName !== undefined) {
366 await updateActorAvatarInstance(actor, result.avatarName, t)
367 }
368
369 // Force update
370 actor.setDataValue('updatedAt', new Date())
371 await actor.save({ transaction: t })
372
373 if (actor.Account) {
374 await actor.save({ transaction: t })
375
376 actor.Account.set('name', result.name)
377 actor.Account.set('description', result.summary)
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)
383 actor.VideoChannel.set('description', result.summary)
384 actor.VideoChannel.set('support', result.support)
385 await actor.VideoChannel.save({ transaction: t })
386 }
387
388 return actor
389 })
390 } catch (err) {
391 logger.warn('Cannot refresh actor.', { err })
392 return actor
393 }
394 }