Commit graph

196 commits

Author SHA1 Message Date
7e868babc3
inspircd: restore logins from replayed accountname metadata
All checks were successful
CI / check (push) Successful in 3m45s
The inbound parser ignored METADATA entirely, so on a services netburst
(after a restart/relink) the ircd's replayed 'accountname' metadata was
dropped — every logged-in user looked logged out to services until they
re-identified, losing ChanServ access. Parse METADATA <uid> accountname
into a new AccountLogin event that restores account_of (empty = logout);
our own echoes and other keys are filtered.
2026-07-16 09:54:35 +00:00
99df7baf10
engine: apply gossiped network bans to the ircd immediately
All checks were successful
CI / check (push) Successful in 3m47s
A local AKILL/SQLINE/etc. pushes an ADDLINE to the ircd at once, but a ban
arriving by gossip was only held for the next netburst — so a user banned on
one network could keep hopping to peers until their next relink. ingest now
signals BanAdded/BanLifted (all xline kinds), and gossip_ingest pushes the
ADDLINE/DELLINE. Renamed AccountChange to IngestEffect to match its broader
role.
2026-07-16 09:28:56 +00:00
886fe65e4d
engine: log out local sessions on a gossiped account suspension
All checks were successful
CI / check (push) Successful in 3m43s
A local SUSPEND logs out the account's active sessions, but a suspension
arriving by gossip only set the flag — a session already identified on a
remote node stayed logged in despite the network-wide suspension. ingest
now signals AccountChange::Suspended, and gossip_ingest ends those sessions
(the account and its channels are kept). Factors the per-session logout into
a shared logout_uid helper.
2026-07-16 09:13:53 +00:00
58af1187fc
engine: part orphaned-channel bots on a gossiped account removal
All checks were successful
CI / check (push) Successful in 3m46s
When a peer's gossiped entry drops or takes over an account, the local node
releases the channels that account founded locally (Local-scope channels can
be founded by a Global, gossiped account). That cleanup runs outside the
dispatch path, so a bot assigned to a released channel lingered until the
next netburst. gossip_ingest now reconciles bots after an account is removed,
matching the expiry sweep.
2026-07-16 08:57:38 +00:00
bbea17e848
engine: part a bot when its channel expires
All checks were successful
CI / check (push) Successful in 3m47s
Command-driven channel drops reconcile bots via the dispatch path, but the
inactivity-expiry sweep runs outside it — so a bot assigned to an expired
(or expired-founder-orphaned) channel lingered there until the next
netburst. The sweep now reconciles at the end, parting any stranded bot.
2026-07-16 08:27:59 +00:00
dc23a44f57
engine: apply auto-join, vhost and memo notice on SASL connect
All checks were successful
CI / check (push) Successful in 3m44s
A user authenticated via SASL during registration never runs the NickServ
IDENTIFY path, so they silently missed their auto-join channels, vhost, and
waiting-memo notice. The UserConnect handler now applies these login
side-effects when the arriving user is already authenticated, mirroring
identify.rs. Non-SASL connections are unaffected (no account set yet).
2026-07-16 03:35:32 +00:00
b749cd2969
OperServ: enforce FORBID EMAIL at registration and SET EMAIL
FORBID accepted and stored EMAIL patterns, but nothing ever checked them:
a forbidden address still registered and could be set via NickServ SET
EMAIL. Both paths now reject a matching address (new ForbiddenEmail
outcome), matching how NICK and CHAN forbids already gate registration.
2026-07-16 03:11:57 +00:00
01ea0fa95e
ChanServ: add SET EMAIL channel contact address
Founders can attach a contact email to a channel, shown in INFO and
cleared when set empty. Mirrors SET URL exactly; Local-scope channel
metadata. Completes the channel DESC/URL/EMAIL trio.
2026-07-16 03:06:08 +00:00
7eb1adce5e
ChanServ: add SET URL channel homepage
Founders can attach a homepage URL to a channel; it shows in INFO and
clears when set empty, mirroring SET DESC. Local-scope (channel metadata),
not gossiped, consistent with the entrymsg/description events.
2026-07-16 02:58:44 +00:00
053f8cf832
NickServ: add SET HIDE STATUS for last-seen privacy
Lets an account keep its last-seen/online line in INFO visible only to
itself and to opers. Default stays public (Anope's default). Accepts
STATUS (with USERMASK as an alias) per Anope's SET HIDE <field> grammar.
2026-07-16 02:33:55 +00:00
2c4bc9079c
NickServ: add SET KILL to toggle nick protection
Nick protection was unconditional. SET KILL OFF lets an account opt out —
an unidentified user keeping one of its nicks is no longer prompted or
renamed to a guest. Stored inverted (no_protect) so protection stays the
default. Accepts Anope's ON/QUICK/IMMED/OFF; grace is a fixed interval
here, so the finer variants simply enable protection like ON.
2026-07-16 01:43:10 +00:00
87e21ff85f
NickServ: add SET AUTOOP per-account auto-op opt-out
A user with SET AUTOOP OFF is never auto-opped on join, even where they
hold channel access — they op themselves via ChanServ UP. Stored inverted
(no_autoop) so the default stays auto-op. Join gates on both the channel
setting and the account preference; either can opt out.
2026-07-16 01:37:31 +00:00
dd43b9a331
BotServ: add BOTLIST and AUTOASSIGN default bot
All checks were successful
CI / check (push) Successful in 3m33s
2026-07-16 00:57:06 +00:00
961dc870da
MemoServ: add STAFF to memo all operators
Some checks failed
CI / check (push) Has been cancelled
2026-07-16 00:48:50 +00:00
2850620be1
ChanServ: add SET AUTOOP
All checks were successful
CI / check (push) Successful in 3m35s
SET AUTOOP {ON|OFF} controls whether access members are auto-opped/voiced
on join (on by default). Stored inverted as a settings flag so the
default stays on, and gated in the join handler.
2026-07-16 00:36:19 +00:00
2c282d036f
MemoServ: add SET NOTIFY and SET LIMIT
All checks were successful
CI / check (push) Successful in 3m35s
SET NOTIFY {ON|OFF} controls whether you're told about new memos on login;
SET LIMIT <n>|NONE sets your mailbox cap (honoured on SEND, NONE = the
network default). New memo_notify/memo_limit account fields + one
MemoPrefsSet event.
2026-07-16 00:29:33 +00:00
b5e08c33cf
MemoServ: add IGNORE
All checks were successful
CI / check (push) Successful in 3m34s
IGNORE ADD/DEL/LIST manages a per-account memo-ignore list; a memo from
an ignored account is silently dropped, and the sender is still told it
was sent so the ignore isn't revealed. New memo_ignore account field +
events, and a check in SEND.
2026-07-16 00:10:34 +00:00
53d3d63464
NickServ GETEMAIL + ChanServ SET RESTRICTED
All checks were successful
CI / check (push) Successful in 3m51s
GETEMAIL (auspex) lists accounts whose email matches a glob. SET
RESTRICTED makes a channel kick anyone without access (or oper) as they
join, mirroring the SECUREOPS on-join enforcement.
2026-07-15 19:25:41 +00:00
be4860c9a8
OperServ: add FORBID (nick/channel/email registration bans)
All checks were successful
CI / check (push) Successful in 3m56s
FORBID ADD/DEL/LIST bans a NICK, CHAN, or EMAIL glob from being
registered — a network-wide policy that gossips like an AKILL and every
node enforces. Nick registration is blocked in pre_register_check (both
NickServ REGISTER and the account-registration relay), and channel
registration in ChanServ REGISTER. New Forbid store + events, mirroring
the akill subsystem.
2026-07-15 19:14:07 +00:00
ac50af92fc
ChanServ: add SET SUCCESSOR with founder inheritance
All checks were successful
CI / check (push) Successful in 3m59s
SET <#channel> SUCCESSOR <account>|OFF names an account that inherits the
channel if the founder's account is dropped or expires, instead of the
channel being released. Adds a ChannelInfo.successor field + event, and
centralises the founder-gone channel release into a single
release_founded_channels store method used by DROP, expiry, and the
gossip account-gone path so all three transfer consistently.
2026-07-15 18:58:08 +00:00
a26ee722d1
Add nick-protection enforcement: prompt then rename an unidentified user on a registered nick
All checks were successful
CI / check (push) Successful in 3m51s
On connect or nick-change to a registered nick the user isn't identified
to, NickServ prompts them to IDENTIFY and schedules a rename; a short
enforcement sweep renames them to a guest nick after a grace period if
they still haven't. Gated on the uplink's initial burst so a relink does
not enforce every already-online user, and cancelled the moment they
identify (via IDENTIFY or SASL) or leave the nick.
2026-07-15 17:41:33 +00:00
54ad013e49
Wire account registration to the ircd's ENCAP SWACCTREG/SWACCTRES relay
All checks were successful
CI / check (push) Successful in 3m53s
Echo is the authority the ircd account module forwards to: parse
ENCAP <us> SWACCTREG <reqid> <origin> <kind> <account> <p2> :<p3> into an
account request, and answer the origin server with
ENCAP <origin> SWACCTRES <reqid> <kind> <account> <status> <code> :<message>.
Thread the origin through the reply so responses route back to the
requesting server. Replaces the placeholder ACCTREGISTER/ACCTREGRESULT
names that nothing on the wire actually spoke.
2026-07-15 16:34:20 +00:00
994e8c7347
Make the SCRAM verifier the sole password credential; finish the account-registration relay
All checks were successful
CI / check (push) Successful in 4m1s
Credentials: the SCRAM-SHA-256 verifier is now the only password
credential. PLAIN and IDENTIFY verify the plaintext against it via
scram::verify_plain, exactly as a SCRAM login proves knowledge of it, so
an account provisioned from a verifier alone (external authority) works
over every mechanism, not only SCRAM. Removed the redundant argon2 hash
entirely: the field, the Credentials member, the AccountPasswordSet
field, hash_password/verify_password, and the argon2 crate. SASL SCRAM
already forces a PBKDF2 verifier into the store, so the hash never raised
the at-rest floor. OS RNG now comes from rand_core.

Registration: finished draft/account-registration over the ircd relay.
VERIFY confirms the emailed code, RESEND reissues it, STATUS reports
state, and REGISTER now emails the code and replies verification_required
on the relay path too, not only through NickServ.
2026-07-15 15:47:41 +00:00
fcca8e4e3b
Front the whole network's help through HelpServ
Each service now exposes its help catalog via Service::help_topics; the
engine collects them into a network-wide index on Network, reachable
through two new NetView methods. HelpServ uses it: HELP <service>
[command] (or a bare service name) serves any service's per-command
help, bare HELP lists the services, and its own ticket commands still
work. No cross-crate dependency: the catalog flows through the SDK's
NetView, keeping every module dependent only on echo-api.
2026-07-14 18:37:45 +00:00
993f5b2eea
Rename the project to Echo (was fedserv)
Rebrand the daemon and workspace: the binary is now `echo`, every crate
is `echo-*` (`echo-api` and the module crates), Rust imports use
`echo_*`, the gRPC proto is `proto/echo.proto` with package `echo.v1`,
and the log filter, gossip peer-name default, and on-disk store default
(`echo.db.jsonl`) follow. Docs updated throughout. No behaviour change;
crate directories and the config schema are untouched.
2026-07-14 15:24:41 +00:00
6f76f9722c
Reorganise the engine into a concern-split module tree
engine/mod.rs had grown past 5k lines, most of it one test module and
a few large method clusters. Lift the tests into engine/tests.rs, and
move the cohesive method groups into sibling files — sasl, kicker
(chat policing), register (account-authority relay), and dispatch
(service routing and fantasy). The Engine type, its master event
handler, shared helpers, and the small state structs stay in mod.rs;
each sibling adds its own impl Engine block. Pure reorganisation: no
behaviour change, tests unchanged and green.
2026-07-14 14:05:56 +00:00
5e6bc4334c
Fantasy !roll: DiceServ answers, the bot speaks it in-channel
Extends the fantasy router so a dice command (!roll/!calc/!exroll/!excalc)
in a bot channel routes to DiceServ (if loaded) instead of ChanServ, and
its reply is turned into the assigned bot speaking the result to the
channel — so a tabletop game rolls in the open, not in a private notice.
Everything else still routes to ChanServ. Factored the dispatch into
service_uid()/route_to() helpers shared by both branches.
2026-07-14 07:38:58 +00:00
a47dd6bda3
Add HelpServ: a help-desk ticket queue
Users open tickets with REQUEST (or HELPME); the queue is event-sourced
and, because a request is an event, the audit feed announces it to staff
and records it in the searchable log — so OperServ LOGSEARCH finds a ticket
by any word, the same trail as reports. A user can CANCEL their own open
ticket. Operators work the queue: LIST [ALL], VIEW, TAKE (claim), NEXT
(claim the oldest unassigned), CLOSE. Filing is rate-limited. Opt-in.

That completes the six third-party modules — all interconnected: they
share the account/channel/oper identity, the event-sourced store, and the
one audit/incident trail.
2026-07-14 04:39:45 +00:00
06227e73da
Add ChanFix: recover opless channels from op-time scores
The engine samples live op state on a short cadence, scoring op-time per
identity (account, else host) on the network view — a net +1 per pass
while opped, -1 while idle, so trusted regulars rise and stale ops fade.
ChanFix (operator-only) reads those scores: SCORES <#chan> shows the
standings, CHANFIX <#chan> reops the present regulars scoring above a
share of the top score, once the channel is opless enough and has real
history. It defers to ChanServ — a registered channel is refused. Opt-in.

C++ note: the original persists scores to the services DB and runs its own
timers; here scoring rides the existing periodic loop and lives on the
ephemeral network view, so it's node-local and rebuilds itself.
2026-07-14 04:33:48 +00:00
09be99785e
Add GroupServ, with groups that grant channel access
User groups (!name) — a founder plus member accounts, each with group-
access flags (the shared flag primitive: F founder, f manage, i invite,
c chan-access, s set, m memo). REGISTER/DROP/INFO/LIST/ADD/DEL/FLAGS;
event-sourced and federated like accounts.

The flagship interconnection: a channel access entry may name a !group, so
every member inherits that channel access. Resolution is group-aware — a
new db.channel_caps() unions a user's direct access with any group they
belong to, and the engine's join handler auto-ops/voices through it. So
"ChanServ FLAGS #chan !team +o" auto-ops every member of !team, and
removing someone from the group revokes it. Opt-in.
2026-07-14 04:26:07 +00:00
c7a4d5be27
Add a shared channel-access FLAGS model
A granular access system, replacing the op/voice-only levels with per-user
flag letters (f full, o auto-op, O op, h auto-halfop, v auto-voice, t
topic, i invite, a modify-access, s settings, g greet). One shared
primitive in the api — level_caps() resolves a stored level to capabilities
(recognising the legacy op/voice/founder presets so nothing breaks) and
apply_flags() applies +/- deltas — so join_mode/is_op/access_rank all
route through it and a flag-granted +o auto-ops exactly like a legacy op
entry.

ChanServ FLAGS <#chan> [account [+/-flags]] lists or edits access;
changing needs the founder or the 'a' flag. This is the foundation
GroupServ will reuse for group membership flags.
2026-07-14 04:15:28 +00:00
5b023c22a4
Add ReportServ, wired into the audit trail
Any user can REPORT <nick|#channel> <reason>; filing is rate-limited per
reporter. Reports are an event-sourced, federated queue operators review
with LIST / VIEW / CLOSE / DEL. The interconnection: because a filed report
is an event, the engine's audit feed already announces it to the staff
channel and records it in the searchable action log — so OperServ
LOGSEARCH finds a report by any word in it, tying reports into the same
trail as bans, kicks, and everything else. Opt-in (add "reportserv").
2026-07-14 04:02:51 +00:00
8d495a948e
Add InfoServ, unifying network bulletins onto the news store
InfoServ becomes the dedicated home for network information bulletins,
backed by the same event-sourced news store OperServ NEWS used — one place
to post, not two. POST/LIST/DEL manage public bulletins (shown to every
user on connect); OPOST/OLIST/ODEL the oper ones (shown to operators on
login). Anyone may LIST the public bulletins; posting/removing is admin-
only. The engine's existing connect/login display hooks render them
unchanged. The OperServ NEWS command is retired accordingly (the news
events and the display path stay). Default service.
2026-07-14 03:56:09 +00:00
3f8649414f
gRPC Accounts: Provision from pre-derived SCRAM verifiers
Adds Accounts.Provision(name, scram256, scram512, email) so an external
authority that stores verifiers rather than passwords (a website) can bulk-
backfill accounts it never had the plaintext for. It creates the account
with the given SCRAM verifiers — SCRAM and keycard login work immediately;
typed-password login stays disabled (empty password_hash) until a real
password lands via Register/SetPassword. Refuses an existing account, so a
re-run can't clobber a fully-credentialed one. scram512 may be empty
(SCRAM-SHA-512 then falls back to 256).
2026-07-14 03:33:17 +00:00
fba91b4d62
Add opt-in external-account mode (delegate identity to the website)
By default fedserv owns accounts itself — nothing changes, no external
service required. Set [auth] external = true and an outside authority (the
website) owns identity instead: NickServ REGISTER / DROP / SET
PASSWORD|EMAIL / RESETPASS / CONFIRM / CERT / GROUP and the IRCv3
registration relay are all refused ("managed on the website"), so IRC
can't mint or change a second identity. Login is unchanged — fedserv still
authenticates locally against the accounts the authority pushes in via the
existing gRPC Accounts API, so lookups stay in-memory fast.

fedserv keeps owning all IRC-domain data (channels, access, vhosts, bans,
memos) keyed by the account name in both modes. One config bool, off by
default, so standalone deployments are unaffected.
2026-07-14 03:12:59 +00:00
4c899d80b0
OperServ: DEFCON network defence levels
DEFCON [1-5] reads or sets a network-wide defence posture, each level
adding a restriction the engine and services enforce:
- 4: channel registrations frozen (ChanServ REGISTER)
- 3: all registrations frozen (the pre_register_check choke-point, covering
     NickServ and the IRCv3 relay, plus channels)
- 2: sessions additionally capped to one per host
- 1: full lockdown — new connections are turned away on connect

Setting a level announces it to the whole network. Node-local level on the
db so services can read the derived freezes; admin-only.
2026-07-14 02:35:09 +00:00
85d01b3ebf
OperServ: temporary OPER grants that auto-expire
OPER ADD <account> <priv[,priv]> [+duration] now takes an optional window,
after which the grant lazily stops conferring privileges — no timer, the
oper_privs merge just hides an expired grant (evaluated on the engine's
clock so it's testable). Permanent grants are unchanged; LIST flags the
temporary ones and hides expired entries. Runtime opers now carry an
OperGrant{privs, expires} instead of a bare priv list.
2026-07-14 02:29:03 +00:00
90824513a7
OperServ: CBAN — ban a channel name
CBAN ADD <#channel-glob> <reason> / DEL / LIST blocks users from creating
or joining a matching channel (m_cban is loaded). Another kind ("CBAN")
in the generalized X-line handler with a channel-mask normalizer; the
too-wide guard now ignores a leading channel prefix so #* is refused.
2026-07-14 02:24:13 +00:00
a7aacf347e
OperServ: LOGSEARCH — search the action log
LOGSEARCH [pattern] searches the incident log newest-first: match a bare
id (as stamped into a kick reason), or any word from the summary; no
argument lists the most recent. Auspex-gated (read-only investigation).

To make it comprehensive, the command audit trail now records every
notable action to the same incident log (not just to the audit channel,
and regardless of whether one is configured) — so registrations, drops,
akills, suspensions, vhosts, notes, and the like are all findable
alongside the kicks and kills stamped at the removal choke-point.
2026-07-14 02:16:59 +00:00
dc2331f439
Loosen two exact kick-reason assertions for the incident-id suffix
The reason now carries a trailing [#id]; match the prefix instead of the
whole string.
2026-07-14 02:12:27 +00:00
d2a8d0ef05
Stamp a traceable incident id into every kick and kill
A single choke-point in handle() gives each outgoing Kick / KillUser a
short id, appends it to the reason the user sees ([#3F]), and records a
searchable summary in a bounded per-node incident ring on the live network
view. So a bot flood-kick, a fantasy kick, a votekick, and an operator
KICK are all logged the same way, and the id in a kick reason ties back to
the log entry. Exposed via NetView::search_incidents (id-exact or summary
substring, newest first) for the LOGSEARCH command to come.
2026-07-14 02:09:45 +00:00
1f7591778f
OperServ: SHUN and CHANKILL
- SHUN <user@host> silences a matching user network-wide without
  disconnecting them — another kind ("SHUN") in the generalized X-line
  handler (m_shun is loaded), same event-sourced list and lazy expiry as
  AKILL/SQLINE/SNLINE.
- CHANKILL <#channel> [reason] AKILLs every distinct member host in one
  shot to clear a spam or attack channel; the G-lines the ircd applies
  also kill the sessions. Deduped by host, and never bans the operator who
  ran it. Both admin-only.
2026-07-14 02:00:17 +00:00
1daebc94a5
OperServ: JUPE — hold a server name against a rogue link
JUPE <server.name> [reason] introduces a fake server holding the name so
the real one can't link; JUPE DEL <name> squits it; JUPE LIST shows them.
Node-local (the introducing node owns the jupe, so it isn't federated —
that would collide SIDs across nodes) and re-asserted at burst. A fake
server id is allocated per jupe. New JupeServer/Squit actions serialise to
the mid-link SERVER introduction and SQUIT. Admin-only.
2026-07-14 01:51:07 +00:00
76a7162227
OperServ: per-IP session limiting with exceptions
The connection that puts an IP over its allowance is killed on connect.
The ircd's per-user IP (UID param 7) is now plumbed through UserConnect and
tracked per-IP in the live network view (incremented on connect, released
on quit).

- [session] default_limit sets the base allowance (0/absent = off).
- OperServ SESSION LIST <min> / VIEW <ip> inspect live counts.
- OperServ EXCEPTION ADD <ip-mask> <limit> [reason] / DEL / LIST override
  the limit per IP-mask (limit 0 = unlimited); event-sourced so they
  federate and survive restart, matched most-permissive-wins.

All admin-gated.
2026-07-14 01:46:09 +00:00
78ad909706
OperServ: SNLINE realname bans
SNLINE ADD [+expiry] <realname-regex> <reason> / DEL / LIST — a realname
R-line the ircd matches against connecting users' realnames. It's just
another kind ("R") in the generalized X-line handler: same event-sourced
list, lazy expiry, and burst re-assertion as AKILL/SQLINE, keyed by
(kind, mask). The mask is kept verbatim (a single-token regex; use . or
\\s for spaces) and an all-wildcard pattern is refused. STATS now counts
it too.
2026-07-14 01:36:08 +00:00
a67409e0d3
OperServ: OPER — runtime operator management
OPER ADD <account> <priv[,priv]> / DEL <account> / LIST grants or revokes
services-operator privileges (auspex, suspend, admin) at runtime, without
editing config and restarting. Runtime grants are event-sourced (Global,
so they federate and survive restart) and UNIONED with the declarative
[[oper]] config at the engine, so either source makes an account an
operator; a config oper can't be revoked here. Admin-only.

Factored the privilege-name parsing into Privs::from_names (+ names/union)
so config loading and runtime grants share one definition.
2026-07-14 01:33:30 +00:00
e45eab2cd6
OperServ: NEWS — logon and oper announcements
NEWS ADD <LOGON|OPER> <text> / DEL <LOGON|OPER> <number> / LIST. Logon news
is shown to every user as they connect; oper news is shown to an operator
when they log in. Admin-only to manage. Each item carries a stable id
assigned at add time and embedded in the log, so deletion is order-
independent under replay (DEL by number resolves to that id).

Enabling groundwork: the replicated network-wide lists (bans, now news)
are consolidated into one NetData struct threaded through apply() as a
single parameter — this replaces the akill parameter rather than adding
one, so the fold stays within its argument budget as the lists grow.
2026-07-14 01:26:12 +00:00
51d4ebf789
OperServ: INFO staff notes on accounts and channels
INFO ADD <target> <note> / DEL <target> / <target> attaches a staff note
to an account or channel (a #name is a channel, else an account). The note
is shown in that service's INFO — but to operators only (Priv::Auspex),
never to the account's own owner. Admin-only to set.

Follows the typed-field-on-the-entity pattern: an oper_note field folded
through AccountOperNoteSet (Global) / ChannelOperNoteSet (Local) events,
read via dedicated Store getters so it stays out of the public views and
the directory feed.
2026-07-14 01:15:00 +00:00
8ccc9fa8ea
OperServ: SVSNICK and SVSJOIN
Force a user's nick change or channel join, reusing the ForceNick and
ForceJoin actions the daemon already relies on (guest renames, auto-join)
so the ircd support is proven. SVSNICK validates the new nick; SVSJOIN
takes an optional key. Both admin-gated and resolve the target via
NetView.
2026-07-14 01:08:46 +00:00
f9aea30f81
OperServ: STATS enforcement overview
STATS reports the live counts OperServ holds — AKILL and SQLINE bans by
kind, and services ignores — as a quick at-a-glance check. Read-only, so
any operator can run it.
2026-07-14 01:02:14 +00:00