Files
Module-uo/server/index.js
Claude 2d1d91e372 feat(guilds): /guild, the module's own chat command
The first command through `api.registerSlashCommands` (MODULE_API 1.6.0,
TEAMS.md §7.1). The definition and the handler both live here; the bot pulls the
definition and runs no line of this module.

`/guild` and not `/team`, deliberately. Core does not own the word for a Team —
that is what deleted its Team pages in phase 3 — so it does not publish the noun
in a channel either. Core ships the dispatcher and zero commands.

The audience rungs are re-resolved in the handler rather than assumed: a shard
that gates guilds to staff does not become public because the question arrived
over Discord. The provider's own staleness guard is honoured too, so a stale
board answers "not connected" instead of reporting what it still holds, and
`resolveUserId` is exported rather than copied so "linked" means here what it
means on the roster.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 18:53:45 -05:00

119 lines
6.1 KiB
JavaScript

// ── module-uo's server entry point ─────────────────────────────────────────
//
// Core requires this file once, synchronously, while `app.js` is still being
// required, and calls the exported function with `(ctx, api)`. The normative
// contract is docs/website/MODULE_API.md §2.2; the three rules that shape every
// line below are worth restating where they will be read:
//
// 1. **No `await`, and no database.** `scripts/routeManifest.js` and
// `swagger/swagger.js` both require core's `app.js` with the pool pointed
// at a dead port, so a module that queried at registration time would hang
// both. Everything needing a live database is in `onBoot`.
// 2. **Never resolve what core owns.** This module lives at
// `<website>/modules/uo/`, outside `server/`, so Node's resolver never
// reaches core's `node_modules` and `require('express')` fails outright.
// express, express-validator, the database, the logger, the middleware and
// the rest of §2.3 arrive on `ctx` and are re-exported by `./core`.
// 3. **Never reach into core's tree.** No relative path may escape this
// module's root; `scripts/checkImports.js` enforces that in CI (§5.1).
//
// **Require order is load-bearing, and it is why the requires below are inside
// the function.** Every ported file reaches core through `./core`, whose members
// resolve `ctx` when called — but a router does `const express = core.express` at
// its own file scope, which runs the moment it is required. So `core.init(ctx)`
// has to happen before the first `require` of anything under `router/`. Hoisting
// these to the top of the file would break the module with an error about `ctx`
// being missing, from a file that never mentions it. Node caches modules, so
// requiring here costs nothing after the first call.
const core = require('./core')
/**
* @param {object} ctx what core hands the module (MODULE_API.md §2.3), frozen
* @param {object} api what the module registers (§2.4)
*/
module.exports = function register(ctx, api) {
core.init(ctx)
/* eslint-disable global-require */
const publicShard = require('./router/public/shard.router')
const publicAtlas = require('./router/public/atlas.router')
const adminShard = require('./router/admin/shard.router')
const adminUoLink = require('./router/admin/uoLink.router')
const playerShard = require('./router/player/shard.router')
const usersShardExtension = require('./router/admin/usersShard.router')
const shardStreams = require('./config/shardStreams')
const townCrierLeg = require('./utils/shardAnnounce')
const teamProvider = require('./model/teamProvider/teamProvider.model')
const guildCommand = require('./commands/guild.command')
const boot = require('./boot')
/* eslint-enable global-require */
const log = core.logger()
// The five prefixes, exactly the ones `module.json` declares — the loader
// compares the two and rejects a mismatch in either direction. Each router
// mounts INSIDE its tier, so it structurally cannot reach above its prefix,
// and the tier's own gate is already applied: `/admin` sits behind
// `noindex, isLoggedIn, requireRole(...)`, `/player` behind
// `noindex, requireAuth`, `/public` behind nothing by design.
//
// The URLs these produce are byte-identical to the ones core served before the
// extraction (§1.2). That is the whole point of moving the code and not the
// paths: the shipped Android app calls `POST /api/v1/admin/shard/kick`, and the
// Discord bot reads `/api/v1/public/shard/*`, and neither knows or needs to
// know that a module answers now.
api.registerRoutes({
public: { '/shard': publicShard, '/atlas': publicAtlas },
admin: { '/shard': adminShard, '/uo-link': adminUoLink },
player: { '/shard': playerShard },
})
// The six `/admin/users/:id/shard/*` URLs, which hang off a CORE resource and
// therefore cannot be a mount of our own (§1.9). Core declares the slot in
// `users.router.js` and we fill it; the router gets `req.params.id` from the
// parent via `mergeParams`. Core's own routes on the resource win any path
// conflict, which is correct — it owns the user.
api.registerExtension('admin.users.detail', usersShardExtension)
// The push catalog and the news leg. Core kept the push infrastructure and the
// announce worker; what it never had was an opinion about *shard* streams or
// about talking to a town crier, and those are content (MODULE_SYSTEM.md §1.8).
//
// Seven of these stream ids and the leg id `towncrier` are grandfathered
// (§6.5) — they are stored in `notification_subs` and `announce_job_legs.leg`
// and read by the shipped Android app, so a rename here is a data migration
// plus a client break rather than a tidy-up.
api.registerNotificationStreams(shardStreams.STREAMS)
api.registerAnnounceLeg(townCrierLeg.leg)
// Teams: a UO guild is a Team, and this module is the authoritative source of
// them for this deployment (MODULE_API 1.6.0). Core asks the three questions;
// everything about what a guild IS stays here.
//
// Registration is a claim, not a call — nothing below runs until core
// reconciles, which is after `onBoot`. That matters because every method reads
// the database, and registration must not.
api.registerTeamProvider(teamProvider)
// `/guild` — the chat surface for the same guilds (MODULE_API 1.6.0, TEAMS.md
// §7.1). The definition travels to the bot; the handler stays here and runs in
// the website process, because the bot container has no `modules` volume and
// cannot load a line of this module's code.
//
// Core registers NO commands of its own. "Guild" is this module's word — core
// does not own it on a page (phase 3) and does not publish it in a channel
// either.
api.registerSlashCommands([guildCommand])
api.onBoot(boot.onBoot)
api.onShutdown(boot.onShutdown)
log.info('registered', {
version: require('../module.json').version,
routes: 'public:/shard,/atlas admin:/shard,/uo-link player:/shard',
streams: shardStreams.STREAMS.length,
})
}