@evanp/activitypub-bot 0.32.2 → 0.33.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.
- package/lib/activitydeliverer.js +4 -2
- package/lib/activitydistributor.js +3 -1
- package/lib/activityhandler.js +4 -2
- package/lib/activitypubclient.js +5 -3
- package/lib/botcontext.js +3 -1
- package/lib/distributionworker.js +26 -18
- package/lib/httpsignature.js +2 -1
- package/lib/httpsignatureauthenticator.js +1 -0
- package/lib/keystorage.js +3 -2
- package/lib/migrations/index.js +3 -3
- package/lib/remotekeystorage.js +80 -15
- package/lib/routes/collection.js +4 -2
- package/lib/routes/inbox.js +4 -2
- package/lib/routes/object.js +2 -1
- package/lib/routes/sharedinbox.js +4 -2
- package/lib/routes/webfinger.js +22 -0
- package/package.json +1 -1
package/lib/activitydeliverer.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import BotMaker from './botmaker.js'
|
|
2
1
|
import assert from 'node:assert'
|
|
3
|
-
|
|
2
|
+
|
|
4
3
|
import * as ttlcachePkg from '@isaacs/ttlcache'
|
|
5
4
|
|
|
5
|
+
import as2 from './activitystreams.js'
|
|
6
|
+
import BotMaker from './botmaker.js'
|
|
7
|
+
|
|
6
8
|
const TTLCache =
|
|
7
9
|
ttlcachePkg.TTLCache ?? ttlcachePkg.default ?? ttlcachePkg
|
|
8
10
|
|
package/lib/activityhandler.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import as2 from './activitystreams.js'
|
|
2
|
-
import { nanoid } from 'nanoid'
|
|
3
1
|
import assert from 'node:assert'
|
|
4
2
|
|
|
3
|
+
import { nanoid } from 'nanoid'
|
|
4
|
+
|
|
5
|
+
import as2 from './activitystreams.js'
|
|
6
|
+
|
|
5
7
|
const AS2 = 'https://www.w3.org/ns/activitystreams#'
|
|
6
8
|
|
|
7
9
|
const THREAD_PROP = 'https://purl.archive.org/socialweb/thread#thread'
|
package/lib/activitypubclient.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import as2 from './activitystreams.js'
|
|
2
|
-
import fetch from 'node-fetch'
|
|
3
1
|
import assert from 'node:assert'
|
|
4
|
-
import createHttpError from 'http-errors'
|
|
5
2
|
import fs from 'node:fs'
|
|
6
3
|
import path from 'node:path'
|
|
7
4
|
import { fileURLToPath } from 'node:url'
|
|
8
5
|
|
|
6
|
+
import fetch from 'node-fetch'
|
|
7
|
+
import createHttpError from 'http-errors'
|
|
8
|
+
|
|
9
|
+
import as2 from './activitystreams.js'
|
|
10
|
+
|
|
9
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
12
|
const __dirname = path.dirname(__filename)
|
|
11
13
|
|
package/lib/botcontext.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { nanoid } from 'nanoid'
|
|
4
4
|
import fetch from 'node-fetch'
|
|
5
5
|
|
|
6
|
+
import as2 from './activitystreams.js'
|
|
7
|
+
|
|
6
8
|
const AS2_TYPES = [
|
|
7
9
|
'application/activity+json',
|
|
8
10
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
|
@@ -58,37 +58,31 @@ export class DistributionWorker {
|
|
|
58
58
|
{ error, activity: activity.id, inbox },
|
|
59
59
|
'Could not deliver activity due to client error'
|
|
60
60
|
)
|
|
61
|
-
if (error.status
|
|
61
|
+
if ([408, 425, 429].includes(error.status)) {
|
|
62
62
|
this.#logger.debug(
|
|
63
63
|
{ error, activity: activity.id, inbox },
|
|
64
|
-
'Retrying on
|
|
64
|
+
'Retrying on recoverable status'
|
|
65
65
|
)
|
|
66
|
-
|
|
67
|
-
if (error.headers && error.headers['retry-after']) {
|
|
68
|
-
this.#logger.debug('using retry-after header')
|
|
69
|
-
const retryAfter = error.headers['retry-after']
|
|
70
|
-
if (/^\d+$/.test(retryAfter)) {
|
|
71
|
-
delay = parseInt(retryAfter, 10) * 1000
|
|
72
|
-
} else {
|
|
73
|
-
delay = new Date(retryAfter) - Date.now()
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
this.#logger.debug('exponential backoff')
|
|
77
|
-
delay = Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
|
|
78
|
-
}
|
|
66
|
+
const delay = this.#retryDelay(error.headers, attempts)
|
|
79
67
|
await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
|
|
80
68
|
} else {
|
|
81
69
|
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
82
70
|
}
|
|
83
71
|
} else if (error.status >= 500 && error.status < 600) {
|
|
84
|
-
if (
|
|
72
|
+
if ([501, 505, 508, 510].includes(error.status)) {
|
|
73
|
+
this.#logger.warn(
|
|
74
|
+
{ error, activity: activity.id, inbox, attempts },
|
|
75
|
+
'Could not deliver activity due to unrecoverable server error'
|
|
76
|
+
)
|
|
77
|
+
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
78
|
+
} else if (attempts >= DistributionWorker.#MAX_ATTEMPTS) {
|
|
85
79
|
this.#logger.warn(
|
|
86
80
|
{ error, activity: activity.id, inbox, attempts },
|
|
87
81
|
'Could not deliver activity due to server error; no more attempts'
|
|
88
82
|
)
|
|
89
83
|
await this.#jobQueue.fail(jobId, this.#workerId)
|
|
90
84
|
} else {
|
|
91
|
-
const delay =
|
|
85
|
+
const delay = this.#retryDelay(error.headers, attempts)
|
|
92
86
|
this.#logger.warn(
|
|
93
87
|
{ error, activity: activity.id, inbox, attempts, delay },
|
|
94
88
|
'Could not deliver activity due to server error; will retry'
|
|
@@ -110,7 +104,7 @@ export class DistributionWorker {
|
|
|
110
104
|
}
|
|
111
105
|
this.#logger.warn({ err, jobId }, 'Error delivering to bot')
|
|
112
106
|
if (jobId) {
|
|
113
|
-
const delay =
|
|
107
|
+
const delay = this.#retryDelay(null, attempts ?? 1)
|
|
114
108
|
this.#logger.warn(
|
|
115
109
|
{ err, jobId, attempts, delay },
|
|
116
110
|
'Retrying job after a delay'
|
|
@@ -121,6 +115,20 @@ export class DistributionWorker {
|
|
|
121
115
|
}
|
|
122
116
|
}
|
|
123
117
|
|
|
118
|
+
#retryDelay (headers, attempts) {
|
|
119
|
+
if (headers?.['retry-after']) {
|
|
120
|
+
this.#logger.debug('using retry-after header')
|
|
121
|
+
const retryAfter = headers['retry-after']
|
|
122
|
+
if (/^\d+$/.test(retryAfter)) {
|
|
123
|
+
return parseInt(retryAfter, 10) * 1000
|
|
124
|
+
} else {
|
|
125
|
+
return new Date(retryAfter) - Date.now()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.#logger.debug('exponential backoff')
|
|
129
|
+
return Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
stop () {
|
|
125
133
|
this.#running = false
|
|
126
134
|
}
|
package/lib/httpsignature.js
CHANGED
package/lib/keystorage.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { promisify } from 'util'
|
|
1
|
+
import { promisify } from 'node:util'
|
|
2
2
|
import crypto from 'node:crypto'
|
|
3
|
-
import HumanHasher from 'humanhash'
|
|
4
3
|
import assert from 'node:assert'
|
|
5
4
|
|
|
5
|
+
import HumanHasher from 'humanhash'
|
|
6
|
+
|
|
6
7
|
const generateKeyPair = promisify(crypto.generateKeyPair)
|
|
7
8
|
|
|
8
9
|
export class KeyStorage {
|
package/lib/migrations/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fileURLToPath } from 'url'
|
|
2
|
-
import { dirname, resolve } from 'path'
|
|
3
|
-
import { readdirSync } from 'fs'
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { readdirSync } from 'node:fs'
|
|
4
4
|
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
6
6
|
|
package/lib/remotekeystorage.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
1
3
|
const SEC_NS = 'https://w3id.org/security#'
|
|
4
|
+
const DEFAULT_NS = '_:'
|
|
2
5
|
|
|
3
6
|
export class RemoteKeyStorage {
|
|
4
7
|
#client = null
|
|
@@ -27,6 +30,10 @@ export class RemoteKeyStorage {
|
|
|
27
30
|
this.debug(`getPublicKey(${id}) - remote not found`)
|
|
28
31
|
return null
|
|
29
32
|
}
|
|
33
|
+
if (!await this.#confirmPublicKey(remote.owner, id)) {
|
|
34
|
+
this.#logger.warn({ owner: remote.owner, id }, 'Mismatched owner and key')
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
30
37
|
await this.#cachePublicKey(id, remote.owner, remote.publicKeyPem)
|
|
31
38
|
return remote
|
|
32
39
|
}
|
|
@@ -55,21 +62,11 @@ export class RemoteKeyStorage {
|
|
|
55
62
|
if (!response) {
|
|
56
63
|
return null
|
|
57
64
|
}
|
|
58
|
-
this.debug(`getRemotePublicKey(${id}) - response: ${
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
owner = response.get(SEC_NS + 'owner')?.first?.id
|
|
64
|
-
publicKeyPem = response.get(SEC_NS + 'publicKeyPem')?.first
|
|
65
|
-
} else if (response.get(SEC_NS + 'publicKey')) {
|
|
66
|
-
this.debug(`getRemotePublicKey(${id}) - publicKey`)
|
|
67
|
-
const publicKey = response.get(SEC_NS + 'publicKey').first
|
|
68
|
-
if (publicKey) {
|
|
69
|
-
owner = publicKey.get(SEC_NS + 'owner')?.first?.id
|
|
70
|
-
publicKeyPem = publicKey.get(SEC_NS + 'publicKeyPem')?.first
|
|
71
|
-
}
|
|
72
|
-
}
|
|
65
|
+
this.debug(`getRemotePublicKey(${id}) - response: ${response.id}`)
|
|
66
|
+
|
|
67
|
+
const owner = this.#getOwner(response)
|
|
68
|
+
const publicKeyPem = this.#getPublicKeyPem(response)
|
|
69
|
+
|
|
73
70
|
if (!owner || !publicKeyPem) {
|
|
74
71
|
return null
|
|
75
72
|
}
|
|
@@ -89,4 +86,72 @@ export class RemoteKeyStorage {
|
|
|
89
86
|
this.#logger.debug(...args)
|
|
90
87
|
}
|
|
91
88
|
}
|
|
89
|
+
|
|
90
|
+
async #confirmPublicKey (owner, id) {
|
|
91
|
+
assert.equal(typeof owner, 'string')
|
|
92
|
+
assert.equal(typeof id, 'string')
|
|
93
|
+
let actor
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
actor = await this.#client.get(owner)
|
|
97
|
+
} catch (err) {
|
|
98
|
+
this.#logger.warn({ err, owner, id }, 'Error getting key owner')
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const publicKeyId = this.#getPublicKeyId(actor)
|
|
103
|
+
|
|
104
|
+
if (!publicKeyId) {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return publicKeyId === id
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#getSecIdProp (obj, prop) {
|
|
112
|
+
assert.strictEqual(typeof obj, 'object')
|
|
113
|
+
assert.strictEqual(typeof prop, 'string')
|
|
114
|
+
let value = obj.get(SEC_NS + prop)
|
|
115
|
+
if (value) {
|
|
116
|
+
return value.first?.id
|
|
117
|
+
}
|
|
118
|
+
value = obj.get(DEFAULT_NS + prop)
|
|
119
|
+
if (value) {
|
|
120
|
+
this.#logger.warn(
|
|
121
|
+
{ objectId: obj.id, prop },
|
|
122
|
+
'security property in default namespace'
|
|
123
|
+
)
|
|
124
|
+
const first = value.first
|
|
125
|
+
return (typeof first === 'string') ? first : first?.id
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#getOwner (obj) {
|
|
131
|
+
assert.strictEqual(typeof obj, 'object')
|
|
132
|
+
return this.#getSecIdProp(obj, 'owner')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#getPublicKeyPem (obj) {
|
|
136
|
+
assert.strictEqual(typeof obj, 'object')
|
|
137
|
+
const prop = 'publicKeyPem'
|
|
138
|
+
let value = obj.get(SEC_NS + prop)
|
|
139
|
+
if (value) {
|
|
140
|
+
return value.first
|
|
141
|
+
}
|
|
142
|
+
value = obj.get(DEFAULT_NS + prop)
|
|
143
|
+
if (value) {
|
|
144
|
+
this.#logger.warn(
|
|
145
|
+
{ objectId: obj.id, prop },
|
|
146
|
+
'security property in default namespace'
|
|
147
|
+
)
|
|
148
|
+
return value.first
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#getPublicKeyId (obj) {
|
|
154
|
+
assert.strictEqual(typeof obj, 'object')
|
|
155
|
+
return this.#getSecIdProp(obj, 'publicKey')
|
|
156
|
+
}
|
|
92
157
|
}
|
package/lib/routes/collection.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
1
3
|
import express from 'express'
|
|
2
|
-
import as2 from '../activitystreams.js'
|
|
3
4
|
import createHttpError from 'http-errors'
|
|
5
|
+
|
|
6
|
+
import as2 from '../activitystreams.js'
|
|
4
7
|
import BotMaker from '../botmaker.js'
|
|
5
|
-
import assert from 'node:assert'
|
|
6
8
|
|
|
7
9
|
const collections = ['outbox', 'liked', 'followers', 'following']
|
|
8
10
|
const router = express.Router()
|
package/lib/routes/inbox.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
|
|
1
3
|
import express from 'express'
|
|
2
|
-
import as2 from '../activitystreams.js'
|
|
3
4
|
import createHttpError from 'http-errors'
|
|
5
|
+
|
|
6
|
+
import as2 from '../activitystreams.js'
|
|
4
7
|
import BotMaker from '../botmaker.js'
|
|
5
|
-
import http from 'node:http'
|
|
6
8
|
|
|
7
9
|
const router = express.Router()
|
|
8
10
|
|
package/lib/routes/object.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
|
|
1
3
|
import express from 'express'
|
|
2
|
-
import as2 from '../activitystreams.js'
|
|
3
4
|
import createHttpError from 'http-errors'
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
import as2 from '../activitystreams.js'
|
|
5
7
|
|
|
6
8
|
const router = express.Router()
|
|
7
9
|
|
package/lib/routes/webfinger.js
CHANGED
|
@@ -33,6 +33,26 @@ async function botWebfinger (username, req, res, next) {
|
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
async function profileWebfinger (username, profileUrl, req, res, next) {
|
|
37
|
+
const { formatter, bots } = req.app.locals
|
|
38
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
39
|
+
if (!bot) {
|
|
40
|
+
return next(createHttpError(404, `No such bot '${username}'`))
|
|
41
|
+
}
|
|
42
|
+
res.status(200)
|
|
43
|
+
res.type('application/jrd+json')
|
|
44
|
+
res.json({
|
|
45
|
+
subject: profileUrl,
|
|
46
|
+
links: [
|
|
47
|
+
{
|
|
48
|
+
rel: 'alternate',
|
|
49
|
+
type: 'application/activity+json',
|
|
50
|
+
href: formatter.format({ username })
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
async function httpsWebfinger (resource, req, res, next) {
|
|
37
57
|
const { formatter } = req.app.locals
|
|
38
58
|
assert.ok(formatter)
|
|
@@ -42,6 +62,8 @@ async function httpsWebfinger (resource, req, res, next) {
|
|
|
42
62
|
const parts = formatter.unformat(resource)
|
|
43
63
|
if (parts.username && !parts.type && !parts.collection) {
|
|
44
64
|
return await botWebfinger(parts.username, req, res, next)
|
|
65
|
+
} else if (parts.username && parts.type === 'profile') {
|
|
66
|
+
return await profileWebfinger(parts.username, resource, req, res, next)
|
|
45
67
|
} else {
|
|
46
68
|
return next(createHttpError(400, `No webfinger lookup for url ${resource}`))
|
|
47
69
|
}
|