@evanp/activitypub-bot 0.8.0 → 0.11.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 (56) hide show
  1. package/.github/workflows/main.yml +34 -0
  2. package/.github/workflows/{tag-docker.yml → tag.yml} +57 -5
  3. package/.nvmrc +1 -0
  4. package/Dockerfile +11 -16
  5. package/README.md +262 -12
  6. package/activitypub-bot.js +68 -0
  7. package/lib/activitydeliverer.js +260 -0
  8. package/lib/activityhandler.js +14 -0
  9. package/lib/activitypubclient.js +52 -1
  10. package/lib/activitystreams.js +31 -0
  11. package/lib/actorstorage.js +18 -28
  12. package/lib/app.js +18 -7
  13. package/lib/bot.js +7 -0
  14. package/lib/botcontext.js +62 -0
  15. package/lib/botdatastorage.js +0 -13
  16. package/lib/botfactory.js +24 -0
  17. package/lib/botmaker.js +23 -0
  18. package/lib/keystorage.js +7 -24
  19. package/lib/migrations/001-initial.js +107 -0
  20. package/lib/migrations/index.js +28 -0
  21. package/lib/objectcache.js +4 -1
  22. package/lib/objectstorage.js +0 -36
  23. package/lib/remotekeystorage.js +0 -24
  24. package/lib/routes/collection.js +6 -2
  25. package/lib/routes/inbox.js +7 -20
  26. package/lib/routes/sharedinbox.js +54 -0
  27. package/lib/routes/user.js +11 -5
  28. package/lib/routes/webfinger.js +11 -2
  29. package/lib/urlformatter.js +8 -0
  30. package/package.json +18 -11
  31. package/tests/activitydistributor.test.js +3 -3
  32. package/tests/activityhandler.test.js +96 -5
  33. package/tests/activitypubclient.test.js +115 -130
  34. package/tests/actorstorage.test.js +26 -4
  35. package/tests/authorizer.test.js +3 -8
  36. package/tests/botcontext.test.js +109 -63
  37. package/tests/botdatastorage.test.js +3 -2
  38. package/tests/botfactory.provincebotfactory.test.js +430 -0
  39. package/tests/fixtures/bots.js +13 -1
  40. package/tests/fixtures/eventloggingbot.js +57 -0
  41. package/tests/fixtures/provincebotfactory.js +53 -0
  42. package/tests/httpsignature.test.js +3 -4
  43. package/tests/httpsignatureauthenticator.test.js +3 -3
  44. package/tests/keystorage.test.js +37 -2
  45. package/tests/microsyntax.test.js +3 -2
  46. package/tests/objectstorage.test.js +4 -3
  47. package/tests/remotekeystorage.test.js +10 -8
  48. package/tests/routes.actor.test.js +7 -0
  49. package/tests/routes.collection.test.js +0 -1
  50. package/tests/routes.inbox.test.js +1 -0
  51. package/tests/routes.object.test.js +44 -38
  52. package/tests/routes.sharedinbox.test.js +473 -0
  53. package/tests/routes.webfinger.test.js +27 -0
  54. package/tests/utils/nock.js +250 -27
  55. package/.github/workflows/main-docker.yml +0 -45
  56. package/index.js +0 -23
@@ -0,0 +1,34 @@
1
+ name: Run tests on main
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+ packages: write
12
+
13
+ jobs:
14
+
15
+ runTests:
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ matrix:
19
+ node-version: [20, 22, 24, 25]
20
+ steps:
21
+
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: actions/setup-node@v4
25
+ with:
26
+ node-version: ${{ matrix.node-version }}
27
+ cache: npm
28
+ cache-dependency-path: package-lock.json
29
+
30
+ - name: Install dependencies
31
+ run: npm ci
32
+
33
+ - name: Run tests
34
+ run: npm run test
@@ -5,8 +5,58 @@ on:
5
5
  tags:
6
6
  - 'v[0-9]+.[0-9]+.[0-9]+'
7
7
 
8
+ permissions:
9
+ contents: read
10
+ packages: write
11
+
8
12
  jobs:
13
+
14
+ runTests:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ matrix:
18
+ node-version: [20, 22, 24, 25]
19
+ steps:
20
+
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: ${{ matrix.node-version }}
26
+ cache: npm
27
+ cache-dependency-path: package-lock.json
28
+
29
+ - name: Install dependencies
30
+ run: npm ci
31
+
32
+ - name: Run tests
33
+ run: npm run test
34
+
35
+ publish-npm:
36
+ needs: runTests
37
+ runs-on: ubuntu-latest
38
+
39
+ permissions:
40
+ contents: read
41
+ id-token: write
42
+
43
+ steps:
44
+ - name: Checkout code
45
+ uses: actions/checkout@v4
46
+
47
+ - name: Set up Node.js
48
+ uses: actions/setup-node@v4
49
+ with:
50
+ node-version: 25
51
+ registry-url: https://registry.npmjs.org
52
+ cache: npm
53
+ cache-dependency-path: package-lock.json
54
+
55
+ - name: Publish to NPM
56
+ run: npm publish --access public --provenance
57
+
9
58
  build-and-push:
59
+ needs: publish-npm
10
60
  runs-on: ubuntu-latest
11
61
 
12
62
  permissions:
@@ -15,10 +65,10 @@ jobs:
15
65
 
16
66
  steps:
17
67
  - name: Checkout code
18
- uses: actions/checkout@v3
68
+ uses: actions/checkout@v4
19
69
 
20
70
  - name: Set up Docker Buildx
21
- uses: docker/setup-buildx-action@v2
71
+ uses: docker/setup-buildx-action@v3
22
72
 
23
73
  - name: Extract version components from tag
24
74
  id: get_version
@@ -33,22 +83,24 @@ jobs:
33
83
  echo "MAJOR_MINOR_VERSION=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
34
84
 
35
85
  - name: Login to GitHub Container Registry
36
- uses: docker/login-action@v2
86
+ uses: docker/login-action@v3
37
87
  with:
38
88
  registry: ghcr.io
39
89
  username: ${{ github.actor }}
40
90
  password: ${{ secrets.GITHUB_TOKEN }}
41
91
 
42
92
  - name: Build and push Docker image
43
- uses: docker/build-push-action@v4
93
+ uses: docker/build-push-action@v5
44
94
  with:
45
95
  context: .
46
96
  push: true
47
97
  platforms: linux/amd64,linux/arm64
98
+ build-args: |
99
+ PACKAGE_VERSION=${{ steps.get_version.outputs.FULL_VERSION }}
48
100
  tags: |
49
101
  ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.FULL_VERSION }}
50
102
  ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.MAJOR_MINOR_VERSION }}
51
103
  ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.MAJOR_VERSION }}
52
104
  ghcr.io/${{ github.repository }}:${{ github.sha }}
53
105
  cache-from: type=gha
54
- cache-to: type=gha,mode=max
106
+ cache-to: type=gha,mode=max
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 24
package/Dockerfile CHANGED
@@ -1,23 +1,18 @@
1
- FROM node:20-alpine AS builder
1
+ FROM node:24-alpine
2
2
 
3
3
  WORKDIR /app
4
4
 
5
- RUN apk add --no-cache python3 make g++ py3-setuptools
6
-
7
- COPY package.json package-lock.json ./
8
- RUN npm ci
9
-
10
- COPY index.js .
11
- COPY lib lib
12
- COPY bots bots
13
- COPY README.md .
14
-
15
- FROM node:20-alpine
5
+ RUN apk add --no-cache libstdc++ sqlite sqlite-libs
16
6
 
17
- WORKDIR /app
7
+ ARG PACKAGE_VERSION
18
8
 
19
- RUN apk add --no-cache libstdc++ sqlite sqlite-libs
9
+ ENV DATABASE_URL \
10
+ ORIGIN \
11
+ PORT \
12
+ BOTS_CONFIG_FILE \
13
+ LOG_LEVEL
20
14
 
21
- COPY --from=builder /app/ ./
15
+ RUN npm init -y \
16
+ && npm install --omit=dev @evanp/activitypub-bot@${PACKAGE_VERSION:-latest}
22
17
 
23
- CMD ["npm", "start"]
18
+ CMD ["npx", "activitypub-bot"]
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  An ActivityPub server-side bot framework
4
4
 
5
- activitypub.bot is [social bot](https://en.wikipedia.org/wiki/Social_bot) server
5
+ activitypub.bot is a [social bot](https://en.wikipedia.org/wiki/Social_bot) server
6
6
  which helps developers create and deploy semi-autonomous actors on the
7
7
  [ActivityPub](https://activitypub.rocks/) network. Unlike general-purpose social
8
8
  networking servers, the bot software does not use a remote API like the
@@ -23,7 +23,7 @@ activitypub.bot was originally developed as sample code for [ActivityPub: Progra
23
23
 
24
24
  ## Security
25
25
 
26
- ### Any optional sections
26
+ Please use the form at [https://github.com/evanp/activitypub-bot/security](https://github.com/evanp/activitypub-bot/security) to report a vulnerability privately.
27
27
 
28
28
  ## Background
29
29
 
@@ -37,36 +37,286 @@ The easiest way to install this server is using [Helm](https://helm.sh). See the
37
37
 
38
38
  There is also a Docker image at [ghcr.io/evanp/activitypub-bot](https://ghcr.io/evanp/activitypub-bot).
39
39
 
40
- It's also an [npm](https://npmjs.org/) package.
40
+ It's also an [npm](https://npmjs.org/) package, [@evanp/activitypub-bot](https://www.npmjs.com/package/@evanp/activitypub-bot).
41
41
 
42
42
  ## Usage
43
43
 
44
+ The server works as an ActivityPub server; bots appear as ActivityPub "actors".
45
+
46
+ You can run it using `npx activitypub-bot`.
47
+
48
+ ### Command-line arguments
49
+
50
+ The program can be configured with command-line arguments.
51
+
52
+ ```sh
53
+ Usage: activitypub-bot [options]
54
+
55
+ Options:
56
+ --database-url <url> Database connection URL
57
+ --origin <url> Public origin URL for the server
58
+ --port <number> Port to listen on
59
+ --bots-config-file <path> Path to bots config module
60
+ --log-level <level> Log level (e.g., info, debug)
61
+ -h, --help Show this help
44
62
  ```
63
+
64
+ #### --database-url
65
+
66
+ A [sequelize](https://sequelize.org) database URI for storing the server data. The default is 'sqlite::memory', which will store data in memory and lose the data when the process shuts down; you probably don't want that. The server comes with Postgres, MySQL, and SQLite libraries; you might need to install libraries if you're using some other dialect.
67
+
68
+ The URI format varies by database backend; see [Postgres](https://www.postgresql.org/docs/current/libpq-connect.html), [MySQL](https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html), or [SQLite](https://sqlite.org/uri.html).
69
+
70
+ The server creates and alters tables at runtime; whatever user you use to connect should have rights to do those things.
71
+
72
+ If unspecified, the server uses the environment variable `DATABASE_URL`.
73
+
74
+ #### --origin
75
+
76
+ The [origin](https://developer.mozilla.org/en-US/docs/Web/API/URL/origin) (protocol + hostname) for the server. This will only be used for formatting IDs, not for running the server. Use this if you're running the server
77
+ behind a load balancer or inside a Kubernetes cluster.
78
+
79
+ Falls back to the `ORIGIN` environment variable. The default is 'https://activitypubbot.test', which doesn't work and probably isn't what you want.
80
+
81
+ #### --port
82
+
83
+ The [port](https://en.wikipedia.org/wiki/Port_(computer_networking)) number to listen on. This is only used for connection; URLs are created using [--origin](#--origin). Must be an integer number between 1 and 65535; the default is 9000.
84
+
85
+ Falls back to the `PORT` environment variable.
86
+
87
+ #### --log-level
88
+
89
+ The minimum [pino](https://getpino.io) [log level](https://getpino.io/#/docs/api?id=logger-level) to output. This can be `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `silent`. Whatever the log level is, messages with
90
+ lower log levels won't appear in the logs. For example, if the log level is `info`, debug and trace log messages will be silently dropped. `silent` turns off logging altogether.
91
+
92
+ Falls back to `LOG_LEVEL` environment variable. The default is `info` (or `silent` when running unit tests).
93
+
94
+ #### --bots-config-file
95
+
96
+ The path to the [config file](#config-file) for this server, which defines the usernames and code for the bots for this server.
97
+
98
+ Falls back to `BOTS_CONFIG_FILE` environment variable. The default is to use the shipped default bot config file, which defines an [OKBot](#okbot) named `ok` and a [DoNothingBot](#donothingbot) named `null`.
99
+
100
+ ### Config file
101
+
102
+ The config file defines the bots provided by this server.
103
+
104
+ The config file is implemented as a JavaScript module. It should export a single object mapping a string name for the bot to an instance of a class that implements the [Bot](#bot) or [BotFactory](#botfactory) interface.
105
+
106
+ For example, the default config file declares two bot accounts: an [OKBot](#okbot) named `ok` and a [DoNothingBot](#donothingbot) named `null`.
107
+
108
+ ```js
109
+ import DoNothingBot from '../lib/bots/donothing.js'
110
+ import OKBot from '../lib/bots/ok.js'
111
+
112
+ export default {
113
+ ok: new OKBot('ok'),
114
+ null: new DoNothingBot('null')
115
+ }
45
116
  ```
46
117
 
47
- Note: The `license` badge image link at the top of this file should be updated with the correct `:user` and `:repo`.
118
+ For another example, the `ProvinceBotFactory` defines bots for all the Canadian provinces and territories ('ab' for Alberta, 'bc' for British Columbia, and so on). The special `*` value is used for identifying a bot factory (only one allowed per server). Named bots will be identified first, and the bot factory will be used as a fallback.
48
119
 
49
- ### Any optional sections
120
+ ```js
121
+ import ProvinceBotFactory from './provincebotfactory.js'
122
+ import DoNothingBot from '../../lib/bots/donothing.js'
123
+
124
+ export default {
125
+ 'other', new DoNothingBot('other')
126
+ '*', new ProvinceBotFactory()
127
+ }
128
+ ```
129
+
130
+ ### Pre-installed bot classes
131
+
132
+ The following bot classes are pre-installed with the server.
133
+
134
+ #### OKBot
135
+
136
+ An *OKBot* instance will reply to any message that it's mentioned in with the constant string 'OK'.
137
+
138
+ #### DoNothingBot
139
+
140
+ A *DoNothingBot* instance will only do default stuff, like accepting follows.
50
141
 
51
142
  ## API
52
143
 
53
- ### Any optional sections
144
+ Custom bots can implement the [Bot](#bot) interface, which is easiest if you inherit from the `Bot` class.
54
145
 
55
- ## More optional sections
146
+ Alternatively, you can implement a whole class of bots with the [BotFactory](#botfactory) interface, inheriting from the `BotFactory` class.
56
147
 
57
- ## Contributing
148
+ Bots will receive a [BotContext](#botcontext) object at initialization. The BotContext is the main way to access data or execute activities.
149
+
150
+ ### Bot
151
+
152
+ The Bot interface has the following methods.
153
+
154
+ #### constructor (username)
155
+
156
+ The constructor; receives the `username` by default. Initialization should probably be deferred to the initialize() method. The default implementation stores the username.
157
+
158
+ #### async initialize (context)
159
+
160
+ Initializes the bot with the given [BotContext](#botcontext). The default implementation stores the context for later use.
161
+
162
+ #### get fullname ()
163
+
164
+ A [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) for the full name of the bot.
165
+
166
+ #### get description ()
167
+
168
+ A [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) for the bio of the bot.
169
+
170
+ #### get username ()
171
+
172
+ A [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) for the username of the bot. Should match the constructor argument.
173
+
174
+ #### get _context ()
175
+
176
+ A protected [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) for the context of the bot. (The default implementation stashes the context in a private variable, so this protected
177
+ getter is needed to retrieve it.)
178
+
179
+ #### async onMention (object, activity)
180
+
181
+ Called when the bot is mentioned in an incoming object. Can be used to implement conversational interfaces. `object` is the object of a `Create` activity, like a `Note`, that mentions the bot; it's represented in [activitystrea.ms](#activitystreams) format. `activity` is the activity itself.
182
+
183
+ #### async onFollow (actor, activity)
184
+
185
+ Called when the bot is followed by another actor. The first argument is the actor, and the second is the `Follow` activity itself. This method is called after the acceptance has been sent and the new
186
+ follower has been added to the `followers` collection.
187
+
188
+ #### async onLike (object, activity)
189
+
190
+ Called when one of the bot's objects is liked by another actor. The first argument is the object, and the second is the `Like` activity itself. This method is called after the like has been added to the
191
+ `likes` collection.
58
192
 
59
- See [the contributing file](CONTRIBUTING.md)!
193
+ #### async onPublic (activity)
194
+
195
+ Called when the server receives a public activity to its shared inbox. This can be used to skim through ActivityPub network content.
196
+
197
+ #### async onAnnounce (object, activity)
198
+
199
+ Called when one of the bot's objects is shared by another actor. The first argument is the object, and the second is the `Announce` activity itself. This method is called after the activity has been added to the `shares` collection.
200
+
201
+ ### BotFactory
202
+
203
+ The BotFactory interface lets you have a lot of bots that act in a similar way without declaring them explicitly in the bots config file.
204
+
205
+ #### async canCreate (username)
206
+
207
+ Can this factory create a bot with this username? Boolean response. For example, the `ProvinceBotFactory` checks that there is a matching 2-letter province code for the username.
208
+
209
+ #### async create (username)
210
+
211
+ Create a bot with this username and return it. The bot must implement the [Bot](#bot) interface.
212
+
213
+ #### async onPublic (activity)
214
+
215
+ When a public activity is received at the shared inbox of the server, this method is called for the bot factory.
216
+
217
+ ### BotContext
218
+
219
+ This is the bot's control panel for working with the rest of the server.
220
+
221
+ #### get botID ()
222
+
223
+ Returns the username of the bot this context was created for.
224
+
225
+ #### get logger ()
226
+
227
+ A [pino](https://getpino.io/) [Logger](https://getpino.io/#/docs/api?id=logger) instance to use for logging messages.
228
+
229
+ #### async setData (key, value)
230
+
231
+ There's a simple key-value data store for bots to keep private data. `key` must be a string (max 512 characters); `value` is any JavaScript value that can be serialized as JSON using [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
232
+
233
+ #### async getData (key)
234
+
235
+ Returns the data previously stored for this key. If it doesn't exist, throws a `NoSuchValueError`. Use `hasData(key)` to check for existence.
236
+
237
+ #### async deleteData (key)
238
+
239
+ Deletes the data stored for this key. There's no backup; it's just gone.
240
+
241
+ #### async hasData (key)
242
+
243
+ Checks to see if any data has been previously stored with this key; returns a boolean.
244
+
245
+ #### async getObject (id)
246
+
247
+ Given an [ActivityPub object identifier](https://www.w3.org/TR/activitypub/#obj-id), returns an [activitystrea.ms](#activitystreams) object.
248
+
249
+ #### async sendNote (content, { to, cc, bto, bcc, audience, inReplyTo, thread, context, conversation })
250
+
251
+ Sends an Activity Streams `Note` with the given `content`. The content will be converted to HTML safely, and transformed according to social microtext rules:
252
+
253
+ - `@username@server.example`: transformed into a link to a user, and a `Mention`
254
+ - `#hashtag`: transformed into a hashtag link, and a `Hashtag` is added to the Note object
255
+ - `https://example.com/` : transformed into a link
256
+
257
+ The optional additional parameters are strings used for ActivityPub properties of the object:
258
+
259
+ - `to`, `cc`, `bto`, `bcc`, `audience`: addressing properties
260
+ - `inReplyTo`: the object the note is in reply to
261
+ - `thread`, `context`, `conversation`: the thread the object is in
262
+
263
+ #### async sendReply (content, object)
264
+
265
+ A shortcut for sending a reply with `content` to the `object`. Extracts and configures the right addressing properties and threading properties from `object`, and passes them to `sendNote()`.
266
+
267
+ #### async likeObject (obj)
268
+
269
+ Sends a `Like` activity for the passed-in object in [activitystrea.ms](#activitystreams) form.
270
+
271
+ #### async unlikeObject (obj)
272
+
273
+ Sends an `Undo`/`Like` activity for the passed-in object in [activitystrea.ms](#activitystreams) form which was previously liked.
274
+
275
+ #### async announceObject (obj)
276
+
277
+ Sends an `Announce` activity for the passed-in object in [activitystrea.ms](#activitystreams) form to followers.
278
+
279
+ #### async followActor (actor)
280
+
281
+ Sends a `Follow` activity for the passed-in actor in [activitystrea.ms](#activitystreams) form.
282
+
283
+ #### async unfollowActor (actor)
284
+
285
+ Sends an `Undo`/`Follow` activity for the passed-in actor in [activitystrea.ms](#activitystreams) form.
286
+
287
+ #### async blockActor (actor)
288
+
289
+ Sends a `Block` activity for the passed-in actor in [activitystrea.ms](#activitystreams) form.
290
+
291
+ #### async unblockActor (actor)
292
+
293
+ Sends an `Undo`/`Block` activity for the passed-in actor in [activitystrea.ms](#activitystreams) form.
294
+
295
+ #### async toActorId (webfinger)
296
+
297
+ Gets the `id` of the [ActivityPub Actor](https://www.w3.org/TR/activitypub/#actors) with the given [WebFinger](https://en.wikipedia.org/wiki/WebFinger) identity.
298
+
299
+ #### async toWebfinger (actorId)
300
+
301
+ Gets the [WebFinger](https://en.wikipedia.org/wiki/WebFinger) identity of the [ActivityPub Actor](https://www.w3.org/TR/activitypub/#actors) with the given `id`.
302
+
303
+ ### activitystrea.ms
304
+
305
+ Activity Streams 2.0 objects are represented internally as [activitystrea.ms](https://www.npmjs.com/package/activitystrea.ms) library objects.
306
+
307
+ ## Contributing
60
308
 
61
309
  PRs accepted.
62
310
 
63
- Small note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
311
+ JavaScript code should use [JavaScript Standard Style](https://standardjs.com).
312
+
313
+ There is a test suite using the Node [test runner](https://nodejs.org/api/test.html#test-runner). If you add a new feature, add tests for it. If you find a bug and fix it, add a test to make sure it stays fixed.
64
314
 
65
- ### Any optional sections
315
+ If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
66
316
 
67
317
  ## License
68
318
 
69
- Copyright (C) 2023-2025 Evan Prodromou <evan@prodromou.name>
319
+ Copyright (C) 2023-2026 Evan Prodromou <evan@prodromou.name>
70
320
 
71
321
  This program is free software: you can redistribute it and/or modify
72
322
  it under the terms of the GNU Affero General Public License as published
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from 'node:util'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { dirname, resolve } from 'node:path'
6
+ import { makeApp } from './lib/app.js'
7
+
8
+ const { values } = parseArgs({
9
+ options: {
10
+ 'database-url': { type: 'string' },
11
+ origin: { type: 'string' },
12
+ port: { type: 'string' },
13
+ 'bots-config-file': { type: 'string' },
14
+ 'log-level': { type: 'string' },
15
+ help: { type: 'boolean', short: 'h' }
16
+ },
17
+ allowPositionals: false
18
+ })
19
+
20
+ if (values.help) {
21
+ console.log(`Usage: activitypub-bot [options]
22
+
23
+ Options:
24
+ --database-url <url> Database connection URL
25
+ --origin <url> Public origin URL for the server
26
+ --port <number> Port to listen on
27
+ --bots-config-file <path> Path to bots config module
28
+ --log-level <level> Log level (e.g., info, debug)
29
+ -h, --help Show this help
30
+ `)
31
+ process.exit(0)
32
+ }
33
+
34
+ const normalize = (value) => (value === '' ? undefined : value)
35
+ const parsePort = (value) => {
36
+ if (!value) return undefined
37
+ const parsed = Number.parseInt(value, 10)
38
+ return Number.isNaN(parsed) ? undefined : parsed
39
+ }
40
+
41
+ const baseDir = dirname(fileURLToPath(import.meta.url))
42
+ const DEFAULT_BOTS_CONFIG_FILE = resolve(baseDir, 'bots', 'index.js')
43
+
44
+ const DATABASE_URL = normalize(values['database-url']) || process.env.DATABASE_URL || 'sqlite::memory:'
45
+ const ORIGIN = normalize(values.origin) || process.env.ORIGIN || 'https://activitypubbot.test'
46
+ const PORT = parsePort(normalize(values.port)) || parsePort(process.env.PORT) || 9000 // HAL
47
+ const BOTS_CONFIG_FILE =
48
+ normalize(values['bots-config-file']) || process.env.BOTS_CONFIG_FILE || DEFAULT_BOTS_CONFIG_FILE
49
+ const LOG_LEVEL =
50
+ normalize(values['log-level']) ||
51
+ process.env.LOG_LEVEL ||
52
+ (process.env.NODE_ENV === 'test' ? 'silent' : 'info')
53
+
54
+ const bots = (await import(BOTS_CONFIG_FILE)).default
55
+
56
+ const app = await makeApp(DATABASE_URL, ORIGIN, bots, LOG_LEVEL)
57
+
58
+ const server = app.listen(parseInt(PORT), () => {
59
+ app.locals.logger.info(`Listening on port ${PORT}`)
60
+ })
61
+
62
+ process.on('SIGTERM', () => {
63
+ console.log('Received SIGTERM')
64
+ server.close(async () => {
65
+ await app.cleanup()
66
+ process.exit(0)
67
+ })
68
+ })