// ── `/online [server]` — how many, and who, where the caller may see it ───── // // The command with the gate. The org lead's rule (2026-09-22) is that nothing // names who is online by default: the COUNT is public and the NAMES reach the // server's presence audience (D42, per-server override D45), staff unless an // operator widened it. // // And D127 on top of it: **a caller inside a narrower-than-public audience gets // the names PRIVATELY.** The answer is posted where the command was run, and a // moderator's `/online` in a public channel would otherwise hand the roll call to // everyone reading the channel. Only a server whose audience is `public` posts // its names in the open. const c = require('./common') const events = require('../model/events/events.model') const visibility = require('../model/visibility/visibility.model') /** Fifty names, then "and N more" (§32.4 reading 2). */ const LIMIT = 50 // The link nudge, and only when it is TRUE — module-uo's rule. Linking a Discord // account to a site account earns `signed_in` and nothing above it; `staff` is a // role an operator grants. So a server that shows names to staff is not a reason // to tell anybody to link. function linkNudge(actor, required) { if (actor && actor.isLinked) return null if (required !== 'signed_in') return null return 'Link your Discord account on the site to see who is on — this server shows names to signed-in members.' } async function forServer(server, actor, level) { const required = await visibility.presenceFor(server.id) const visible = visibility.meets(level, required) const count = server.online ? server.players : 0 // Offline, or nobody on: there are no names to decide about, and saying who // was on when the server went down would be the presence board's last word // presented as now. if (!server.online) return { text: `${server.name} is offline.`, url: c.serverUrl(server) } if (!visible || count === 0) { return { title: `${server.name} — ${count} online`, url: c.serverUrl(server), ...(visible ? {} : { notice: linkNudge(actor, required) }), } } const players = await events.online(server.id) const names = players.map((p) => (p.sleeping ? `${p.name || 'Unknown player'} (sleeping)` : p.name || 'Unknown player')) return { title: `${server.name} — ${players.length} online`, url: c.serverUrl(server), text: c.fitLines(names, { max: LIMIT, limit: 1900 }), // D127. Anything short of `public` is for the caller alone. ephemeral: required !== 'public', } } async function handler({ options, actor }) { const now = Date.now() const picked = c.pickServer(await c.listServers(now), options && options.server) const refusal = c.pickRefusal(picked) if (refusal) return refusal const level = await c.levelFor(actor) if (picked.server) return forServer(picked.server, actor, level) // The fleet: counts only, which are public. Names are one server's question, // and the closing line offers it only when naming a server would show some. const visibleSomewhere = ( await Promise.all(picked.all.map(async (s) => visibility.meets(level, await visibility.presenceFor(s.id)))) ).some(Boolean) return { title: 'Online now', url: c.pageUrl('/rust'), fields: picked.all.slice(0, 25).map((s) => ({ name: s.name, value: s.online ? `${s.players} online` : 'Offline', inline: true, })), ...(visibleSomewhere ? { text: 'Name a server to see who is on.' } : {}), } } module.exports = { name: 'online', description: 'How many are on a Rust server, and who, where this site shows names', options: [{ name: 'server', type: 'string', description: 'Server name or id (counts for all when left out)', required: false }], // Everyone, deliberately: `linked` would hide the command from the unlinked // members the nudge exists to invite. The gate is inside the handler. access: 'everyone', handler, }