@evanp/activitypub-bot 0.8.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 +262 -12
  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 +18 -11
  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
@@ -17,6 +17,9 @@ import bots from './fixtures/bots.js'
17
17
  import { nockSetup, postInbox, makeActor, nockFormat } from './utils/nock.js'
18
18
  import { Digester } from '../lib/digester.js'
19
19
  import { HTTPSignature } from '../lib/httpsignature.js'
20
+ import { runMigrations } from '../lib/migrations/index.js'
21
+ import { BotContext } from '../lib/botcontext.js'
22
+ import { Transformer } from '../lib/microsyntax.js'
20
23
 
21
24
  describe('ActivityHandler', () => {
22
25
  const domain = 'activitypubbot.example'
@@ -36,27 +39,46 @@ describe('ActivityHandler', () => {
36
39
  let botId = null
37
40
  const botName = 'ok'
38
41
  let bot = null
42
+ const loggerBotName = 'logging'
43
+ let lb = null
44
+ let lbId = null
45
+ let transformer = null
39
46
  before(async () => {
40
47
  logger = Logger({ level: 'silent' })
41
48
  formatter = new UrlFormatter(origin)
42
- connection = new Sequelize('sqlite::memory:', { logging: false })
49
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
43
50
  await connection.authenticate()
51
+ await runMigrations(connection)
44
52
  botDataStorage = new BotDataStorage(connection)
45
- await botDataStorage.initialize()
46
53
  objectStorage = new ObjectStorage(connection)
47
- await objectStorage.initialize()
48
54
  keyStorage = new KeyStorage(connection, logger)
49
- await keyStorage.initialize()
50
55
  actorStorage = new ActorStorage(connection, formatter)
51
- await actorStorage.initialize()
52
56
  const signer = new HTTPSignature(logger)
53
57
  const digester = new Digester(logger)
54
58
  client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
55
59
  distributor = new ActivityDistributor(client, formatter, actorStorage, logger)
56
60
  authz = new Authorizer(actorStorage, formatter, client)
57
61
  cache = new ObjectCache({ longTTL: 3600 * 1000, shortTTL: 300 * 1000, maxItems: 1000 })
62
+ transformer = new Transformer(`${origin}/tag/`, client)
63
+ await Promise.all(
64
+ Object.values(bots).map(bot => bot.initialize(
65
+ new BotContext(
66
+ bot.username,
67
+ botDataStorage,
68
+ objectStorage,
69
+ actorStorage,
70
+ client,
71
+ distributor,
72
+ formatter,
73
+ transformer,
74
+ logger
75
+ )
76
+ ))
77
+ )
58
78
  botId = formatter.format({ username: botName })
79
+ lbId = formatter.format({ username: loggerBotName })
59
80
  bot = bots[botName]
81
+ lb = bots[loggerBotName]
60
82
  await objectStorage.create(await as2.import({
61
83
  id: formatter.format({ username: 'test1', type: 'object', nanoid: '_pEWsKke-7lACTdM3J_qd' }),
62
84
  type: 'Object',
@@ -299,6 +321,19 @@ describe('ActivityHandler', () => {
299
321
  await handler.onIdle()
300
322
  assert.ok(!postInbox.follower3)
301
323
  })
324
+ it('notifies the bot of a follow activity', async () => {
325
+ const actor = await makeActor('follower4')
326
+ const activity = await as2.import({
327
+ type: 'Follow',
328
+ id: 'https://social.example/user/follower4/follow/1',
329
+ actor: actor.id,
330
+ object: lbId,
331
+ to: lbId
332
+ })
333
+ await handler.handleActivity(lb, activity)
334
+ assert.ok(lb.follows.has(activity.id))
335
+ await handler.onIdle()
336
+ })
302
337
  it('can handle an accept activity', async () => {
303
338
  const actor = await makeActor('accepter1')
304
339
  const followActivity = await as2.import({
@@ -811,6 +846,34 @@ describe('ActivityHandler', () => {
811
846
  await objectStorage.isInCollection(note.id, 'likes', activity2)
812
847
  )
813
848
  })
849
+ it('notifies the bot of a like activity', async () => {
850
+ const actor = await makeActor('liker9')
851
+ const note = await as2.import({
852
+ attributedTo: lbId,
853
+ id: formatter.format({
854
+ username: loggerBotName,
855
+ type: 'note',
856
+ nanoid: 'IGeAucyHD-s3Ywg9X9sCo'
857
+ }),
858
+ type: 'Note',
859
+ content: 'Hello, world!',
860
+ to: 'as:Public'
861
+ })
862
+ await objectStorage.create(note)
863
+ const activity = await as2.import({
864
+ type: 'Like',
865
+ actor: actor.id,
866
+ id: nockFormat({
867
+ username: 'liker9',
868
+ type: 'Like',
869
+ nanoid: '3fKK6LcMtqrAp1Ekn471u'
870
+ }),
871
+ object: note.id,
872
+ to: [lbId, 'as:Public']
873
+ })
874
+ await handler.handleActivity(lb, activity)
875
+ assert.ok(lb.likes.has(activity.id))
876
+ })
814
877
  it('can handle an announce activity', async () => {
815
878
  const actor = await makeActor('announcer1')
816
879
  const note = await as2.import({
@@ -1017,6 +1080,34 @@ describe('ActivityHandler', () => {
1017
1080
  await objectStorage.isInCollection(note.id, 'shares', activity2)
1018
1081
  )
1019
1082
  })
1083
+ it('notifies the bot on an announce activity', async () => {
1084
+ const actor = await makeActor('announcer9')
1085
+ const note = await as2.import({
1086
+ attributedTo: lbId,
1087
+ id: formatter.format({
1088
+ username: loggerBotName,
1089
+ type: 'note',
1090
+ nanoid: 'LNCVgovrjpA6oSKnGDax2'
1091
+ }),
1092
+ type: 'Note',
1093
+ content: 'Hello, world!',
1094
+ to: 'as:Public'
1095
+ })
1096
+ await objectStorage.create(note)
1097
+ const activity = await as2.import({
1098
+ type: 'Announce',
1099
+ actor: actor.id,
1100
+ id: nockFormat({
1101
+ username: 'announcer9',
1102
+ type: 'Announce',
1103
+ nanoid: 'LmVvlEBHNf2X6nfgzMe6F'
1104
+ }),
1105
+ object: note.id,
1106
+ to: [lbId, 'as:Public']
1107
+ })
1108
+ await handler.handleActivity(lb, activity)
1109
+ assert.ok(lb.shares.has(activity.id))
1110
+ })
1020
1111
  it('can handle a block activity', async () => {
1021
1112
  const actor = await makeActor('blocker1')
1022
1113
  await actorStorage.addToCollection(botName, 'followers', actor)
@@ -4,128 +4,62 @@ import { UrlFormatter } from '../lib/urlformatter.js'
4
4
  import { ActivityPubClient } from '../lib/activitypubclient.js'
5
5
  import assert from 'node:assert'
6
6
  import { Sequelize } from 'sequelize'
7
- import nock from 'nock'
8
7
  import as2 from '../lib/activitystreams.js'
9
8
  import Logger from 'pino'
10
9
  import { HTTPSignature } from '../lib/httpsignature.js'
11
10
  import { Digester } from '../lib/digester.js'
12
-
13
- const makeActor = (username) =>
14
- as2.import({
15
- id: `https://social.example/user/${username}`,
16
- type: 'Person',
17
- preferredUsername: username,
18
- inbox: `https://social.example/user/${username}/inbox`,
19
- outbox: `https://social.example/user/${username}/outbox`,
20
- followers: `https://social.example/user/${username}/followers`,
21
- following: `https://social.example/user/${username}/following`,
22
- liked: `https://social.example/user/${username}/liked`,
23
- publicKey: {
24
- id: `https://social.example/user/${username}/publickey`,
25
- owner: `https://social.example/user/${username}`,
26
- type: 'CryptographicKey',
27
- publicKeyPem: '-----BEGIN PUBLIC KEY-----\nFAKEFAKEFAKE\n-----END PUBLIC KEY-----'
28
- }
29
- })
30
-
31
- const makeKey = (username) =>
32
- as2.import({
33
- id: `https://social.example/user/${username}/publickey`,
34
- owner: `https://social.example/user/${username}`,
35
- type: 'CryptographicKey',
36
- publicKeyPem: '-----BEGIN PUBLIC KEY-----\nFAKEFAKEFAKE\n-----END PUBLIC KEY-----'
37
- })
38
-
39
- const makeNote = (username, num) =>
40
- as2.import({
41
- id: `https://social.example/user/${username}/note/${num}`,
42
- type: 'Object',
43
- attributedTo: `https://social.example/user/${username}`,
44
- to: 'https://www.w3.org/ns/activitystreams#Public',
45
- content: `This is note ${num} by ${username}.`
46
- })
11
+ import { runMigrations } from '../lib/migrations/index.js'
12
+ import {
13
+ nockSetup,
14
+ getRequestHeaders,
15
+ resetRequestHeaders,
16
+ addToCollection,
17
+ nockFormat
18
+ } from './utils/nock.js'
47
19
 
48
20
  describe('ActivityPubClient', async () => {
49
21
  let connection = null
50
22
  let keyStorage = null
51
23
  let formatter = null
52
24
  let client = null
53
- let postInbox = null
54
- let signature = null
55
- let digest = null
56
- let date = null
57
25
  let signer = null
58
26
  let digester = null
59
27
  let logger = null
28
+ const remoteUser = 'remote1'
29
+ const remoteCollection = 1
30
+ const remoteOrderedCollection = 2
31
+ const remotePagedCollection = 3
32
+ const remotePagedOrderedCollection = 4
33
+ const maxItems = 10
60
34
  before(async () => {
61
35
  logger = new Logger({
62
36
  level: 'silent'
63
37
  })
64
38
  digester = new Digester(logger)
65
39
  signer = new HTTPSignature(logger)
66
- connection = new Sequelize('sqlite::memory:', { logging: false })
40
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
67
41
  await connection.authenticate()
42
+ await runMigrations(connection)
68
43
  keyStorage = new KeyStorage(connection, logger)
69
- await keyStorage.initialize()
70
44
  formatter = new UrlFormatter('https://activitypubbot.example')
71
- const remote = 'https://social.example'
72
- nock(remote)
73
- .get(/\/user\/(\w+)$/)
74
- .reply(async function (uri, requestBody) {
75
- const headers = this.req.headers
76
- signature[remote + uri] = headers.signature
77
- digest[remote + uri] = headers.digest
78
- date[remote + uri] = headers.date
79
- const username = uri.match(/\/user\/(\w+)$/)[1]
80
- const actor = await makeActor(username)
81
- const actorText = await actor.write()
82
- return [200, actorText, { 'Content-Type': 'application/activity+json' }]
83
- })
84
- .persist()
85
- .post(/\/user\/(\w+)\/inbox$/)
86
- .reply(async function (uri, requestBody) {
87
- const headers = this.req.headers
88
- signature[remote + uri] = headers.signature
89
- digest[remote + uri] = headers.digest
90
- date[remote + uri] = headers.date
91
- const username = uri.match(/\/user\/(\w+)\/inbox$/)[1]
92
- if (username in postInbox) {
93
- postInbox[username] += 1
94
- } else {
95
- postInbox[username] = 1
96
- }
97
- return [202, 'accepted']
98
- })
99
- .persist()
100
- .get(/\/user\/(\w+)\/note\/(\d+)$/)
101
- .reply(async function (uri, requestBody) {
102
- const headers = this.req.headers
103
- signature[remote + uri] = headers.signature
104
- digest[remote + uri] = headers.digest
105
- date[remote + uri] = headers.date
106
- const match = uri.match(/\/user\/(\w+)\/note\/(\d+)$/)
107
- const username = match[1]
108
- const num = match[2]
109
- const obj = await makeNote(username, num)
110
- const objText = await obj.write()
111
- return [200, objText, { 'Content-Type': 'application/activity+json' }]
112
- })
113
- .get(/\/user\/(\w+)\/inbox$/)
114
- .reply(async function (uri, requestBody) {
115
- return [403, 'Forbidden', { 'Content-Type': 'text/plain' }]
116
- })
117
- .get(/\/user\/(\w+)\/publickey$/)
118
- .reply(async function (uri, requestBody) {
119
- const headers = this.req.headers
120
- signature[remote + uri] = headers.signature
121
- digest[remote + uri] = headers.digest
122
- date[remote + uri] = headers.date
123
- const username = uri.match(/\/user\/(\w+)\/publickey$/)[1]
124
- const key = await makeKey(username)
125
- const keyText = await key.write()
126
- return [200, keyText, { 'Content-Type': 'application/activity+json' }]
127
- })
128
- .persist()
45
+ const remote = 'social.example'
46
+ nockSetup(remote, logger)
47
+ for (let i = 0; i < maxItems; i++) {
48
+ const id = nockFormat({ username: remoteUser, type: 'note', num: i })
49
+ addToCollection(remoteUser, remoteCollection, id, remote)
50
+ }
51
+ for (let i = maxItems; i < 2 * maxItems; i++) {
52
+ const id = nockFormat({ username: remoteUser, type: 'note', num: i })
53
+ addToCollection(remoteUser, remoteOrderedCollection, id, remote)
54
+ }
55
+ for (let i = 2 * maxItems; i < 7 * maxItems; i++) {
56
+ const id = nockFormat({ username: remoteUser, type: 'note', num: i })
57
+ addToCollection(remoteUser, remotePagedCollection, id, remote)
58
+ }
59
+ for (let i = 7 * maxItems; i < 12 * maxItems; i++) {
60
+ const id = nockFormat({ username: remoteUser, type: 'note', num: i })
61
+ addToCollection(remoteUser, remotePagedOrderedCollection, id, remote)
62
+ }
129
63
  })
130
64
  after(async () => {
131
65
  await connection.close()
@@ -133,17 +67,12 @@ describe('ActivityPubClient', async () => {
133
67
  connection = null
134
68
  formatter = null
135
69
  client = null
136
- postInbox = null
137
- signature = null
138
70
  logger = null
139
71
  digester = null
140
72
  signer = null
141
73
  })
142
74
  beforeEach(async () => {
143
- signature = {}
144
- digest = {}
145
- postInbox = {}
146
- date = {}
75
+ resetRequestHeaders()
147
76
  })
148
77
  it('can initialize', () => {
149
78
  client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
@@ -155,13 +84,14 @@ describe('ActivityPubClient', async () => {
155
84
  assert.ok(obj)
156
85
  assert.equal(typeof obj, 'object')
157
86
  assert.equal(obj.id, id)
158
- assert.ok(signature[id])
159
- assert.match(signature[id], /^keyId="https:\/\/activitypubbot\.example\/user\/foobot\/publickey",headers="\(request-target\) host date user-agent accept",signature=".*",algorithm="rsa-sha256"$/)
160
- assert.equal(typeof digest[id], 'undefined')
161
- assert.equal(typeof date[id], 'string')
162
- assert.match(date[id], /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
87
+ const h = getRequestHeaders(id)
88
+ assert.ok(h.signature)
89
+ assert.match(h.signature, /^keyId="https:\/\/activitypubbot\.example\/user\/foobot\/publickey",headers="\(request-target\) host date user-agent accept",signature=".*",algorithm="rsa-sha256"$/)
90
+ assert.equal(typeof h.digest, 'undefined')
91
+ assert.equal(typeof h.date, 'string')
92
+ assert.match(h.date, /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
163
93
  assert.doesNotThrow(() => {
164
- Date.parse(date[id])
94
+ Date.parse(h.date)
165
95
  })
166
96
  })
167
97
  it('can get a remote object without a username', async () => {
@@ -170,13 +100,14 @@ describe('ActivityPubClient', async () => {
170
100
  assert.ok(obj)
171
101
  assert.equal(typeof obj, 'object')
172
102
  assert.equal(obj.id, id)
173
- assert.ok(signature[id])
174
- assert.match(signature[id], /^keyId="https:\/\/activitypubbot\.example\/publickey",headers="\(request-target\) host date user-agent accept",signature=".*",algorithm="rsa-sha256"$/)
175
- assert.equal(typeof digest[id], 'undefined')
176
- assert.equal(typeof date[id], 'string')
177
- assert.match(date[id], /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
103
+ const h = getRequestHeaders(id)
104
+ assert.ok(h.signature)
105
+ assert.match(h.signature, /^keyId="https:\/\/activitypubbot\.example\/publickey",headers="\(request-target\) host date user-agent accept",signature=".*",algorithm="rsa-sha256"$/)
106
+ assert.equal(typeof h.digest, 'undefined')
107
+ assert.equal(typeof h.date, 'string')
108
+ assert.match(h.date, /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
178
109
  assert.doesNotThrow(() => {
179
- Date.parse(date[id])
110
+ Date.parse(h.date)
180
111
  })
181
112
  })
182
113
  it('can get a remote key without a signature', async () => {
@@ -185,12 +116,13 @@ describe('ActivityPubClient', async () => {
185
116
  assert.ok(obj)
186
117
  assert.equal(typeof obj, 'object')
187
118
  assert.equal(obj.id, id)
188
- assert.equal(signature[id], undefined)
189
- assert.equal(typeof digest[id], 'undefined')
190
- assert.equal(typeof date[id], 'string')
191
- assert.match(date[id], /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
119
+ const h = getRequestHeaders(id)
120
+ assert.equal(h.signature, undefined)
121
+ assert.equal(typeof h.digest, 'undefined')
122
+ assert.equal(typeof h.date, 'string')
123
+ assert.match(h.date, /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
192
124
  assert.doesNotThrow(() => {
193
- Date.parse(date[id])
125
+ Date.parse(h.date)
194
126
  })
195
127
  })
196
128
  it('can deliver an activity', async () => {
@@ -202,14 +134,15 @@ describe('ActivityPubClient', async () => {
202
134
  .get()
203
135
  const inbox = 'https://social.example/user/evan/inbox'
204
136
  await client.post(inbox, obj, 'foobot')
205
- assert.ok(signature[inbox])
206
- assert.ok(digest[inbox])
207
- assert.match(signature[inbox], /^keyId="https:\/\/activitypubbot\.example\/user\/foobot\/publickey",headers="\(request-target\) host date user-agent content-type digest",signature=".*",algorithm="rsa-sha256"$/)
208
- assert.match(digest[inbox], /^sha-256=[0-9a-zA-Z=+/]*$/)
209
- assert.equal(typeof date[inbox], 'string')
210
- assert.match(date[inbox], /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
137
+ const h = getRequestHeaders(inbox)
138
+ assert.ok(h.signature)
139
+ assert.ok(h.digest)
140
+ assert.match(h.signature, /^keyId="https:\/\/activitypubbot\.example\/user\/foobot\/publickey",headers="\(request-target\) host date user-agent content-type digest",signature=".*",algorithm="rsa-sha256"$/)
141
+ assert.match(h.digest, /^sha-256=[0-9a-zA-Z=+/]*$/)
142
+ assert.equal(typeof h.date, 'string')
143
+ assert.match(h.date, /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/)
211
144
  assert.doesNotThrow(() => {
212
- Date.parse(date[inbox])
145
+ Date.parse(h.date)
213
146
  })
214
147
  })
215
148
  it('throws an error on a non-2xx response', async () => {
@@ -222,4 +155,56 @@ describe('ActivityPubClient', async () => {
222
155
  assert.equal(error.status, 403)
223
156
  }
224
157
  })
158
+ it('can iterate over a Collection', async () => {
159
+ const collectionUri = nockFormat({
160
+ username: remoteUser,
161
+ type: 'Collection',
162
+ num: remoteCollection
163
+ })
164
+ let counter = 0
165
+ for await (const item of client.items(collectionUri)) {
166
+ assert.ok(item)
167
+ counter = counter + 1
168
+ }
169
+ assert.strictEqual(counter, maxItems)
170
+ })
171
+ it('can iterate over an OrderedCollection', async () => {
172
+ const collectionUri = nockFormat({
173
+ username: remoteUser,
174
+ type: 'OrderedCollection',
175
+ num: remoteOrderedCollection
176
+ })
177
+ let counter = 0
178
+ for await (const item of client.items(collectionUri)) {
179
+ assert.ok(item)
180
+ counter = counter + 1
181
+ }
182
+ assert.strictEqual(counter, maxItems)
183
+ })
184
+ it('can iterate over a paged Collection', async () => {
185
+ const collectionUri = nockFormat({
186
+ username: remoteUser,
187
+ type: 'PagedCollection', // Fake type
188
+ num: remotePagedCollection
189
+ })
190
+ let counter = 0
191
+ for await (const item of client.items(collectionUri)) {
192
+ assert.ok(item)
193
+ counter = counter + 1
194
+ }
195
+ assert.strictEqual(counter, 5 * maxItems)
196
+ })
197
+ it('can iterate over a paged OrderedCollection', async () => {
198
+ const collectionUri = nockFormat({
199
+ username: remoteUser,
200
+ type: 'PagedOrderedCollection', // Fake type
201
+ num: remotePagedOrderedCollection
202
+ })
203
+ let counter = 0
204
+ for await (const item of client.items(collectionUri)) {
205
+ assert.ok(item)
206
+ counter = counter + 1
207
+ }
208
+ assert.strictEqual(counter, 5 * maxItems)
209
+ })
225
210
  })
@@ -4,6 +4,7 @@ import { ActorStorage } from '../lib/actorstorage.js'
4
4
  import { Sequelize } from 'sequelize'
5
5
  import { UrlFormatter } from '../lib/urlformatter.js'
6
6
  import as2 from '../lib/activitystreams.js'
7
+ import { runMigrations } from '../lib/migrations/index.js'
7
8
 
8
9
  const AS2_NS = 'https://www.w3.org/ns/activitystreams#'
9
10
 
@@ -12,14 +13,20 @@ describe('ActorStorage', () => {
12
13
  let storage = null
13
14
  let formatter = null
14
15
  let other = null
16
+ let unfollowed = null
15
17
  before(async () => {
16
- connection = new Sequelize('sqlite::memory:', { logging: false })
18
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
17
19
  await connection.authenticate()
20
+ await runMigrations(connection)
18
21
  formatter = new UrlFormatter('https://activitypubbot.example')
19
22
  other = await as2.import({
20
23
  id: 'https://social.example/user/test2',
21
24
  type: 'Person'
22
25
  })
26
+ unfollowed = await as2.import({
27
+ id: 'https://social.example/user/test3',
28
+ type: 'Person'
29
+ })
23
30
  })
24
31
  after(async () => {
25
32
  await connection.close()
@@ -31,7 +38,6 @@ describe('ActorStorage', () => {
31
38
  assert.ok(storage instanceof ActorStorage)
32
39
  })
33
40
  it('can initialize the storage', async () => {
34
- await storage.initialize()
35
41
  })
36
42
  it('can get an actor', async () => {
37
43
  const actor = await storage.getActor('test')
@@ -252,10 +258,26 @@ describe('ActorStorage', () => {
252
258
  assert.strictEqual(actor.get('preferredUsername').first, 'test8')
253
259
  assert.strictEqual(actor.name.get(), 'Test User')
254
260
  assert.strictEqual(actor.summary.get(), 'A test user')
255
- console.log(actor.type)
256
- console.log(await actor.write())
257
261
  assert.ok(Array.isArray(actor.type))
258
262
  assert.ok(actor.type.includes(AS2_NS + 'Person'))
259
263
  assert.ok(actor.type.includes(AS2_NS + 'Service'))
260
264
  })
265
+
266
+ it('can get all actors with an object in a collection', async () => {
267
+ for (const i of [101, 102, 103, 104, 105]) {
268
+ await storage.addToCollection(`test${i}`, 'following', other)
269
+ }
270
+ const usernames = await storage.getUsernamesWith('following', other)
271
+ assert.strictEqual(usernames.length, 5)
272
+ assert.ok(usernames.includes('test101'))
273
+ assert.ok(usernames.includes('test102'))
274
+ assert.ok(usernames.includes('test103'))
275
+ assert.ok(usernames.includes('test104'))
276
+ assert.ok(usernames.includes('test105'))
277
+ })
278
+
279
+ it('gets zero usernames when an object is in no collection', async () => {
280
+ const usernames = await storage.getUsernamesWith('following', unfollowed)
281
+ assert.strictEqual(usernames.length, 0)
282
+ })
261
283
  })
@@ -3,7 +3,6 @@ import { Authorizer } from '../lib/authorizer.js'
3
3
  import { ActorStorage } from '../lib/actorstorage.js'
4
4
  import { Sequelize } from 'sequelize'
5
5
  import { UrlFormatter } from '../lib/urlformatter.js'
6
- import { ObjectStorage } from '../lib/objectstorage.js'
7
6
  import { KeyStorage } from '../lib/keystorage.js'
8
7
  import { ActivityPubClient } from '../lib/activitypubclient.js'
9
8
  import as2 from '../lib/activitystreams.js'
@@ -12,13 +11,13 @@ import { nanoid } from 'nanoid'
12
11
  import { HTTPSignature } from '../lib/httpsignature.js'
13
12
  import Logger from 'pino'
14
13
  import { Digester } from '../lib/digester.js'
14
+ import { runMigrations } from '../lib/migrations/index.js'
15
15
 
16
16
  describe('Authorizer', () => {
17
17
  let authorizer = null
18
18
  let actorStorage = null
19
19
  let formatter = null
20
20
  let connection = null
21
- let objectStorage = null
22
21
  let keyStorage = null
23
22
  let client = null
24
23
 
@@ -39,14 +38,11 @@ describe('Authorizer', () => {
39
38
  level: 'silent'
40
39
  })
41
40
  formatter = new UrlFormatter('https://activitypubbot.example')
42
- connection = new Sequelize('sqlite::memory:', { logging: false })
41
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
43
42
  await connection.authenticate()
43
+ await runMigrations(connection)
44
44
  actorStorage = new ActorStorage(connection, formatter)
45
- await actorStorage.initialize()
46
- objectStorage = new ObjectStorage(connection)
47
- await objectStorage.initialize()
48
45
  keyStorage = new KeyStorage(connection, logger)
49
- await keyStorage.initialize()
50
46
  const signer = new HTTPSignature(logger)
51
47
  const digester = new Digester(logger)
52
48
  client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
@@ -134,7 +130,6 @@ describe('Authorizer', () => {
134
130
  actorStorage = null
135
131
  connection = null
136
132
  authorizer = null
137
- objectStorage = null
138
133
  })
139
134
 
140
135
  it('should be a class', async () => {