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.
This commit is contained in:
Jean Chevronnet 2026-07-14 15:24:41 +00:00
parent e7037572e5
commit 993f5b2eea
No known key found for this signature in database
159 changed files with 660 additions and 660 deletions

View file

@ -36,7 +36,7 @@ pub struct Config {
// Per-IP session limiting. Absent = unlimited.
#[serde(default)]
pub session: Option<Session>,
// Account authority. Absent = built-in (fedserv owns accounts). With
// Account authority. Absent = built-in (echo owns accounts). With
// `external = true`, an outside authority (e.g. the website) owns identity
// and pushes accounts in; IRC can only authenticate.
#[serde(default)]
@ -109,10 +109,10 @@ pub struct Oper {
impl Config {
// The account -> privileges table (casefolded keys) built from [[oper]].
pub fn opers(&self) -> std::collections::HashMap<String, fedserv_api::Privs> {
pub fn opers(&self) -> std::collections::HashMap<String, echo_api::Privs> {
let mut map = std::collections::HashMap::new();
for o in &self.oper {
map.insert(o.account.to_ascii_lowercase(), fedserv_api::Privs::from_names(&o.privs));
map.insert(o.account.to_ascii_lowercase(), echo_api::Privs::from_names(&o.privs));
}
map
}
@ -234,7 +234,7 @@ pub struct Peer {
}
fn default_peer_name() -> String {
"fedserv".to_string()
"echo".to_string()
}
#[derive(Debug, Deserialize)]

View file

@ -39,9 +39,9 @@ pub use event::Event;
pub(crate) use event::{apply, Scope};
// Error kinds, the emailed-code purpose, and the module-facing views live in the
// fedserv-api SDK crate; re-exported so the engine keeps naming them locally and
// echo-api SDK crate; re-exported so the engine keeps naming them locally and
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
pub use fedserv_api::{
pub use echo_api::{
AccountView, AjoinView, AkillView, BotView, Caps, GroupView, HelpView, IgnoreView, MemoView, NewsView, Privs, ReportView, SuspensionView, ChanAccessView, ChanAkickView, ChanError, ChanSetting, ChannelView, CertError, CodeKind, Kicker, RegError, Store, TriggerView, VhostView,
};
@ -538,7 +538,7 @@ impl ChannelInfo {
self.access
.iter()
.find(|a| a.account.eq_ignore_ascii_case(account))
.and_then(|a| fedserv_api::level_caps(&a.level).auto)
.and_then(|a| echo_api::level_caps(&a.level).auto)
}
/// The matching auto-kick entry for `hostmask` (nick!user@host), if any.

View file

@ -376,7 +376,7 @@ impl Db {
self.net.groups.iter().find(|g| key(&g.name) == k).map(|g| GroupView {
name: g.name.clone(),
founder: g.founder.clone(),
members: g.members.iter().map(|m| fedserv_api::GroupMemberView { account: m.account.clone(), flags: m.flags.clone() }).collect(),
members: g.members.iter().map(|m| echo_api::GroupMemberView { account: m.account.clone(), flags: m.flags.clone() }).collect(),
})
}
@ -409,7 +409,7 @@ impl Db {
pub fn channel_caps(&self, channel: &str, account: &str) -> Caps {
let Some(c) = self.channels.get(&key(channel)) else { return Caps::default() };
if c.founder.eq_ignore_ascii_case(account) {
return fedserv_api::level_caps("founder");
return echo_api::level_caps("founder");
}
let mut caps = Caps::default();
for a in &c.access {
@ -418,7 +418,7 @@ impl Db {
None => a.account.eq_ignore_ascii_case(account),
};
if applies {
caps = caps.union(fedserv_api::level_caps(&a.level));
caps = caps.union(echo_api::level_caps(&a.level));
}
}
caps

View file

@ -1,7 +1,7 @@
use super::*;
fn tmp(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!("fedserv-log-{name}.jsonl"));
let p = std::env::temp_dir().join(format!("echo-log-{name}.jsonl"));
let _ = std::fs::remove_file(&p);
p
}
@ -12,8 +12,8 @@
#[test]
fn formats_unix_time_as_utc() {
assert_eq!(fedserv_api::human_time(0), "1970-01-01 00:00:00 UTC");
assert_eq!(fedserv_api::human_time(1783844590), "2026-07-12 08:23:10 UTC");
assert_eq!(echo_api::human_time(0), "1970-01-01 00:00:00 UTC");
assert_eq!(echo_api::human_time(1783844590), "2026-07-12 08:23:10 UTC");
}
#[test]

View file

@ -12,7 +12,7 @@ use tokio::sync::mpsc;
use crate::proto::{NetAction, NetEvent, RegReply};
use db::{Db, LogEntry, RegError};
use scram::Verifier;
use fedserv_api::Privs;
use echo_api::Privs;
use service::{Sender, Service, ServiceCtx};
use state::Network;
@ -468,14 +468,14 @@ impl Engine {
if let Some(lead) = self.expire_warn.filter(|_| self.db.email_enabled()) {
if let Some(ttl) = self.account_ttl {
for (account, email, left) in self.db.accounts_to_warn(now, ttl, lead) {
let mail = fedserv_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), "account", &account, &human_duration(left));
let mail = echo_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), "account", &account, &human_duration(left));
self.emit_irc(NetAction::SendEmail { to: email, subject: mail.subject, text: mail.text, html: Some(mail.html) });
self.db.mark_account_warned(&account);
}
}
if let Some(ttl) = self.channel_ttl {
for (channel, email, left) in self.db.channels_to_warn(now, ttl, lead) {
let mail = fedserv_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), "channel", &channel, &human_duration(left));
let mail = echo_api::email::expiry_warning(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), "channel", &channel, &human_duration(left));
self.emit_irc(NetAction::SendEmail { to: email, subject: mail.subject, text: mail.text, html: Some(mail.html) });
self.db.mark_channel_warned(&channel);
}

View file

@ -40,7 +40,7 @@ impl Engine {
if status == AuthorityStatus::Ok && !self.db.is_verified(name) {
if let Some(addr) = addr {
let code = self.db.issue_code(name, db::CodeKind::Confirm);
let mail = fedserv_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code);
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code);
self.emit_irc(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
}
}
@ -177,7 +177,7 @@ impl Engine {
if ok && !self.db.is_verified(account) {
if let (Some(addr), RegReply::NickServ { agent, uid, .. }) = (addr, &reply) {
let code = self.db.issue_code(account, db::CodeKind::Confirm);
let mail = fedserv_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code);
let mail = echo_api::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code);
out.push(NetAction::SendEmail { to: addr, subject: mail.subject, text: mail.text, html: Some(mail.html) });
out.push(NetAction::Notice { from: agent.clone(), to: uid.clone(), text: "A confirmation code has been emailed to you. Confirm with \x02CONFIRM <code>\x02.".to_string() });
}

View file

@ -1,4 +1,4 @@
// Sender, ServiceCtx, and the Service trait a pseudo-client implements all live
// in the fedserv-api SDK crate; re-exported so the engine and the service
// in the echo-api SDK crate; re-exported so the engine and the service
// modules keep using `crate::engine::service::{...}`.
pub use fedserv_api::{Sender, Service, ServiceCtx};
pub use echo_api::{Sender, Service, ServiceCtx};

View file

@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
// The read-only network view a module sees; re-exported so the engine keeps
// naming it locally.
pub use fedserv_api::{IncidentView, NetView, SeenView};
pub use echo_api::{IncidentView, NetView, SeenView};
// The most recent moderation/action incidents kept for LOGSEARCH.
const INCIDENT_CAP: usize = 10_000;

File diff suppressed because it is too large Load diff

View file

@ -265,10 +265,10 @@ async fn send(tx: &mpsc::Sender<String>, msg: &Msg) -> Result<(), ()> {
mod tests {
use super::*;
use crate::engine::db::Db;
use fedserv_nickserv::NickServ;
use echo_nickserv::NickServ;
fn engine(origin: &str, tag: &str) -> (Shared, Outbound) {
let path = std::env::temp_dir().join(format!("fedserv-gossip-{tag}.jsonl"));
let path = std::env::temp_dir().join(format!("echo-gossip-{tag}.jsonl"));
let _ = std::fs::remove_file(&path);
let (tx, _) = broadcast::channel(1024);
let mut db = Db::open(&path, origin);
@ -411,18 +411,18 @@ mod tests {
// Generate a CA + node cert with openssl into a temp dir; None if unavailable.
fn gen_certs() -> Option<String> {
use std::process::Command;
let dir = std::env::temp_dir().join(format!("fedserv-tls-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("echo-tls-{}", std::process::id()));
std::fs::create_dir_all(&dir).ok()?;
let d = dir.to_str()?.to_string();
let run = |args: &[&str]| Command::new("openssl").args(args).status().map(|s| s.success()).unwrap_or(false);
let ok = run(&["req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes",
"-keyout", &format!("{d}/ca.key"), "-out", &format!("{d}/ca.crt"), "-days", "1", "-subj", "/CN=fedserv-ca"])
"-keyout", &format!("{d}/ca.key"), "-out", &format!("{d}/ca.crt"), "-days", "1", "-subj", "/CN=echo-ca"])
&& run(&["req", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes",
"-keyout", &format!("{d}/node.key"), "-out", &format!("{d}/node.csr"), "-subj", "/CN=fedserv"]);
"-keyout", &format!("{d}/node.key"), "-out", &format!("{d}/node.csr"), "-subj", "/CN=echo"]);
if !ok {
return None;
}
std::fs::write(format!("{d}/ext"), "subjectAltName=DNS:fedserv,IP:127.0.0.1\n").ok()?;
std::fs::write(format!("{d}/ext"), "subjectAltName=DNS:echo,IP:127.0.0.1\n").ok()?;
run(&["x509", "-req", "-in", &format!("{d}/node.csr"), "-CA", &format!("{d}/ca.crt"),
"-CAkey", &format!("{d}/ca.key"), "-CAcreateserial", "-out", &format!("{d}/node.crt"),
"-days", "1", "-extfile", &format!("{d}/ext")])
@ -441,7 +441,7 @@ mod tests {
a.lock().await.test_register("alice");
let (sside, cside) = tokio::io::duplex(64 * 1024);
let name = ServerName::try_from("fedserv").unwrap();
let name = ServerName::try_from("echo").unwrap();
let (server, client) = tokio::join!(acceptor.accept(sside), connector.connect(name, cside));
let sa = tokio::spawn(session(server.expect("tls accept"), a.clone(), "s3cret".into(), "A".into(), atx));
let sb = tokio::spawn(session(client.expect("tls connect"), b.clone(), "s3cret".into(), "B".into(), btx));

View file

@ -1,6 +1,6 @@
// Directory replication over gRPC: lets a website (Django, or anything else
// with a protoc-generated client) mirror the account/channel directory this
// node owns. See proto/fedserv.proto for the wire contract and exactly which
// node owns. See proto/echo.proto for the wire contract and exactly which
// fields are exposed — no credentials of any kind cross this API.
use std::pin::Pin;
use std::sync::Arc;
@ -17,7 +17,7 @@ use crate::engine::db::{Account, ChannelInfo, Db, Event, LogEntry};
use crate::engine::{AuthorityStatus, Engine};
pub mod pb {
tonic::include_proto!("fedserv.v1");
tonic::include_proto!("echo.v1");
}
use pb::accounts_server::{Accounts, AccountsServer};
@ -414,12 +414,12 @@ pub async fn run(engine: Shared, cfg: GrpcCfg, outbound: broadcast::Sender<LogEn
mod tests {
use super::*;
use crate::engine::db::Db;
use fedserv_nickserv::NickServ;
use echo_nickserv::NickServ;
use tokio_stream::StreamExt;
use tonic::metadata::MetadataValue;
fn engine_with(tag: &str) -> (Shared, broadcast::Sender<LogEntry>) {
let path = std::env::temp_dir().join(format!("fedserv-grpc-{tag}.jsonl"));
let path = std::env::temp_dir().join(format!("echo-grpc-{tag}.jsonl"));
let _ = std::fs::remove_file(&path);
let (tx, _) = broadcast::channel(1024);
let mut db = Db::open(&path, "A");
@ -670,12 +670,12 @@ mod tests {
}
// Full round trip through the real confirmation-email pipeline: register with
// email confirmation on, capture the code fedserv actually emails out (never
// email confirmation on, capture the code echo actually emails out (never
// returned by the RPC — proving the caller can't skip owning the inbox), then
// confirm with it.
#[tokio::test]
async fn confirm_completes_with_the_real_emailed_code() {
let path = std::env::temp_dir().join("fedserv-grpc-acct-confirm.jsonl");
let path = std::env::temp_dir().join("echo-grpc-acct-confirm.jsonl");
let _ = std::fs::remove_file(&path);
let (tx, _) = broadcast::channel(1024);
let mut db = Db::open(&path, "A");

View file

@ -46,7 +46,7 @@ pub async fn run(engine: Shared, cfg: JsonRpcCfg) {
.with_state(state);
match tls {
// TLS terminated here → HTTP/2 (ALPN) plus HTTP/1.1, encrypted to fedserv.
// TLS terminated here → HTTP/2 (ALPN) plus HTTP/1.1, encrypted to echo.
Some(t) => {
// The dependency tree carries more than one rustls crypto provider, so
// pin one before building any TLS config (ignored if already set).
@ -155,7 +155,7 @@ mod tests {
use crate::engine::db::Db;
fn engine_with_stat(tag: &str) -> Shared {
let path = std::env::temp_dir().join(format!("fedserv-jsonrpc-{tag}.jsonl"));
let path = std::env::temp_dir().join(format!("echo-jsonrpc-{tag}.jsonl"));
let _ = std::fs::remove_file(&path);
let mut e = Engine::new(vec![], Db::open(&path, "42S"));
e.bump("botserv.messages");

View file

@ -106,7 +106,7 @@ fn dispatch_email(email: &Option<crate::config::Email>, to: String, subject: Str
let headers = format!("From: {}\r\nTo: {to}\r\nSubject: {subject}\r\nMIME-Version: 1.0\r\n", email.from);
let msg = match html {
Some(html) => {
let b = "fedserv-alt-boundary-x9";
let b = "echo-alt-boundary-x9";
format!(
"{headers}Content-Type: multipart/alternative; boundary=\"{b}\"\r\n\r\n\
--{b}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{text}\r\n\

View file

@ -1,6 +1,6 @@
// Core lives in src/; the pseudo-clients and the ircd link are external module
// crates (fedserv-nickserv, fedserv-chanserv, fedserv-inspircd), each depending
// only on the fedserv-api SDK.
// crates (echo-nickserv, echo-chanserv, echo-inspircd), each depending
// only on the echo-api SDK.
mod config;
mod engine;
mod gossip;
@ -15,28 +15,28 @@ use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
use engine::Engine;
use fedserv_botserv::BotServ;
use fedserv_chanserv::ChanServ;
use fedserv_memoserv::MemoServ;
use fedserv_statserv::StatServ;
use fedserv_hostserv::HostServ;
use fedserv_operserv::OperServ;
use fedserv_diceserv::DiceServ;
use fedserv_infoserv::InfoServ;
use fedserv_reportserv::ReportServ;
use fedserv_groupserv::GroupServ;
use fedserv_chanfix::ChanFix;
use fedserv_helpserv::HelpServ;
use fedserv_example::ExampleServ;
use fedserv_inspircd::InspIrcd;
use fedserv_nickserv::NickServ;
use echo_botserv::BotServ;
use echo_chanserv::ChanServ;
use echo_memoserv::MemoServ;
use echo_statserv::StatServ;
use echo_hostserv::HostServ;
use echo_operserv::OperServ;
use echo_diceserv::DiceServ;
use echo_infoserv::InfoServ;
use echo_reportserv::ReportServ;
use echo_groupserv::GroupServ;
use echo_chanfix::ChanFix;
use echo_helpserv::HelpServ;
use echo_example::ExampleServ;
use echo_inspircd::InspIrcd;
use echo_nickserv::NickServ;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "fedserv=debug".into()),
.unwrap_or_else(|_| "echo=debug".into()),
)
.init();
@ -131,7 +131,7 @@ async fn main() -> Result<()> {
}));
}
let (gossip_tx, _) = tokio::sync::broadcast::channel::<engine::db::LogEntry>(1024);
let mut db = engine::db::Db::open("fedserv.db.jsonl", &cfg.server.sid);
let mut db = engine::db::Db::open("echo.db.jsonl", &cfg.server.sid);
db.scram_iterations = cfg.server.scram_iterations;
db.set_outbound(gossip_tx.clone());
db.set_email_enabled(cfg.email.is_some());

View file

@ -1,4 +1,4 @@
// The normalized protocol vocabulary lives in the fedserv-api SDK crate;
// The normalized protocol vocabulary lives in the echo-api SDK crate;
// re-exported so the engine keeps referring to it as `crate::proto::*`. The
// concrete ircd link (InspIRCd) is an external module crate, not part of core.
pub use fedserv_api::{NetAction, NetEvent, Protocol, RegReply};
pub use echo_api::{NetAction, NetEvent, Protocol, RegReply};