Commit graph

275 commits

Author SHA1 Message Date
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
39d7421c06
NickServ: show last-seen (and online status) in INFO
The account's last-active time was tracked but never surfaced. INFO now
reports it, reading 'now (online)' when the account has a live session.
Threads NetView into the INFO handler for the session lookup and carries
last_seen on AccountView.
2026-07-16 01:56:51 +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
0027decdb7
OperServ: add SET READONLY lockdown
All checks were successful
CI / check (push) Successful in 3m31s
2026-07-16 01:05:48 +00:00
b2442f85e6
OperServ: add SHUTDOWN and RESTART
All checks were successful
CI / check (push) Successful in 3m31s
2026-07-16 01:02:10 +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
cd6d8b1b93
ChanServ: add SYNC as an alias for ENFORCE
All checks were successful
CI / check (push) Successful in 3m33s
2026-07-16 00:50:42 +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
c646ccc0ec
NickServ: add SASET for operator account edits
Some checks failed
CI / check (push) Has been cancelled
2026-07-16 00:45:18 +00:00
d819b2c75f
MemoServ: add RSEND read receipts
Some checks failed
CI / check (push) Has been cancelled
2026-07-16 00:43:01 +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
125b8c7701
NickServ: RECOVER regains your nick
All checks were successful
CI / check (push) Successful in 3m41s
Split RECOVER from GHOST: GHOST just frees a session off a nick you own,
while RECOVER also puts you back onto it (frees the ghost if present, then
SVSNICKs you to the nick). RECOVER works on an already-free nick too.
2026-07-16 00:21:07 +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
ccbbb91fda
OperServ: add SVSPART
All checks were successful
CI / check (push) Successful in 3m36s
SVSPART <nick> <#channel> [reason] forces a user out of a channel
(admin-only), completing the SVS command family alongside SVSNICK and
SVSJOIN. New ForcePart action rendered as the ircd SVSPART command.
2026-07-16 00:00:18 +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
6e3a758cc1
ChanServ: add OWNER/PROTECT/HALFOP status commands
All checks were successful
CI / check (push) Successful in 3m47s
OWNER/DEOWNER (+q), PROTECT/DEPROTECT (+a, alias ADMIN), and
HALFOP/DEHALFOP (+h) set the higher channel-status modes. They reuse the
OP handler's target resolution and PEACE check; OWNER is founder-only,
the rest need op access.
2026-07-15 18:40:19 +00:00
30e91bb295
MemoServ SENDALL + ChanServ AKICK CLEAR
All checks were successful
CI / check (push) Successful in 3m46s
SENDALL (admin) leaves a memo on every registered account for a
network-wide announcement that persists until read. AKICK CLEAR empties a
channel's auto-kick list in one command.
2026-07-15 18:32:46 +00:00
21efbd32c1
NickServ: add UPDATE and LIST
All checks were successful
CI / check (push) Successful in 3m46s
UPDATE re-applies your auto-joins and vhost and re-checks for waiting
memos without re-identifying. LIST (auspex-gated) shows registered
accounts matching a glob, capped at 100. Adds an accounts_matching store
method.
2026-07-15 18:24:52 +00:00
959d085e7a
ChanServ: add UP and DOWN
All checks were successful
CI / check (push) Successful in 3m47s
UP re-applies the op/voice status your channel access entitles you to;
DOWN removes your status. Mirrors the OP/VOICE command's access check and
mode emission, and requires you to actually be in the channel.
2026-07-15 18:14:39 +00:00
98ecbca414
MemoServ: add CANCEL, CHECK, and INFO
All checks were successful
CI / check (push) Successful in 3m49s
CANCEL recalls the sender's most recent unread memo to a target (one the
recipient already read can't be recalled); CHECK reports whether the last
memo you sent someone has been read; INFO summarises your mailbox. Adds
memo_cancel/memo_check store methods and shares MAX_MEMOS from the module
root instead of duplicating it in send.rs.
2026-07-15 18:09:33 +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
9ed40a2e7f
License (AGPL-3.0) and graceful shutdown
All checks were successful
CI / check (push) Successful in 3m48s
Add the AGPL-3.0 license (matching Orbit) and set it in Cargo.toml so
the crate metadata is complete. Handle SIGTERM/Ctrl-C: the run loop now
selects against a shutdown signal and exits cleanly, so systemctl stop
returns promptly instead of waiting out the kill timeout (every event is
already fsync'd, so nothing is lost either way). Verified: on SIGTERM the
daemon logs and exits 0.
2026-07-15 12:05:39 +00:00
e551a01cbf
Harden for production: fsync writes, deployment + backup tooling
All checks were successful
CI / check (push) Successful in 3m55s
The event log is the account/channel authority, so make it durable:
persist() now fsyncs every committed event before reporting success, and
a serialisation failure is an error instead of a silently-dropped blank
line. Add a hardened systemd unit (scripts/echo.service), an atomic
gzip backup script with retention (scripts/backup.sh), and a Deployment
wiki page (install, durability, backup/restore, upgrade, monitoring).
2026-07-15 11:45:50 +00:00
0ed6684522
Validate config on load; document the SDK's core traits
Some checks failed
CI / check (push) Failing after 53s
Config::load now checks the SID is 3 alphanumeric chars and that
server.name and uplink.host are set, failing with a clear message
instead of a cryptic link-time error. Give the Service and Protocol
traits and ServiceCtx proper rustdoc so cargo doc is useful to module
authors.
2026-07-14 23:31:44 +00:00
955e55d47f
Add per-service integration tests
The service crates had almost no tests. Exercise each one's core path
through a real Engine: MemoServ delivery, GroupServ register/add,
ReportServ file/close, InfoServ post (admin-gated), HostServ
request/activate, OperServ akill add/remove, BotServ bot add (oper),
StatServ SERVER gating, and ChanFix/DiceServ basics. +9 tests.
2026-07-14 23:28:08 +00:00
d9c0878b00
Log store write failures at the source; share the oper guard
The store's map_err(|_| Internal) discarded the real IO error, so a
disk-full or permissions failure was invisible; log it once in the log
append path where it happens. Add echo_api::require_oper (parameterised
by the required privilege) and route the three copied per-service guards
through it, keeping each service's own wording and policy.
2026-07-14 23:19:00 +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
dfc11bd1c4
Make every pseudo-client a first-class default service
GroupServ, ChanFix, ReportServ, HelpServ and DiceServ were left out of
the default service suite, so they came up only when explicitly opted
in — an add-on tier the others don't have. They are core services like
any other: put them in default_services() so the standard suite is the
full set of pseudo-clients, and correct the stale 'NickServ + ChanServ'
default in the config comment, main.rs, README and MODULES.
2026-07-14 15:02:13 +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
0c7976e80d
Reorganise the store into a domain-split module tree
engine/db.rs had grown past 4k lines. Split the single Db type's
operations into sibling files by domain — account, channel, network,
the event log, and the module-facing store facade — while all type
definitions, the log plumbing, and shared helpers stay in db/mod.rs.
Pure reorganisation: no behaviour change, tests unchanged and green.
2026-07-14 13:57:05 +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
1e8a5eab36
Add DiceServ: dice-roll and math-expression service
A stateless service for tabletop games over IRC. ROLL/CALC evaluate a dice
+ math expression (ROLL rounds to a whole number, CALC keeps decimals);
EXROLL/EXCALC also show each die. Supports NdM / dM / d% dice, + - * / ^ %,
parentheses, implicit multiplication, unary minus, pi/e, a set of functions
(sqrt, floor, sin, min, …), and N~expr to repeat a roll.

Modernises the C++ original: it ships its own Mersenne-Twister RNG in C
and parses via shunting-yard with a variadic-argument hack; this is a
typed AST from a small recursive-descent parser plus the rand crate —
shorter, and the dice/sides/total caps make abuse (999999d6) a clean error
instead of a hang. Opt-in (add "diceserv" to [modules].services).
2026-07-14 03:46:34 +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