// Read-only client for the main site's PUBLIC API (no shared secret — this is // the same unauthenticated data any visitor's browser can fetch). Used by // /wiki (search) and /announce (re-post an existing news item). Distinct from // botInternalClient.js, which is the shared-secret-gated server<->bot channel. const createLogger = require('../utils/logger') const log = createLogger('site-api') const BASE_URL = (process.env.SITE_PUBLIC_URL || 'http://localhost:3000/api/v1/public').replace(/\/+$/, '') const TIMEOUT_MS = 5000 async function call(path) { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS) try { const res = await fetch(`${BASE_URL}${path}`, { signal: controller.signal }) const data = await res.json().catch(() => null) // Public content routes 503 with this shape while the site is in // maintenance mode (see server/src/middleware/siteMode.js) — surface it // distinctly so commands can show a clear message instead of a generic error. if (res.status === 503 && data?.mode === 'maintenance') { return { ok: false, maintenance: true, message: data.message } } if (!res.ok) return { ok: false, error: `site responded ${res.status}` } return { ok: true, data } } catch (err) { log.warn('site API call failed', { path, message: err.message }) return { ok: false, error: err.message } } finally { clearTimeout(timeout) } } function getNewsPost(idOrSlug) { return call(`/posts/news/${encodeURIComponent(idOrSlug)}`) } function searchWiki(query) { return call(`/wiki?q=${encodeURIComponent(query)}`) } module.exports = { getNewsPost, searchWiki }