Commit graph

138 commits

Author SHA1 Message Date
2fd5676ff2
Clippy sweep: clean across the workspace
- Box the oversized enum variants (Event::AccountRegistered, gossip
  Msg::Entry) so small events aren't sized to a full Account; serde is
  transparent so the wire/log format is unchanged.
- Use the configured server description in the SERVER line (it was
  hardcoded and the config field went unread).
- Derive the ChanServ SET label from the ChanSetting (drops toggle's 8th
  arg); rename inspircd from_us -> sourced.
- allow the two framework-idiom lints with rationale (tonic's Status in
  authorize; the guest-rename handler's inherent arg count).
- Auto-fixed map_or/contains/split_once style lints.
2026-07-13 23:30:51 +00:00
31d23cf6b3
HostServ: normalise vhosts + enforce uniqueness
Canonicalise a requested vhost the way the ircd displays it (a disallowed
underscore becomes a hyphen) before storing and applying, so what we keep
matches what shows on the network. A vhost can no longer be assigned to two
accounts: prepare_vhost normalises, validates, and checks vhost_owner at
every assignment point (SET/REQUEST/ACTIVATE/TAKE/DEFAULT), so two inputs
that collapse to the same host are caught as one.

Approach recommended by siniStar (the ns_nethost normalisation logic).
2026-07-13 23:05:41 +00:00
271e06fa77
HostServ: temporary vhosts (expiry)
SET <account> <host> [duration] assigns a vhost that lapses after the
duration (e.g. 30d); a Vhost carries an optional expiry and the store hides
an expired one lazily (like a suspension), so it stops applying on identify/
ON without a sweep. LIST flags temporary vhosts.
2026-07-13 22:53:04 +00:00
87bad7fbcc
HostServ: throttle vhost requests
A member may request a vhost at most once a minute; a request sooner is
refused with the wait time. In-memory per-account (not event-logged), the
same shape as the identify brute-force throttle.
2026-07-13 22:47:02 +00:00
6a8e02f004
HostServ: forbidden-vhost patterns + auto-vhost template
FORBID <regex> / FORBIDLIST / FORBIDDEL let operators block impersonating
user requests (e.g. (?i)(admin|staff)); REQUEST refuses a matching host.
Matching reuses the linear-time RegexSet, so untrusted patterns are safe.

TEMPLATE <$account...> sets a network auto-vhost pattern; DEFAULT gives a
user $account.users.example with their sanitised account name. HostServ's
node config (offers/forbidden/template) is consolidated into one HostConfig
threaded through the log replay.
2026-07-13 22:42:25 +00:00
2c32dcdc63
HostServ: vhost OFFER menu, plus SETALL/DELALL/GROUP
A curated self-serve menu: operators OFFER <host> / OFFERDEL <n>, anyone
sees OFFERLIST, and a user picks one with TAKE <n> (assigned and applied
at once). Offers are a global event-sourced list (VhostOfferAdded/Removed,
threaded through apply like bots). SETALL/DELALL alias SET/DEL and GROUP
is a no-op reassurance, since the per-account vhost already covers every
grouped nick.
2026-07-13 22:11:58 +00:00
898461d1c2
HostServ: ident@host vhosts (CHGIDENT)
A vhost may be ident@host, not just host: apply_vhost splits it and sends
CHGIDENT for the ident plus CHGHOST for the host. valid_vhost validates
the optional ident part; ON/SET/ACTIVATE/identify all route through
apply_vhost, so a bare host still works unchanged. New SetIdent action.
2026-07-13 22:04:16 +00:00
1edf250952
HostServ: vhost request/approval flow
REQUEST <host> records a pending vhost (typed Option<PendingVhost> on the
account, VhostRequested/VhostRequestCleared events); operators review with
WAITING and either ACTIVATE (assign + apply to online sessions) or REJECT
(clear it). Host validation and the operator gate are shared with SET.
2026-07-13 21:30:58 +00:00
1227e26b95
Add HostServ: virtual hosts
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>.
2026-07-13 21:26:38 +00:00
07eb9939d7
Add a JSON-RPC stats endpoint for websites
Plain HTTP + JSON-RPC 2.0 (axum, already in the tree) so a site backend
can pull stats without gRPC stubs. Methods: stats.get (counters + gauges)
and stats.channel { channel } (lines + top talkers). Optional [jsonrpc]
config, off unless set.

Security: bearer-token authenticated with a constant-time compare, refuses
to start with an empty token, read-only (only stats.* methods — no way to
mutate state), method-allowlisted, a 64 KiB body limit, and meant to bind
to localhost beside the backend.
2026-07-13 20:21:41 +00:00
04f927d3db
Promote BotServ BOTSTATS to a StatServ pseudo-service
Stats now have their own service. StatServ answers /msg StatServ <#channel>
(per-channel activity — lines + top talkers, founder-or-admin) and
/msg StatServ SERVER (the shared cross-service counter registry + gauges,
operators only). The shared counter map moved from the engine onto the
network view so both StatServ (via NetView::stat_counters) and the gRPC
Stats API read one source. BotServ's BOTSTATS command is removed; the
per-channel activity is still recorded in the dispatch loop.
2026-07-13 20:04:03 +00:00
ad61addb84
BotServ: community !votekick / !voteban
SET <#channel> VOTEKICK <n> enables in-channel voting: members type
!votekick <nick> (or !voteban), each voter counts once, and when n votes
are reached within the window the assigned bot kicks (or bans then kicks)
the target. Votes are ephemeral and lapse after two minutes. Crowd
moderation with no op online.
2026-07-13 19:32:25 +00:00
374748b1f9
BotServ: richer TRIGGERs — capture groups + cooldown
Responses can now use $1..$9 to substitute regex capture groups (e.g.
ADD hi (\w+)|Hi $1!), and an optional trailing |<secs> sets a per-trigger
cooldown so a trigger can't be spammed. The cache compiles each pattern to
its own Regex (for captures) and tracks last-fired for the cooldown.
2026-07-13 19:25:35 +00:00
766ef6ca47
BotServ: BOTSTATS per-channel activity
BOTSTATS <#channel> reports the lines the bot has seen this session and
the top talkers. Tracked live in the network view (bounded per-nick map),
recorded for bot-assigned channels only, and also fed to the shared stats
registry as botserv.messages.
2026-07-13 19:20:52 +00:00
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
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
801bfc5c96
BotServ: fantasy commands in-channel
A message beginning with ! in a channel that has a bot assigned is a
fantasy command: !op, !kick, !topic and the rest route through ChanServ's
existing handlers (the word becomes the command, the channel its first
argument), so access checks and behaviour are reused verbatim. The
resulting modes, kicks and replies are re-sourced from the bot, so the
bot is the visible actor.
2026-07-13 14:22:49 +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
f94b0afcd9
botserv: introduce bots as live pseudo-clients (slice 2)
The engine now reconciles the bot registry against the network: at burst and
after each command it introduces any bot not yet live (minting a SID-prefixed
uid) and quits (QuitUser) any removed from the registry. So BOT ADD makes the
bot appear immediately and BOT DEL removes it; a reconnect re-introduces all
registered bots fresh. Adds set_sid + per-connection bot-uid tracking. Next:
ASSIGN a bot to a channel, then in-channel fantasy commands.
2026-07-13 13:23:51 +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
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
d99eb16dab
grpc: full account-authority write API (Register through UngroupNick)
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.
2026-07-12 23:57:55 +00:00
e5a0d2acdf
grpc: directory replication API for websites to mirror accounts/channels
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.
2026-07-12 23:10:21 +00:00
bdcce01f11
email: optional logo image in the header
A configurable logo URL renders as an image beside the brand name in the
email header (falling back to the brand text when unset). Email clients
need a hosted image, so it takes a URL rather than an inline asset.
2026-07-12 16:36:06 +00:00
98630c7d36
email: premium HTML template with accent colour and copy affordance
Redesigns the mail template (branded header bar, hero code block, copy
button, muted footer) and adds a configurable accent colour alongside the
brand name.
2026-07-12 16:07:36 +00:00
06e9da4dc8
email: HTML templates and multipart messages
Emails now render an HTML template from templates/email with a plaintext
fallback, sent as multipart/alternative. A configurable brand name shows
in the template. Reset and confirmation mails share one styled base.
2026-07-12 15:57:34 +00:00
fbcf0eaac7
nickserv: email confirmation (CONFIRM) on registration
When email is configured and a REGISTER includes an address, the account
starts unverified and a confirmation code is emailed. CONFIRM <code>
verifies it (a federated AccountVerified event); INFO shows an unconfirmed
email until then. Emailed codes now carry a purpose (reset vs confirm).
2026-07-12 15:36:30 +00:00
a2957ffe02
email: outbound mail subsystem and RESETPASS
Adds a config-driven mailer: a SendEmail action the link layer pipes to
a configured command (sendmail -t, msmtp, ...) off-thread. NickServ
RESETPASS <account> emails a short-lived code to the address on file;
RESETPASS <account> <code> <newpassword> completes the reset. Codes are
node-local; the password change itself federates.
2026-07-12 15:28:15 +00:00
020e9bd576
nickserv: grouped nicks (GROUP/GLIST/UNGROUP) and GHOST
A nick can be grouped to an account and then identifies to it; the store
resolves an alias to its account everywhere it authenticates. GHOST (and
RECOVER) rename off a session using a nick you own. Grouped nicks
federate and are dropped with their account.
2026-07-12 14:59:23 +00:00
ef89f97158
nickserv: add SET (password/email) and DROP
SET PASSWORD changes your password (derived off-thread like register) and
SET EMAIL sets an address. DROP deletes your account, releases and drops
the channels you founded, and logs you out. A dropped account federates,
and each node logs out any local session that was relying on it.
2026-07-12 14:32:53 +00:00