feat(news): auto-push published news to the in-game Town Cryer News gump (2.1)

Phase 4: sync the site's published news posts into the Protocol 2.1 News gump.

- uoLinkClient.postNews / deleteNews.
- utils/newsGump.js — a STATE SYNC (not a one-shot announce leg): an article
  stays in the gump while its post is published news and is pulled when it leaves
  that state. buildArticle renders a compact gump-HTML block (centred title +
  plain-text excerpt — the gump supports only a small HTML subset) with a
  "more info" link to /site/news and an optional gump image from the
  `news_gump_image` setting. Every call is best-effort / never-throws.
- Hooked into the posts pipeline alongside the existing announce enqueue:
  syncPost on create/update/publish (fresh publish announces; edits refresh
  silently; leaving published-news pulls the article), removePost on delete.
- reassertAll() runs in uoLinkSocket.backfill on every WS (re)connect —
  reconciles the gump to our source of truth and recovers any article whose
  original live push failed (silent, so a reconnect never re-proclaims old news).

Server 185/185, swagger regenerated. Refs .plans/protocol2-integration.md (Phase 4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 15:40:47 -05:00
parent 2957708bab
commit 55a3adea99
5 changed files with 218 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ const settings = require('../../../model/settings/settings.model')
const users = require('../../../model/users/users.model')
const activity = require('../../../model/activity/activity.model')
const announceJobs = require('../../../model/announceJobs/announceJobs.model')
const newsGump = require('../../../utils/newsGump')
const { cleanBody } = require('../../../utils/sanitizeHtml')
const log = require('../../../utils/logger')('admin')
@@ -22,6 +23,11 @@ const log = require('../../../utils/logger')('admin')
// enqueueIfNeeded swallows its own errors, so a pipeline hiccup can't break save.
async function announceIfNewlyPublished(post, transition) {
await announceJobs.enqueueIfNeeded(post, transition)
// Keep the in-game Town Cryer News gump in sync with the same transition: push
// the article when it becomes published news, refresh it silently on an edit,
// and pull it when it leaves published-news. Best-effort (never throws), so a
// sidecar hiccup never breaks saving a post — same guarantee as the enqueue.
await newsGump.syncPost(post, transition)
}
// ── Dashboard & site mode ─────────────────────────────────────────────
@@ -173,8 +179,11 @@ async function publishPost(req, res) {
async function deletePost(req, res) {
const id = Number(req.params.id)
try {
const current = await posts.getById(id)
await posts.remove(id)
await activity.log({ req, action: 'post.delete', detail: { id } })
// If it was live in the News gump, pull it (best-effort).
if (newsGump.inGump(current)) await newsGump.removePost(id)
return res.json({ id })
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })