Add orbit.server plugin API: read-only caps/isupport/network/numeric

This commit is contained in:
Jean Chevronnet 2026-07-06 17:44:24 +00:00
parent d24833e213
commit e302e38d24
No known key found for this signature in database
2 changed files with 36 additions and 8 deletions

View file

@ -54,7 +54,7 @@ bound to the app's React) — runtime template markup, no build step. Prefer
| Member | Description |
|---|---|
| `Orbit.version` / `Orbit.commit` | app version + git commit (build-time) |
| `Orbit.apiVersion` | plugin API contract version (bumped on breaking changes) — guard with `if (Orbit.apiVersion < N) …` |
| `Orbit.apiVersion` | plugin API contract version (bumped on surface changes) — feature-detect with `if (Orbit.apiVersion >= N) …` |
| `Orbit.plugin(name, fn)` | register a plugin; `fn(orbit, log)` |
| `Orbit.on/once/off/emit(event, …)` | the app event bus |
| `Orbit.config()` | the resolved runtime config |
@ -68,6 +68,11 @@ bound to the app's React) — runtime template markup, no build step. Prefer
| `orbit.state.nick()` / `account()` | your nick / logged-in account |
| `orbit.state.buffers()` | open buffer names |
| `orbit.state.get()` | full store snapshot (read-only) |
| `orbit.server.hasCap(cap)` | is an IRCv3 capability negotiated? — gate cap-dependent features so you never 421 a leaner server |
| `orbit.server.caps()` | every cap Orbit negotiates, each `{name, available, enabled}` |
| `orbit.server.isupport()` | read-only snapshot of the ISUPPORT (005) tokens |
| `orbit.server.network()` | network name (ISUPPORT NETWORK) |
| `orbit.server.numeric(code)` | RPL/ERR name for a numeric reply, e.g. `'433'``'ERR_NICKNAMEINUSE'` |
| `orbit.irc.send(line)` | send a raw IRC line |
| `orbit.irc.msg(target, text)` | PRIVMSG a target |
| `orbit.irc.say(text)` | send to the active buffer |

View file

@ -1,10 +1,11 @@
// The `window.Orbit` plugin API — the surface plugins register against.
//
// We deliberately expose only safe, stable capabilities (events, read-only
// state, IRC actions, theming, namespaced storage, named UI slots) and keep
// internal modules private so the app's still-evolving internals stay free to
// change. UI authoring uses HTM (htm.bind to the app's React) so plugins write
// template-literal markup at runtime, with no build step.
// state, read-only server/capability info, IRC actions, theming, namespaced
// storage, named UI slots) and keep internal modules private so the app's
// still-evolving internals stay free to change. UI authoring uses HTM (htm.bind
// to the app's React) so plugins write template-literal markup at runtime, with
// no build step.
import React, { type ReactNode } from 'react';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import * as ReactDOM from 'react-dom';
@ -21,9 +22,10 @@ import { usePluginRegistry, type UiSlot, type MessageInfo, type UserActionCtx, t
const html = htm.bind(React.createElement);
const THEMES: Theme[] = ['light', 'dark', 'orbit', 'orbit-dark', 'yomirc', 'yomirc-dark'];
// Plugin API contract version. Bumped on a breaking change to the surface below
// so plugins can guard (e.g. `if (Orbit.apiVersion < 2) …`). Still experimental.
const API_VERSION = 5;
// Plugin API contract version. Bumped on any change to the surface below so
// plugins can feature-detect (e.g. `if (Orbit.apiVersion >= 6) orbit.server.hasCap(…)`).
// Still experimental.
const API_VERSION = 6;
const registered = new Map<string, OrbitPluginApi>();
@ -59,6 +61,20 @@ export interface OrbitPluginApi {
buffers: () => string[];
get: () => ReturnType<typeof useChat.getState>;
};
// ── read-only server / capability info (client.server + ircv3 + numerics) ──
server: {
/** Network name (ISUPPORT NETWORK), '' until known. */
network: () => string;
/** Read-only snapshot of the server's ISUPPORT (005) tokens. */
isupport: () => Record<string, string>;
/** True once the given IRCv3 capability is negotiated and in use check this
* before using a cap-dependent feature so you never 421 a leaner server. */
hasCap: (cap: string) => boolean;
/** Every cap Orbit negotiates + whether the server advertises / enabled it. */
caps: () => { name: string; available: boolean; enabled: boolean }[];
/** RPL/ERR name for a numeric reply seen via on('raw'), e.g. '433' → 'ERR_NICKNAMEINUSE'. */
numeric: (code: string) => string | undefined;
};
// ── IRC actions ──────────────────────────────────────────────────────────
irc: {
send: (line: string) => void;
@ -134,6 +150,13 @@ function makeApi(name: string): OrbitPluginApi {
// through the curated irc.* methods; they never get a credential handle.
get: () => ({ ...activeStore().getState(), client: null }),
},
server: {
network: () => activeStore().getState().client?.server.network ?? '',
isupport: () => ({ ...(activeStore().getState().client?.server.isupport ?? {}) }),
hasCap: (cap) => activeStore().getState().client?.ircv3.hasCap(cap) ?? false,
caps: () => activeStore().getState().client?.ircv3.listCaps() ?? [],
numeric: (code) => activeStore().getState().client?.numerics.name(code),
},
irc: {
send: (line) => activeStore().getState().client?.send(line),
msg: (target, text) => activeStore().getState().client?.privmsg(target, text),