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>
119 lines
6.1 KiB
JavaScript
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,
|
|
})
|
|
}
|