From 9443795dc9196a524ababb11f04d9d32f23fa858 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 31 Mar 2026 21:49:07 +0200 Subject: [PATCH] feat: wire settings into all consumers --- lib/batch-broadcast.js | 20 +++++++++++--------- lib/batch-refollow.js | 21 +++++++++++++-------- lib/inbox-handlers.js | 4 +++- lib/mastodon/routes/accounts.js | 5 +++-- lib/mastodon/routes/instance.js | 14 ++++++++------ lib/mastodon/routes/oauth.js | 9 ++++++--- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/lib/batch-broadcast.js b/lib/batch-broadcast.js index aae86ec..017e02c 100644 --- a/lib/batch-broadcast.js +++ b/lib/batch-broadcast.js @@ -4,9 +4,7 @@ * @module batch-broadcast */ import { logActivity } from "./activity-log.js"; - -const BATCH_SIZE = 25; -const BATCH_DELAY_MS = 5000; +import { getSettings } from "./settings.js"; /** * Broadcast an activity to all followers via batch delivery. @@ -29,6 +27,10 @@ export async function batchBroadcast({ label, objectUrl, }) { + const settings = await getSettings(collections); + const batchSize = settings.broadcastBatchSize; + const batchDelay = settings.broadcastBatchDelay; + const ctx = federation.createContext(new URL(publicationUrl), { handle, publicationUrl, @@ -54,11 +56,11 @@ export async function batchBroadcast({ console.info( `[ActivityPub] Broadcasting ${label} to ${uniqueRecipients.length} ` + - `unique inboxes (${followers.length} followers) in batches of ${BATCH_SIZE}`, + `unique inboxes (${followers.length} followers) in batches of ${batchSize}`, ); - for (let i = 0; i < uniqueRecipients.length; i += BATCH_SIZE) { - const batch = uniqueRecipients.slice(i, i + BATCH_SIZE); + for (let i = 0; i < uniqueRecipients.length; i += batchSize) { + const batch = uniqueRecipients.slice(i, i + batchSize); const recipients = batch.map((f) => ({ id: new URL(f.actorUrl), inboxId: new URL(f.inbox || f.sharedInbox), @@ -75,12 +77,12 @@ export async function batchBroadcast({ } catch (error) { failed += batch.length; console.warn( - `[ActivityPub] ${label} batch ${Math.floor(i / BATCH_SIZE) + 1} failed: ${error.message}`, + `[ActivityPub] ${label} batch ${Math.floor(i / batchSize) + 1} failed: ${error.message}`, ); } - if (i + BATCH_SIZE < uniqueRecipients.length) { - await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY_MS)); + if (i + batchSize < uniqueRecipients.length) { + await new Promise((resolve) => setTimeout(resolve, batchDelay)); } } diff --git a/lib/batch-refollow.js b/lib/batch-refollow.js index 583fdc8..1f4dd93 100644 --- a/lib/batch-refollow.js +++ b/lib/batch-refollow.js @@ -16,10 +16,8 @@ import { lookupWithSecurity } from "./lookup-helpers.js"; import { Follow } from "@fedify/fedify/vocab"; import { logActivity } from "./activity-log.js"; import { cacheGet, cacheSet } from "./redis-cache.js"; +import { getSettings } from "./settings.js"; -const BATCH_SIZE = 10; -const DELAY_PER_FOLLOW = 3_000; -const DELAY_BETWEEN_BATCHES = 30_000; const STARTUP_DELAY = 30_000; const RETRY_COOLDOWN = 60 * 60 * 1_000; // 1 hour const MAX_RETRIES = 3; @@ -104,7 +102,9 @@ export async function resumeBatchRefollow(options) { } await setJobState("running"); - _timer = setTimeout(() => processNextBatch(options), DELAY_BETWEEN_BATCHES); + const { collections: resumeCollections } = options; + const resumeSettings = await getSettings(resumeCollections); + _timer = setTimeout(() => processNextBatch(options), resumeSettings.refollowBatchDelay); console.info("[ActivityPub] Batch refollow: resumed"); } @@ -158,9 +158,14 @@ async function processNextBatch(options) { const state = await cacheGet(KV_KEY); if (state?.status !== "running") return; + const settings = await getSettings(collections); + const batchSize = settings.refollowBatchSize; + const delayPerFollow = settings.refollowDelay; + const delayBetweenBatches = settings.refollowBatchDelay; + // Claim a batch atomically: set source to "refollow:pending" const entries = []; - for (let i = 0; i < BATCH_SIZE; i++) { + for (let i = 0; i < batchSize; i++) { const doc = await collections.ap_following.findOneAndUpdate( { source: "import" }, { $set: { source: "refollow:pending" } }, @@ -172,7 +177,7 @@ async function processNextBatch(options) { // Also pick up retryable entries (failed but not permanently) const retryCutoff = new Date(Date.now() - RETRY_COOLDOWN).toISOString(); - const retrySlots = BATCH_SIZE - entries.length; + const retrySlots = batchSize - entries.length; for (let i = 0; i < retrySlots; i++) { const doc = await collections.ap_following.findOneAndUpdate( { @@ -211,14 +216,14 @@ async function processNextBatch(options) { for (const entry of entries) { await processOneFollow(options, entry); // Delay between individual follows - await sleep(DELAY_PER_FOLLOW); + await sleep(delayPerFollow); } // Update job state timestamp await setJobState("running"); // Schedule next batch - _timer = setTimeout(() => processNextBatch(options), DELAY_BETWEEN_BATCHES); + _timer = setTimeout(() => processNextBatch(options), delayBetweenBatches); } /** diff --git a/lib/inbox-handlers.js b/lib/inbox-handlers.js index f40ca46..72e7cd0 100644 --- a/lib/inbox-handlers.js +++ b/lib/inbox-handlers.js @@ -38,6 +38,7 @@ import { addNotification } from "./storage/notifications.js"; import { addMessage } from "./storage/messages.js"; import { fetchAndStorePreviews, fetchAndStoreQuote } from "./og-unfurl.js"; import { getFollowedTags } from "./storage/followed-tags.js"; +import { getSettings } from "./settings.js"; /** @type {string} ActivityStreams Public Collection constant */ const PUBLIC = "https://www.w3.org/ns/activitystreams#Public"; @@ -760,7 +761,8 @@ export async function handleCreate(item, collections, ctx, handle) { // Each ancestor is stored with isContext: true to distinguish from organic timeline items. if (inReplyTo) { try { - await fetchReplyChain(object, collections, authLoader, 5); + const settings = await getSettings(collections); + await fetchReplyChain(object, collections, authLoader, settings.replyChainDepth); } catch (error) { // Non-critical — incomplete context is acceptable console.warn("[inbox-handlers] Reply chain fetch failed:", error.message); diff --git a/lib/mastodon/routes/accounts.js b/lib/mastodon/routes/accounts.js index 8486110..f47cbbf 100644 --- a/lib/mastodon/routes/accounts.js +++ b/lib/mastodon/routes/accounts.js @@ -60,10 +60,11 @@ router.get("/api/v1/accounts/verify_credentials", tokenRequired, scopeRequired(" // ─── GET /api/v1/preferences ───────────────────────────────────────────────── router.get("/api/v1/preferences", tokenRequired, scopeRequired("read", "read:accounts"), (req, res) => { + const apSettings = req.app.locals.apSettings; res.json({ - "posting:default:visibility": "public", + "posting:default:visibility": apSettings?.defaultVisibility || "public", "posting:default:sensitive": false, - "posting:default:language": "en", + "posting:default:language": apSettings?.defaultLanguage || "en", "reading:expand:media": "default", "reading:expand:spoilers": false, }); diff --git a/lib/mastodon/routes/instance.js b/lib/mastodon/routes/instance.js index 5cd7d53..7b3f31d 100644 --- a/lib/mastodon/routes/instance.js +++ b/lib/mastodon/routes/instance.js @@ -17,6 +17,7 @@ router.get("/api/v2/instance", async (req, res, next) => { const domain = req.get("host"); const collections = req.app.locals.mastodonCollections; const pluginOptions = req.app.locals.mastodonPluginOptions || {}; + const apSettings = req.app.locals.apSettings; const profile = await collections.ap_profile.findOne({}); const contactAccount = profile @@ -44,7 +45,7 @@ router.get("/api/v2/instance", async (req, res, next) => { versions: {}, }, icon: [], - languages: ["en"], + languages: apSettings?.instanceLanguages || ["en"], configuration: { urls: { streaming: "", @@ -54,8 +55,8 @@ router.get("/api/v2/instance", async (req, res, next) => { max_pinned_statuses: 10, }, statuses: { - max_characters: 5000, - max_media_attachments: 4, + max_characters: apSettings?.maxCharacters || 5000, + max_media_attachments: apSettings?.maxMediaAttachments || 4, characters_reserved_per_url: 23, }, media_attachments: { @@ -116,6 +117,7 @@ router.get("/api/v1/instance", async (req, res, next) => { const domain = req.get("host"); const collections = req.app.locals.mastodonCollections; const pluginOptions = req.app.locals.mastodonPluginOptions || {}; + const apSettings = req.app.locals.apSettings; const profile = await collections.ap_profile.findOne({}); @@ -160,14 +162,14 @@ router.get("/api/v1/instance", async (req, res, next) => { domain_count: domainCount, }, thumbnail: profile?.icon || null, - languages: ["en"], + languages: apSettings?.instanceLanguages || ["en"], registrations: false, approval_required: true, invites_enabled: false, configuration: { statuses: { - max_characters: 5000, - max_media_attachments: 4, + max_characters: apSettings?.maxCharacters || 5000, + max_media_attachments: apSettings?.maxMediaAttachments || 4, characters_reserved_per_url: 23, }, media_attachments: { diff --git a/lib/mastodon/routes/oauth.js b/lib/mastodon/routes/oauth.js index 3965263..ab75105 100644 --- a/lib/mastodon/routes/oauth.js +++ b/lib/mastodon/routes/oauth.js @@ -502,13 +502,15 @@ router.post("/oauth/token", async (req, res, next) => { // Rotate: new access token + new refresh token const newAccessToken = randomHex(64); const newRefreshToken = randomHex(64); + const refreshTtlDaysRotate = req.app.locals.apSettings?.refreshTokenTtlDays || 90; + const refreshTtlMsRotate = refreshTtlDaysRotate * 24 * 3600 * 1000; await collections.ap_oauth_tokens.updateOne( { _id: existing._id }, { $set: { accessToken: newAccessToken, refreshToken: newRefreshToken, - refreshExpiresAt: new Date(Date.now() + 90 * 24 * 3600 * 1000), + refreshExpiresAt: new Date(Date.now() + refreshTtlMsRotate), }, $unset: { expiresAt: "" }, }, @@ -589,8 +591,9 @@ router.post("/oauth/token", async (req, res, next) => { // Generate access token and refresh token. // Access tokens do not expire (matching Mastodon behavior — valid until revoked). - // Refresh tokens expire after 90 days as a safety measure. - const REFRESH_TOKEN_TTL = 90 * 24 * 3600 * 1000; // 90 days + // Refresh tokens expire after a configurable number of days (default 90). + const refreshTtlDays = req.app.locals.apSettings?.refreshTokenTtlDays || 90; + const REFRESH_TOKEN_TTL = refreshTtlDays * 24 * 3600 * 1000; const accessToken = randomHex(64); const refreshToken = randomHex(64); await collections.ap_oauth_tokens.updateOne(