@evanp/activitypub-bot 0.46.5 → 0.48.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/CHANGELOG.md CHANGED
@@ -9,6 +9,28 @@ and this project adheres to
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.48.0] - 2026-05-28
13
+
14
+ ### Added
15
+
16
+ - /favicon.ico with a little robot head emoji
17
+ - very permissive /robots.txt
18
+
19
+ ## [0.47.1] - 2026-05-28
20
+
21
+ ### Fixed
22
+
23
+ - GroupBot, GroupBotFactory exported from module
24
+ - Unhandled jobs in BotContext, LitePubRelayClient tests
25
+
26
+ ## [0.47.0] - 2026-05-28
27
+
28
+ ### Added
29
+
30
+ - getLastActivity() method on BotContext
31
+ - GroupBot class
32
+ - GroupBotFactory class
33
+
12
34
  ## [0.46.5] - 2026-05-25
13
35
 
14
36
  ### Fixed
package/README.md CHANGED
@@ -240,6 +240,29 @@ A *LitePubRelayClientBot* can be the client of a LitePub (Pleroma) relay.
240
240
 
241
241
  A *LitePubRelayServerBot* will act as a relay server for remote Pleroma servers and other LitePub-relay-compliant servers.
242
242
 
243
+ #### GroupBot
244
+
245
+ A *GroupBot* is a simple [FEP
246
+ 1b12](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md)-compatible
247
+ group object. When mentioned, it will boost the mentioning object to all its followers.
248
+
249
+ ### Pre-installed factory classes
250
+
251
+ A [BotFactory](#botfactory) lets you create a whole class of bots that are
252
+ magically created as needed.
253
+
254
+ #### GroupBotFactory
255
+
256
+ This factory class lets you add a lot of groups at once. Just include this line
257
+ in your bot config:
258
+
259
+ ```javascript
260
+ '*': new GroupBotFactory()
261
+ ```
262
+
263
+ Any bot names that don't match one of the other lines in the bot config will be
264
+ passed to the group bot factory.
265
+
243
266
  ## API
244
267
 
245
268
  Custom bots can implement the [Bot](#bot) interface, which is easiest if you inherit from the `Bot` class.
@@ -517,6 +540,12 @@ bot can forward a public activity it received through a side-channel
517
540
  (e.g. a relay-forwarded `Announce`) to all local bots. Errors in any
518
541
  individual bot's `onPublic` are logged and do not interrupt the fanout.
519
542
 
543
+ #### async getLastActivity (type, object)
544
+
545
+ Get the id of the last activity that has the type `type` and the object
546
+ `object`. Great for double-checking if an object has been `Announce`d or
547
+ `Follow`ed before.
548
+
520
549
  #### async onIdle ()
521
550
 
522
551
  Resolves when the background distribution queue has drained. Intended for test code that needs to wait for outbound activities to finish being delivered before asserting on their effects.
package/lib/app.js CHANGED
@@ -34,6 +34,8 @@ import sharedInboxRouter from './routes/sharedinbox.js'
34
34
  import profileRouter from './routes/profile.js'
35
35
  import proxyRouter from './routes/proxy.js'
36
36
  import nodeinfoRouter from './routes/nodeinfo.js'
37
+ import robotsTxtRouter from './routes/robotstxt.js'
38
+ import faviconRouter from './routes/favicon.js'
37
39
  import { BotContext } from './botcontext.js'
38
40
  import { Transformer } from './microsyntax.js'
39
41
  import { HTTPSignatureAuthenticator } from './httpsignatureauthenticator.js'
@@ -329,6 +331,8 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
329
331
  app.use('/', webfingerRouter)
330
332
  app.use('/', proxyRouter)
331
333
  app.use('/', nodeinfoRouter)
334
+ app.use('/', robotsTxtRouter)
335
+ app.use('/', faviconRouter)
332
336
 
333
337
  // These should check HTTP Signature if provided
334
338
 
package/lib/botcontext.js CHANGED
@@ -549,6 +549,13 @@ export class BotContext {
549
549
  }))
550
550
  }
551
551
 
552
+ async getLastActivity (type, object) {
553
+ assert.strictEqual(typeof type, 'string')
554
+ assert.strictEqual(typeof object, 'object')
555
+ assert.ok(object !== null)
556
+ return await this.#actorStorage.getLastActivity(this.#botId, type, object)
557
+ }
558
+
552
559
  async #isInCollection (name, obj) {
553
560
  assert.ok(name)
554
561
  assert.equal(typeof name, 'string')
@@ -0,0 +1,36 @@
1
+ import Bot from '../bot.js'
2
+
3
+ const DEFAULT_DESCRIPTION = 'An echo group'
4
+
5
+ export default class GroupBot extends Bot {
6
+ constructor (username, options = {}) {
7
+ if (typeof username !== 'string') {
8
+ throw new Error('username must be a string')
9
+ }
10
+ if (typeof options !== 'object') {
11
+ throw new Error('options must be an object')
12
+ }
13
+ super(username,
14
+ {
15
+ fullname: `${username} Group`,
16
+ description: DEFAULT_DESCRIPTION,
17
+ ...options
18
+ })
19
+ }
20
+
21
+ get type () {
22
+ return 'Group'
23
+ }
24
+
25
+ async onMention (object, activity) {
26
+ const last = await this._context.getLastActivity('Announce', object)
27
+ if (last) {
28
+ this._context.logger.info(
29
+ { object: object.id, activity: activity.id, last },
30
+ 'Skipping re-announce of activity that was previously announced'
31
+ )
32
+ return
33
+ }
34
+ await this._context.announceObject(object)
35
+ }
36
+ }
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert'
2
+
3
+ import BotFactory from '../botfactory.js'
4
+ import GroupBot from './group.js'
5
+
6
+ const USERNAME_PATTERN = /^[a-z0-9]{1,64}$/
7
+
8
+ export default class GroupBotFactory extends BotFactory {
9
+ #options
10
+
11
+ constructor (options = {}) {
12
+ super()
13
+ this.#options = options
14
+ }
15
+
16
+ async canCreate (username) {
17
+ return USERNAME_PATTERN.test(username)
18
+ }
19
+
20
+ async create (username) {
21
+ assert.ok(username)
22
+ assert.ok(username.match(USERNAME_PATTERN))
23
+ const bot = new GroupBot(username, this.#options)
24
+ await bot.initialize(await this._context.duplicate(username))
25
+ return bot
26
+ }
27
+ }
package/lib/index.js CHANGED
@@ -9,3 +9,5 @@ export { default as FollowBackBot } from './bots/followback.js'
9
9
  export { default as LitePubRelayClientBot } from './bots/litepubrelayclient.js'
10
10
  export { default as LitePubRelayServerBot } from './bots/litepubrelayserver.js'
11
11
  export { default as LoggingBot } from './bots/logging.js'
12
+ export { default as GroupBot } from './bots/group.js'
13
+ export { default as GroupBotFactory } from './bots/groupfactory.js'
@@ -0,0 +1,16 @@
1
+ import { Router } from 'express'
2
+
3
+ const router = Router()
4
+
5
+ const FAVICON_ICO =
6
+ `<svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 100 100">
9
+ <text y=".9em" font-size="90">🤖</text>
10
+ </svg>`
11
+
12
+ router.get('/favicon.ico', async (req, res, next) => {
13
+ res.status(200).type('image/svg+xml').end(FAVICON_ICO)
14
+ })
15
+
16
+ export default router
@@ -0,0 +1,15 @@
1
+ import { Router } from 'express'
2
+
3
+ const router = Router()
4
+
5
+ const ROBOTS_TXT =
6
+ `# We are all bots here
7
+ User-agent: *
8
+ Disallow:
9
+ `
10
+
11
+ router.get('/robots.txt', async (req, res, next) => {
12
+ res.status(200).type('text/plain').end(ROBOTS_TXT)
13
+ })
14
+
15
+ export default router
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.46.5",
3
+ "version": "0.48.0",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",