@evanp/activitypub-bot 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/main.yml +34 -0
  2. package/.github/workflows/{tag-docker.yml → tag.yml} +57 -5
  3. package/.nvmrc +1 -0
  4. package/Dockerfile +10 -16
  5. package/README.md +87 -13
  6. package/bin/activitypub-bot.js +68 -0
  7. package/lib/activitydeliverer.js +260 -0
  8. package/lib/activityhandler.js +14 -0
  9. package/lib/activitypubclient.js +52 -1
  10. package/lib/activitystreams.js +31 -0
  11. package/lib/actorstorage.js +18 -28
  12. package/lib/app.js +18 -7
  13. package/lib/bot.js +7 -0
  14. package/lib/botcontext.js +62 -0
  15. package/lib/botdatastorage.js +0 -13
  16. package/lib/botfactory.js +24 -0
  17. package/lib/botmaker.js +23 -0
  18. package/lib/index.js +3 -0
  19. package/lib/keystorage.js +7 -24
  20. package/lib/migrations/001-initial.js +107 -0
  21. package/lib/migrations/index.js +28 -0
  22. package/lib/objectcache.js +4 -1
  23. package/lib/objectstorage.js +0 -36
  24. package/lib/remotekeystorage.js +0 -24
  25. package/lib/routes/collection.js +6 -2
  26. package/lib/routes/inbox.js +7 -20
  27. package/lib/routes/sharedinbox.js +54 -0
  28. package/lib/routes/user.js +11 -5
  29. package/lib/routes/webfinger.js +11 -2
  30. package/lib/urlformatter.js +8 -0
  31. package/package.json +14 -7
  32. package/tests/activitydistributor.test.js +3 -3
  33. package/tests/activityhandler.test.js +96 -5
  34. package/tests/activitypubclient.test.js +115 -130
  35. package/tests/actorstorage.test.js +26 -4
  36. package/tests/authorizer.test.js +3 -8
  37. package/tests/botcontext.test.js +109 -63
  38. package/tests/botdatastorage.test.js +3 -2
  39. package/tests/botfactory.provincebotfactory.test.js +430 -0
  40. package/tests/fixtures/bots.js +13 -1
  41. package/tests/fixtures/eventloggingbot.js +57 -0
  42. package/tests/fixtures/provincebotfactory.js +53 -0
  43. package/tests/httpsignature.test.js +3 -4
  44. package/tests/httpsignatureauthenticator.test.js +3 -3
  45. package/tests/index.test.js +10 -0
  46. package/tests/keystorage.test.js +37 -2
  47. package/tests/microsyntax.test.js +3 -2
  48. package/tests/objectstorage.test.js +4 -3
  49. package/tests/remotekeystorage.test.js +10 -8
  50. package/tests/routes.actor.test.js +7 -0
  51. package/tests/routes.collection.test.js +0 -1
  52. package/tests/routes.inbox.test.js +1 -0
  53. package/tests/routes.object.test.js +44 -38
  54. package/tests/routes.sharedinbox.test.js +473 -0
  55. package/tests/routes.webfinger.test.js +27 -0
  56. package/tests/utils/nock.js +250 -27
  57. package/.github/workflows/main-docker.yml +0 -45
  58. package/index.js +0 -23
@@ -0,0 +1,473 @@
1
+ import { describe, it, before } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import as2 from '../lib/activitystreams.js'
4
+ import request from 'supertest'
5
+
6
+ import { makeApp } from '../lib/app.js'
7
+
8
+ import {
9
+ nockSetup,
10
+ nockSignature,
11
+ nockFormat,
12
+ makeActor,
13
+ addFollower,
14
+ addFollowing,
15
+ addToCollection
16
+ } from './utils/nock.js'
17
+ import { makeDigest } from './utils/digest.js'
18
+ import bots from './fixtures/bots.js'
19
+
20
+ describe('routes.sharedinbox', async () => {
21
+ const host = 'activitypubbot.test'
22
+ const remoteHost = 'social.example'
23
+ const origin = `https://${host}`
24
+ const databaseUrl = 'sqlite::memory:'
25
+ let app = null
26
+ let formatter = null
27
+ let actorStorage = null
28
+
29
+ before(async () => {
30
+ nockSetup(remoteHost)
31
+ app = await makeApp(databaseUrl, origin, bots, 'silent')
32
+ formatter = app.locals.formatter
33
+ actorStorage = app.locals.actorStorage
34
+ })
35
+
36
+ describe('can handle an directly addressed activity', async () => {
37
+ const username = 'actor1'
38
+ const botName = 'test0'
39
+ const path = '/shared/inbox'
40
+ const url = `${origin}${path}`
41
+ const date = new Date().toUTCString()
42
+ let response = null
43
+ let signature = null
44
+ let body = null
45
+ let digest = null
46
+ let activity = null
47
+ before(async () => {
48
+ activity = await as2.import({
49
+ type: 'Activity',
50
+ actor: nockFormat({ username }),
51
+ id: nockFormat({ username, type: 'activity', num: 1 }),
52
+ to: formatter.format({ username: botName })
53
+ })
54
+ body = await activity.write()
55
+ digest = makeDigest(body)
56
+ signature = await nockSignature({
57
+ method: 'POST',
58
+ username,
59
+ url,
60
+ digest,
61
+ date
62
+ })
63
+ })
64
+ it('should work without an error', async () => {
65
+ response = await request(app)
66
+ .post(path)
67
+ .send(body)
68
+ .set('Signature', signature)
69
+ .set('Date', date)
70
+ .set('Host', host)
71
+ .set('Digest', digest)
72
+ .set('Content-Type', 'application/activity+json')
73
+ assert.ok(response)
74
+ await app.onIdle()
75
+ })
76
+ it('should return a 200 status', async () => {
77
+ assert.strictEqual(response.status, 200)
78
+ })
79
+ it('should appear in the inbox', async () => {
80
+ assert.strictEqual(
81
+ true,
82
+ await actorStorage.isInCollection(
83
+ botName,
84
+ 'inbox',
85
+ activity
86
+ )
87
+ )
88
+ })
89
+ })
90
+
91
+ describe('can handle an followers-only activity', async () => {
92
+ const username = 'actor2'
93
+ const botNames = ['test1', 'test2']
94
+ const path = '/shared/inbox'
95
+ const url = `${origin}${path}`
96
+ const date = new Date().toUTCString()
97
+ let response = null
98
+ let signature = null
99
+ let body = null
100
+ let digest = null
101
+ let activity = null
102
+ let actor = null
103
+ before(async () => {
104
+ actor = await makeActor(username, remoteHost)
105
+ for (const botName of botNames) {
106
+ const botId = formatter.format({ username: botName })
107
+ addFollower(username, botId, remoteHost)
108
+ await actorStorage.addToCollection(botName, 'following', actor)
109
+ }
110
+ activity = await as2.import({
111
+ type: 'Activity',
112
+ actor: actor.id,
113
+ id: nockFormat({ username, type: 'activity', num: 1 }),
114
+ to: nockFormat({ username, collection: 'followers' })
115
+ })
116
+ body = await activity.write()
117
+ digest = makeDigest(body)
118
+ signature = await nockSignature({
119
+ method: 'POST',
120
+ username,
121
+ url,
122
+ digest,
123
+ date
124
+ })
125
+ })
126
+ it('should work without an error', async () => {
127
+ response = await request(app)
128
+ .post(path)
129
+ .send(body)
130
+ .set('Signature', signature)
131
+ .set('Date', date)
132
+ .set('Host', host)
133
+ .set('Digest', digest)
134
+ .set('Content-Type', 'application/activity+json')
135
+ assert.ok(response)
136
+ await app.onIdle()
137
+ })
138
+ it('should return a 200 status', async () => {
139
+ assert.strictEqual(response.status, 200)
140
+ })
141
+ it('should appear in all inboxes', async () => {
142
+ for (const botName of botNames) {
143
+ assert.strictEqual(
144
+ true,
145
+ await actorStorage.isInCollection(
146
+ botName,
147
+ 'inbox',
148
+ activity
149
+ )
150
+ )
151
+ }
152
+ })
153
+ })
154
+
155
+ describe('can handle an public activity', async () => {
156
+ const username = 'actor3'
157
+ const path = '/shared/inbox'
158
+ const url = `${origin}${path}`
159
+ const date = new Date().toUTCString()
160
+ let response = null
161
+ let signature = null
162
+ let body = null
163
+ let digest = null
164
+ let activity = null
165
+ let actor = null
166
+ before(async () => {
167
+ actor = await makeActor(username, remoteHost)
168
+ activity = await as2.import({
169
+ type: 'Activity',
170
+ actor: actor.id,
171
+ id: nockFormat({ username, type: 'activity', num: 1 }),
172
+ to: 'as:Public'
173
+ })
174
+ body = await activity.write()
175
+ digest = makeDigest(body)
176
+ signature = await nockSignature({
177
+ method: 'POST',
178
+ username,
179
+ url,
180
+ digest,
181
+ date
182
+ })
183
+ })
184
+ it('should work without an error', async () => {
185
+ response = await request(app)
186
+ .post(path)
187
+ .send(body)
188
+ .set('Signature', signature)
189
+ .set('Date', date)
190
+ .set('Host', host)
191
+ .set('Digest', digest)
192
+ .set('Content-Type', 'application/activity+json')
193
+ assert.ok(response)
194
+ await app.onIdle()
195
+ })
196
+ it('should return a 200 status', async () => {
197
+ assert.strictEqual(response.status, 200)
198
+ })
199
+ it('should appear in all inboxes', async () => {
200
+ const lb = bots.logging
201
+ assert.ok(lb.publics.has(activity.id))
202
+ })
203
+ })
204
+
205
+ describe('can handle an activity to local followers collection', async () => {
206
+ const username = 'actor4'
207
+ const botNames = ['test3', 'test4', 'test5']
208
+ const followedBot = 'test6'
209
+ const path = '/shared/inbox'
210
+ const url = `${origin}${path}`
211
+ const date = new Date().toUTCString()
212
+ let response = null
213
+ let signature = null
214
+ let body = null
215
+ let digest = null
216
+ let activity = null
217
+ let actor = null
218
+ before(async () => {
219
+ actor = await makeActor(username, remoteHost)
220
+ const followed = await as2.import({
221
+ id: formatter.format({ username: followedBot })
222
+ })
223
+ for (const botName of botNames) {
224
+ const botId = formatter.format({ username: botName })
225
+ const bot = await as2.import({ id: botId })
226
+ await actorStorage.addToCollection(followedBot, 'followers', bot)
227
+ await actorStorage.addToCollection(botName, 'following', followed)
228
+ }
229
+ activity = await as2.import({
230
+ type: 'Activity',
231
+ actor: actor.id,
232
+ id: nockFormat({ username, type: 'activity', num: 1 }),
233
+ to: formatter.format({ username: followedBot, collection: 'followers' })
234
+ })
235
+ body = await activity.write()
236
+ digest = makeDigest(body)
237
+ signature = await nockSignature({
238
+ method: 'POST',
239
+ username,
240
+ url,
241
+ digest,
242
+ date
243
+ })
244
+ })
245
+ it('should work without an error', async () => {
246
+ response = await request(app)
247
+ .post(path)
248
+ .send(body)
249
+ .set('Signature', signature)
250
+ .set('Date', date)
251
+ .set('Host', host)
252
+ .set('Digest', digest)
253
+ .set('Content-Type', 'application/activity+json')
254
+ assert.ok(response)
255
+ await app.onIdle()
256
+ })
257
+ it('should return a 200 status', async () => {
258
+ assert.strictEqual(response.status, 200)
259
+ })
260
+ it('should appear in all inboxes', async () => {
261
+ for (const botName of botNames) {
262
+ assert.strictEqual(
263
+ true,
264
+ await actorStorage.isInCollection(
265
+ botName,
266
+ 'inbox',
267
+ activity
268
+ )
269
+ )
270
+ }
271
+ })
272
+ })
273
+
274
+ describe('can handle an activity to local following collection', async () => {
275
+ const username = 'actor5'
276
+ const botNames = ['test7', 'test8']
277
+ const followingBot = 'test9'
278
+ const path = '/shared/inbox'
279
+ const url = `${origin}${path}`
280
+ const date = new Date().toUTCString()
281
+ let response = null
282
+ let signature = null
283
+ let body = null
284
+ let digest = null
285
+ let activity = null
286
+ let actor = null
287
+ before(async () => {
288
+ actor = await makeActor(username, remoteHost)
289
+ const following = await as2.import({
290
+ id: formatter.format({ username: followingBot })
291
+ })
292
+ for (const botName of botNames) {
293
+ const botId = formatter.format({ username: botName })
294
+ const bot = await as2.import({ id: botId })
295
+ await actorStorage.addToCollection(followingBot, 'following', bot)
296
+ await actorStorage.addToCollection(botName, 'followers', following)
297
+ }
298
+ activity = await as2.import({
299
+ type: 'Activity',
300
+ actor: actor.id,
301
+ id: nockFormat({ username, type: 'activity', num: 1 }),
302
+ to: formatter.format({
303
+ username: followingBot,
304
+ collection: 'following'
305
+ })
306
+ })
307
+ body = await activity.write()
308
+ digest = makeDigest(body)
309
+ signature = await nockSignature({
310
+ method: 'POST',
311
+ username,
312
+ url,
313
+ digest,
314
+ date
315
+ })
316
+ })
317
+ it('should work without an error', async () => {
318
+ response = await request(app)
319
+ .post(path)
320
+ .send(body)
321
+ .set('Signature', signature)
322
+ .set('Date', date)
323
+ .set('Host', host)
324
+ .set('Digest', digest)
325
+ .set('Content-Type', 'application/activity+json')
326
+ assert.ok(response)
327
+ await app.onIdle()
328
+ })
329
+ it('should return a 200 status', async () => {
330
+ assert.strictEqual(response.status, 200)
331
+ })
332
+ it('should appear in all inboxes', async () => {
333
+ for (const botName of botNames) {
334
+ assert.strictEqual(
335
+ true,
336
+ await actorStorage.isInCollection(
337
+ botName,
338
+ 'inbox',
339
+ activity
340
+ )
341
+ )
342
+ }
343
+ })
344
+ })
345
+
346
+ describe('can handle an activity to remote following collection', async () => {
347
+ const username = 'actor6'
348
+ const botNames = ['test10', 'test11', 'test12']
349
+ const path = '/shared/inbox'
350
+ const url = `${origin}${path}`
351
+ const date = new Date().toUTCString()
352
+ let response = null
353
+ let signature = null
354
+ let body = null
355
+ let digest = null
356
+ let activity = null
357
+ let actor = null
358
+ before(async () => {
359
+ actor = await makeActor(username, remoteHost)
360
+ for (const botName of botNames) {
361
+ const botId = formatter.format({ username: botName })
362
+ addFollowing(username, botId, remoteHost)
363
+ await actorStorage.addToCollection(botName, 'followers', actor)
364
+ }
365
+ activity = await as2.import({
366
+ type: 'Activity',
367
+ actor: actor.id,
368
+ id: nockFormat({ username, type: 'activity', num: 1 }),
369
+ to: nockFormat({ username, collection: 'following' })
370
+ })
371
+ body = await activity.write()
372
+ digest = makeDigest(body)
373
+ signature = await nockSignature({
374
+ method: 'POST',
375
+ username,
376
+ url,
377
+ digest,
378
+ date
379
+ })
380
+ })
381
+ it('should work without an error', async () => {
382
+ response = await request(app)
383
+ .post(path)
384
+ .send(body)
385
+ .set('Signature', signature)
386
+ .set('Date', date)
387
+ .set('Host', host)
388
+ .set('Digest', digest)
389
+ .set('Content-Type', 'application/activity+json')
390
+ assert.ok(response)
391
+ await app.onIdle()
392
+ })
393
+ it('should return a 200 status', async () => {
394
+ assert.strictEqual(response.status, 200)
395
+ })
396
+ it('should appear in all inboxes', async () => {
397
+ for (const botName of botNames) {
398
+ assert.strictEqual(
399
+ true,
400
+ await actorStorage.isInCollection(
401
+ botName,
402
+ 'inbox',
403
+ activity
404
+ )
405
+ )
406
+ }
407
+ })
408
+ })
409
+
410
+ describe('can handle an activity to remote actor collection', async () => {
411
+ const username = 'actor7'
412
+ const botNames = ['test13', 'test14', 'test15']
413
+ const path = '/shared/inbox'
414
+ const url = `${origin}${path}`
415
+ const collection = 1
416
+ const date = new Date().toUTCString()
417
+ let response = null
418
+ let signature = null
419
+ let body = null
420
+ let digest = null
421
+ let activity = null
422
+ let actor = null
423
+ before(async () => {
424
+ actor = await makeActor(username, remoteHost)
425
+ for (const botName of botNames) {
426
+ const botId = formatter.format({ username: botName })
427
+ addToCollection(username, collection, botId, remoteHost)
428
+ }
429
+ activity = await as2.import({
430
+ type: 'Activity',
431
+ actor: actor.id,
432
+ id: nockFormat({ username, type: 'activity', num: 1 }),
433
+ to: nockFormat({ username, type: 'collection', num: collection })
434
+ })
435
+ body = await activity.write()
436
+ digest = makeDigest(body)
437
+ signature = await nockSignature({
438
+ method: 'POST',
439
+ username,
440
+ url,
441
+ digest,
442
+ date
443
+ })
444
+ })
445
+ it('should work without an error', async () => {
446
+ response = await request(app)
447
+ .post(path)
448
+ .send(body)
449
+ .set('Signature', signature)
450
+ .set('Date', date)
451
+ .set('Host', host)
452
+ .set('Digest', digest)
453
+ .set('Content-Type', 'application/activity+json')
454
+ assert.ok(response)
455
+ await app.onIdle()
456
+ })
457
+ it('should return a 200 status', async () => {
458
+ assert.strictEqual(response.status, 200)
459
+ })
460
+ it('should appear in all inboxes', async () => {
461
+ for (const botName of botNames) {
462
+ assert.strictEqual(
463
+ true,
464
+ await actorStorage.isInCollection(
465
+ botName,
466
+ 'inbox',
467
+ activity
468
+ )
469
+ )
470
+ }
471
+ })
472
+ })
473
+ })
@@ -38,4 +38,31 @@ describe('webfinger routes', async () => {
38
38
  assert.strictEqual(response.body.links[0].href, 'https://activitypubbot.test/user/ok')
39
39
  })
40
40
  })
41
+ describe('Webfinger discovery for non-existent user', async () => {
42
+ let response = null
43
+ it('should work without an error', async () => {
44
+ response = await request(app).get('/.well-known/webfinger?resource=acct%3Adne%40activitypubbot.test')
45
+ })
46
+ it('should return 404 Not Found', async () => {
47
+ assert.strictEqual(response.status, 404)
48
+ })
49
+ })
50
+ describe('Webfinger discovery for wrong domain', async () => {
51
+ let response = null
52
+ it('should work without an error', async () => {
53
+ response = await request(app).get('/.well-known/webfinger?resource=acct%3Adne%wrongdomain.test')
54
+ })
55
+ it('should return 400 Bad Request', async () => {
56
+ assert.strictEqual(response.status, 400)
57
+ })
58
+ })
59
+ describe('Webfinger discovery for HTTPS', async () => {
60
+ let response = null
61
+ it('should work without an error', async () => {
62
+ response = await request(app).get('/.well-known/webfinger?resource=' + encodeURIComponent('https://activitypubbot.test/user/ok'))
63
+ })
64
+ it('should return 400 Bad Request', async () => {
65
+ assert.strictEqual(response.status, 400)
66
+ })
67
+ })
41
68
  })