@evanp/activitypub-bot 0.46.2 → 0.46.4

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,23 @@ and this project adheres to
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.46.4] - 2026-05-25
13
+
14
+ ### Added
15
+
16
+ - server_stats table to cache the calculations for nodeinfo.
17
+
18
+ ### Changed
19
+
20
+ - nodeinfo uses cached server stats
21
+ when available.
22
+
23
+ ## [0.46.3] - 2026-05-25
24
+
25
+ ### Fixed
26
+
27
+ - Inefficient index on actorcollectionpage.
28
+
12
29
  ## [0.46.2] - 2026-05-25
13
30
 
14
31
  ### Fixed
package/lib/app.js CHANGED
@@ -53,6 +53,7 @@ import { HTTPMessageSignature } from './httpmessagesignature.js'
53
53
  import { SignaturePolicyStorage } from './signaturepolicystorage.js'
54
54
  import { SafeFetcher } from './safefetcher.js'
55
55
  import { EndpointCache } from './endpointcache.js'
56
+ import { ServerStats } from './serverstats.js'
56
57
 
57
58
  const currentDir = dirname(fileURLToPath(import.meta.url))
58
59
  const DEFAULT_INDEX_FILENAME = resolve(currentDir, '..', 'web', 'index.html')
@@ -130,7 +131,7 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
130
131
  client,
131
132
  jobQueue
132
133
  )
133
-
134
+ const stats = new ServerStats(keyStorage, actorStorage, formatter.hostname, connection)
134
135
  // TODO: Make an endpoint for tagged objects
135
136
  const transformer = new Transformer(
136
137
  origin + '/tag/', client, fetcher, formatter
@@ -222,7 +223,8 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
222
223
  deliverer,
223
224
  deliveryWorkers,
224
225
  indexFileName,
225
- profileFileName
226
+ profileFileName,
227
+ stats
226
228
  }
227
229
 
228
230
  app.disable('x-powered-by')
@@ -0,0 +1,12 @@
1
+ export const id = '013-actor-outbox-createdat-username'
2
+
3
+ export async function up (connection, queryOptions = {}) {
4
+ await connection.query(`
5
+ DROP INDEX actorcollectionpage_outbox_createdat;
6
+ `, queryOptions)
7
+ await connection.query(`
8
+ CREATE INDEX actorcollectionpage_outbox_createdat_username
9
+ ON actorcollectionpage (createdat, username)
10
+ WHERE property = 'outbox';
11
+ `, queryOptions)
12
+ }
@@ -0,0 +1,17 @@
1
+ export const id = '014-server-stats'
2
+
3
+ export async function up (connection, queryOptions = {}) {
4
+ const tsType = (connection.getDialect() === 'postgres')
5
+ ? 'TIMESTAMPTZ'
6
+ : 'TIMESTAMP'
7
+ await connection.query(`
8
+ CREATE TABLE server_stats (
9
+ domain VARCHAR(256) PRIMARY KEY,
10
+ total_users INT NOT NULL DEFAULT 0,
11
+ active_monthly INT NOT NULL DEFAULT 0,
12
+ active_half_yearly INT NOT NULL DEFAULT 0,
13
+ created_at ${tsType} NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+ updated_at ${tsType} NOT NULL DEFAULT CURRENT_TIMESTAMP
15
+ )
16
+ `, queryOptions)
17
+ }
@@ -33,10 +33,11 @@ router.get('/.well-known/nodeinfo', async (req, res, next) => {
33
33
 
34
34
  router.get('/nodeinfo/:nodeinfoVersion', async (req, res, next) => {
35
35
  const { nodeinfoVersion } = req.params
36
- const { keyStorage, actorStorage } = req.app.locals
36
+ const { stats } = req.app.locals
37
37
  if (!VERSIONS.includes(nodeinfoVersion)) {
38
38
  throw new ProblemDetailsError(404, 'unsupported nodeinfo version')
39
39
  }
40
+ const data = await stats.get()
40
41
  res.status(200).json({
41
42
  version: nodeinfoVersion,
42
43
  software: {
@@ -51,9 +52,9 @@ router.get('/nodeinfo/:nodeinfoVersion', async (req, res, next) => {
51
52
  openRegistrations: false,
52
53
  usage: {
53
54
  users: {
54
- total: await keyStorage.count(),
55
- activeMonth: await actorStorage.activeUsers(30),
56
- activeHalfyear: await actorStorage.activeUsers(180)
55
+ total: data.totalUsers,
56
+ activeMonth: data.activeMonthly,
57
+ activeHalfyear: data.activeHalfYearly
57
58
  }
58
59
  },
59
60
  metadata: {}
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert'
2
+
3
+ const STATS_EXPIRY = 24 * 60 * 60 * 1000
4
+
5
+ export class ServerStats {
6
+ #keyStorage
7
+ #actorStorage
8
+ #domain
9
+ #connection
10
+
11
+ constructor (keyStorage, actorStorage, domain, connection) {
12
+ assert.strictEqual(typeof keyStorage, 'object')
13
+ assert.strictEqual(typeof actorStorage, 'object')
14
+ assert.strictEqual(typeof domain, 'string')
15
+ assert.strictEqual(typeof connection, 'object')
16
+
17
+ this.#keyStorage = keyStorage
18
+ this.#actorStorage = actorStorage
19
+ this.#domain = domain
20
+ this.#connection = connection
21
+ }
22
+
23
+ async get () {
24
+ let data = await this.#get()
25
+ if (!data || new Date() - data.updatedAt > STATS_EXPIRY) {
26
+ data = {
27
+ totalUsers: await this.#keyStorage.count(),
28
+ activeMonthly: await this.#actorStorage.activeUsers(30),
29
+ activeHalfYearly: await this.#actorStorage.activeUsers(180),
30
+ updatedAt: new Date()
31
+ }
32
+ await this.#update(data)
33
+ }
34
+ return data
35
+ }
36
+
37
+ async #update (data) {
38
+ await this.#connection.query(
39
+ `
40
+ INSERT INTO server_stats (domain, total_users, active_monthly, active_half_yearly)
41
+ VALUES (?, ?, ?, ?)
42
+ ON CONFLICT (domain) DO UPDATE
43
+ SET total_users = EXCLUDED.total_users,
44
+ active_monthly = EXCLUDED.active_monthly,
45
+ active_half_yearly = EXCLUDED.active_half_yearly,
46
+ updated_at = CURRENT_TIMESTAMP
47
+ `,
48
+ {
49
+ replacements: [
50
+ this.#domain,
51
+ data.totalUsers,
52
+ data.activeMonthly,
53
+ data.activeHalfYearly
54
+ ]
55
+ }
56
+ )
57
+ return data
58
+ }
59
+
60
+ async #get () {
61
+ const [rows] = await this.#connection.query(
62
+ `SELECT total_users, active_monthly, active_half_yearly, updated_at
63
+ FROM server_stats
64
+ WHERE domain = ?
65
+ ;`,
66
+ { replacements: [this.#domain] }
67
+ )
68
+
69
+ if (rows.length === 0) {
70
+ return null
71
+ } else {
72
+ return {
73
+ totalUsers: rows[0].total_users,
74
+ activeMonthly: rows[0].active_monthly,
75
+ activeHalfYearly: rows[0].active_half_yearly,
76
+ updatedAt: rows[0].updated_at
77
+ }
78
+ }
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.46.2",
3
+ "version": "0.46.4",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",