From 596630df531cb0c217fc930173888a85eea7f882 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 13 Jul 2026 01:33:56 +0000 Subject: [PATCH] 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. --- Cargo.lock | 24 +++++++++++++++++ Cargo.toml | 5 +++- README.md | 26 ++++++++++++------- {src => api/src}/email.rs | 0 api/src/lib.rs | 22 ++++++++++++++++ {templates => api/templates}/email/base.html | 0 chanserv/Cargo.toml | 8 ++++++ {modules/chanserv => chanserv/src}/access.rs | 4 +-- {modules/chanserv => chanserv/src}/akick.rs | 4 +-- {modules/chanserv => chanserv/src}/ban.rs | 6 ++--- {modules/chanserv => chanserv/src}/clone.rs | 4 +-- {modules/chanserv => chanserv/src}/enforce.rs | 6 ++--- .../chanserv => chanserv/src}/entrymsg.rs | 4 +-- {modules/chanserv => chanserv/src}/getkey.rs | 6 ++--- {modules/chanserv => chanserv/src}/invite.rs | 6 ++--- {modules/chanserv => chanserv/src}/kick.rs | 6 ++--- .../chanserv.rs => chanserv/src/lib.rs | 8 +++--- {modules/chanserv => chanserv/src}/list.rs | 4 +-- {modules/chanserv => chanserv/src}/mode.rs | 4 +-- {modules/chanserv => chanserv/src}/op.rs | 6 ++--- {modules/chanserv => chanserv/src}/seen.rs | 6 ++--- {modules/chanserv => chanserv/src}/set.rs | 4 +-- {modules/chanserv => chanserv/src}/status.rs | 6 ++--- {modules/chanserv => chanserv/src}/topic.rs | 4 +-- {modules/chanserv => chanserv/src}/unban.rs | 6 ++--- {modules/chanserv => chanserv/src}/xop.rs | 4 +-- inspircd/Cargo.toml | 8 ++++++ .../inspircd.rs => inspircd/src/lib.rs | 2 +- modules/README.md | 19 -------------- modules/protocol/mod.rs | 6 ----- nickserv/Cargo.toml | 8 ++++++ {modules/nickserv => nickserv/src}/alist.rs | 4 +-- {modules/nickserv => nickserv/src}/cert.rs | 4 +-- {modules/nickserv => nickserv/src}/confirm.rs | 4 +-- {modules/nickserv => nickserv/src}/drop.rs | 6 ++--- {modules/nickserv => nickserv/src}/ghost.rs | 6 ++--- {modules/nickserv => nickserv/src}/glist.rs | 4 +-- {modules/nickserv => nickserv/src}/group.rs | 4 +-- .../nickserv => nickserv/src}/identify.rs | 4 +-- {modules/nickserv => nickserv/src}/info.rs | 4 +-- .../nickserv.rs => nickserv/src/lib.rs | 6 ++--- {modules/nickserv => nickserv/src}/logout.rs | 2 +- .../nickserv => nickserv/src}/register.rs | 4 +-- .../nickserv => nickserv/src}/resetpass.rs | 6 ++--- {modules/nickserv => nickserv/src}/set.rs | 4 +-- {modules/nickserv => nickserv/src}/ungroup.rs | 4 +-- src/engine/db.rs | 23 ++-------------- src/engine/mod.rs | 20 +++++++------- src/gossip.rs | 2 +- src/grpc.rs | 2 +- src/main.rs | 16 +++++------- src/proto.rs | 4 +++ 52 files changed, 197 insertions(+), 162 deletions(-) rename {src => api/src}/email.rs (100%) rename {templates => api/templates}/email/base.html (100%) create mode 100644 chanserv/Cargo.toml rename {modules/chanserv => chanserv/src}/access.rs (97%) rename {modules/chanserv => chanserv/src}/akick.rs (97%) rename {modules/chanserv => chanserv/src}/ban.rs (87%) rename {modules/chanserv => chanserv/src}/clone.rs (94%) rename {modules/chanserv => chanserv/src}/enforce.rs (92%) rename {modules/chanserv => chanserv/src}/entrymsg.rs (95%) rename {modules/chanserv => chanserv/src}/getkey.rs (84%) rename {modules/chanserv => chanserv/src}/invite.rs (87%) rename {modules/chanserv => chanserv/src}/kick.rs (85%) rename modules/chanserv/chanserv.rs => chanserv/src/lib.rs (98%) rename {modules/chanserv => chanserv/src}/list.rs (86%) rename {modules/chanserv => chanserv/src}/mode.rs (92%) rename {modules/chanserv => chanserv/src}/op.rs (88%) rename {modules/chanserv => chanserv/src}/seen.rs (78%) rename {modules/chanserv => chanserv/src}/set.rs (96%) rename {modules/chanserv => chanserv/src}/status.rs (90%) rename {modules/chanserv => chanserv/src}/topic.rs (87%) rename {modules/chanserv => chanserv/src}/unban.rs (86%) rename {modules/chanserv => chanserv/src}/xop.rs (96%) create mode 100644 inspircd/Cargo.toml rename modules/protocol/inspircd.rs => inspircd/src/lib.rs (99%) delete mode 100644 modules/README.md delete mode 100644 modules/protocol/mod.rs create mode 100644 nickserv/Cargo.toml rename {modules/nickserv => nickserv/src}/alist.rs (92%) rename {modules/nickserv => nickserv/src}/cert.rs (97%) rename {modules/nickserv => nickserv/src}/confirm.rs (92%) rename {modules/nickserv => nickserv/src}/drop.rs (91%) rename {modules/nickserv => nickserv/src}/ghost.rs (92%) rename {modules/nickserv => nickserv/src}/glist.rs (88%) rename {modules/nickserv => nickserv/src}/group.rs (92%) rename {modules/nickserv => nickserv/src}/identify.rs (94%) rename {modules/nickserv => nickserv/src}/info.rs (91%) rename modules/nickserv/nickserv.rs => nickserv/src/lib.rs (95%) rename {modules/nickserv => nickserv/src}/logout.rs (91%) rename {modules/nickserv => nickserv/src}/register.rs (88%) rename {modules/nickserv => nickserv/src}/resetpass.rs (89%) rename {modules/nickserv => nickserv/src}/set.rs (94%) rename {modules/nickserv => nickserv/src}/ungroup.rs (93%) create mode 100644 src/proto.rs diff --git a/Cargo.lock b/Cargo.lock index d773d48..150378e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,9 @@ dependencies = [ "argon2", "base64", "fedserv-api", + "fedserv-chanserv", + "fedserv-inspircd", + "fedserv-nickserv", "hmac", "pbkdf2", "prost", @@ -307,6 +310,27 @@ dependencies = [ name = "fedserv-api" version = "0.0.1" +[[package]] +name = "fedserv-chanserv" +version = "0.0.1" +dependencies = [ + "fedserv-api", +] + +[[package]] +name = "fedserv-inspircd" +version = "0.0.1" +dependencies = [ + "fedserv-api", +] + +[[package]] +name = "fedserv-nickserv" +version = "0.0.1" +dependencies = [ + "fedserv-api", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 5446bd0..5532ce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["api"] +members = ["api", "inspircd", "chanserv", "nickserv"] [package] name = "fedserv" @@ -9,6 +9,9 @@ description = "Federated IRC services daemon (protocol-agnostic core, InspIRCd l [dependencies] fedserv-api = { path = "api" } +fedserv-inspircd = { path = "inspircd" } +fedserv-chanserv = { path = "chanserv" } +fedserv-nickserv = { path = "nickserv" } tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time", "sync", "process"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/README.md b/README.md index ef15779..a3c9be7 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,25 @@ derived from any project's source. ## Design -The core lives in `src/`; the pluggable parts live in `modules/` (see -`modules/README.md`). +The core lives in `src/`. The pluggable parts are separate crates in a Cargo +workspace, each depending only on the `fedserv-api` SDK crate. +- **`api/`** (`fedserv-api`) the module SDK: the `Service`, `Protocol`, `Store` + and `NetView` traits a module implements or is handed, plus the normalized + `NetEvent`/`NetAction` vocabulary and the read views. It has no storage or + runtime dependencies, so a third-party module builds against it alone. - **`src/engine/`** the services engine: live network `state`, the account store, - and the `Service` trait. The store is event-sourced: every change is an `Event` - appended to a per-node log, and state is a fold over that log. + and the log. The store is event-sourced: every change is an `Event` appended to + a per-node log, and state is a fold over that log. A service touches it only + through the `Store`/`NetView` traits, so the log, gossip and credential material + stay out of a module's reach. - **`src/gossip.rs`** node-to-node replication over the logs. -- **`modules/protocol/`** the ircd link layer. A `Protocol` trait maps raw - server-to-server lines to and from a normalized `NetEvent`/`NetAction` model, so - the engine never touches a raw line and a new ircd is one new module. InspIRCd is - the first. -- **`modules/nickserv/`, `modules/chanserv/`** the pseudo-clients, one directory - each. OperServ and others follow the same shape. +- **`inspircd/`** (`fedserv-inspircd`) the ircd link layer. A `Protocol` impl maps + raw server-to-server lines to and from the normalized model, so the engine never + touches a raw line and a new ircd is one new crate. +- **`nickserv/`, `chanserv/`** (`fedserv-nickserv`, `fedserv-chanserv`) the + pseudo-clients, each a crate implementing `Service`. OperServ and others follow + the same shape. ## Replication diff --git a/src/email.rs b/api/src/email.rs similarity index 100% rename from src/email.rs rename to api/src/email.rs diff --git a/api/src/lib.rs b/api/src/lib.rs index 30bd37d..fd18f65 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -2,6 +2,9 @@ //! it carries the traits a module implements and the normalized vocabulary the //! engine speaks, with no storage or runtime dependencies of its own. +// Branded account emails (confirm / reset), shared by the engine and modules. +pub mod email; + // --------------------------------------------------------------------------- // Protocol vocabulary // --------------------------------------------------------------------------- @@ -448,3 +451,22 @@ fn glob_match(pattern: &str, text: &str) -> bool { } pi == p.len() } + +// Format a Unix timestamp (seconds) as "YYYY-MM-DD HH:MM:SS UTC", using Howard +// Hinnant's civil-from-days algorithm so no date crate is needed. +pub fn human_time(ts: u64) -> String { + let days = (ts / 86400) as i64; + let rem = ts % 86400; + let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60); + let z = days + 719468; + let era = (if z >= 0 { z } else { z - 146096 }) / 146097; + let doe = z - era * 146097; + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let day = doy - (153 * mp + 2) / 5 + 1; + let month = if mp < 10 { mp + 3 } else { mp - 9 }; + let year = y + i64::from(month <= 2); + format!("{year:04}-{month:02}-{day:02} {hh:02}:{mm:02}:{ss:02} UTC") +} diff --git a/templates/email/base.html b/api/templates/email/base.html similarity index 100% rename from templates/email/base.html rename to api/templates/email/base.html diff --git a/chanserv/Cargo.toml b/chanserv/Cargo.toml new file mode 100644 index 0000000..e663162 --- /dev/null +++ b/chanserv/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fedserv-chanserv" +version = "0.0.1" +edition = "2021" +description = "ChanServ channel-registration service module for fedserv." + +[dependencies] +fedserv-api = { path = "../api" } diff --git a/modules/chanserv/access.rs b/chanserv/src/access.rs similarity index 97% rename from modules/chanserv/access.rs rename to chanserv/src/access.rs index d02741a..075e5b7 100644 --- a/modules/chanserv/access.rs +++ b/chanserv/src/access.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // ACCESS <#channel> LIST | ADD | DEL pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { diff --git a/modules/chanserv/akick.rs b/chanserv/src/akick.rs similarity index 97% rename from modules/chanserv/akick.rs rename to chanserv/src/akick.rs index d8f45e2..c466abb 100644 --- a/modules/chanserv/akick.rs +++ b/chanserv/src/akick.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // AKICK <#channel> ADD [reason] | DEL | LIST // Masks are nick!user@host globs; matching users are banned and kicked on join. diff --git a/modules/chanserv/ban.rs b/chanserv/src/ban.rs similarity index 87% rename from modules/chanserv/ban.rs rename to chanserv/src/ban.rs index 3c3296a..26295c6 100644 --- a/modules/chanserv/ban.rs +++ b/chanserv/src/ban.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // BAN <#channel> [reason]: ban *!*@host and kick the user. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { diff --git a/modules/chanserv/clone.rs b/chanserv/src/clone.rs similarity index 94% rename from modules/chanserv/clone.rs rename to chanserv/src/clone.rs index c4a4fcf..fa502eb 100644 --- a/modules/chanserv/clone.rs +++ b/chanserv/src/clone.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // CLONE : copy a channel's settings (mode lock, access, // auto-kick, description, entry message) into another. Founder of both. diff --git a/modules/chanserv/enforce.rs b/chanserv/src/enforce.rs similarity index 92% rename from modules/chanserv/enforce.rs rename to chanserv/src/enforce.rs index ab5e0b1..b7c2e87 100644 --- a/modules/chanserv/enforce.rs +++ b/chanserv/src/enforce.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // ENFORCE <#channel>: re-apply the channel's settings to everyone present — // the mode lock, access status modes, and the auto-kick list. diff --git a/modules/chanserv/entrymsg.rs b/chanserv/src/entrymsg.rs similarity index 95% rename from modules/chanserv/entrymsg.rs rename to chanserv/src/entrymsg.rs index 61199c3..b28f935 100644 --- a/modules/chanserv/entrymsg.rs +++ b/chanserv/src/entrymsg.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // ENTRYMSG <#channel> [CLEAR | ]: message noticed to users as they join. // With no argument, show the current message. diff --git a/modules/chanserv/getkey.rs b/chanserv/src/getkey.rs similarity index 84% rename from modules/chanserv/getkey.rs rename to chanserv/src/getkey.rs index 0cfc910..e509cce 100644 --- a/modules/chanserv/getkey.rs +++ b/chanserv/src/getkey.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // GETKEY <#channel>: report the channel key (+k), for ops who need to let // someone in. diff --git a/modules/chanserv/invite.rs b/chanserv/src/invite.rs similarity index 87% rename from modules/chanserv/invite.rs rename to chanserv/src/invite.rs index 75a81cd..7efe36f 100644 --- a/modules/chanserv/invite.rs +++ b/chanserv/src/invite.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // INVITE <#channel> [nick]: invite a user (self if no nick) into the channel. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { diff --git a/modules/chanserv/kick.rs b/chanserv/src/kick.rs similarity index 85% rename from modules/chanserv/kick.rs rename to chanserv/src/kick.rs index b6c0ff4..64baf03 100644 --- a/modules/chanserv/kick.rs +++ b/chanserv/src/kick.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // KICK <#channel> [reason]: kick a user. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { diff --git a/modules/chanserv/chanserv.rs b/chanserv/src/lib.rs similarity index 98% rename from modules/chanserv/chanserv.rs rename to chanserv/src/lib.rs index 193bc3b..5f97df6 100644 --- a/modules/chanserv/chanserv.rs +++ b/chanserv/src/lib.rs @@ -1,6 +1,6 @@ -use crate::engine::db::{ChanError, ChannelView, Store}; -use crate::engine::service::{Sender, Service, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::{ChanError, ChannelView, Store}; +use fedserv_api::{Sender, Service, ServiceCtx}; +use fedserv_api::NetView; #[path = "mode.rs"] mod mode; @@ -100,7 +100,7 @@ impl Service for ChanServ { if !info.desc.is_empty() { ctx.notice(me, from.uid, format!(" Description: {}", info.desc)); } - ctx.notice(me, from.uid, format!(" Registered : {}", crate::engine::db::human_time(info.ts))); + ctx.notice(me, from.uid, format!(" Registered : {}", fedserv_api::human_time(info.ts))); } None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), } diff --git a/modules/chanserv/list.rs b/chanserv/src/list.rs similarity index 86% rename from modules/chanserv/list.rs rename to chanserv/src/list.rs index 5672cb7..b75900e 100644 --- a/modules/chanserv/list.rs +++ b/chanserv/src/list.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // LIST: show all registered channels. pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { diff --git a/modules/chanserv/mode.rs b/chanserv/src/mode.rs similarity index 92% rename from modules/chanserv/mode.rs rename to chanserv/src/mode.rs index 60c19b7..5f207d6 100644 --- a/modules/chanserv/mode.rs +++ b/chanserv/src/mode.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // MODE <#channel> : the founder sets channel modes via ChanServ. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { diff --git a/modules/chanserv/op.rs b/chanserv/src/op.rs similarity index 88% rename from modules/chanserv/op.rs rename to chanserv/src/op.rs index 014df57..e62aa6a 100644 --- a/modules/chanserv/op.rs +++ b/chanserv/src/op.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // OP/DEOP/VOICE/DEVOICE <#channel> [nick]: set a status mode on a user (self if // no nick given). `mode` is the mode to apply, e.g. "+o". diff --git a/modules/chanserv/seen.rs b/chanserv/src/seen.rs similarity index 78% rename from modules/chanserv/seen.rs rename to chanserv/src/seen.rs index 60deb84..120a619 100644 --- a/modules/chanserv/seen.rs +++ b/chanserv/src/seen.rs @@ -1,5 +1,5 @@ -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // SEEN : when a nick was last seen, and doing what. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) { @@ -12,7 +12,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: return; } match net.last_seen(nick) { - Some(s) => ctx.notice(me, from.uid, format!("\x02{}\x02 was last seen {} ({}).", s.nick, crate::engine::db::human_time(s.ts), s.what)), + Some(s) => ctx.notice(me, from.uid, format!("\x02{}\x02 was last seen {} ({}).", s.nick, fedserv_api::human_time(s.ts), s.what)), None => ctx.notice(me, from.uid, format!("I have no record of \x02{nick}\x02.")), } } diff --git a/modules/chanserv/set.rs b/chanserv/src/set.rs similarity index 96% rename from modules/chanserv/set.rs rename to chanserv/src/set.rs index d067f63..44655a0 100644 --- a/modules/chanserv/set.rs +++ b/chanserv/src/set.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // SET <#channel> FOUNDER | DESC : founder-only channel settings. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { diff --git a/modules/chanserv/status.rs b/chanserv/src/status.rs similarity index 90% rename from modules/chanserv/status.rs rename to chanserv/src/status.rs index 2ee7af0..2f28d4b 100644 --- a/modules/chanserv/status.rs +++ b/chanserv/src/status.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // STATUS <#channel> [nick]: show a user's access level (self if no nick). pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { diff --git a/modules/chanserv/topic.rs b/chanserv/src/topic.rs similarity index 87% rename from modules/chanserv/topic.rs rename to chanserv/src/topic.rs index 4b350cc..8cf099d 100644 --- a/modules/chanserv/topic.rs +++ b/chanserv/src/topic.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // TOPIC <#channel> : set the channel topic (empty clears it). pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { diff --git a/modules/chanserv/unban.rs b/chanserv/src/unban.rs similarity index 86% rename from modules/chanserv/unban.rs rename to chanserv/src/unban.rs index 82d82bb..08e8d04 100644 --- a/modules/chanserv/unban.rs +++ b/chanserv/src/unban.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // UNBAN <#channel> [nick]: remove the *!*@host ban of a user (self if no nick). pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { diff --git a/modules/chanserv/xop.rs b/chanserv/src/xop.rs similarity index 96% rename from modules/chanserv/xop.rs rename to chanserv/src/xop.rs index f6790dd..4ca25b0 100644 --- a/modules/chanserv/xop.rs +++ b/chanserv/src/xop.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // AOP/SOP/VOP <#channel> ADD | DEL | LIST — tiered // shortcuts over the access list. `level` is the access level they map to diff --git a/inspircd/Cargo.toml b/inspircd/Cargo.toml new file mode 100644 index 0000000..5087247 --- /dev/null +++ b/inspircd/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fedserv-inspircd" +version = "0.0.1" +edition = "2021" +description = "InspIRCd server-to-server protocol module for fedserv." + +[dependencies] +fedserv-api = { path = "../api" } diff --git a/modules/protocol/inspircd.rs b/inspircd/src/lib.rs similarity index 99% rename from modules/protocol/inspircd.rs rename to inspircd/src/lib.rs index 5e885fe..aefaa85 100644 --- a/modules/protocol/inspircd.rs +++ b/inspircd/src/lib.rs @@ -1,7 +1,7 @@ // InspIRCd spanning-tree link protocol. Handshake + UID + PING mirror the // sequence in Network-Links (protocols/inspircd.py); mode/burst details get // firmed up against a live insp4 uplink. -use super::{NetAction, NetEvent, Protocol}; +use fedserv_api::{NetAction, NetEvent, Protocol}; use std::time::{SystemTime, UNIX_EPOCH}; pub struct InspIrcd { diff --git a/modules/README.md b/modules/README.md deleted file mode 100644 index 137ddec..0000000 --- a/modules/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# modules/ - -The pluggable parts of fedserv: one directory per protocol and per pseudoclient. -The core (engine, event log, gossip, link loop) stays in `../src/`. - -``` -modules/ - protocol/ ircd link protocols (inspircd.rs) - nickserv/ NickServ pseudoclient - chanserv/ ChanServ pseudoclient -``` - -`src/main.rs` pulls each in with `#[path = "../modules/..."]`, so crate paths -stay flat (`crate::proto`, `crate::nickserv`, `crate::chanserv`). - -## Naming - -A service directory holds its pseudoclient file plus, as commands are split out, -one file per command: `register.rs`, `mode.rs`, etc. diff --git a/modules/protocol/mod.rs b/modules/protocol/mod.rs deleted file mode 100644 index c6ac0d9..0000000 --- a/modules/protocol/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Protocol layer. The normalized vocabulary (NetEvent / NetAction / RegReply) -// and the Protocol trait live in the fedserv-api SDK crate; re-exported here so -// the engine and the ircd modules keep referring to them as `crate::proto::*`. -pub mod inspircd; - -pub use fedserv_api::{NetAction, NetEvent, Protocol, RegReply}; diff --git a/nickserv/Cargo.toml b/nickserv/Cargo.toml new file mode 100644 index 0000000..1477c9a --- /dev/null +++ b/nickserv/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fedserv-nickserv" +version = "0.0.1" +edition = "2021" +description = "NickServ account-registration service module for fedserv." + +[dependencies] +fedserv-api = { path = "../api" } diff --git a/modules/nickserv/alist.rs b/nickserv/src/alist.rs similarity index 92% rename from modules/nickserv/alist.rs rename to nickserv/src/alist.rs index 7378d7a..7c58b17 100644 --- a/modules/nickserv/alist.rs +++ b/nickserv/src/alist.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // ALIST: list the channels the sender's account founds or has access on. pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { diff --git a/modules/nickserv/cert.rs b/nickserv/src/cert.rs similarity index 97% rename from modules/nickserv/cert.rs rename to nickserv/src/cert.rs index 9dbcb5d..82bb149 100644 --- a/modules/nickserv/cert.rs +++ b/nickserv/src/cert.rs @@ -1,5 +1,5 @@ -use crate::engine::db::{CertError, Store}; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::{CertError, Store}; +use fedserv_api::{Sender, ServiceCtx}; // CERT ADD|DEL|LIST [fingerprint]: manage the TLS certificate // fingerprints that may log in to your account via SASL EXTERNAL. Each diff --git a/modules/nickserv/confirm.rs b/nickserv/src/confirm.rs similarity index 92% rename from modules/nickserv/confirm.rs rename to nickserv/src/confirm.rs index 63bc234..f192bb3 100644 --- a/modules/nickserv/confirm.rs +++ b/nickserv/src/confirm.rs @@ -1,5 +1,5 @@ -use crate::engine::db::{CodeKind, Store}; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::{CodeKind, Store}; +use fedserv_api::{Sender, ServiceCtx}; // CONFIRM : confirm your account's email with the code you were emailed. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { diff --git a/modules/nickserv/drop.rs b/nickserv/src/drop.rs similarity index 91% rename from modules/nickserv/drop.rs rename to nickserv/src/drop.rs index a16685b..5a53506 100644 --- a/modules/nickserv/drop.rs +++ b/nickserv/src/drop.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // DROP : delete your account. Re-authenticates as confirmation, releases // and drops the channels you found, and logs you out. diff --git a/modules/nickserv/ghost.rs b/nickserv/src/ghost.rs similarity index 92% rename from modules/nickserv/ghost.rs rename to nickserv/src/ghost.rs index d79beac..a1cc770 100644 --- a/modules/nickserv/ghost.rs +++ b/nickserv/src/ghost.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::NetView; // GHOST/RECOVER [password]: rename off a session using a nick you own, // either by being identified to its account or giving that account's password. diff --git a/modules/nickserv/glist.rs b/nickserv/src/glist.rs similarity index 88% rename from modules/nickserv/glist.rs rename to nickserv/src/glist.rs index 8272537..ff2be75 100644 --- a/modules/nickserv/glist.rs +++ b/nickserv/src/glist.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // GLIST: list the nicks grouped to your account. pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { diff --git a/modules/nickserv/group.rs b/nickserv/src/group.rs similarity index 92% rename from modules/nickserv/group.rs rename to nickserv/src/group.rs index 70867f9..141d852 100644 --- a/modules/nickserv/group.rs +++ b/nickserv/src/group.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // GROUP : link your current nick to an existing account, so // you can identify to it under this nick too. diff --git a/modules/nickserv/identify.rs b/nickserv/src/identify.rs similarity index 94% rename from modules/nickserv/identify.rs rename to nickserv/src/identify.rs index 9b496cb..dc00ec9 100644 --- a/modules/nickserv/identify.rs +++ b/nickserv/src/identify.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // IDENTIFY [account] : log in. The account defaults to the current // nick, so both the bare-password and account+password forms work. diff --git a/modules/nickserv/info.rs b/nickserv/src/info.rs similarity index 91% rename from modules/nickserv/info.rs rename to nickserv/src/info.rs index 50743f7..94334b9 100644 --- a/modules/nickserv/info.rs +++ b/nickserv/src/info.rs @@ -1,5 +1,5 @@ -use crate::engine::db::{human_time, Store}; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::{human_time, Store}; +use fedserv_api::{Sender, ServiceCtx}; // INFO [account]: show an account's registration details. The email is shown // only to the account's own owner. diff --git a/modules/nickserv/nickserv.rs b/nickserv/src/lib.rs similarity index 95% rename from modules/nickserv/nickserv.rs rename to nickserv/src/lib.rs index 9ef786f..89207d5 100644 --- a/modules/nickserv/nickserv.rs +++ b/nickserv/src/lib.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, Service, ServiceCtx}; -use crate::engine::state::NetView; +use fedserv_api::Store; +use fedserv_api::{Sender, Service, ServiceCtx}; +use fedserv_api::NetView; #[path = "register.rs"] mod register; diff --git a/modules/nickserv/logout.rs b/nickserv/src/logout.rs similarity index 91% rename from modules/nickserv/logout.rs rename to nickserv/src/logout.rs index 4d9fde0..cf8f472 100644 --- a/modules/nickserv/logout.rs +++ b/nickserv/src/logout.rs @@ -1,4 +1,4 @@ -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::{Sender, ServiceCtx}; // LOGOUT: log out and rename to a guest nick (prefix + a per-logout sequence). pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ctx: &mut ServiceCtx) { diff --git a/modules/nickserv/register.rs b/nickserv/src/register.rs similarity index 88% rename from modules/nickserv/register.rs rename to nickserv/src/register.rs index be9dbd8..6a1ecca 100644 --- a/modules/nickserv/register.rs +++ b/nickserv/src/register.rs @@ -1,5 +1,5 @@ -use crate::engine::service::{Sender, ServiceCtx}; -use crate::proto::RegReply; +use fedserv_api::{Sender, ServiceCtx}; +use fedserv_api::RegReply; // REGISTER [email]: register the sender's current nick. The engine // derives the password off-thread, commits, and answers. diff --git a/modules/nickserv/resetpass.rs b/nickserv/src/resetpass.rs similarity index 89% rename from modules/nickserv/resetpass.rs rename to nickserv/src/resetpass.rs index bb8d96d..47cd3e9 100644 --- a/modules/nickserv/resetpass.rs +++ b/nickserv/src/resetpass.rs @@ -1,5 +1,5 @@ -use crate::engine::db::{CodeKind, Store}; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::{CodeKind, Store}; +use fedserv_api::{Sender, ServiceCtx}; // RESETPASS : email a reset code to the address on file. // RESETPASS : complete the reset with that code. @@ -19,7 +19,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: return; }; let code = db.issue_code(&canonical, CodeKind::Reset); - let mail = crate::email::reset(db.email_brand(), db.email_accent(), db.email_logo(), &canonical, &code); + let mail = fedserv_api::email::reset(db.email_brand(), db.email_accent(), db.email_logo(), &canonical, &code); ctx.send_email(email, mail.subject, mail.text, Some(mail.html)); ctx.notice(me, from.uid, format!("A reset code has been emailed to the address on file for \x02{canonical}\x02.")); } diff --git a/modules/nickserv/set.rs b/nickserv/src/set.rs similarity index 94% rename from modules/nickserv/set.rs rename to nickserv/src/set.rs index 535e6f0..9894493 100644 --- a/modules/nickserv/set.rs +++ b/nickserv/src/set.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // SET PASSWORD | SET EMAIL [address]: change your account settings. pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { diff --git a/modules/nickserv/ungroup.rs b/nickserv/src/ungroup.rs similarity index 93% rename from modules/nickserv/ungroup.rs rename to nickserv/src/ungroup.rs index 364e435..80a3b7f 100644 --- a/modules/nickserv/ungroup.rs +++ b/nickserv/src/ungroup.rs @@ -1,5 +1,5 @@ -use crate::engine::db::Store; -use crate::engine::service::{Sender, ServiceCtx}; +use fedserv_api::Store; +use fedserv_api::{Sender, ServiceCtx}; // UNGROUP [nick]: remove a nick grouped to your account (defaults to your current nick). pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { diff --git a/src/engine/db.rs b/src/engine/db.rs index 05580a7..b34dd14 100644 --- a/src/engine/db.rs +++ b/src/engine/db.rs @@ -471,25 +471,6 @@ fn now() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) } -// Format a Unix timestamp (seconds) as "YYYY-MM-DD HH:MM:SS UTC", using Howard -// Hinnant's civil-from-days algorithm so no date crate is needed. -pub(crate) fn human_time(ts: u64) -> String { - let days = (ts / 86400) as i64; - let rem = ts % 86400; - let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60); - let z = days + 719468; - let era = (if z >= 0 { z } else { z - 146096 }) / 146097; - let doe = z - era * 146097; - let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - let y = yoe + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let day = doy - (153 * mp + 2) / 5 + 1; - let month = if mp < 10 { mp + 3 } else { mp - 9 }; - let year = y + i64::from(month <= 2); - format!("{year:04}-{month:02}-{day:02} {hh:02}:{mm:02}:{ss:02} UTC") -} - impl Db { pub fn open(path: impl Into, origin: impl Into) -> Self { let (log, events) = EventLog::open(path.into(), origin.into()); @@ -1325,8 +1306,8 @@ mod tests { #[test] fn formats_unix_time_as_utc() { - assert_eq!(human_time(0), "1970-01-01 00:00:00 UTC"); - assert_eq!(human_time(1783844590), "2026-07-12 08:23:10 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"); } #[test] diff --git a/src/engine/mod.rs b/src/engine/mod.rs index bc40aa3..06c3a59 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -168,7 +168,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 = crate::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), name, &code); + let mail = fedserv_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) }); } } @@ -691,7 +691,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 = crate::email::confirm(self.db.email_brand(), self.db.email_accent(), self.db.email_logo(), account, &code); + let mail = fedserv_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 \x02.".to_string() }); } @@ -830,7 +830,7 @@ impl RegLimiter { #[cfg(test)] mod tests { use super::*; - use crate::nickserv::NickServ; + use fedserv_nickserv::NickServ; fn plain(authzid: &[u8], authcid: &[u8], passwd: &[u8]) -> String { let mut payload = Vec::new(); @@ -1222,7 +1222,7 @@ mod tests { // notifies them over the services-initiated outbound path. #[test] fn lost_conflict_logs_out_local_session() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-lostconf.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "test"); @@ -1271,7 +1271,7 @@ mod tests { // lists the channels the account founds or has access on. #[test] fn nickserv_info_and_alist() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-nsinfo.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "test"); @@ -1473,7 +1473,7 @@ mod tests { // only the founder can drop. #[test] fn chanserv_register_info_drop() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-chanserv.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "42S"); @@ -1533,7 +1533,7 @@ mod tests { // ChanServ moderation: an op can op/kick/ban users; a non-op is refused. #[test] fn chanserv_moderation() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-cs-mod.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "42S"); @@ -1577,7 +1577,7 @@ mod tests { // ChanServ topic, invite, auto-kick (with enforcement on join), list and status. #[test] fn chanserv_topic_invite_akick() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-cs-tia.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "42S"); @@ -1618,7 +1618,7 @@ mod tests { // ChanServ SET: description and founder transfer, founder-gated. #[test] fn chanserv_set() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-cs-set.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "42S"); @@ -1653,7 +1653,7 @@ mod tests { // ChanServ entrymsg, enforce, getkey, seen, clone and the xop lists. #[test] fn chanserv_extended() { - use crate::chanserv::ChanServ; + use fedserv_chanserv::ChanServ; let path = std::env::temp_dir().join("fedserv-cs-ext.jsonl"); let _ = std::fs::remove_file(&path); let mut db = Db::open(&path, "42S"); diff --git a/src/gossip.rs b/src/gossip.rs index 96440d1..cf8143e 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -263,7 +263,7 @@ async fn send(tx: &mpsc::Sender, msg: &Msg) -> Result<(), ()> { mod tests { use super::*; use crate::engine::db::Db; - use crate::nickserv::NickServ; + use fedserv_nickserv::NickServ; fn engine(origin: &str, tag: &str) -> (Shared, Outbound) { let path = std::env::temp_dir().join(format!("fedserv-gossip-{tag}.jsonl")); diff --git a/src/grpc.rs b/src/grpc.rs index fde42bc..ae3a86c 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -316,7 +316,7 @@ pub async fn run(engine: Shared, cfg: GrpcCfg, outbound: broadcast::Sender Result<()> { diff --git a/src/proto.rs b/src/proto.rs new file mode 100644 index 0000000..91032ec --- /dev/null +++ b/src/proto.rs @@ -0,0 +1,4 @@ +// The normalized protocol vocabulary lives in the fedserv-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};