Commit graph

44 commits

Author SHA1 Message Date
3f97a66dbb
migrate: carry channel SET flags, noexpire, and account hide-status
All checks were successful
CI / check (push) Successful in 3m45s
Completes the importer: Anope's per-channel bool flags (KEEPTOPIC, PEACE,
SIGNKICK, BS_GREET, and defensively SECUREOPS/RESTRICTED/PRIVATE/TOPICLOCK/
NOAUTOOP/NOBOT) now map to ChanSettings, CS_NO_EXPIRE to channel noexpire, and
NickCore HIDE_MASK to the account's hide-status. PERSIST/SECUREFOUNDER/
KEEP_MODES/FANTASY have no Echo equivalent and are skipped. Verified: #devs
carries its full flag set + noexpire off the real backup.
2026-07-16 15:03:54 +00:00
062eae703e
add Anope->Echo migration importer (echo import)
All checks were successful
CI / check (push) Successful in 3m41s
New 'echo import <anope.json> <out.log> [node]' subcommand builds a fresh event
log from an Anope db_json: accounts (identity only — Anope's one-way password
hashes can't become SCRAM verifiers, so migrated users auth by certfp/external
authority), grouped nicks, cert fingerprints, vhosts, channels (+desc/topic/
assigned bot), access mapped to op/voice by each channel's AUTOOP/AUTOVOICE
thresholds, param-less mode locks, BotServ bots (service pseudo-clients skipped),
and memos. Read-only on the source; self-verifies by replaying the produced log.
Verified against the live backup (11 accounts, 10 channels, 10 bots) + a unit
test over a synthetic fixture. The prerequisite for a safe cutover.
2026-07-16 13:05:45 +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
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
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
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
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
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
76a7162227
OperServ: per-IP session limiting with exceptions
The connection that puts an IP over its allowance is killed on connect.
The ircd's per-user IP (UID param 7) is now plumbed through UserConnect and
tracked per-IP in the live network view (incremented on connect, released
on quit).

- [session] default_limit sets the base allowance (0/absent = off).
- OperServ SESSION LIST <min> / VIEW <ip> inspect live counts.
- OperServ EXCEPTION ADD <ip-mask> <limit> [reason] / DEL / LIST override
  the limit per IP-mask (limit 0 = unlimited); event-sourced so they
  federate and survive restart, matched most-permissive-wins.

All admin-gated.
2026-07-14 01:46:09 +00:00
57e104e510
Email owners before inactivity-expiry drops their account or channel
When [expire] warn_days is set and email is configured, the expiry sweep
now emails a heads-up to owners entering the warning window (inactive past
the threshold minus the lead time, but not yet expired) for whom an
address is on file — the founder's address for a channel. It's a chance to
keep the record by simply using it before the deadline.

Each idle spell warns at most once: a new AccountExpiryWarned /
ChannelExpiryWarned event marks the record, and the next activity stamp
clears the mark so a later idle spell warns afresh. The warning email is a
first-class api template (fedserv_api:📧:expiry_warning), alongside
the reset and confirm mails.
2026-07-14 00:18:43 +00:00
d2c1d076fb
Add OperServ with AKILL network bans
The first operator module. AKILL manages network bans as a typed,
event-sourced list with lazy expiry, mirroring the patterns the other
modules use:

- AKILL ADD [+expiry] <user@host> <reason> stores the ban and drives an
  ircd G-line (applied to matching users already online and to future
  connections); AKILL DEL <mask|number> lifts it; AKILL LIST [pattern]
  shows the live list, numbered. Admin-only.
- Bans are Global events, so every node holds the list and re-asserts each
  one at burst with its remaining duration. Expired bans are hidden lazily
  and dropped at compaction.
- Masks are normalised to user@host (a nick! prefix is stripped, both
  sides lowercased) and an all-wildcard mask is refused.

Email (send_email) is already reachable from the api via ServiceCtx, and
adds AddLine/DelLine to the action vocabulary for any future X-line kinds.
2026-07-14 00:12:29 +00:00
253d4c2a2d
Add lazy inactivity-expiry for accounts and channels
Accounts and channels now carry a last-activity stamp and are dropped
once inactive past a configured threshold. A login stamps the account, a
join stamps the channel — both coalesced to at most one log entry per day
so routine traffic doesn't bloat the log, and stamps replicate so
activity on any node keeps a record alive network-wide.

Expiry is lazy in the proven style: a periodic pass computes it from the
stored stamps with no per-record timer, reusing the same relation the
vhost and suspension expiries use. The sweep spares operator accounts,
accounts with a live session, occupied channels, and anything an operator
has pinned with the new NOEXPIRE command (NickServ NOEXPIRE <account>,
ChanServ NOEXPIRE <#channel>, both Priv::Admin). Each expiry is announced
to the audit channel and clears the channel's +r.

Configured under [expire] (accounts_days / channels_days); a zero or
omitted field leaves that kind never expiring, and omitting the section
disables expiry entirely.
2026-07-14 00:02:36 +00:00
698d604954
Add a staff audit feed for notable service actions
A [log] channel now receives a one-line announcement of each notable
state change a service command makes — registrations, drops, vhosts,
suspensions, akicks, access and founder changes, bot and oper host-config
changes — naming the actor (by nick, plus account when it differs) and
what changed. The feed reads the typed event log directly: dispatch marks
the log before running a command and diffs it after, so each announcement
is exactly that command's footprint, sourced from the services server so
it reaches the channel without a pseudo-client having to be present.

Private and cosmetic events are deliberately never surfaced: memo bodies,
password/verifier material, greets, auto-join, personal and channel
cosmetic settings. Omitting [log] disables the feed.
2026-07-13 23:48:23 +00:00
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
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
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
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
e0bb3a1fea
modules: [modules] config + example service crate + SDK guide
A [modules] block selects which services start (default: NickServ + ChanServ);
main.rs constructs each compiled-in module only when named, so a node can run a
subset and optional modules ship dormant. Add fedserv-example, a minimal
read-only Service that depends on fedserv-api alone -- the template a new
pseudo-client is copied from, enabled by adding "example" to the config.
MODULES.md documents writing and registering a service or protocol module.
2026-07-13 01:48:09 +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
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
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
aab64baf86
nickserv: log out the loser of a registration conflict
When a peer's registration wins a name a local session was logged into,
that session authenticated against a credential that no longer exists.
The node now clears its accountname (the ircd emits RPL_LOGGEDOUT) and
tells the user why. A new outbound channel lets the engine push these
services-initiated actions to the uplink from the gossip path, drained
by the link loop alongside incoming traffic.
2026-07-12 13:34:38 +00:00
677963924a
Reorganize protocol and pseudoclients into a modules/ tree
The core stays in src/; protocol and pseudoclients move to modules/,
pulled in with #[path].
2026-07-12 10:37:59 +00:00
89593ebeb4
Add ChanServ with channel registration
REGISTER/INFO/DROP with founder auth, stored as ChannelRegistered and
ChannelDropped events so channel registrations replicate across nodes
and compact like accounts. Registration requires an identified account;
only the founder can drop.
2026-07-12 08:19:58 +00:00
cc0265548f
Compact the event log into per-account snapshots
Once the log grows past ~3x the account count it is rewritten to one
AccountRegistered per account (churned cert events dropped), via a temp
file and rename. Cert replay is now idempotent, so a snapshot plus
re-delivered events converge and a peer syncing from a compacted node
still gets every account. Runs on a 30m timer.
2026-07-12 07:24:44 +00:00
df7b413862
Push new log entries to peers on commit
A broadcast channel forwards each freshly committed entry straight to
connected peers, so a registration propagates in milliseconds. The
periodic digest drops to a 10s anti-entropy backstop.
2026-07-12 07:20:25 +00:00
d0556ebe8c
Add gossip replication between nodes
Nodes run anti-entropy over a TCP link: each advertises its version
vector, the peer replies with the log entries it lacks, and ingest is
idempotent so re-delivery and reconnect after a split both converge.
The engine is shared behind a mutex; config gains [gossip] and [[peer]].
2026-07-12 07:05:07 +00:00
c066f1072f
Add EventLog with origin/seq metadata for replication 2026-07-12 06:08:27 +00:00
60fc687162
Add NickServ LOGOUT command
Clears the account (RPL_LOGGEDOUT) and renames the user to a
configurable guest nick (default Guest) via SVSNICK.
2026-07-12 05:06:34 +00:00
496c0f99a6
Add SASL SCRAM-SHA-256 and SCRAM-SHA-512 2026-07-12 03:59:24 +00:00
9285105afd
Add persisted account store with NickServ REGISTER/IDENTIFY
Accounts are an append-only JSONL event log (state is the fold of the log) with
argon2id-hashed passwords. NickServ gains REGISTER <password> [email] and
IDENTIFY <password>; the engine tracks UID->nick from the burst so a command
resolves to the sender's current nick. Classic and ircd-agnostic — the cap-based
account-registration forward is the next step.
2026-07-11 20:28:42 +00:00
f82ab0c70a
Scaffold the federated services daemon
Protocol-agnostic core: a Protocol trait maps raw server-to-server lines to a
normalized event/action model, so the engine never touches a raw line and a new
ircd is one new module. Adds event-log state (state is a fold over the log) and
a service framework. First protocol impl links to an InspIRCd (insp4) uplink —
completes the CAPAB/SERVER handshake, bursts, and introduces NickServ.
2026-07-11 19:28:30 +00:00