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:
@@ -51,7 +51,7 @@ function inScope(scope, serverId) {
|
||||
* trips per group or one join that repeats every group row once per member.
|
||||
*/
|
||||
async function overview() {
|
||||
const [groups, groupPermissions, members, grants, sync, drift, catalogue] = await Promise.all([
|
||||
const [groups, groupPermissions, members, grants, sync, drift, catalogue, groupChat] = await Promise.all([
|
||||
db.listGroups(),
|
||||
db.listGroupPermissions(),
|
||||
db.listGroupMembers(),
|
||||
@@ -59,9 +59,16 @@ async function overview() {
|
||||
db.listSync(),
|
||||
db.listDrift(),
|
||||
db.listCatalogue(),
|
||||
db.listGroupChat(),
|
||||
])
|
||||
|
||||
const byGroup = new Map(groups.map((group) => [group.name, { ...group, permissions: [], members: [] }]))
|
||||
const byGroup = new Map(groups.map((group) => [group.name, { ...group, permissions: [], members: [], chat: null }]))
|
||||
|
||||
// Phase 17: a group's BetterChat style, or null for a group without one.
|
||||
for (const [name, fields] of chatByGroup(groupChat)) {
|
||||
const group = byGroup.get(name)
|
||||
if (group) group.chat = fields
|
||||
}
|
||||
|
||||
for (const row of groupPermissions) {
|
||||
const group = byGroup.get(row.groupName)
|
||||
@@ -99,11 +106,23 @@ async function overview() {
|
||||
groups: [...byGroup.values()],
|
||||
grants: collapseGrants(grants),
|
||||
servers: sync.map(shapeSync),
|
||||
drift,
|
||||
drift: drift.map((row) => ({ ...row, detail: row.detail === undefined ? null : row.detail })),
|
||||
catalogue: catalogueByPermission(catalogue),
|
||||
}
|
||||
}
|
||||
|
||||
/** Style rows folded into one object per group: `name → { Field: value }`. */
|
||||
function chatByGroup(rows) {
|
||||
const out = new Map()
|
||||
|
||||
for (const row of rows || []) {
|
||||
if (!out.has(row.groupName)) out.set(row.groupName, {})
|
||||
out.get(row.groupName)[row.field] = row.value
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* One row per grant, not one per linked account.
|
||||
*
|
||||
@@ -278,13 +297,14 @@ async function forPlayer(userId, steamIds, serverRows) {
|
||||
* server is six times the queries for the same rows.
|
||||
*/
|
||||
async function readAuthored() {
|
||||
const [groups, groupPermissions, members, grants, links, runGrants] = await Promise.all([
|
||||
const [groups, groupPermissions, members, grants, links, runGrants, groupChat] = await Promise.all([
|
||||
db.listGroups(),
|
||||
db.listGroupPermissions(),
|
||||
db.listGroupMembers(),
|
||||
db.listGrants(),
|
||||
db.listLinks(),
|
||||
db.listRunGrants(),
|
||||
db.listGroupChat(),
|
||||
])
|
||||
|
||||
const steamIdsByUser = new Map()
|
||||
@@ -294,7 +314,7 @@ async function readAuthored() {
|
||||
steamIdsByUser.get(link.userId).push(link.steamId)
|
||||
}
|
||||
|
||||
return { groups, groupPermissions, members, grants, runGrants, steamIdsByUser }
|
||||
return { groups, groupPermissions, members, grants, runGrants, steamIdsByUser, groupChat }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,6 +441,24 @@ function buildDesired(serverId, authored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── A group's BetterChat style (phase 17, D138) ───────────────────────
|
||||
//
|
||||
// One ledger row per FIELD (`chat-field`, subject the group, object the
|
||||
// field), carrying its value: the diff that retires a style is the same
|
||||
// `pushed − desired` as everything else, and the value is what the next sync
|
||||
// sends as `expect`. The value is not in the row's identity — a changed value
|
||||
// is the same field pushed again, not a retirement.
|
||||
const chat = chatByGroup(authored.groupChat)
|
||||
|
||||
for (const group of scopedGroups) {
|
||||
const fields = chat.get(group.name)
|
||||
if (!fields) continue
|
||||
|
||||
for (const field of Object.keys(fields).sort()) {
|
||||
rows.push({ kind: 'chat-field', subject: group.name, object: field, value: fields[field] })
|
||||
}
|
||||
}
|
||||
|
||||
const creditRows = [...credits.entries()]
|
||||
.map(([key, count]) => {
|
||||
const bar = key.indexOf('|')
|
||||
@@ -435,6 +473,9 @@ function buildDesired(serverId, authored) {
|
||||
rank: group.rank,
|
||||
permissions: permissionsByGroup.get(group.name),
|
||||
members: membersByGroup.get(group.name),
|
||||
// The values only; `permSync` adds what each one expects to find, which
|
||||
// is per server and comes from the ledger.
|
||||
...(chat.has(group.name) ? { chat: chat.get(group.name) } : {}),
|
||||
})),
|
||||
grants: [...permissionsBySteamId.entries()].map(([steamId, permissions]) => ({
|
||||
steamId,
|
||||
@@ -450,8 +491,11 @@ function buildDesired(serverId, authored) {
|
||||
// Credits are in the digest, so a new reward or a revert pushes, but they are
|
||||
// NOT in `rows`: those are the pushed ledger's, and a use of a kit is not
|
||||
// something in the permission store to retire.
|
||||
//
|
||||
// A style field's VALUE goes into the digest the same way, since it is not in
|
||||
// the row's identity: a colour changed on the site must push.
|
||||
const hashed = [
|
||||
...rows,
|
||||
...rows.map((row) => (row.kind === 'chat-field' ? { ...row, object: `${row.object}=${row.value}` } : row)),
|
||||
...creditRows.map((c) => ({ kind: 'credit', subject: c.steamId, object: `${c.kit}#${c.count}` })),
|
||||
]
|
||||
|
||||
@@ -504,4 +548,5 @@ module.exports = {
|
||||
rowKey,
|
||||
collapseGrants,
|
||||
shapeSync,
|
||||
chatByGroup,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user