Five read-only commands registered with api.registerSlashCommands: /status, /wipe, /top, /online and /clan (D126). Every refusal is private, and any answer narrower than public (online names, a clan roster) goes to the caller alone (D127). No command asks a sidecar. The next wipe (D128, D130): six nullable columns on rust_servers, a pure nextWipe(row, now) with the zone arithmetic through Intl, computed on every read. The public server shape gains nextWipe; the admin shape gains the stored schedule; PUT /admin/rust/servers/:id takes the six fields and writes them only when wipeRule is present. server/commands joins ci/bundle.json, which checkBundle caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
86 lines
3.3 KiB
JavaScript
86 lines
3.3 KiB
JavaScript
// ── `/top [stat] [server] [alltime]` — the leaderboard's top ten (D126) ─────
|
|
//
|
|
// Public at every setting, as on the web: the leaderboard's NAMES are public, and
|
|
// only `lastSeen` sits behind presence (a tally refreshes it every minute a player
|
|
// is on, so it is the Online tab by another name). This answer never carries a
|
|
// `lastSeen`, and never a Steam id — `events.leaderboard` is asked with
|
|
// `presence: false`, so the field is not there to leak.
|
|
|
|
const c = require('./common')
|
|
const events = require('../model/events/events.model')
|
|
|
|
/** Ten rows: a summary a person reads, not a table they scroll (§32.4 reading 2). */
|
|
const LIMIT = 10
|
|
|
|
// `stat`'s choices are fixed, so they are a `choices` list rather than free
|
|
// text. The value is the leaderboard's own sort key.
|
|
const STATS = {
|
|
kills: { sort: 'kills', label: 'kills', value: (r) => r.kills },
|
|
deaths: { sort: 'deaths', label: 'deaths', value: (r) => r.deaths },
|
|
npckills: { sort: 'npcKills', label: 'NPC kills', value: (r) => r.npcKills },
|
|
playtime: { sort: 'playtime', label: 'playtime', value: (r) => r.playtimeSec, format: hours },
|
|
}
|
|
|
|
function hours(sec) {
|
|
const h = Math.floor(sec / 3600)
|
|
const m = Math.floor((sec % 3600) / 60)
|
|
return h ? `${h}h ${m}m` : `${m}m`
|
|
}
|
|
|
|
async function handler({ options }) {
|
|
const now = Date.now()
|
|
const stat = STATS[(options && options.stat) || 'kills'] || STATS.kills
|
|
|
|
// A leaderboard is one server's. With several and none named the command asks
|
|
// rather than choosing one, privately — the question is for the caller.
|
|
const picked = c.pickServer(await c.listServers(now), options && options.server)
|
|
const refusal = c.pickRefusal(picked, { needOne: true })
|
|
if (refusal) return refusal
|
|
const { server } = picked
|
|
|
|
// The current wipe unless asked for all-time. A server that has not reported
|
|
// a wipe yet has nothing to scope to, and all-time is the honest answer.
|
|
const allTime = Boolean(options && options.alltime) || !server.wipeId
|
|
const rows = await events.leaderboard({
|
|
serverId: server.id,
|
|
wipeId: allTime ? null : server.wipeId,
|
|
sort: stat.sort,
|
|
limit: LIMIT,
|
|
presence: false,
|
|
})
|
|
|
|
const scope = allTime ? 'all time' : 'this wipe'
|
|
const title = `${server.name} — top ${stat.label}, ${scope}`
|
|
if (!rows.length) return { title, url: c.serverUrl(server), text: `Nobody is on the board for ${scope} yet.` }
|
|
|
|
const format = stat.format || String
|
|
return {
|
|
title,
|
|
url: c.serverUrl(server),
|
|
text: rows.map((r, i) => `${i + 1}. ${r.name || 'Unknown player'} — ${format(stat.value(r))}`).join('\n'),
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
name: 'top',
|
|
description: 'The top ten players on a Rust server by kills, deaths, NPC kills or playtime',
|
|
options: [
|
|
{
|
|
name: 'stat',
|
|
type: 'string',
|
|
description: 'What to rank by (kills when left out)',
|
|
required: false,
|
|
choices: [
|
|
{ name: 'Kills', value: 'kills' },
|
|
{ name: 'Deaths', value: 'deaths' },
|
|
{ name: 'NPC kills', value: 'npckills' },
|
|
{ name: 'Playtime', value: 'playtime' },
|
|
],
|
|
},
|
|
{ name: 'server', type: 'string', description: 'Server name or id (needed when there are several)', required: false },
|
|
{ name: 'alltime', type: 'boolean', description: 'Every wipe rather than the current one', required: false },
|
|
],
|
|
access: 'everyone',
|
|
handler,
|
|
}
|