feat(rust): chat titles, BetterChat group styles, the voice and popups (phase 17)
PLAN.md §33, D134-D143. Protocol 12. - Chat titles (D135-D137): per-server rules (stat, top N, text, colour) that rank the current wipe, and a mode (first | all | up to N). Worked out once in model/titles and read three ways: pushed whole to the game by a new titleSync loop (on change, restart or wipe), and on every leaderboard row as `titles`. Admin: PUT /servers/:id/titles. - Group styles (D138, D139): a site group may carry all twelve BetterChat fields (rust_perm_group_chat). They ride perm.sync with `expect` from the pushed ledger, which gains a value column; a field changed in game is a `chat-field` drift row with the game's value, adopted into the style or put back. A withdrawn style is one `chat-group` retirement, never for `default`, cleared from the ledger only once BetterChat removed it. - The voice (D140): one fleet setting naming a styled group; news and rust.announce chat lines carry its format and the plugin says them with no sender. Admin: GET/PUT /voice. - Popups (D141, D142): rust.announce gains `delivery` (still version 1, from rust.options.delivery); each server gains news_delivery beside the news switch; `popup-unavailable` is not retried. - GET /servers/:id/integrations reads, live, which optional mods a server has loaded. README lists BetterChat and PopupNotifications as optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -199,7 +199,15 @@ async function describe() {
|
||||
// On this page because it is the one that lists every server with a setting
|
||||
// of its own, and it answers the same kind of question — what a server shows.
|
||||
news: {
|
||||
servers: servers.map((s) => ({ id: s.id, name: s.name, enabled: Boolean(s.enabled), on: Boolean(Number(s.announceNews)) })),
|
||||
servers: servers.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
enabled: Boolean(s.enabled),
|
||||
on: Boolean(Number(s.announceNews)),
|
||||
// D142: where the post goes when the switch is on. A word this build
|
||||
// does not know reads as chat, which every server can do.
|
||||
delivery: s.newsDelivery === 'popup' ? 'popup' : 'chat',
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -214,7 +222,7 @@ async function describe() {
|
||||
* Resolves `{ ok, changed }`, or `{ ok: false, status, message }` — a refusal is a
|
||||
* sentence the page can show.
|
||||
*/
|
||||
async function update({ fleet, servers, clanRoster, news } = {}, actor = null) {
|
||||
async function update({ fleet, servers, clanRoster, news, newsDelivery } = {}, actor = null) {
|
||||
if (fleet !== undefined && !isAudience(fleet)) {
|
||||
return { ok: false, status: 400, message: `"${fleet}" is not an audience. Choose one of: ${AUDIENCES.join(', ')}.` }
|
||||
}
|
||||
@@ -249,6 +257,17 @@ async function update({ fleet, servers, clanRoster, news } = {}, actor = null) {
|
||||
}
|
||||
}
|
||||
|
||||
const deliveryChanges = Object.entries(newsDelivery || {})
|
||||
for (const [id, value] of deliveryChanges) {
|
||||
if (value !== 'chat' && value !== 'popup') {
|
||||
return { ok: false, status: 400, message: `News goes to chat or to a popup on server ${id}, not "${value}".` }
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if ((await db.getServerPresence(id)) === undefined) {
|
||||
return { ok: false, status: 404, message: `There is no server called ${id}.` }
|
||||
}
|
||||
}
|
||||
|
||||
const userId = actor && actor.id != null ? actor.id : null
|
||||
|
||||
if (fleet !== undefined) await db.setSetting(PRESENCE_KEY, fleet, userId)
|
||||
@@ -261,6 +280,10 @@ async function update({ fleet, servers, clanRoster, news } = {}, actor = null) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db.setServerNews(id, on)
|
||||
}
|
||||
for (const [id, delivery] of deliveryChanges) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db.setServerNewsDelivery(id, delivery)
|
||||
}
|
||||
|
||||
// What was written, for the controller's audit row. Recorded there rather than
|
||||
// here because the activity log takes the REQUEST (who, from where), and a
|
||||
@@ -272,6 +295,7 @@ async function update({ fleet, servers, clanRoster, news } = {}, actor = null) {
|
||||
...(clanRoster !== undefined ? { clanRoster } : {}),
|
||||
servers: Object.fromEntries(changes.map(([id, value]) => [id, value === null ? 'inherit' : value])),
|
||||
...(newsChanges.length ? { news: Object.fromEntries(newsChanges) } : {}),
|
||||
...(deliveryChanges.length ? { newsDelivery: Object.fromEntries(deliveryChanges) } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user