// ── Town Cryer News gump sync (Protocol 2.1) ─────────────────────────────── // // Keeps the in-game Town Cryer *News* gump in sync with the site's published // news posts. Distinct from the scrolling town-crier lines (that's a one-shot // announce leg in announceWorker); this is a STATE SYNC — an article stays in the // gump while its post is published news, and is pulled when the post is // unpublished/deleted/re-categorised. // // The website is the source of truth. POST /news is idempotent (re-post replaces), // so a refresh or a reconnect re-assert is safe. Every call is best-effort and // never throws — a sidecar/shard hiccup must never break saving or deleting a // post. Reliability comes from reassertAll() on every WS (re)connect // (uoLinkSocket.backfill), which re-pushes the current published set silently and // closes the gap if an earlier live push failed. const posts = require('../model/posts/posts.model') const uoLinkClient = require('./uoLinkClient') const settings = require('../model/settings/settings.model') const { deriveExcerpt } = require('./sanitizeHtml') const log = require('./logger')('news-gump') const MAX_TITLE = 120 const MAX_BODY = 900 function baseUrl() { return (process.env.APP_BASE_URL || 'http://localhost:5173').replace(/\/+$/, '') } function clamp(value, max) { const s = String(value == null ? '' : value).replace(/\s+/g, ' ').trim() return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…` } // A post belongs in the gump exactly when it is published AND in the news category. function inGump(post) { return Boolean(post && post.published && post.category === 'news') } // Optional UO gump image id for news articles (a shard art id), from the // `news_gump_image` setting. Omitted → the sidecar uses a neutral scroll. async function gumpImage() { try { const raw = await settings.get('news_gump_image') const n = Number(raw) return Number.isInteger(n) && n > 0 ? n : undefined } catch { return undefined } } // Build the in-game News article from a post. Body is a compact gump-HTML block // (title centred + a plain-text excerpt) rather than the post's full rich HTML — // the UO gump only supports a small HTML subset, so we keep it predictable. The // "more info" URL is the public news list (news posts have no per-post route). async function buildArticle(post, { announce = true } = {}) { const title = clamp(post.title, MAX_TITLE) const excerpt = clamp(post.excerpt || deriveExcerpt(post.body, MAX_BODY) || '', MAX_BODY) const body = excerpt ? `