@evanp/activitypub-bot 0.8.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 (67) hide show
  1. package/.github/dependabot.yml +11 -0
  2. package/.github/workflows/main-docker.yml +45 -0
  3. package/.github/workflows/tag-docker.yml +54 -0
  4. package/Dockerfile +23 -0
  5. package/LICENSE +661 -0
  6. package/README.md +82 -0
  7. package/bots/index.js +7 -0
  8. package/docs/activitypub.bot.drawio +110 -0
  9. package/index.js +23 -0
  10. package/lib/activitydistributor.js +263 -0
  11. package/lib/activityhandler.js +999 -0
  12. package/lib/activitypubclient.js +126 -0
  13. package/lib/activitystreams.js +41 -0
  14. package/lib/actorstorage.js +300 -0
  15. package/lib/app.js +173 -0
  16. package/lib/authorizer.js +133 -0
  17. package/lib/bot.js +44 -0
  18. package/lib/botcontext.js +520 -0
  19. package/lib/botdatastorage.js +87 -0
  20. package/lib/bots/donothing.js +11 -0
  21. package/lib/bots/ok.js +41 -0
  22. package/lib/digester.js +23 -0
  23. package/lib/httpsignature.js +195 -0
  24. package/lib/httpsignatureauthenticator.js +81 -0
  25. package/lib/keystorage.js +113 -0
  26. package/lib/microsyntax.js +140 -0
  27. package/lib/objectcache.js +48 -0
  28. package/lib/objectstorage.js +319 -0
  29. package/lib/remotekeystorage.js +116 -0
  30. package/lib/routes/collection.js +92 -0
  31. package/lib/routes/health.js +24 -0
  32. package/lib/routes/inbox.js +83 -0
  33. package/lib/routes/object.js +69 -0
  34. package/lib/routes/server.js +47 -0
  35. package/lib/routes/user.js +63 -0
  36. package/lib/routes/webfinger.js +36 -0
  37. package/lib/urlformatter.js +97 -0
  38. package/package.json +51 -0
  39. package/tests/activitydistributor.test.js +606 -0
  40. package/tests/activityhandler.test.js +2185 -0
  41. package/tests/activitypubclient.test.js +225 -0
  42. package/tests/actorstorage.test.js +261 -0
  43. package/tests/app.test.js +17 -0
  44. package/tests/authorizer.test.js +306 -0
  45. package/tests/bot.donothing.test.js +30 -0
  46. package/tests/bot.ok.test.js +101 -0
  47. package/tests/botcontext.test.js +674 -0
  48. package/tests/botdatastorage.test.js +87 -0
  49. package/tests/digester.test.js +56 -0
  50. package/tests/fixtures/bots.js +15 -0
  51. package/tests/httpsignature.test.js +200 -0
  52. package/tests/httpsignatureauthenticator.test.js +463 -0
  53. package/tests/keystorage.test.js +89 -0
  54. package/tests/microsyntax.test.js +122 -0
  55. package/tests/objectcache.test.js +133 -0
  56. package/tests/objectstorage.test.js +148 -0
  57. package/tests/remotekeystorage.test.js +76 -0
  58. package/tests/routes.actor.test.js +207 -0
  59. package/tests/routes.collection.test.js +434 -0
  60. package/tests/routes.health.test.js +41 -0
  61. package/tests/routes.inbox.test.js +135 -0
  62. package/tests/routes.object.test.js +519 -0
  63. package/tests/routes.server.test.js +69 -0
  64. package/tests/routes.webfinger.test.js +41 -0
  65. package/tests/urlformatter.test.js +164 -0
  66. package/tests/utils/digest.js +7 -0
  67. package/tests/utils/nock.js +276 -0
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # activitypub.bot
2
+
3
+ An ActivityPub server-side bot framework
4
+
5
+ activitypub.bot is [social bot](https://en.wikipedia.org/wiki/Social_bot) server
6
+ which helps developers create and deploy semi-autonomous actors on the
7
+ [ActivityPub](https://activitypub.rocks/) network. Unlike general-purpose social
8
+ networking servers, the bot software does not use a remote API like the
9
+ ActivityPub API or the Mastodon API. Instead, the bot software runs inside the
10
+ server, using an in-process API.
11
+
12
+ activitypub.bot was originally developed as sample code for [ActivityPub: Programming for the Social Web](https://evanp.me/activitypub-book/) (2024) from O'Reilly Media.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Security](#security)
17
+ - [Background](#background)
18
+ - [Install](#install)
19
+ - [Usage](#usage)
20
+ - [API](#api)
21
+ - [Contributing](#contributing)
22
+ - [License](#license)
23
+
24
+ ## Security
25
+
26
+ ### Any optional sections
27
+
28
+ ## Background
29
+
30
+ [Mastodon](https://joinmastodon.org/) and other ActivityPub servers implement bots using their API. This requires having a separate deployment for the API client, either as a long-running process, a cron job, or some other implementation.
31
+
32
+ This server, instead, deploys the bot code inside the server process. This simplifies the interactions between the bot and the server, with the downside that deploying a new bot requires re-deploying the server.
33
+
34
+ ## Install
35
+
36
+ The easiest way to install this server is using [Helm](https://helm.sh). See the [evanp/activitypub-bot-chart](https://github.com/evanp/activitypub-bot-chart) for instructions.
37
+
38
+ There is also a Docker image at [ghcr.io/evanp/activitypub-bot](https://ghcr.io/evanp/activitypub-bot).
39
+
40
+ It's also an [npm](https://npmjs.org/) package.
41
+
42
+ ## Usage
43
+
44
+ ```
45
+ ```
46
+
47
+ Note: The `license` badge image link at the top of this file should be updated with the correct `:user` and `:repo`.
48
+
49
+ ### Any optional sections
50
+
51
+ ## API
52
+
53
+ ### Any optional sections
54
+
55
+ ## More optional sections
56
+
57
+ ## Contributing
58
+
59
+ See [the contributing file](CONTRIBUTING.md)!
60
+
61
+ PRs accepted.
62
+
63
+ Small note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
64
+
65
+ ### Any optional sections
66
+
67
+ ## License
68
+
69
+ Copyright (C) 2023-2025 Evan Prodromou <evan@prodromou.name>
70
+
71
+ This program is free software: you can redistribute it and/or modify
72
+ it under the terms of the GNU Affero General Public License as published
73
+ by the Free Software Foundation, either version 3 of the License, or
74
+ (at your option) any later version.
75
+
76
+ This program is distributed in the hope that it will be useful,
77
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
78
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
79
+ GNU Affero General Public License for more details.
80
+
81
+ You should have received a copy of the GNU Affero General Public License
82
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
package/bots/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import DoNothingBot from '../lib/bots/donothing.js'
2
+ import OKBot from '../lib/bots/ok.js'
3
+
4
+ export default {
5
+ ok: new OKBot('ok'),
6
+ null: new DoNothingBot('null')
7
+ }
@@ -0,0 +1,110 @@
1
+ <mxfile host="app.diagrams.net" modified="2024-01-14T17:20:53.624Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0" etag="UDjGppbSRivOII49-YrU" version="22.1.16" type="github">
2
+ <diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">
3
+ <mxGraphModel dx="2074" dy="1057" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
4
+ <root>
5
+ <mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
6
+ <mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
7
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--0" value="BotServer" style="swimlane;fontStyle=2;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
8
+ <mxGeometry x="220" y="120" width="160" height="138" as="geometry">
9
+ <mxRectangle x="230" y="140" width="160" height="26" as="alternateBounds" />
10
+ </mxGeometry>
11
+ </mxCell>
12
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--1" value="Name" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
13
+ <mxGeometry y="26" width="160" height="26" as="geometry" />
14
+ </mxCell>
15
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--2" value="Phone Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
16
+ <mxGeometry y="52" width="160" height="26" as="geometry" />
17
+ </mxCell>
18
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--3" value="Email Address" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
19
+ <mxGeometry y="78" width="160" height="26" as="geometry" />
20
+ </mxCell>
21
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--4" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
22
+ <mxGeometry y="104" width="160" height="8" as="geometry" />
23
+ </mxCell>
24
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--5" value="Purchase Parking Pass" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
25
+ <mxGeometry y="112" width="160" height="26" as="geometry" />
26
+ </mxCell>
27
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--6" value="Activity" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
28
+ <mxGeometry x="120" y="360" width="160" height="138" as="geometry">
29
+ <mxRectangle x="130" y="380" width="160" height="26" as="alternateBounds" />
30
+ </mxGeometry>
31
+ </mxCell>
32
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--7" value="Student Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
33
+ <mxGeometry y="26" width="160" height="26" as="geometry" />
34
+ </mxCell>
35
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--8" value="Average Mark" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
36
+ <mxGeometry y="52" width="160" height="26" as="geometry" />
37
+ </mxCell>
38
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--9" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
39
+ <mxGeometry y="78" width="160" height="8" as="geometry" />
40
+ </mxCell>
41
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--10" value="Is Eligible To Enroll" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontStyle=4" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
42
+ <mxGeometry y="86" width="160" height="26" as="geometry" />
43
+ </mxCell>
44
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--11" value="Get Seminars Taken" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
45
+ <mxGeometry y="112" width="160" height="26" as="geometry" />
46
+ </mxCell>
47
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--12" value="" style="endArrow=block;endSize=10;endFill=0;shadow=0;strokeWidth=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--6" target="zkfFHV4jXpPFQw0GAbJ--0" edge="1">
48
+ <mxGeometry width="160" relative="1" as="geometry">
49
+ <mxPoint x="200" y="203" as="sourcePoint" />
50
+ <mxPoint x="200" y="203" as="targetPoint" />
51
+ </mxGeometry>
52
+ </mxCell>
53
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--13" value="Object" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
54
+ <mxGeometry x="330" y="360" width="160" height="70" as="geometry">
55
+ <mxRectangle x="340" y="380" width="170" height="26" as="alternateBounds" />
56
+ </mxGeometry>
57
+ </mxCell>
58
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--14" value="Salary" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--13" vertex="1">
59
+ <mxGeometry y="26" width="160" height="26" as="geometry" />
60
+ </mxCell>
61
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--15" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--13" vertex="1">
62
+ <mxGeometry y="52" width="160" height="8" as="geometry" />
63
+ </mxCell>
64
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--16" value="" style="endArrow=block;endSize=10;endFill=0;shadow=0;strokeWidth=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--13" target="zkfFHV4jXpPFQw0GAbJ--0" edge="1">
65
+ <mxGeometry width="160" relative="1" as="geometry">
66
+ <mxPoint x="210" y="373" as="sourcePoint" />
67
+ <mxPoint x="310" y="271" as="targetPoint" />
68
+ </mxGeometry>
69
+ </mxCell>
70
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--17" value="Bot" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
71
+ <mxGeometry x="508" y="120" width="160" height="216" as="geometry">
72
+ <mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
73
+ </mxGeometry>
74
+ </mxCell>
75
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--23" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
76
+ <mxGeometry y="26" width="160" height="8" as="geometry" />
77
+ </mxCell>
78
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--24" value="+ powerUp(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
79
+ <mxGeometry y="34" width="160" height="26" as="geometry" />
80
+ </mxCell>
81
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--25" value="+ powerDown(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
82
+ <mxGeometry y="60" width="160" height="26" as="geometry" />
83
+ </mxCell>
84
+ <mxCell id="dP12apakX7S5GaSoqi7g-0" value="+ onReply(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="zkfFHV4jXpPFQw0GAbJ--17">
85
+ <mxGeometry y="86" width="160" height="26" as="geometry" />
86
+ </mxCell>
87
+ <mxCell id="dP12apakX7S5GaSoqi7g-1" value="+ onMention(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="zkfFHV4jXpPFQw0GAbJ--17">
88
+ <mxGeometry y="112" width="160" height="26" as="geometry" />
89
+ </mxCell>
90
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--26" value="" style="endArrow=open;shadow=0;strokeWidth=1;rounded=0;endFill=1;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--0" target="zkfFHV4jXpPFQw0GAbJ--17" edge="1">
91
+ <mxGeometry x="0.5" y="41" relative="1" as="geometry">
92
+ <mxPoint x="380" y="192" as="sourcePoint" />
93
+ <mxPoint x="540" y="192" as="targetPoint" />
94
+ <mxPoint x="-40" y="32" as="offset" />
95
+ </mxGeometry>
96
+ </mxCell>
97
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--27" value="1" style="resizable=0;align=left;verticalAlign=bottom;labelBackgroundColor=none;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" connectable="0" vertex="1">
98
+ <mxGeometry x="-1" relative="1" as="geometry">
99
+ <mxPoint y="4" as="offset" />
100
+ </mxGeometry>
101
+ </mxCell>
102
+ <mxCell id="zkfFHV4jXpPFQw0GAbJ--28" value="0.." style="resizable=0;align=right;verticalAlign=bottom;labelBackgroundColor=none;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" connectable="0" vertex="1">
103
+ <mxGeometry x="1" relative="1" as="geometry">
104
+ <mxPoint x="-7" y="4" as="offset" />
105
+ </mxGeometry>
106
+ </mxCell>
107
+ </root>
108
+ </mxGraphModel>
109
+ </diagram>
110
+ </mxfile>
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import { makeApp } from './lib/app.js'
2
+
3
+ const DATABASE_URL = process.env.DATABASE_URL || 'sqlite::memory:'
4
+ const ORIGIN = process.env.ORIGIN || 'https://activitypubbot.test'
5
+ const PORT = process.env.PORT || 9000 // HAL
6
+ const BOTS_CONFIG_FILE = process.env.BOTS_CONFIG_FILE || './bots/index.js'
7
+ const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'test' ? 'silent' : 'info')
8
+
9
+ const bots = (await import(BOTS_CONFIG_FILE)).default
10
+
11
+ const app = await makeApp(DATABASE_URL, ORIGIN, bots, LOG_LEVEL)
12
+
13
+ const server = app.listen(parseInt(PORT), () => {
14
+ app.locals.logger.info(`Listening on port ${PORT}`)
15
+ })
16
+
17
+ process.on('SIGTERM', () => {
18
+ console.log('Received SIGTERM')
19
+ server.close(async () => {
20
+ await app.cleanup()
21
+ process.exit(0)
22
+ })
23
+ })
@@ -0,0 +1,263 @@
1
+ import assert from 'node:assert'
2
+ import as2 from './activitystreams.js'
3
+ import { LRUCache } from 'lru-cache'
4
+ import PQueue from 'p-queue'
5
+ import { setTimeout } from 'node:timers/promises'
6
+
7
+ export class ActivityDistributor {
8
+ static #MAX_CACHE_SIZE = 1000000
9
+ static #CONCURRENCY = 32
10
+ static #MAX_ATTEMPTS = 16
11
+ static #PUBLIC = [
12
+ 'https://www.w3.org/ns/activitystreams#Public',
13
+ 'as:Public',
14
+ 'Public'
15
+ ]
16
+
17
+ #client = null
18
+ #formatter = null
19
+ #actorStorage = null
20
+ #directInboxCache = null
21
+ #sharedInboxCache = null
22
+ #queue = null
23
+ #retryQueue = null
24
+ #logger = null
25
+
26
+ constructor (client, formatter, actorStorage, logger = null) {
27
+ this.#client = client
28
+ this.#formatter = formatter
29
+ this.#actorStorage = actorStorage
30
+ this.#logger = logger.child({ class: this.constructor.name })
31
+ this.#directInboxCache = new LRUCache({ max: ActivityDistributor.#MAX_CACHE_SIZE })
32
+ this.#sharedInboxCache = new LRUCache({ max: ActivityDistributor.#MAX_CACHE_SIZE })
33
+ this.#queue = new PQueue({ concurrency: ActivityDistributor.#CONCURRENCY })
34
+ this.#retryQueue = new PQueue()
35
+ }
36
+
37
+ async distribute (activity, username) {
38
+ const stripped = await this.#strip(activity)
39
+ const actorId = this.#formatter.format({ username })
40
+
41
+ const delivered = new Set()
42
+ const localDelivered = new Set()
43
+
44
+ for await (const recipient of this.#public(activity, username)) {
45
+ if (await this.#isLocal(recipient)) {
46
+ if (recipient !== actorId && !localDelivered.has(recipient)) {
47
+ localDelivered.add(recipient)
48
+ this.#queue.add(() =>
49
+ this.#deliverLocal(recipient, stripped, username))
50
+ }
51
+ } else {
52
+ const inbox = await this.#getInbox(recipient, username)
53
+ if (!inbox) {
54
+ this.#logger.warn({ id: recipient.id }, 'No inbox')
55
+ } else if (!delivered.has(inbox)) {
56
+ delivered.add(inbox)
57
+ this.#queue.add(() =>
58
+ this.#deliver(inbox, stripped, username)
59
+ )
60
+ }
61
+ }
62
+ }
63
+
64
+ for await (const recipient of this.#private(activity, username)) {
65
+ if (await this.#isLocal(recipient)) {
66
+ if (recipient !== actorId && !localDelivered.has(recipient)) {
67
+ localDelivered.add(recipient)
68
+ this.#queue.add(() =>
69
+ this.#deliverLocal(recipient, stripped, username))
70
+ }
71
+ } else {
72
+ const inbox = await this.#getDirectInbox(recipient, username)
73
+ if (!inbox) {
74
+ this.#logger.warn({ id: recipient.id }, 'No direct inbox')
75
+ } else if (!delivered.has(inbox)) {
76
+ delivered.add(inbox)
77
+ this.#queue.add(() =>
78
+ this.#deliver(inbox, stripped, username)
79
+ )
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ async onIdle () {
86
+ await this.#retryQueue.onIdle()
87
+ await this.#queue.onIdle()
88
+ }
89
+
90
+ async * #public (activity, username) {
91
+ const followers = this.#formatter.format({
92
+ username,
93
+ collection: 'followers'
94
+ })
95
+ for (const prop of ['to', 'cc', 'audience']) {
96
+ const p = activity.get(prop)
97
+ if (p) {
98
+ for (const value of p) {
99
+ const id = value.id
100
+ if (id === followers ||
101
+ ActivityDistributor.#PUBLIC.includes(id)) {
102
+ for await (const follower of this.#actorStorage.items(username, 'followers')) {
103
+ yield follower.id
104
+ }
105
+ } else {
106
+ yield id
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ async * #private (activity, username) {
114
+ const followers = this.#formatter.format({
115
+ username,
116
+ collection: 'followers'
117
+ })
118
+ for (const prop of ['bto', 'bcc']) {
119
+ const p = activity.get(prop)
120
+ if (p) {
121
+ for (const value of p) {
122
+ const id = value.id
123
+ if (id === followers ||
124
+ ActivityDistributor.#PUBLIC.includes(id)) {
125
+ for await (const follower of this.#actorStorage.items(username, 'followers')) {
126
+ yield follower.id
127
+ }
128
+ } else {
129
+ yield id
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ async #getInbox (actorId, username) {
137
+ assert.ok(actorId)
138
+ assert.equal(typeof actorId, 'string')
139
+ assert.ok(username)
140
+ assert.equal(typeof username, 'string')
141
+
142
+ let sharedInbox = this.#sharedInboxCache.get(actorId)
143
+
144
+ if (sharedInbox) {
145
+ return sharedInbox
146
+ }
147
+
148
+ const obj = await this.#client.get(actorId, username)
149
+
150
+ // Get the shared inbox if it exists
151
+
152
+ const endpoints = obj.get('endpoints')
153
+ if (endpoints) {
154
+ const firstEndpoint = Array.from(endpoints)[0]
155
+ const sharedInboxEndpoint = firstEndpoint.get('sharedInbox')
156
+ if (sharedInboxEndpoint) {
157
+ const firstSharedInbox = Array.from(sharedInboxEndpoint)[0]
158
+ sharedInbox = firstSharedInbox.id
159
+ this.#sharedInboxCache.set(actorId, sharedInbox)
160
+ return sharedInbox
161
+ }
162
+ }
163
+
164
+ let directInbox = this.#directInboxCache.get(actorId)
165
+ if (directInbox) {
166
+ return directInbox
167
+ }
168
+
169
+ if (!obj.inbox) {
170
+ return null
171
+ }
172
+ const inboxes = Array.from(obj.inbox)
173
+ if (inboxes.length === 0) {
174
+ return null
175
+ }
176
+ directInbox = inboxes[0].id
177
+ this.#directInboxCache.set(actorId, directInbox)
178
+ return directInbox
179
+ }
180
+
181
+ async #getDirectInbox (actorId, username) {
182
+ assert.ok(actorId)
183
+ assert.equal(typeof actorId, 'string')
184
+ assert.ok(username)
185
+ assert.equal(typeof username, 'string')
186
+ let directInbox = this.#directInboxCache.get(actorId)
187
+ if (directInbox) {
188
+ return directInbox
189
+ }
190
+
191
+ const obj = await this.#client.get(actorId, username)
192
+
193
+ if (!obj.inbox) {
194
+ return null
195
+ }
196
+ const inboxes = Array.from(obj.inbox)
197
+ if (inboxes.length === 0) {
198
+ return null
199
+ }
200
+ directInbox = inboxes[0].id
201
+ this.#directInboxCache.set(actorId, directInbox)
202
+ return directInbox
203
+ }
204
+
205
+ async #strip (activity) {
206
+ const exported = await activity.export({ useOriginalContext: true })
207
+ delete exported.bcc
208
+ delete exported.bto
209
+ return await as2.import(exported)
210
+ }
211
+
212
+ async #deliver (inbox, activity, username, attempt = 1) {
213
+ try {
214
+ await this.#client.post(inbox, activity, username)
215
+ this.#logInfo(`Delivered ${activity.id} to ${inbox}`)
216
+ } catch (error) {
217
+ if (!error.status) {
218
+ this.#logError(`Could not deliver ${activity.id} to ${inbox}: ${error.message}`)
219
+ this.#logError(error.stack)
220
+ } else if (error.status >= 300 && error.status < 400) {
221
+ this.#logError(`Unexpected redirect code delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}`)
222
+ } else if (error.status >= 400 && error.status < 500) {
223
+ this.#logError(`Bad request delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}`)
224
+ } else if (error.status >= 500 && error.status < 600) {
225
+ if (attempt >= ActivityDistributor.#MAX_ATTEMPTS) {
226
+ this.#logError(`Server error delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}; giving up after ${attempt} attempts`)
227
+ }
228
+ const delay = Math.round((2 ** (attempt - 1) * 1000) * (0.5 + Math.random()))
229
+ this.#logWarning(`Server error delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}; will retry in ${delay} ms (${attempt} of ${ActivityDistributor.#MAX_ATTEMPTS})`)
230
+ this.#retryQueue.add(() => setTimeout(delay).then(() => this.#deliver(inbox, activity, username, attempt + 1)))
231
+ }
232
+ }
233
+ }
234
+
235
+ #logError (message) {
236
+ if (this.#logger) {
237
+ this.#logger.error(message)
238
+ }
239
+ }
240
+
241
+ #logWarning (message) {
242
+ if (this.#logger) {
243
+ this.#logger.warn(message)
244
+ }
245
+ }
246
+
247
+ #logInfo (message) {
248
+ if (this.#logger) {
249
+ this.#logger.info(message)
250
+ }
251
+ }
252
+
253
+ #isLocal (id) {
254
+ return this.#formatter.isLocal(id)
255
+ }
256
+
257
+ async #deliverLocal (id, activity) {
258
+ const username = this.#formatter.getUserName(id)
259
+ if (username) {
260
+ await this.#actorStorage.addToCollection(username, 'inbox', activity)
261
+ }
262
+ }
263
+ }