docs(teams): phase 7 as built — five amendments to §7.1
The command that proves the seam is the MODULE's `/guild`, not core's `/team`: §7.1 was written before phase 3 settled that Teams is a contract primitive with no core surface, and a core `/team` publishes the same invented noun that got core's Team pages deleted. Its deep link comes from `pageUrlTemplate` for the same reason — `/teams/:slug` does not exist. The re-register nudge is its own bot endpoint rather than a ride on `/internal/config`, whose body carries the decrypted bot token. `actor` carries `role` beside `isStaff`, since a module with its own audience rungs cannot place a caller from a boolean. And "deregistration is free" needed a second half: it holds across the restart an uninstall asks for, not across the runtime toggle, so liveness is asked at both the pull and the dispatch. MODULE_API.md stops saying `registerSlashCommands` throws and documents it — every member of 1.6.0 is live now. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -59,11 +59,9 @@ the provider's optional `projectRoster` and `pageUrlTemplate` · `api.registerSl
|
||||
> `registry.declareModuleSlot(id, name)` lets a MODULE declare a place on its own page for CORE to
|
||||
> fill, and `Slot` joins the UI kit so the module can render it. See §3.7a.
|
||||
|
||||
**The number covers the whole surface; the members arrive by phase, and each is marked below.** Seven
|
||||
are live now. `api.registerSlashCommands` is **present and throws**, with an error naming the phase
|
||||
that will implement it — chosen over leaving it absent so that a module written against the published
|
||||
version fails at registration with a sentence explaining itself, rather than at whatever moment
|
||||
someone first exercises the feature. Do not call it yet; do not treat its throw as a bug.
|
||||
**Every member of 1.6.0 is live as of phase 7.** `api.registerSlashCommands` was the last one still
|
||||
throwing, and it now registers — the staged rollout the paragraphs above describe is finished. A
|
||||
module may call any member of this version and get the behaviour documented below.
|
||||
|
||||
**`registerTeamProvider` is the first registration where core calls the MODULE and waits.** Every
|
||||
existing one is either the module claiming a mount or core notifying it; the closest precedent is
|
||||
@@ -301,7 +299,7 @@ api.registerNotificationStreams(streams)
|
||||
api.registerAnnounceLeg({ leg, label, dispatch, classify })
|
||||
api.registerPostHook({ onSaved, onDeleted })
|
||||
api.registerTeamProvider({ getTeams, getTeamMembers, getTeamLeaders }) // 1.6.0
|
||||
api.registerSlashCommands([...]) // 1.6.0, throws until phase 7
|
||||
api.registerSlashCommands([{ name, description, options, access, handler }]) // 1.6.0
|
||||
api.onBoot(async (ctx) => {})
|
||||
api.onShutdown(async () => {})
|
||||
```
|
||||
@@ -492,10 +490,74 @@ removals. It defaults to `true` when omitted, so the ordinary authoritative case
|
||||
omitted from a roster is indistinguishable, downstream, from that member having left — core would
|
||||
mark them departed on the strength of a broken payload. Refusing costs one interval of staleness.
|
||||
|
||||
**`registerSlashCommands(commands)`** — declared in API 1.6.0 and **not yet implemented**: calling it
|
||||
throws with an error naming the phase that will. Present rather than absent so a module written
|
||||
against the published version fails at registration with an explanation, instead of at the moment
|
||||
someone first types the command.
|
||||
**`registerSlashCommands(commands)`** — chat-platform commands whose definition AND handler both
|
||||
belong to the module, live since phase 7 (TEAMS.md §7.1).
|
||||
|
||||
```js
|
||||
api.registerSlashCommands([{
|
||||
name: 'guild', // lowercase, 1-32, no dots
|
||||
description: 'Show a guild on this shard', // 1-100 characters
|
||||
options: [ // the restricted schema, below
|
||||
{ name: 'name', type: 'string', description: 'Guild name or abbreviation', required: false },
|
||||
],
|
||||
access: 'everyone', // 'everyone' | 'linked' | 'staff'
|
||||
async handler({ command, options, actor }) {
|
||||
return { title, text, fields, url, ephemeral, notice } // every field optional
|
||||
},
|
||||
}])
|
||||
```
|
||||
|
||||
**The handler runs in the WEBSITE process, never in the bot.** The bot container has no `modules`
|
||||
volume and cannot load a line of module code, so it pulls the definitions over an internal API and
|
||||
owns every platform-specific concern — deferral, the acknowledgement deadline, ephemerality,
|
||||
follow-ups, embeds. A module that wanted to call `interaction.deferReply()` would be a module holding
|
||||
a Discord handle, and this split is the reason a second platform could implement the same contract.
|
||||
|
||||
**`actor` is resolved by core before the handler is entered**, and is the whole of what a handler
|
||||
learns about the caller:
|
||||
|
||||
| field | |
|
||||
| --- | --- |
|
||||
| `platform` | `'discord'` today; the only platform-shaped thing a handler ever sees |
|
||||
| `platformUserId` | the caller's id on that platform |
|
||||
| `guildId` | the platform community the command was run in, or `null` |
|
||||
| `userId` | the site account, or `null` when the platform identity is not linked |
|
||||
| `role` | that account's role — a module with audience rungs needs more than a boolean |
|
||||
| `isLinked` | whether `userId` resolved |
|
||||
| `isStaff` | `admin` or `moderator`, the same two roles every other Team surface means |
|
||||
|
||||
A **banned or disabled** account resolves as unlinked, so a chat surface is never the one place a ban
|
||||
does not reach. The Discord provider is found by `auth_providers.kind`, not by its id — the id is an
|
||||
operator-chosen slug.
|
||||
|
||||
**`access` is enforced twice, and only the server half is the gate.** The bot sets a platform-side
|
||||
permission default from it where the platform can express one; core re-checks it in the dispatcher on
|
||||
every call. `'linked'` has no Discord equivalent at all — there is no "has a website account"
|
||||
predicate — so it is simply not advertised, which is exactly why the client half cannot be the
|
||||
boundary.
|
||||
|
||||
**The option schema is deliberately small: `string | integer | boolean | user`,** each with
|
||||
`required` and optional `choices` (`string` and `integer` only). No subcommand groups, autocomplete,
|
||||
attachments, modals or component interactions — those are the features whose semantics do not survive
|
||||
a second platform. A command needing them is a bot-side command, written in the bot.
|
||||
|
||||
**A definition the platform would reject fails at `register()`**, not at the next connection: the bot
|
||||
registers the whole set in one call, so one bad option type would cost every command, the bot's own
|
||||
included. Names are validated (lowercase, 1-32, no dots), as are description lengths, the option
|
||||
types, and the ordering rule that a required option may not follow an optional one.
|
||||
|
||||
**Commands are NOT namespaced under the module id**, unlike stream ids and announce legs — Discord's
|
||||
name grammar has no `.` in it. Collisions are first-come with the holder named, and a name that
|
||||
collides with one of the bot's own built-ins is dropped by the bot, which is the one collision core
|
||||
cannot see.
|
||||
|
||||
**A handler's failure is its own.** A throw, or a handler still running after core's timeout, becomes
|
||||
a refusal the platform renders; the handler never runs in the bot process, so it cannot cost anything
|
||||
but its own reply. `ok` is core's verdict and sits outside the envelope, so a handler cannot forge it.
|
||||
|
||||
**A disabled module's commands stop answering immediately.** Registration has no removal path — a
|
||||
claim is made once, at load — so liveness is asked at both the pull and the dispatch: an operator who
|
||||
switches a module off does not leave a live handler behind it.
|
||||
|
||||
**`onBoot(fn)` / `onShutdown(fn)`** — §2.5.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user