New HostServ pseudo-service assigns and applies vhosts. A vhost is a typed
Option<Vhost>{host,setter,ts} on the account (VhostSet/VhostDeleted events),
applied to the displayed host on identify and re-applied by ON; OFF restores
the normal host for the session. Operators SET/DEL (applied at once to
online sessions) and LIST; hosts are validated as dot-separated labels.
New SetHost NetAction -> InspIRCd ENCAP <target-sid> CHGHOST <uid> <host>.
A single namespaced counter map on the engine that every service reports
into: ServiceCtx::count(key) is folded in after each command, the engine
bumps its own events (botserv.kick/ban/warn/greet/trigger), and a
per-service command counter (nickserv.command, chanserv.command, …) is
tallied in dispatch. stats_snapshot() merges those with live gauges from
the store (accounts/channels/bots/opers total).
Exposed over gRPC as a new Stats service (Get -> map<string,uint64>),
authorized by the same bearer token as Directory/Accounts — so any
consumer (a dashboard, Django) reads every module's stats through one
pipe. New service is additive to the proto; existing RPCs untouched.
TRIGGER <#channel> ADD <regex>|<response> | DEL <num> | LIST | CLEAR: when
a channel line matches <regex>, the assigned bot says <response> ($nick
becomes the speaker's nick). Only the first matching trigger fires, and a
line the bot just kicked for gets no response.
Same engine machinery as badwords: patterns compile into one RegexSet per
channel (linear-time, ReDoS-safe), cached and rebuilt only on a revision
change. Patterns validated + size-limited at add time.
BADWORDS <#channel> ADD|DEL|LIST|CLEAR manages a channel's badword
patterns, each a regular expression, so a channel matches whatever it
wants — an improvement over Anope's fixed ANY/SINGLE/START/END. Enable
with KICK <#channel> BADWORDS ON.
Uses the regex crate: it's finite-automata based with linear-time
matching, so untrusted user patterns can't cause catastrophic
backtracking (ReDoS). A channel's patterns compile into one RegexSet
(single-pass match against all of them), cached in the engine and rebuilt
only when the list's revision changes — so the hot path is one is_match
call, no per-message compilation or allocation. Patterns are validated
(and size-limited) at add time; matching is case-insensitive by default.
The assigned bot now enforces kickers on channel lines: KICK <#channel>
CAPS ON [min [percent]], and BOLDS/COLORS/UNDERLINES/REVERSES/ITALICS
for control-code formatting. DONTKICKOPS exempts channel operators. A
tripping line is kicked by the bot itself.
Kicker config is a typed KickerSettings struct on the channel (event-
sourced like ChanSettings) with a violation() check; the engine runs it
on every channel PRIVMSG. Factored the founder-or-admin gate into a
shared require_channel_admin helper (assign/set/kick).
Members set a personal greet with NickServ SET GREET; a channel founder
enables display with BotServ SET <#channel> GREET ON. On join, if greets
are enabled and the member has channel access and a non-empty greet, the
assigned bot shows [account] greet in the channel.
Per-account greet is a typed field on Account (AccountGreetSet event, rides
the account snapshot); the per-channel toggle is a new ChanSetting::BotGreet
bool. Greet is public in NickServ INFO.
A channel gains a typed assigned_bot (event-logged, snapshotted). ASSIGN
<#channel> <bot> / UNASSIGN <#channel> is founder-gated (or admin oper). The
engine reconcile now also diffs channel assignments against tracked bot
memberships: a live bot IJOINs its assigned channels and PARTs unassigned ones
(new ServiceJoin/ServicePart actions), at burst and after commands; a deleted
bot's memberships are dropped. Engine test for join-on-assign / part-on-unassign.
Finish MemoServ on the typed, event-logged memo model: Db memo_send/list/read/
del/unread_memos + Store facade, and a new fedserv-memoserv crate (a default
service). Beyond the basics: SEND caps a mailbox (30) so nobody's flooded, READ
takes <num>|NEW|ALL, DEL takes <num>|ALL, LIST previews and flags unread, and
NickServ notifies you of waiting memos on login. Memos persist and federate
(Global scope) like other account data. Also lands NetAction::QuitUser (BotServ
plumbing). Data + delivery/login-notify tests.
Adds BotServ as a new module crate (fedserv-botserv) and a persisted bot
registry: a typed Bot{nick,user,host,gecos} threaded through the event log
(new bots map folded in apply, snapshotted, Local scope). BOT ADD/DEL/LIST
gated on the typed Priv::Admin. BotServ is a default service (uid suffix
AAAAAD). Next slices: introduce bots on the network, ASSIGN to channels, and
in-channel fantasy commands. Data + oper-gated engine tests.
Freezes a channel gated on Priv::Suspend: a typed Suspension on ChannelInfo
(event-logged, snapshotted, lazy expiry — same shape as the account one, no
Anope Extensible/Checker or expiry timer). While suspended, ChanServ refuses
all management (guards in require_op/require_founder and the founder-inline
SET/DROP/MLOCK), the engine skips Join enforcement (no auto-op/akick/entrymsg),
and suspending kicks everyone out; INFO shows it. parse_duration moved into the
SDK so both SUSPEND commands share it. Data + full-flow tests.
Freezes an account (data kept, login blocked) gated on the typed Priv::Suspend.
A typed Suspension{by,reason,ts,expires} on the account, folded through the
event log (persists, replays, federates) -- no stringly Extensible/Checker like
Anope. Expiry is evaluated lazily at check-time, so there is no expiry timer.
Blocks IDENTIFY and all SASL mechanisms (via one sasl_login gate); logs out any
live sessions on suspend; the login refusal names who/when/why and when it
lifts. Shown in INFO to owner/auspex. Data + full-flow tests.
Adds topic tracking: a TopicChange event (parsed from InspIRCd FTOPIC, our own
changes filtered by SID like FMODE) and a stored topic on the channel, folded
through the event log. TOPICLOCK reverts a topic change by a user without
op-level access back to the stored topic; KEEPTOPIC remembers the topic and
restores it when a registered channel is recreated. Both via the typed
ChanSettings path; shown in INFO. Parse + engine tests cover both.
Introduce a typed ChanSettings struct + ChanSetting enum for ChanServ's on/off
options instead of a bag of string flags, so a new option is one field and the
compiler proves every site handles it. First two, both functional: SIGNKICK
attributes ChanServ kicks with who requested them; PRIVATE hides the channel
from LIST. Folded through the event log like the other channel state (node-local,
no gossip), shown in INFO. One ChannelSettingsSet event, one Store method.
Adds the account auto-join list (AJOIN ADD/DEL/LIST): NickServ SVSJOINs you
into your listed channels each time you identify. The list is a typed
Vec<AjoinEntry> on the account, folded through the event log like the cert and
akick lists (persists and replays; no separate store), reached by modules
through the Store trait. New ForceJoin action serialises to InspIRCd SVSJOIN.
Shown in INFO. Operates on the caller's own account; the oper form for other
users waits on a privilege system.
nickserv, chanserv and the InspIRCd protocol move out of the binary into their
own workspace crates (fedserv-nickserv, fedserv-chanserv, fedserv-inspircd),
each depending only on fedserv-api. human_time and the branded account emails
move into the SDK crate so a module needs nothing from core; the engine keeps
its own inherent methods and builds emails via fedserv-api too. The bin now
constructs each module from its crate instead of an in-tree #[path] include.
Proves the SDK is self-sufficient: a third-party module is the same shape.
A trusted caller can now do everything a user does through NickServ commands
— Register, Authenticate, SetPassword, SetEmail, Confirm, Drop, ForceLogout,
GroupNick, UngroupNick — over gRPC instead of IRC. Each mirrors its NickServ
command's exact behavior (Drop reuses the same cleanup a peer's gossiped drop
already triggers; SetPassword/Register derive credentials off the shared
engine lock, same as the IRC path) but is privileged: the bearer token is the
authorization, so most calls skip the account's own password check the IRC
command requires — the same trust model as an admin-level JSON-RPC
integration. A write commits locally and gossips to every other node exactly
like an IRC-originated one, no new propagation path needed.
Verified end-to-end over the real wire, not just unit tests: registered an
account via gRPC, changed its password via gRPC, then logged into a live
IRC session with that exact password.
Snapshot (full read) + Subscribe (live stream) over the same committed-entry
channel gossip uses. Identity and metadata only — password hashes, SCRAM
verifiers, cert fingerprints, and the finer channel-ops-list events never
cross this API. Bearer-token authenticated, optional server TLS. Verified
end-to-end against a live test-net registration, not just unit tests.