@evanp/activitypub-bot 0.45.8 → 0.45.10
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.
- package/CHANGELOG.md +13 -0
- package/lib/activitydistributor.js +14 -20
- package/lib/app.js +5 -2
- package/lib/distributionworker.js +25 -8
- package/lib/endpointcache.js +57 -0
- package/lib/migrations/010-endpoint-cache.js +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,19 @@ and this project adheres to
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.45.10] - 2026-05-09
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Upgraded nock, ipaddr.js, and mysql2.
|
|
17
|
+
- inbox and sharedInbox caches are now persistent and shared across servers.
|
|
18
|
+
|
|
19
|
+
## [0.45.9] - 2026-04-29
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Don't infinitely retry on 429 status code.
|
|
24
|
+
|
|
12
25
|
## [0.45.8] - 2026-04-29
|
|
13
26
|
|
|
14
27
|
### Added
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
|
|
3
|
-
import { LRUCache } from 'lru-cache'
|
|
4
|
-
|
|
5
3
|
import as2 from './activitystreams.js'
|
|
6
4
|
|
|
7
5
|
const NS = 'https://www.w3.org/ns/activitystreams#'
|
|
@@ -11,11 +9,13 @@ const COLLECTION_TYPES = [
|
|
|
11
9
|
`${NS}OrderedCollection`
|
|
12
10
|
]
|
|
13
11
|
|
|
12
|
+
const INBOX = 'inbox'
|
|
13
|
+
const SHARED_INBOX = 'sharedInbox'
|
|
14
|
+
|
|
14
15
|
export class ActivityDistributor {
|
|
15
16
|
static #DISTRIBUTION_QUEUE_ID = 'distribution'
|
|
16
17
|
static #DELIVERY_QUEUE_ID = 'delivery'
|
|
17
18
|
static #FANOUT_QUEUE_ID = 'fanout'
|
|
18
|
-
static #MAX_CACHE_SIZE = 1000000
|
|
19
19
|
static #PUBLIC = [
|
|
20
20
|
'https://www.w3.org/ns/activitystreams#Public',
|
|
21
21
|
'as:Public',
|
|
@@ -25,24 +25,23 @@ export class ActivityDistributor {
|
|
|
25
25
|
#client = null
|
|
26
26
|
#formatter = null
|
|
27
27
|
#actorStorage = null
|
|
28
|
-
#directInboxCache = null
|
|
29
|
-
#sharedInboxCache = null
|
|
30
28
|
#logger = null
|
|
31
29
|
#jobQueue = null
|
|
30
|
+
#cache
|
|
32
31
|
|
|
33
|
-
constructor (client, formatter, actorStorage, logger, jobQueue) {
|
|
32
|
+
constructor (client, formatter, actorStorage, logger, jobQueue, cache) {
|
|
34
33
|
assert.ok(client)
|
|
35
34
|
assert.ok(formatter)
|
|
36
35
|
assert.ok(actorStorage)
|
|
37
36
|
assert.ok(logger)
|
|
38
37
|
assert.ok(jobQueue)
|
|
38
|
+
assert.ok(cache)
|
|
39
39
|
this.#client = client
|
|
40
40
|
this.#formatter = formatter
|
|
41
41
|
this.#actorStorage = actorStorage
|
|
42
42
|
this.#logger = logger.child({ class: this.constructor.name })
|
|
43
43
|
this.#jobQueue = jobQueue
|
|
44
|
-
this.#
|
|
45
|
-
this.#sharedInboxCache = new LRUCache({ max: ActivityDistributor.#MAX_CACHE_SIZE })
|
|
44
|
+
this.#cache = cache
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
async distribute (activity, username) {
|
|
@@ -189,7 +188,7 @@ export class ActivityDistributor {
|
|
|
189
188
|
assert.ok(username)
|
|
190
189
|
assert.equal(typeof username, 'string')
|
|
191
190
|
|
|
192
|
-
let sharedInbox = this.#
|
|
191
|
+
let sharedInbox = await this.#cache.get(actorId, SHARED_INBOX)
|
|
193
192
|
|
|
194
193
|
if (sharedInbox) {
|
|
195
194
|
return sharedInbox
|
|
@@ -202,20 +201,15 @@ export class ActivityDistributor {
|
|
|
202
201
|
const endpoints = obj.get('endpoints')
|
|
203
202
|
if (endpoints) {
|
|
204
203
|
const firstEndpoint = Array.from(endpoints)[0]
|
|
205
|
-
const sharedInboxEndpoint = firstEndpoint.get(
|
|
204
|
+
const sharedInboxEndpoint = firstEndpoint.get(SHARED_INBOX)
|
|
206
205
|
if (sharedInboxEndpoint) {
|
|
207
206
|
const firstSharedInbox = Array.from(sharedInboxEndpoint)[0]
|
|
208
207
|
sharedInbox = firstSharedInbox.id
|
|
209
|
-
this.#
|
|
208
|
+
await this.#cache.set(actorId, SHARED_INBOX, sharedInbox)
|
|
210
209
|
return sharedInbox
|
|
211
210
|
}
|
|
212
211
|
}
|
|
213
212
|
|
|
214
|
-
let directInbox = this.#directInboxCache.get(actorId)
|
|
215
|
-
if (directInbox) {
|
|
216
|
-
return directInbox
|
|
217
|
-
}
|
|
218
|
-
|
|
219
213
|
if (!obj.inbox) {
|
|
220
214
|
return null
|
|
221
215
|
}
|
|
@@ -223,8 +217,8 @@ export class ActivityDistributor {
|
|
|
223
217
|
if (inboxes.length === 0) {
|
|
224
218
|
return null
|
|
225
219
|
}
|
|
226
|
-
directInbox = inboxes[0].id
|
|
227
|
-
this.#
|
|
220
|
+
const directInbox = inboxes[0].id
|
|
221
|
+
await this.#cache.set(actorId, INBOX, directInbox)
|
|
228
222
|
return directInbox
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -233,7 +227,7 @@ export class ActivityDistributor {
|
|
|
233
227
|
assert.equal(typeof actorId, 'string')
|
|
234
228
|
assert.ok(username)
|
|
235
229
|
assert.equal(typeof username, 'string')
|
|
236
|
-
let directInbox = this.#
|
|
230
|
+
let directInbox = await this.#cache.get(actorId, INBOX)
|
|
237
231
|
if (directInbox) {
|
|
238
232
|
return directInbox
|
|
239
233
|
}
|
|
@@ -248,7 +242,7 @@ export class ActivityDistributor {
|
|
|
248
242
|
return null
|
|
249
243
|
}
|
|
250
244
|
directInbox = inboxes[0].id
|
|
251
|
-
this.#
|
|
245
|
+
await this.#cache.set(actorId, INBOX, directInbox)
|
|
252
246
|
return directInbox
|
|
253
247
|
}
|
|
254
248
|
|
package/lib/app.js
CHANGED
|
@@ -10,6 +10,7 @@ import Logger from 'pino'
|
|
|
10
10
|
import HTTPLogger from 'pino-http'
|
|
11
11
|
import { RedisStore } from 'rate-limit-redis'
|
|
12
12
|
import { createClient } from 'redis'
|
|
13
|
+
import { rateLimit } from 'express-rate-limit'
|
|
13
14
|
|
|
14
15
|
import { ActivityDistributor } from './activitydistributor.js'
|
|
15
16
|
import { ActivityPubClient, ActivityPubClientError } from './activitypubclient.js'
|
|
@@ -52,7 +53,7 @@ import { RemoteObjectCache } from './remoteobjectcache.js'
|
|
|
52
53
|
import { HTTPMessageSignature } from './httpmessagesignature.js'
|
|
53
54
|
import { SignaturePolicyStorage } from './signaturepolicystorage.js'
|
|
54
55
|
import { SafeAgent } from './safeagent.js'
|
|
55
|
-
import {
|
|
56
|
+
import { EndpointCache } from './endpointcache.js'
|
|
56
57
|
|
|
57
58
|
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
58
59
|
const DEFAULT_INDEX_FILENAME = resolve(currentDir, '..', 'web', 'index.html')
|
|
@@ -105,12 +106,14 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
105
106
|
const remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
|
|
106
107
|
const signature = new HTTPSignatureAuthenticator(remoteKeyStorage, signer, messageSigner, digester, logger)
|
|
107
108
|
const jobQueue = new JobQueue(connection, logger)
|
|
109
|
+
const endpointCache = new EndpointCache(connection, logger)
|
|
108
110
|
const distributor = new ActivityDistributor(
|
|
109
111
|
client,
|
|
110
112
|
formatter,
|
|
111
113
|
actorStorage,
|
|
112
114
|
logger,
|
|
113
|
-
jobQueue
|
|
115
|
+
jobQueue,
|
|
116
|
+
endpointCache
|
|
114
117
|
)
|
|
115
118
|
const authorizer = new Authorizer(actorStorage, formatter, client)
|
|
116
119
|
const cache = new ObjectCache({
|
|
@@ -44,13 +44,21 @@ export class DistributionWorker extends Worker {
|
|
|
44
44
|
'Could not deliver activity due to client error'
|
|
45
45
|
)
|
|
46
46
|
if ([408, 425, 429].includes(err.status)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
if (attempts >= DistributionWorker.#MAX_ATTEMPTS) {
|
|
48
|
+
this._logger.warn(
|
|
49
|
+
{ err, activity: activity.id, inbox, attempts },
|
|
50
|
+
'Could not deliver activity due to client error; no more attempts'
|
|
51
|
+
)
|
|
52
|
+
throw err
|
|
53
|
+
} else {
|
|
54
|
+
this._logger.debug(
|
|
55
|
+
{ err, activity: activity.id, inbox },
|
|
56
|
+
'Retrying on recoverable status'
|
|
57
|
+
)
|
|
58
|
+
const recoverable = new RecoverableError(err.message)
|
|
59
|
+
recoverable.delay = this.#retryDelay(err.headers, attempts)
|
|
60
|
+
throw recoverable
|
|
61
|
+
}
|
|
54
62
|
} else {
|
|
55
63
|
throw err
|
|
56
64
|
}
|
|
@@ -87,6 +95,12 @@ export class DistributionWorker extends Worker {
|
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
#retryDelay (headers, attempts) {
|
|
98
|
+
const headerDelay = this.#headerDelay(headers)
|
|
99
|
+
const attemptsDelay = this.#attemptsDelay(attempts)
|
|
100
|
+
return Math.max(headerDelay, attemptsDelay)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#headerDelay (headers) {
|
|
90
104
|
if (headers?.['retry-after']) {
|
|
91
105
|
this._logger.debug('using retry-after header')
|
|
92
106
|
const retryAfter = headers['retry-after']
|
|
@@ -96,7 +110,10 @@ export class DistributionWorker extends Worker {
|
|
|
96
110
|
return new Date(retryAfter) - Date.now()
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
|
-
|
|
113
|
+
return 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#attemptsDelay (attempts) {
|
|
100
117
|
return Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
|
|
101
118
|
}
|
|
102
119
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
3
|
+
const EXPIRY = 7 * 24 * 60 * 60 * 1000
|
|
4
|
+
|
|
5
|
+
export class EndpointCache {
|
|
6
|
+
#connection
|
|
7
|
+
#logger
|
|
8
|
+
|
|
9
|
+
constructor (connection, logger) {
|
|
10
|
+
assert.ok(connection)
|
|
11
|
+
assert.equal(typeof connection, 'object')
|
|
12
|
+
assert.ok(logger)
|
|
13
|
+
assert.equal(typeof logger, 'object')
|
|
14
|
+
this.#connection = connection
|
|
15
|
+
this.#logger = logger
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get (actorId, name) {
|
|
19
|
+
assert.ok(actorId)
|
|
20
|
+
assert.equal(typeof actorId, 'string')
|
|
21
|
+
assert.ok(name)
|
|
22
|
+
assert.equal(typeof name, 'string')
|
|
23
|
+
const [rows] = await this.#connection.query(
|
|
24
|
+
`SELECT url, expiry FROM endpoint_cache
|
|
25
|
+
WHERE actor_id = ? AND name = ?`,
|
|
26
|
+
{ replacements: [actorId, name] }
|
|
27
|
+
)
|
|
28
|
+
if (rows && rows.length > 0) {
|
|
29
|
+
if (new Date(rows[0].expiry) > new Date()) {
|
|
30
|
+
return rows[0].url
|
|
31
|
+
} else {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async set (actorId, name, url) {
|
|
39
|
+
assert.ok(actorId)
|
|
40
|
+
assert.equal(typeof actorId, 'string')
|
|
41
|
+
assert.ok(name)
|
|
42
|
+
assert.equal(typeof name, 'string')
|
|
43
|
+
assert.ok(url)
|
|
44
|
+
assert.equal(typeof url, 'string')
|
|
45
|
+
await this.#connection.query(
|
|
46
|
+
`
|
|
47
|
+
INSERT INTO endpoint_cache (actor_id, name, url, expiry)
|
|
48
|
+
VALUES (?, ?, ?, ?)
|
|
49
|
+
ON CONFLICT (actor_id, name) DO UPDATE SET
|
|
50
|
+
url = EXCLUDED.url,
|
|
51
|
+
expiry = EXCLUDED.expiry,
|
|
52
|
+
updated_at = CURRENT_TIMESTAMP
|
|
53
|
+
`,
|
|
54
|
+
{ replacements: [actorId, name, url, new Date(Date.now() + EXPIRY)] }
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const id = '010-endpoint-cache'
|
|
2
|
+
|
|
3
|
+
export async function up (connection, queryOptions = {}) {
|
|
4
|
+
await connection.query(`
|
|
5
|
+
CREATE TABLE endpoint_cache (
|
|
6
|
+
actor_id varchar(512) NOT NULL,
|
|
7
|
+
name varchar(64) NOT NULL,
|
|
8
|
+
url varchar(512) NOT NULL,
|
|
9
|
+
expiry TIMESTAMP NOT NULL,
|
|
10
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
11
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
PRIMARY KEY (actor_id, name)
|
|
13
|
+
);
|
|
14
|
+
`, queryOptions)
|
|
15
|
+
await connection.query(`
|
|
16
|
+
CREATE INDEX endpoint_cache_expiry on endpoint_cache (expiry);
|
|
17
|
+
`, queryOptions)
|
|
18
|
+
await connection.query(`
|
|
19
|
+
CREATE INDEX endpoint_cache_url on endpoint_cache (url);
|
|
20
|
+
`, queryOptions)
|
|
21
|
+
}
|