diff --git a/server/commands/guild.command.js b/server/commands/guild.command.js index 2958a56..9908016 100644 --- a/server/commands/guild.command.js +++ b/server/commands/guild.command.js @@ -42,13 +42,22 @@ async function levelFor(actor) { return visibility.viewerLevel({ user: { id: actor.userId, role: actor.role } }) } -// The nudge §9 answer 5 asks for, and only when it is TRUE. An unlinked caller -// who was told nothing because the shard publishes nothing is not helped by -// being invited to link; the prompt appears when linking is what would actually -// change the answer. +// The nudge §9 answer 5 asks for, and only when it is TRUE. +// +// **Linking reaches exactly two rungs and no further.** Signing in gets a caller +// to `logged_in` and linking a game account to `player`; `staff` and `admin` are +// roles an operator grants and no amount of linking will earn. So a shard that +// gates guilds to staff refuses an unlinked caller WITHOUT the invitation — +// telling them to link would be telling them to do something that changes +// nothing, which is worse than saying no. +// +// The live walk found this: gated to `staff`, the refusal still read "this shard +// shows guild information to linked players". +const LINKING_REACHES = new Set(['logged_in', 'player']) + function linkPrompt(actor, audience) { if (actor.isLinked) return null - if (audience === 'anonymous') return null + if (!LINKING_REACHES.has(audience)) return null return 'Link your account on the site to see more — this shard shows guild information to linked players.' } @@ -154,7 +163,10 @@ async function handler({ options, actor }) { if (!wanted) { const top = [...rows].sort((a, b) => (b.members || 0) - (a.members || 0)).slice(0, LIST_LIMIT) return { - title: `Guilds on ${core.baseUrl.replace(/^https?:\/\//, '')}`, + // Not "Guilds on ": `ctx.site` carries a base URL and no brand name, + // so naming the deployment here can only mean printing its hostname into + // an embed title, which is noise on a shard's own Discord server. + title: 'Guilds on this shard', fields: top.map((g) => ({ name: g.abbr ? `${g.name} [${g.abbr}]` : g.name, value: `${g.members || 0} members · ${g.online || 0} online`, diff --git a/server/test/guildCommand.test.js b/server/test/guildCommand.test.js index af33659..7782632 100644 --- a/server/test/guildCommand.test.js +++ b/server/test/guildCommand.test.js @@ -88,6 +88,14 @@ test('an unlinked caller is invited to link — but only when linking would chan stub({ audience: 'anonymous', level: 'anonymous' }) const open = await command.handler({ options: {}, actor: anonymous }) assert.equal(open.notice, null) + + // Gated to staff: linking reaches `player` and stops there, so the invitation + // would be an instruction to do something that changes nothing. Found on the + // live rig, where a staff-gated shard still offered it. + stub({ audience: 'staff', level: 'anonymous' }) + const unreachable = await command.handler({ options: {}, actor: anonymous }) + assert.match(unreachable.text, /not shown to your account/) + assert.equal(unreachable.notice, null) }) test('a stale board answers offline rather than reporting what it still holds', async () => { @@ -99,6 +107,7 @@ test('a stale board answers offline rather than reporting what it still holds', test('no argument lists the largest guilds', async () => { stub() const res = await command.handler({ options: {}, actor: anonymous }) + assert.equal(res.title, 'Guilds on this shard') assert.equal(res.fields.length, 2) assert.match(res.fields[0].name, /Knights of the Codex/) assert.match(res.fields[0].value, /12 members · 3 online/)