echo/src/main.rs
Jean 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

96 lines
3.4 KiB
Rust

// 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.
mod config;
mod engine;
mod gossip;
mod grpc;
mod link;
mod proto;
use anyhow::Result;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
use engine::Engine;
use fedserv_chanserv::ChanServ;
use fedserv_inspircd::InspIrcd;
use fedserv_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()),
)
.init();
let path = std::env::args().nth(1).unwrap_or_else(|| "config.toml".to_string());
let cfg = config::Config::load(&path)?;
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let proto = Box::new(InspIrcd::new(
cfg.server.name.clone(),
cfg.server.sid.clone(),
cfg.uplink.password.clone(),
cfg.server.protocol,
ts,
));
let services: Vec<Box<dyn engine::service::Service>> = vec![
Box::new(NickServ {
uid: format!("{}AAAAAA", cfg.server.sid),
guest_nick: cfg.server.guest_nick.clone(),
guest_seq: (ts % 100_000) as u32,
}),
Box::new(ChanServ {
uid: format!("{}AAAAAB", cfg.server.sid),
}),
];
let (gossip_tx, _) = tokio::sync::broadcast::channel::<engine::db::LogEntry>(1024);
let mut db = engine::db::Db::open("fedserv.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());
if let Some(email) = &cfg.email {
db.set_email_brand(&email.brand);
db.set_email_accent(&email.accent);
db.set_email_logo(&email.logo);
}
let engine = Arc::new(Mutex::new(Engine::new(services, db)));
// Channel for services-initiated actions to reach the uplink (drained by the link loop).
let (irc_tx, irc_rx) = tokio::sync::mpsc::unbounded_channel();
engine.lock().await.set_irc_out(irc_tx);
if let Some(gossip) = cfg.gossip.clone() {
tracing::info!(peers = cfg.peer.len(), "starting gossip");
tokio::spawn(gossip::run(engine.clone(), gossip, cfg.peer.clone(), cfg.server.sid.clone(), gossip_tx.clone()));
}
// Directory replication for websites (Django, etc.) — subscribes to the same
// committed-entry stream gossip does, so a website sees the account/channel
// directory update in real time without touching IRC at all.
if let Some(grpc_cfg) = cfg.grpc.clone() {
tokio::spawn(grpc::run(engine.clone(), grpc_cfg, gossip_tx));
}
// Periodically fold log churn into a snapshot when it grows past the accounts.
{
let engine = engine.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1800)).await;
if let Err(e) = engine.lock().await.maybe_compact() {
tracing::warn!(%e, "compaction failed");
}
}
});
}
let addr = format!("{}:{}", cfg.uplink.host, cfg.uplink.port);
tracing::info!(server = %cfg.server.name, %addr, "linking to uplink");
link::run(proto, engine, &addr, irc_rx, cfg.email.clone()).await
}