@evanp/activitypub-bot 0.52.1 → 0.53.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/.dockerignore +8 -0
- package/CHANGELOG.md +28 -1
- package/lib/app.js +2 -0
- package/lib/microsyntax.js +48 -31
- package/lib/routes/webfinger.js +12 -3
- package/lib/urlformatter.js +5 -5
- package/package.json +2 -2
package/.dockerignore
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,31 @@ and this project adheres to
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.53.0] - 2026-10-08
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Upgrade activitypub-webfinger to 0.3.0 and add BotContext tests for forward
|
|
17
|
+
and reverse discovery with non-ASCII usernames and domains (#282).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Linkify mentions with non-ASCII usernames and domains or ASCII punctuation
|
|
22
|
+
in usernames, preserving HTML escaping and URL fragments; add regression
|
|
23
|
+
tests for Unicode handles and username punctuation (#282).
|
|
24
|
+
- Normalize server origins to support Unicode domains; add regression tests
|
|
25
|
+
for discovery, actor and collection endpoints, and OKBot replies (#282).
|
|
26
|
+
- Support Unicode local usernames by encoding URL paths and acct: URIs and
|
|
27
|
+
decoding usernames during URL parsing and WebFinger lookup; return 400 for
|
|
28
|
+
malformed WebFinger username escapes and add regression tests (#282).
|
|
29
|
+
|
|
30
|
+
## [0.52.2] - 2026-10-05
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Build Docker images from the local package instead of waiting for the new
|
|
35
|
+
release to become available on npm.
|
|
36
|
+
|
|
12
37
|
## [0.52.1] - 2026-10-05
|
|
13
38
|
|
|
14
39
|
### Changed
|
|
@@ -1688,7 +1713,9 @@ Hot patch on the `stable` branch.
|
|
|
1688
1713
|
- Docker multi-platform build workflow.
|
|
1689
1714
|
- Dependabot configuration.
|
|
1690
1715
|
|
|
1691
|
-
[Unreleased]: https://github.com/evanp/activitypub-bot/compare/v0.
|
|
1716
|
+
[Unreleased]: https://github.com/evanp/activitypub-bot/compare/v0.53.0...HEAD
|
|
1717
|
+
[0.53.0]: https://github.com/evanp/activitypub-bot/compare/v0.52.2...v0.53.0
|
|
1718
|
+
[0.52.2]: https://github.com/evanp/activitypub-bot/compare/v0.52.1...v0.52.2
|
|
1692
1719
|
[0.52.1]: https://github.com/evanp/activitypub-bot/compare/v0.52.0...v0.52.1
|
|
1693
1720
|
[0.52.0]: https://github.com/evanp/activitypub-bot/compare/v0.51.4...v0.52.0
|
|
1694
1721
|
[0.51.4]: https://github.com/evanp/activitypub-bot/compare/v0.51.3...v0.51.4
|
package/lib/app.js
CHANGED
|
@@ -72,6 +72,8 @@ function createWorkers (logger, count, WorkerClass, ...args) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent', deliveryWorkerCount = 2, distributionWorkerCount = 8, fanoutWorkerCount = 4, intakeWorkerCount = 2, indexFileName = DEFAULT_INDEX_FILENAME, profileFileName = DEFAULT_PROFILE_FILENAME, allowPrivateNetworkRequests = false, redisUrl, trustProxy, domainBlockFileName }) {
|
|
75
|
+
// normalize the origin, including Unicode -> punycode
|
|
76
|
+
origin = (new URL(origin)).origin
|
|
75
77
|
const logger = Logger({
|
|
76
78
|
level: logLevel
|
|
77
79
|
})
|
package/lib/microsyntax.js
CHANGED
|
@@ -30,8 +30,8 @@ export class Transformer {
|
|
|
30
30
|
let html = this.#escape(text)
|
|
31
31
|
let tag = [];
|
|
32
32
|
({ html, tag } = this.#replaceUrls(html, tag));
|
|
33
|
-
({ html, tag } = this.#
|
|
34
|
-
({ html, tag } =
|
|
33
|
+
({ html, tag } = await this.#replaceMentions(html, tag));
|
|
34
|
+
({ html, tag } = this.#replaceHashtags(html, tag))
|
|
35
35
|
html = `<p>${html}</p>`
|
|
36
36
|
return { html, tag }
|
|
37
37
|
}
|
|
@@ -67,36 +67,19 @@ export class Transformer {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async #replaceMentions (html, tag) {
|
|
70
|
-
const self = this
|
|
71
|
-
const webfinger = /@[a-zA-Z0-9_]+([a-zA-Z0-9_.-]+[a-zA-Z0-9_]+)?@[a-zA-Z0-9_.-]+/g
|
|
72
70
|
const segments = this.#segment(html)
|
|
73
71
|
for (const i in segments) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
segments[i] = await this.#replaceAsync(segments[i], webfinger, async (match) => {
|
|
77
|
-
const href = await self.#homePage(match.slice(1))
|
|
78
|
-
if (!href) return match
|
|
79
|
-
let url
|
|
80
|
-
try {
|
|
81
|
-
url = new URL(href)
|
|
82
|
-
} catch (err) {
|
|
83
|
-
return match
|
|
84
|
-
}
|
|
85
|
-
if (url.protocol !== 'https:') {
|
|
86
|
-
return match
|
|
87
|
-
}
|
|
88
|
-
tag.push({ type: 'Mention', name: match, href: url.href })
|
|
89
|
-
const escaped = this.#escape(url.href)
|
|
90
|
-
const escapedMatch = this.#escape(match)
|
|
91
|
-
return `<a href="${escaped}">${escapedMatch}</a>`
|
|
92
|
-
})
|
|
72
|
+
if (this.#isLink(segments[i])) continue
|
|
73
|
+
segments[i] = await this.#replaceMention(this.#unescape(segments[i]), tag)
|
|
93
74
|
}
|
|
94
75
|
return { html: segments.join(''), tag }
|
|
95
76
|
}
|
|
96
77
|
|
|
97
78
|
async #homePage (webfinger) {
|
|
98
79
|
if (!this.#client) return null
|
|
99
|
-
const
|
|
80
|
+
const delimiter = webfinger.lastIndexOf('@')
|
|
81
|
+
const username = webfinger.slice(0, delimiter)
|
|
82
|
+
const domain = webfinger.slice(delimiter + 1)
|
|
100
83
|
if (domain.toLowerCase() === this.#formatter.hostname) {
|
|
101
84
|
return this.#formatter.format({ username, type: 'profile' })
|
|
102
85
|
}
|
|
@@ -136,18 +119,43 @@ export class Transformer {
|
|
|
136
119
|
return html.split(/(<[^>]+>[^<]+<\/[^>]+>)/)
|
|
137
120
|
}
|
|
138
121
|
|
|
139
|
-
async #
|
|
122
|
+
async #transformMention (match, tag) {
|
|
123
|
+
const escapedMatch = this.#escape(match)
|
|
124
|
+
const href = await this.#homePage(match.slice(1))
|
|
125
|
+
if (!href) return escapedMatch
|
|
126
|
+
let url
|
|
127
|
+
try {
|
|
128
|
+
url = new URL(href)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return escapedMatch
|
|
131
|
+
}
|
|
132
|
+
if (url.protocol !== 'https:') {
|
|
133
|
+
return escapedMatch
|
|
134
|
+
}
|
|
135
|
+
tag.push({ type: 'Mention', name: match, href: url.href })
|
|
136
|
+
const escaped = this.#escape(url.href)
|
|
137
|
+
return `<a href="${escaped}">${escapedMatch}</a>`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #replaceMention (str, tag) {
|
|
141
|
+
const webfinger = /(@[\x21-\x7E\p{L}\p{Nd}\p{M}]+@[\p{L}\p{Nd}](?:[\p{L}\p{Nd}\p{M}-]*[\p{L}\p{Nd}\p{M}])?(?:\.[\p{L}\p{Nd}](?:[\p{L}\p{Nd}\p{M}-]*[\p{L}\p{Nd}\p{M}])?)*)/ug
|
|
142
|
+
|
|
143
|
+
const parts = str.split(webfinger)
|
|
144
|
+
|
|
140
145
|
const promises = []
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
promises
|
|
144
|
-
|
|
146
|
+
|
|
147
|
+
for (const i in parts) {
|
|
148
|
+
promises[i] = (i % 2 === 0)
|
|
149
|
+
? new Promise((resolve, reject) => {
|
|
150
|
+
resolve(this.#escape(parts[i]))
|
|
151
|
+
})
|
|
152
|
+
: this.#transformMention(parts[i], tag)
|
|
153
|
+
}
|
|
145
154
|
|
|
146
155
|
// Wait for all async replacements to resolve
|
|
147
156
|
const replacements = await Promise.all(promises)
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
return str.replace(regex, () => replacements.shift())
|
|
158
|
+
return replacements.join('')
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
#escape (text) {
|
|
@@ -158,4 +166,13 @@ export class Transformer {
|
|
|
158
166
|
.replace(/"/g, '"')
|
|
159
167
|
.replace(/'/g, ''')
|
|
160
168
|
}
|
|
169
|
+
|
|
170
|
+
#unescape (text) {
|
|
171
|
+
return text
|
|
172
|
+
.replace(/</g, '<')
|
|
173
|
+
.replace(/>/g, '>')
|
|
174
|
+
.replace(/"/g, '"')
|
|
175
|
+
.replace(/'/g, '\'')
|
|
176
|
+
.replace(/&/g, '&')
|
|
177
|
+
}
|
|
161
178
|
}
|
package/lib/routes/webfinger.js
CHANGED
|
@@ -70,15 +70,24 @@ async function httpsWebfinger (resource, req, res, next) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function acctWebfinger (resource, req, res, next) {
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const address = resource.substring(5)
|
|
74
|
+
const delimiter = address.lastIndexOf('@')
|
|
75
|
+
if (delimiter <= 0 || delimiter === address.length - 1) {
|
|
75
76
|
return next(new ProblemDetailsError(400, `Invalid resource parameter ${resource}`))
|
|
76
77
|
}
|
|
78
|
+
const username = address.slice(0, delimiter)
|
|
79
|
+
const domain = address.slice(delimiter + 1)
|
|
77
80
|
const { host } = new URL(req.app.locals.origin)
|
|
78
81
|
if (domain !== host) {
|
|
79
82
|
return next(new ProblemDetailsError(400, `Invalid domain ${domain} in resource parameter`))
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
let decoded
|
|
85
|
+
try {
|
|
86
|
+
decoded = decodeURIComponent(username)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return next(new ProblemDetailsError(400, `Invalid percent-encoded username ${username} in resource parameter`))
|
|
89
|
+
}
|
|
90
|
+
return await botWebfinger(decoded, req, res, next)
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
router.get('/.well-known/webfinger', async (req, res, next) => {
|
package/lib/urlformatter.js
CHANGED
|
@@ -16,9 +16,9 @@ export class UrlFormatter {
|
|
|
16
16
|
if (server) {
|
|
17
17
|
return this.format({ username: this.#hostname, type, nanoid, collection, page, server: false })
|
|
18
18
|
} else if (username && type && type === 'profile') {
|
|
19
|
-
return `${this.#origin}/profile/${username}`
|
|
19
|
+
return `${this.#origin}/profile/${encodeURIComponent(username)}`
|
|
20
20
|
} else if (username) {
|
|
21
|
-
base = `${this.#origin}/user/${username}`
|
|
21
|
+
base = `${this.#origin}/user/${encodeURIComponent(username)}`
|
|
22
22
|
} else {
|
|
23
23
|
throw new Error('Cannot format URL without username or server')
|
|
24
24
|
}
|
|
@@ -67,11 +67,11 @@ export class UrlFormatter {
|
|
|
67
67
|
}
|
|
68
68
|
let pathParts = parsed.pathname.slice(1).split('/')
|
|
69
69
|
if (pathParts.length > 0 && pathParts[0] === 'user') {
|
|
70
|
-
parts.username = pathParts[1]
|
|
70
|
+
parts.username = decodeURIComponent(pathParts[1])
|
|
71
71
|
pathParts = pathParts.slice(2)
|
|
72
72
|
parts.server = (parts.username === this.#hostname)
|
|
73
73
|
} else if (pathParts.length > 0 && pathParts[0] === 'profile') {
|
|
74
|
-
parts.username = pathParts[1]
|
|
74
|
+
parts.username = decodeURIComponent(pathParts[1])
|
|
75
75
|
parts.type = 'profile'
|
|
76
76
|
return parts
|
|
77
77
|
}
|
|
@@ -108,7 +108,7 @@ export class UrlFormatter {
|
|
|
108
108
|
|
|
109
109
|
acct (username = null) {
|
|
110
110
|
return (username)
|
|
111
|
-
? `acct:${username}@${this.#hostname}`
|
|
111
|
+
? `acct:${encodeURIComponent(username)}@${this.#hostname}`
|
|
112
112
|
: `acct:${this.#hostname}@${this.#hostname}`
|
|
113
113
|
}
|
|
114
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evanp/activitypub-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "server-side ActivityPub bot framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@isaacs/ttlcache": "^2.1.4",
|
|
34
34
|
"@socialwebfoundation/ap-components": "^1.4.2",
|
|
35
|
-
"activitypub-webfinger": "^0.
|
|
35
|
+
"activitypub-webfinger": "^0.3.0",
|
|
36
36
|
"activitystrea.ms": "^4.0.1",
|
|
37
37
|
"csv-parse": "^7.0.0",
|
|
38
38
|
"express": "^5.2.1",
|