@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.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/main-docker.yml +45 -0
- package/.github/workflows/tag-docker.yml +54 -0
- package/Dockerfile +23 -0
- package/LICENSE +661 -0
- package/README.md +82 -0
- package/bots/index.js +7 -0
- package/docs/activitypub.bot.drawio +110 -0
- package/index.js +23 -0
- package/lib/activitydistributor.js +263 -0
- package/lib/activityhandler.js +999 -0
- package/lib/activitypubclient.js +126 -0
- package/lib/activitystreams.js +41 -0
- package/lib/actorstorage.js +300 -0
- package/lib/app.js +173 -0
- package/lib/authorizer.js +133 -0
- package/lib/bot.js +44 -0
- package/lib/botcontext.js +520 -0
- package/lib/botdatastorage.js +87 -0
- package/lib/bots/donothing.js +11 -0
- package/lib/bots/ok.js +41 -0
- package/lib/digester.js +23 -0
- package/lib/httpsignature.js +195 -0
- package/lib/httpsignatureauthenticator.js +81 -0
- package/lib/keystorage.js +113 -0
- package/lib/microsyntax.js +140 -0
- package/lib/objectcache.js +48 -0
- package/lib/objectstorage.js +319 -0
- package/lib/remotekeystorage.js +116 -0
- package/lib/routes/collection.js +92 -0
- package/lib/routes/health.js +24 -0
- package/lib/routes/inbox.js +83 -0
- package/lib/routes/object.js +69 -0
- package/lib/routes/server.js +47 -0
- package/lib/routes/user.js +63 -0
- package/lib/routes/webfinger.js +36 -0
- package/lib/urlformatter.js +97 -0
- package/package.json +51 -0
- package/tests/activitydistributor.test.js +606 -0
- package/tests/activityhandler.test.js +2185 -0
- package/tests/activitypubclient.test.js +225 -0
- package/tests/actorstorage.test.js +261 -0
- package/tests/app.test.js +17 -0
- package/tests/authorizer.test.js +306 -0
- package/tests/bot.donothing.test.js +30 -0
- package/tests/bot.ok.test.js +101 -0
- package/tests/botcontext.test.js +674 -0
- package/tests/botdatastorage.test.js +87 -0
- package/tests/digester.test.js +56 -0
- package/tests/fixtures/bots.js +15 -0
- package/tests/httpsignature.test.js +200 -0
- package/tests/httpsignatureauthenticator.test.js +463 -0
- package/tests/keystorage.test.js +89 -0
- package/tests/microsyntax.test.js +122 -0
- package/tests/objectcache.test.js +133 -0
- package/tests/objectstorage.test.js +148 -0
- package/tests/remotekeystorage.test.js +76 -0
- package/tests/routes.actor.test.js +207 -0
- package/tests/routes.collection.test.js +434 -0
- package/tests/routes.health.test.js +41 -0
- package/tests/routes.inbox.test.js +135 -0
- package/tests/routes.object.test.js +519 -0
- package/tests/routes.server.test.js +69 -0
- package/tests/routes.webfinger.test.js +41 -0
- package/tests/urlformatter.test.js +164 -0
- package/tests/utils/digest.js +7 -0
- 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,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
|
+
}
|