@evanp/activitypub-bot 0.9.0 → 0.11.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 (56) 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 +11 -16
  5. package/README.md +87 -13
  6. package/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/keystorage.js +7 -24
  19. package/lib/migrations/001-initial.js +107 -0
  20. package/lib/migrations/index.js +28 -0
  21. package/lib/objectcache.js +4 -1
  22. package/lib/objectstorage.js +0 -36
  23. package/lib/remotekeystorage.js +0 -24
  24. package/lib/routes/collection.js +6 -2
  25. package/lib/routes/inbox.js +7 -20
  26. package/lib/routes/sharedinbox.js +54 -0
  27. package/lib/routes/user.js +11 -5
  28. package/lib/routes/webfinger.js +11 -2
  29. package/lib/urlformatter.js +8 -0
  30. package/package.json +14 -7
  31. package/tests/activitydistributor.test.js +3 -3
  32. package/tests/activityhandler.test.js +96 -5
  33. package/tests/activitypubclient.test.js +115 -130
  34. package/tests/actorstorage.test.js +26 -4
  35. package/tests/authorizer.test.js +3 -8
  36. package/tests/botcontext.test.js +109 -63
  37. package/tests/botdatastorage.test.js +3 -2
  38. package/tests/botfactory.provincebotfactory.test.js +430 -0
  39. package/tests/fixtures/bots.js +13 -1
  40. package/tests/fixtures/eventloggingbot.js +57 -0
  41. package/tests/fixtures/provincebotfactory.js +53 -0
  42. package/tests/httpsignature.test.js +3 -4
  43. package/tests/httpsignatureauthenticator.test.js +3 -3
  44. package/tests/keystorage.test.js +37 -2
  45. package/tests/microsyntax.test.js +3 -2
  46. package/tests/objectstorage.test.js +4 -3
  47. package/tests/remotekeystorage.test.js +10 -8
  48. package/tests/routes.actor.test.js +7 -0
  49. package/tests/routes.collection.test.js +0 -1
  50. package/tests/routes.inbox.test.js +1 -0
  51. package/tests/routes.object.test.js +44 -38
  52. package/tests/routes.sharedinbox.test.js +473 -0
  53. package/tests/routes.webfinger.test.js +27 -0
  54. package/tests/utils/nock.js +250 -27
  55. package/.github/workflows/main-docker.yml +0 -45
  56. package/index.js +0 -23
@@ -15,12 +15,14 @@ import {
15
15
  postInbox,
16
16
  resetInbox,
17
17
  makeActor,
18
- makeObject
18
+ makeObject,
19
+ nockFormat
19
20
  } from './utils/nock.js'
20
21
  import Logger from 'pino'
21
22
  import as2 from '../lib/activitystreams.js'
22
23
  import { HTTPSignature } from '../lib/httpsignature.js'
23
24
  import { Digester } from '../lib/digester.js'
25
+ import { runMigrations } from '../lib/migrations/index.js'
24
26
 
25
27
  const AS2_NS = 'https://www.w3.org/ns/activitystreams#'
26
28
 
@@ -40,21 +42,19 @@ describe('BotContext', () => {
40
42
  let note = null
41
43
  let transformer = null
42
44
  let logger = null
45
+ const botName = 'test1'
43
46
  before(async () => {
44
47
  logger = Logger({
45
48
  level: 'silent'
46
49
  })
47
50
  formatter = new UrlFormatter('https://activitypubbot.example')
48
- connection = new Sequelize('sqlite::memory:', { logging: false })
51
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
49
52
  await connection.authenticate()
53
+ await runMigrations(connection)
50
54
  botDataStorage = new BotDataStorage(connection)
51
- await botDataStorage.initialize()
52
55
  objectStorage = new ObjectStorage(connection)
53
- await objectStorage.initialize()
54
56
  keyStorage = new KeyStorage(connection, logger)
55
- await keyStorage.initialize()
56
57
  actorStorage = new ActorStorage(connection, formatter)
57
- await actorStorage.initialize()
58
58
  const signer = new HTTPSignature(logger)
59
59
  const digester = new Digester(logger)
60
60
  client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
@@ -63,12 +63,12 @@ describe('BotContext', () => {
63
63
  await objectStorage.create(
64
64
  await as2.import({
65
65
  id: formatter.format({
66
- username: 'test1',
66
+ username: botName,
67
67
  type: 'object',
68
68
  nanoid: '_pEWsKke-7lACTdM3J_qd'
69
69
  }),
70
70
  type: 'Object',
71
- attributedTo: formatter.format({ username: 'test1' }),
71
+ attributedTo: formatter.format({ username: botName }),
72
72
  to: 'https://www.w3.org/ns/activitystreams#Public'
73
73
  })
74
74
  )
@@ -91,7 +91,7 @@ describe('BotContext', () => {
91
91
  })
92
92
  it('can initialize', async () => {
93
93
  context = new BotContext(
94
- 'test1',
94
+ botName,
95
95
  botDataStorage,
96
96
  objectStorage,
97
97
  actorStorage,
@@ -103,7 +103,7 @@ describe('BotContext', () => {
103
103
  )
104
104
  })
105
105
  it('can get the bot ID', () => {
106
- assert.strictEqual(context.botId, 'test1')
106
+ assert.strictEqual(context.botId, botName)
107
107
  })
108
108
  it('can set a value', async () => {
109
109
  await context.setData('key1', 'value1')
@@ -126,7 +126,7 @@ describe('BotContext', () => {
126
126
  })
127
127
  it('can get a local object', async () => {
128
128
  const id = formatter.format({
129
- username: 'test1',
129
+ username: botName,
130
130
  type: 'object',
131
131
  nanoid: '_pEWsKke-7lACTdM3J_qd'
132
132
  })
@@ -150,8 +150,8 @@ describe('BotContext', () => {
150
150
  })
151
151
  it('can send a note', async () => {
152
152
  const actor2 = await makeActor('test2')
153
- await actorStorage.addToCollection('test1', 'followers', actor2)
154
- let followers = await actorStorage.getCollection('test1', 'followers')
153
+ await actorStorage.addToCollection(botName, 'followers', actor2)
154
+ let followers = await actorStorage.getCollection(botName, 'followers')
155
155
  assert.strictEqual(followers.totalItems, 1)
156
156
  const content = 'Hello World'
157
157
  const to = 'https://www.w3.org/ns/activitystreams#Public'
@@ -169,11 +169,11 @@ describe('BotContext', () => {
169
169
  assert.strictEqual(typeof note.id, 'string')
170
170
  await context.onIdle()
171
171
  assert.strictEqual(postInbox.test2, 1)
172
- const outbox = await actorStorage.getCollection('test1', 'outbox')
172
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
173
173
  assert.strictEqual(outbox.totalItems, 1)
174
- const inbox = await actorStorage.getCollection('test1', 'inbox')
174
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
175
175
  assert.strictEqual(inbox.totalItems, 1)
176
- followers = await actorStorage.getCollection('test1', 'followers')
176
+ followers = await actorStorage.getCollection(botName, 'followers')
177
177
  assert.strictEqual(followers.totalItems, 1)
178
178
  })
179
179
  it('can like an object', async () => {
@@ -182,11 +182,11 @@ describe('BotContext', () => {
182
182
  await context.likeObject(obj)
183
183
  await context.onIdle()
184
184
  assert.strictEqual(postInbox.test2, 1)
185
- const outbox = await actorStorage.getCollection('test1', 'outbox')
185
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
186
186
  assert.strictEqual(outbox.totalItems, 2)
187
- const inbox = await actorStorage.getCollection('test1', 'inbox')
187
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
188
188
  assert.strictEqual(inbox.totalItems, 2)
189
- const liked = await actorStorage.getCollection('test1', 'liked')
189
+ const liked = await actorStorage.getCollection(botName, 'liked')
190
190
  assert.strictEqual(liked.totalItems, 1)
191
191
  })
192
192
  it('can unlike an object', async () => {
@@ -195,11 +195,11 @@ describe('BotContext', () => {
195
195
  await context.unlikeObject(obj)
196
196
  await context.onIdle()
197
197
  assert.strictEqual(postInbox.test2, 1)
198
- const outbox = await actorStorage.getCollection('test1', 'outbox')
198
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
199
199
  assert.strictEqual(outbox.totalItems, 3)
200
- const inbox = await actorStorage.getCollection('test1', 'inbox')
200
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
201
201
  assert.strictEqual(inbox.totalItems, 3)
202
- const liked = await actorStorage.getCollection('test1', 'liked')
202
+ const liked = await actorStorage.getCollection(botName, 'liked')
203
203
  assert.strictEqual(liked.totalItems, 0)
204
204
  })
205
205
  it('can follow an actor', async () => {
@@ -207,12 +207,12 @@ describe('BotContext', () => {
207
207
  await context.followActor(actor3)
208
208
  await context.onIdle()
209
209
  assert.strictEqual(postInbox.test3, 1)
210
- const outbox = await actorStorage.getCollection('test1', 'outbox')
210
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
211
211
  assert.strictEqual(outbox.totalItems, 4)
212
- const inbox = await actorStorage.getCollection('test1', 'inbox')
212
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
213
213
  assert.strictEqual(inbox.totalItems, 4)
214
214
  const pendingFollowing = await actorStorage.getCollection(
215
- 'test1',
215
+ botName,
216
216
  'pendingFollowing'
217
217
  )
218
218
  assert.strictEqual(pendingFollowing.totalItems, 1)
@@ -221,96 +221,96 @@ describe('BotContext', () => {
221
221
  await context.unfollowActor(actor3)
222
222
  await context.onIdle()
223
223
  assert.strictEqual(postInbox.test3, 1)
224
- const outbox = await actorStorage.getCollection('test1', 'outbox')
224
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
225
225
  assert.strictEqual(outbox.totalItems, 5)
226
- const inbox = await actorStorage.getCollection('test1', 'inbox')
226
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
227
227
  assert.strictEqual(inbox.totalItems, 5)
228
228
  const pendingFollowing = await actorStorage.getCollection(
229
- 'test1',
229
+ botName,
230
230
  'pendingFollowing'
231
231
  )
232
232
  assert.strictEqual(pendingFollowing.totalItems, 0)
233
233
  })
234
234
  it('can unfollow a followed actor', async () => {
235
235
  const actor4 = await makeActor('test4')
236
- await actorStorage.addToCollection('test1', 'following', actor4)
237
- let following = await actorStorage.getCollection('test1', 'following')
236
+ await actorStorage.addToCollection(botName, 'following', actor4)
237
+ let following = await actorStorage.getCollection(botName, 'following')
238
238
  assert.strictEqual(following.totalItems, 1)
239
239
  await context.unfollowActor(actor4)
240
240
  await context.onIdle()
241
241
  assert.strictEqual(postInbox.test4, 1)
242
- const outbox = await actorStorage.getCollection('test1', 'outbox')
242
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
243
243
  assert.strictEqual(outbox.totalItems, 6)
244
- const inbox = await actorStorage.getCollection('test1', 'inbox')
244
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
245
245
  assert.strictEqual(inbox.totalItems, 6)
246
- following = await actorStorage.getCollection('test1', 'following')
246
+ following = await actorStorage.getCollection(botName, 'following')
247
247
  assert.strictEqual(following.totalItems, 0)
248
248
  })
249
249
  it('can block an actor without a relationship', async () => {
250
- let followers = await actorStorage.getCollection('test1', 'followers')
250
+ let followers = await actorStorage.getCollection(botName, 'followers')
251
251
  assert.strictEqual(followers.totalItems, 1)
252
252
  actor5 = await makeActor('test5')
253
253
  await context.blockActor(actor5)
254
254
  await context.onIdle()
255
255
  assert.ok(!postInbox.test5)
256
- const outbox = await actorStorage.getCollection('test1', 'outbox')
256
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
257
257
  assert.strictEqual(outbox.totalItems, 7)
258
- const inbox = await actorStorage.getCollection('test1', 'inbox')
258
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
259
259
  assert.strictEqual(inbox.totalItems, 7)
260
- const blocked = await actorStorage.getCollection('test1', 'blocked')
260
+ const blocked = await actorStorage.getCollection(botName, 'blocked')
261
261
  assert.strictEqual(blocked.totalItems, 1)
262
- followers = await actorStorage.getCollection('test1', 'followers')
262
+ followers = await actorStorage.getCollection(botName, 'followers')
263
263
  assert.strictEqual(followers.totalItems, 1)
264
264
  })
265
265
  it('can unblock an actor without a relationship', async () => {
266
- let followers = await actorStorage.getCollection('test1', 'followers')
266
+ let followers = await actorStorage.getCollection(botName, 'followers')
267
267
  assert.strictEqual(followers.totalItems, 1)
268
268
  await context.unblockActor(actor5)
269
269
  await context.onIdle()
270
270
  assert.ok(!postInbox.test5)
271
- const outbox = await actorStorage.getCollection('test1', 'outbox')
271
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
272
272
  assert.strictEqual(outbox.totalItems, 8)
273
- const inbox = await actorStorage.getCollection('test1', 'inbox')
273
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
274
274
  assert.strictEqual(inbox.totalItems, 8)
275
- const blocked = await actorStorage.getCollection('test1', 'blocked')
275
+ const blocked = await actorStorage.getCollection(botName, 'blocked')
276
276
  assert.strictEqual(blocked.totalItems, 0)
277
- followers = await actorStorage.getCollection('test1', 'followers')
277
+ followers = await actorStorage.getCollection(botName, 'followers')
278
278
  assert.strictEqual(followers.totalItems, 1)
279
279
  })
280
280
  it('can block an actor with a relationship', async () => {
281
281
  actor6 = await makeActor('test6')
282
- let followers = await actorStorage.getCollection('test1', 'followers')
282
+ let followers = await actorStorage.getCollection(botName, 'followers')
283
283
  assert.strictEqual(followers.totalItems, 1)
284
- await actorStorage.addToCollection('test1', 'following', actor6)
285
- await actorStorage.addToCollection('test1', 'followers', actor6)
286
- followers = await actorStorage.getCollection('test1', 'followers')
284
+ await actorStorage.addToCollection(botName, 'following', actor6)
285
+ await actorStorage.addToCollection(botName, 'followers', actor6)
286
+ followers = await actorStorage.getCollection(botName, 'followers')
287
287
  assert.strictEqual(followers.totalItems, 2)
288
288
  await context.blockActor(actor6)
289
289
  await context.onIdle()
290
290
  assert.ok(!postInbox.test6)
291
- const outbox = await actorStorage.getCollection('test1', 'outbox')
291
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
292
292
  assert.strictEqual(outbox.totalItems, 9)
293
- const inbox = await actorStorage.getCollection('test1', 'inbox')
293
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
294
294
  assert.strictEqual(inbox.totalItems, 9)
295
- const blocked = await actorStorage.getCollection('test1', 'blocked')
295
+ const blocked = await actorStorage.getCollection(botName, 'blocked')
296
296
  assert.strictEqual(blocked.totalItems, 1)
297
- const following = await actorStorage.getCollection('test1', 'following')
297
+ const following = await actorStorage.getCollection(botName, 'following')
298
298
  assert.strictEqual(following.totalItems, 0)
299
- followers = await actorStorage.getCollection('test1', 'followers')
299
+ followers = await actorStorage.getCollection(botName, 'followers')
300
300
  assert.strictEqual(followers.totalItems, 1)
301
301
  })
302
302
  it('can unblock an actor with a former relationship', async () => {
303
303
  await context.unblockActor(actor6)
304
304
  assert.ok(!postInbox.test6)
305
- const outbox = await actorStorage.getCollection('test1', 'outbox')
305
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
306
306
  assert.strictEqual(outbox.totalItems, 10)
307
- const inbox = await actorStorage.getCollection('test1', 'inbox')
307
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
308
308
  assert.strictEqual(inbox.totalItems, 10)
309
- const blocked = await actorStorage.getCollection('test1', 'blocked')
309
+ const blocked = await actorStorage.getCollection(botName, 'blocked')
310
310
  assert.strictEqual(blocked.totalItems, 0)
311
- const following = await actorStorage.getCollection('test1', 'following')
311
+ const following = await actorStorage.getCollection(botName, 'following')
312
312
  assert.strictEqual(following.totalItems, 0)
313
- const followers = await actorStorage.getCollection('test1', 'followers')
313
+ const followers = await actorStorage.getCollection(botName, 'followers')
314
314
  assert.strictEqual(followers.totalItems, 1)
315
315
  })
316
316
  it('can update a note', async () => {
@@ -318,9 +318,9 @@ describe('BotContext', () => {
318
318
  await context.updateNote(note, content)
319
319
  await context.onIdle()
320
320
  assert.strictEqual(postInbox.test2, 1)
321
- const outbox = await actorStorage.getCollection('test1', 'outbox')
321
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
322
322
  assert.strictEqual(outbox.totalItems, 11)
323
- const inbox = await actorStorage.getCollection('test1', 'inbox')
323
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
324
324
  assert.strictEqual(inbox.totalItems, 11)
325
325
  const copy = await context.getObject(note.id)
326
326
  assert.strictEqual(copy.content.get(), content)
@@ -329,9 +329,9 @@ describe('BotContext', () => {
329
329
  await context.deleteNote(note)
330
330
  await context.onIdle()
331
331
  assert.strictEqual(postInbox.test2, 1)
332
- const outbox = await actorStorage.getCollection('test1', 'outbox')
332
+ const outbox = await actorStorage.getCollection(botName, 'outbox')
333
333
  assert.strictEqual(outbox.totalItems, 12)
334
- const inbox = await actorStorage.getCollection('test1', 'inbox')
334
+ const inbox = await actorStorage.getCollection(botName, 'inbox')
335
335
  assert.strictEqual(inbox.totalItems, 12)
336
336
  const copy = await context.getObject(note.id)
337
337
  assert.ok(copy)
@@ -475,7 +475,7 @@ describe('BotContext', () => {
475
475
  await context.onIdle()
476
476
  assert.strictEqual(postInbox.test2, 2)
477
477
  let found = false
478
- for await (const item of actorStorage.items('test1', 'inbox')) {
478
+ for await (const item of actorStorage.items(botName, 'inbox')) {
479
479
  const full = await objectStorage.read(item.id)
480
480
  if (full.object?.first?.id === reply.id) {
481
481
  found = true
@@ -500,7 +500,7 @@ describe('BotContext', () => {
500
500
  }
501
501
  }
502
502
  assert.ok(found)
503
- for await (const item of actorStorage.items('test1', 'inbox')) {
503
+ for await (const item of actorStorage.items(botName, 'inbox')) {
504
504
  const full = await objectStorage.read(item.id)
505
505
  if (full.object?.first?.inReplyTo?.first?.id === note.id) {
506
506
  found = full
@@ -671,4 +671,50 @@ describe('BotContext', () => {
671
671
  assert.strictEqual(context.id, conversationIn.id)
672
672
  })
673
673
  })
674
+
675
+ it('can duplicate', async () => {
676
+ const username = 'dupe1'
677
+ let dupe = null
678
+ dupe = await context.duplicate(username)
679
+ assert.ok(dupe)
680
+ assert.strictEqual(dupe.botId, username)
681
+ })
682
+
683
+ it('can announce an object', async () => {
684
+ const username = 'test9'
685
+ const type = 'Note'
686
+ const num = 3035
687
+
688
+ const id = nockFormat({ username, type, num })
689
+
690
+ const obj = await context.getObject(id)
691
+ const activity = await context.announceObject(obj)
692
+
693
+ assert.ok(activity)
694
+
695
+ assert.strictEqual(activity.type, `${AS2_NS}Announce`)
696
+ assert.strictEqual(activity.object?.first?.id, obj.id)
697
+
698
+ await context.onIdle()
699
+
700
+ assert.strictEqual(postInbox[username], 1)
701
+
702
+ let foundInOutbox = false
703
+ for await (const item of actorStorage.items(botName, 'outbox')) {
704
+ if (item.id === activity.id) {
705
+ foundInOutbox = true
706
+ break
707
+ }
708
+ }
709
+ assert.ok(foundInOutbox)
710
+
711
+ let foundInInbox = false
712
+ for await (const item of actorStorage.items(botName, 'inbox')) {
713
+ if (item.id === activity.id) {
714
+ foundInInbox = true
715
+ break
716
+ }
717
+ }
718
+ assert.ok(foundInInbox)
719
+ })
674
720
  })
@@ -2,20 +2,21 @@ import { describe, before, after, it } from 'node:test'
2
2
  import { BotDataStorage, NoSuchValueError } from '../lib/botdatastorage.js'
3
3
  import assert from 'node:assert'
4
4
  import { Sequelize } from 'sequelize'
5
+ import { runMigrations } from '../lib/migrations/index.js'
5
6
 
6
7
  describe('BotDataStorage', async () => {
7
8
  let connection = null
8
9
  let storage = null
9
10
  before(async () => {
10
- connection = new Sequelize('sqlite::memory:', { logging: false })
11
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
11
12
  await connection.authenticate()
13
+ await runMigrations(connection)
12
14
  })
13
15
  after(async () => {
14
16
  await connection.close()
15
17
  })
16
18
  it('can initialize', async () => {
17
19
  storage = new BotDataStorage(connection)
18
- await storage.initialize()
19
20
  })
20
21
  it('can set a value', async () => {
21
22
  await storage.set('test', 'key1', 'value1')