Commit graph

131 commits

Author SHA1 Message Date
d4e2c905f2
Shared stats registry + gRPC Stats API
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.
2026-07-13 19:13:31 +00:00
5d1bfb3144
BotServ: DONTKICKVOICES, plus neutral code comments
KICK <#channel> DONTKICKVOICES <on|off> exempts voiced users from the
kickers. The network view now tracks +v the same way it tracks +o: a new
ChannelVoice event is parsed from FMODE +v/-v and from +v join prefixes,
and set_voice/is_voiced mirror set_op/is_op (voice is cleared on part and
quit). scan_ops generalised to scan_status for any status letter.

Also scrubbed third-party project name-drops from code comments.
2026-07-13 18:19:37 +00:00
929a4cdd92
BotServ: TRIGGER — regex auto-responses
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.
2026-07-13 18:03:46 +00:00
47b1fd351e
BotServ: KICK <#channel> WARN — one warning before the first kick
With WARN on, a user's first kicker offence gets a private warning from
the bot instead of a kick; the next one is kicked (and then TTB can ban).
The warned flag rides the existing per-user chatter state.
2026-07-13 17:57:40 +00:00
c8be5b4e95
BotServ: COPY <#src> <#dst> to clone bot config
Copies a channel's kickers, badword list, greet and nobot settings onto
another channel (founder-or-admin on both). Cheap because the config is a
typed KickerSettings + Vec<String>, not a bag of stringly extensibles.
2026-07-13 17:55:19 +00:00
0edc4d4e87
BotServ: KICK <#channel> TEST <message> dry-run
Reports which content kicker (caps/formatting/badwords) a line would trip,
without kicking anyone — so an op can tune caps% or a badword regex safely.
Reuses the typed KickerSettings::violation() and the shared badword-set
builder (moved to db so the test and the live path share it).
2026-07-13 17:53:13 +00:00
9f5f0ed0d2
BotServ: BOT DEL * mass removal (Anope #315)
BOT DEL * removes every registered bot at once, quitting each pseudo-
client, for cleaning up after a bot spree.
2026-07-13 17:50:50 +00:00
0e90dd8020
BotServ: richer INFO
INFO <#channel> now lists the BotServ options in effect (greets, kickers,
nobot) alongside the assigned bot; INFO <bot> flags a private bot.
2026-07-13 17:38:51 +00:00
5bc41f2e60
BotServ: NOBOT and PRIVATE assignment protections
SET <#channel> NOBOT <on|off> reserves (un)assigning a bot for services
operators (the founder is refused). SET <bot> PRIVATE <on|off> (admin
only) marks a bot operators-only to assign; it's flagged in BOT LIST and
the private flag survives BOT CHANGE. Both gates live in the ASSIGN path.
2026-07-13 17:36:34 +00:00
1cc438f4ed
BotServ: BOT CHANGE
BOT CHANGE <oldnick> <newnick> [user [host [gecos]]] renames or
re-identifies a bot; omitted fields keep the current values. A rename
moves every channel the bot was assigned to onto the new nick. reconcile
now tracks each live bot's identity hash, so a change that keeps the nick
but alters user/host/gecos quits the stale pseudo-client and reintroduces
it (rejoining its channels) — a rename does the same via the registry.
2026-07-13 17:30:27 +00:00
ec3205bf75
BotServ kickers: times-to-ban (TTB) + BANEXPIRE
KICK <#channel> TTB <n> makes the bot ban a user after they've been
kicked n times (by any kicker) instead of only kicking; SET <#channel>
BANEXPIRE <duration|off> controls how long that ban lasts. One clear
per-channel knob rather than Anope's per-kicker ttb array.

The kick count rides in the existing ephemeral per-user chatter state, so
it's dropped when the user parts or quits. On the ban the bot sets +b on
an ideal host mask and, if BANEXPIRE is set, queues an unban that is swept
off the next event — no timer thread. Ban time is injectable for tests.
2026-07-13 17:12:12 +00:00
ca95184359
BotServ BADWORDS: user-configurable regex kicker
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.
2026-07-13 16:16:44 +00:00
43def8ee23
BotServ kickers: FLOOD and REPEAT
KICK <#channel> FLOOD ON [lines [secs]] kicks a user who sends too many
lines too fast (default 6 in 10s); KICK <#channel> REPEAT ON [times]
kicks one who repeats the same line (default 3, case-insensitive).

These are stateful, so the engine keeps per-channel/per-user counters in
an ephemeral map that is never event-logged. Hot-path conscious: a channel
with no kickers costs one lookup and returns; a returning speaker allocates
nothing (get_mut, not entry); repeat comparison is an allocation-free
case-folding 64-bit hash. Counters are dropped when a user parts or quits,
so the map stays bounded. Flood time is injectable for deterministic tests.
2026-07-13 15:18:39 +00:00
ed7da9c713
BotServ kickers: caps + formatting, with DONTKICKOPS
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).
2026-07-13 14:54:14 +00:00
ad8041bd9d
BotServ GREET + NickServ SET GREET
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.
2026-07-13 14:43:48 +00:00
c7d05a8b77
BotServ: add SAY and ACT
The channel's assigned bot can now speak: SAY <#channel> <text> and ACT
<#channel> <text> (CTCP ACTION), gated on channel-operator access. Bots
are tracked in the network view so uid_by_nick resolves them, and the
message is sourced from the bot's own uid via a new ServiceCtx::privmsg.
2026-07-13 14:15:20 +00:00
b25870e14b
botserv: ASSIGN / UNASSIGN a bot to a channel
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.
2026-07-13 13:42:14 +00:00
577c05ad2e
memoserv: complete + improve the memo service
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.
2026-07-13 04:50:27 +00:00
651cb683de
botserv: bot registry + BOT ADD/DEL/LIST (first slice)
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.
2026-07-13 04:33:27 +00:00
cb081a2e95
chanserv: channel SUSPEND / UNSUSPEND (oper-gated)
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.
2026-07-13 04:17:17 +00:00
bbbe2c6cb8
nickserv: account SUSPEND / UNSUSPEND (oper-gated)
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.
2026-07-13 04:01:30 +00:00
f1415bedb2
engine: services-operator privilege foundation
A typed Priv enum (Auspex/Suspend/Admin) and a Copy Privs bitset carried on
Sender, instead of Anope's stringly-typed privilege namespace and its two
separate gating mechanisms. Opers are declared in TOML ([[oper]] account +
privs); the engine loads them and resolves the sender's privileges per command,
so a module just checks from.privs.has(Priv::X). First use: NickServ INFO
reveals hidden fields to an auspex oper. Unblocks SUSPEND, admin LIST, SASET,
GETEMAIL. Priv bitset test + end-to-end auspex engine test.
2026-07-13 03:42:59 +00:00
4600ee062c
chanserv: SET TOPICLOCK and KEEPTOPIC (topic subsystem)
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.
2026-07-13 03:30:04 +00:00
d2c6448c6b
chanserv: SET SECUREOPS (strip ops from users without access)
When SECUREOPS is on, a user who gains channel-operator status without op-level
access is immediately deopped. Enforced in the engine's ChannelOp hook (manual
FMODE +o) and the Join hook (FJOIN prefix ops), sourced from ChanServ. Same
typed-settings path; shown in INFO; engine test covers on and off.
2026-07-13 03:14:43 +00:00
3bf6b2be77
chanserv: SET PEACE (block acting against equal-or-higher access)
PEACE stops a channel op from using ChanServ KICK/BAN/DEOP/DEVOICE against
someone whose access rank (founder>op>voice) is equal to or above their own.
Adds a typed access_rank on ChannelView and a peace_blocks guard in those
commands. Same typed-settings path as SIGNKICK/PRIVATE; shown in INFO.
2026-07-13 03:06:58 +00:00
75019c867a
chanserv: SET SIGNKICK and PRIVATE (typed channel settings)
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.
2026-07-13 02:54:46 +00:00
f388e3d650
nickserv/security: password policy, auth throttle, unguessable codes
Three brute-force / weak-secret gaps closed:
- REGISTER and SET PASSWORD now enforce a length range and reject a password
  equal to the nick (Anope's minpasslen/not-nick rules), with tests.
- IDENTIFY is throttled per account: a few free tries then an exponential
  backoff that clears on success, so a password can't be ground down. SASL had
  a limiter; interactive login had none.
- Emailed confirm/reset codes go from 6 digits to 8 base32 chars (~40 bits) and
  are burned after a few wrong guesses, closing a reset-code takeover window.
Throttle and code state are node-local and in-memory (not in the event log).
2026-07-13 02:28:47 +00:00
75ba9b35b4
nickserv: AJOIN auto-join list
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.
2026-07-13 02:09:03 +00:00
596630df53
modules: split services and the ircd link into external crates
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.
2026-07-13 01:33:56 +00:00
8ed1a9ab70
modules: capability-scoped Store/NetView boundary (Tier 2)
Services no longer receive the concrete account store. on_command now takes
&mut dyn Store + &dyn NetView (both defined in fedserv-api); the engine's Db
and Network implement them. Reads hand back plain views (AccountView,
ChannelView, ...) carrying only non-secret fields, so the event log, gossip,
and credential material (password hash, SCRAM verifiers) are unreachable from
a module. All command modules ported; behaviour unchanged.
2026-07-13 01:04:07 +00:00
931b8727e9
modules: extract fedserv-api leaf crate (protocol + service vocabulary)
Move NetEvent/NetAction/RegReply/Protocol and Sender/ServiceCtx into a
dependency-free fedserv-api crate that a module builds against. Core keeps
referring to them through re-export shims at crate::proto and
crate::engine::service, so no consumer changes. Groundwork for external
service and protocol modules.
2026-07-13 00:36:42 +00:00