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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
The gRPC build script required a system protoc, which a fresh clone and
the CI container lack (build failed with 'Could not find protoc'). Set
PROTOC from protoc-bin-vendored in build.rs (unless one is already set),
so cargo build works out of the box everywhere.
actions/checkout is a Node action and the rust:* container has no node,
so its post-step failed with 'node: not found'. Use node:20-bookworm
(node present for checkout) and install Rust via rustup, running build,
clippy, and test in one shell.
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.
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.
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.
Push-to-deploy had no automated gate. This Forgejo Actions workflow
builds the workspace, runs clippy as an error gate, and runs the tests
on every push and PR. Adjust runs-on to match the runner's label.
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.
ChanServ, BotServ, HostServ, MemoServ, OperServ, and StatServ now route
HELP through the shared renderer with per-command topic tables, so every
service supports HELP <command>. The renderer matches slash-grouped
names (OP/DEOP/VOICE/DEVOICE) so a paired command resolves as one topic.
Adds unit tests for the renderer.
New echo-api primitive: a HelpEntry table plus a shared help() renderer.
HELP lists a service's commands; HELP <command> prints that command's
detail. Every service routes its HELP arm through the one renderer, so
the shape is identical across the network. Wired into NickServ and the
six newer services so far.
Turn the README into a lean landing page: what Echo is, highlights, a
quick start, the service suite, a three-layer architecture summary, and
links into the wiki for the detail. The extended docs (architecture,
federation, services, configuration, the directory API, and the
module-writing guide) now live as wiki pages, so MODULES.md is retired.
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.
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.
They were appended to the module list as a bare afterthought; give each
service its own line and one-line role, formatted identically to the
others. The code already treated them as equals — same Service trait,
same store access, same registration; the docs now match.
The one command handler (ROLL/EXROLL/CALC/EXCALC all route to it) and
its formatting helpers move to roll.rs; lib.rs keeps the dispatcher, the
expression-evaluator module, and its tests. No behaviour change.
POST/DEL/LIST each move to their own file (each still parameterised by
bulletin kind, so OPOST/ODEL/OLIST route to the same handler); the kind
constants stay in lib.rs beside the dispatcher. No behaviour change.
REQUEST/CANCEL/LIST/VIEW/TAKE/NEXT/CLOSE each move to their own file;
the oper guard and the shared claim helper stay in lib.rs beside the
dispatcher. No behaviour change.
REGISTER/DROP/INFO/LIST/ADD/DEL/FLAGS each move to their own file; the
two shared guards (login check, can-manage) stay in lib.rs beside the
dispatcher. No behaviour change.
lib.rs kept only the struct and the dispatcher; SCORES and CHANFIX now
each live in their own file (scores.rs, fix.rs), matching every other
service. No behaviour change.
The protocol link is a different kind of module from the service
pseudo-clients, so give it its own home: modules/inspircd ->
modules/protocol/inspircd. A future ircd link is a new crate alongside
it. Workspace member, dependency path, the crate's own api path, and the
docs are updated to match. No code change.
The service pseudo-clients and the ircd protocol link sat flat at the
repo root, mixed in with the daemon core and the SDK. Move them all
under modules/ so the tree separates concerns cleanly: the daemon in
src/, the SDK every module links against in api/, and the loadable
modules — the pseudo-clients plus the protocol link — in modules/.
Workspace members, the daemon's per-crate dependency paths, and each
module's api path are updated to match; the docs follow. No code change.
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.
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.
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.