]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/activitypub/actor.ts
Improve tests when waiting pending jobs
[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
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
72 const options = {
73 arguments: [ actor ],
74 errorMessage: 'Cannot refresh actor if needed with many retries.'
75 }
76 return retryTransactionWrapper(refreshActorIfNeeded, options)
77 }
78
79 function 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
97 async 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
115 async 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) {
121 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
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
136 async function fetchActorTotalItems (url: string) {
137 const options = {
138 uri: url,
139 method: 'GET',
140 json: true,
141 activityPub: true
142 }
143
144 try {
145 const { body } = await doRequest(options)
146 return body.totalItems ? body.totalItems : 0
147 } catch (err) {
148 logger.warn('Cannot fetch remote actor count %s.', url, { err })
149 return 0
150 }
151 }
152
153 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
154 if (
155 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
156 isActivityPubUrlValid(actorJSON.icon.url)
157 ) {
158 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
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
174 async 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
189 export {
190 getOrCreateActorAndServerAndModel,
191 buildActorInstance,
192 setAsyncActorKeys,
193 fetchActorTotalItems,
194 fetchAvatarIfExists,
195 updateActorInstance,
196 updateActorAvatarInstance,
197 addFetchOutboxJob
198 }
199
200 // ---------------------------------------------------------------------------
201
202 function 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
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
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)
240 const [ actorCreated ] = await ActorModel.findOrCreate({
241 defaults: actor.toJSON(),
242 where: {
243 url: actor.url
244 },
245 transaction: t
246 })
247
248 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
249 actorCreated.Account = await saveAccount(actorCreated, result, t)
250 actorCreated.Account.Actor = actorCreated
251 } else if (actorCreated.type === 'Group') { // Video channel
252 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
253 actorCreated.VideoChannel.Actor = actorCreated
254 }
255
256 return actorCreated
257 }
258 }
259
260 type FetchRemoteActorResult = {
261 actor: ActorModel
262 name: string
263 summary: string
264 support?: string
265 avatarName?: string
266 attributedTo: ActivityPubAttributedTo[]
267 }
268 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
269 const options = {
270 uri: actorUrl,
271 method: 'GET',
272 json: true,
273 activityPub: true
274 }
275
276 logger.info('Fetching remote actor %s.', actorUrl)
277
278 const requestResult = await doRequest(options)
279 normalizeActor(requestResult.body)
280
281 const actorJSON: ActivityPubActor = requestResult.body
282
283 if (isActorObjectValid(actorJSON) === false) {
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,
294 preferredUsername: actorJSON.preferredUsername,
295 url: actorJSON.id,
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
307 const avatarName = await fetchAvatarIfExists(actorJSON)
308
309 const name = actorJSON.name || actorJSON.preferredUsername
310 return {
311 actor,
312 name,
313 avatarName,
314 summary: actorJSON.summary,
315 support: actorJSON.support,
316 attributedTo: actorJSON.attributedTo
317 }
318 }
319
320 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
321 const [ accountCreated ] = await AccountModel.findOrCreate({
322 defaults: {
323 name: result.name,
324 description: result.summary,
325 actorId: actor.id
326 },
327 where: {
328 actorId: actor.id
329 },
330 transaction: t
331 })
332
333 return accountCreated
334 }
335
336 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
337 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
338 defaults: {
339 name: result.name,
340 description: result.summary,
341 support: result.support,
342 actorId: actor.id,
343 accountId: ownerActor.Account.id
344 },
345 where: {
346 actorId: actor.id
347 },
348 transaction: t
349 })
350
351 return videoChannelCreated
352 }
353
354 async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
355 if (!actor.isOutdated()) return actor
356
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
363 }
364
365 return sequelizeTypescript.transaction(async t => {
366 updateInstanceWithAnother(actor, result.actor)
367
368 if (result.avatarName !== undefined) {
369 await updateActorAvatarInstance(actor, result.avatarName, t)
370 }
371
372 // Force update
373 actor.setDataValue('updatedAt', new Date())
374 await actor.save({ transaction: t })
375
376 if (actor.Account) {
377 await actor.save({ transaction: t })
378
379 actor.Account.set('name', result.name)
380 actor.Account.set('description', result.summary)
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)
386 actor.VideoChannel.set('description', result.summary)
387 actor.VideoChannel.set('support', result.support)
388 await actor.VideoChannel.save({ transaction: t })
389 }
390
391 return actor
392 })
393 } catch (err) {
394 logger.warn('Cannot refresh actor.', { err })
395 return actor
396 }
397 }