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.
This commit is contained in:
parent
8ed1a9ab70
commit
596630df53
52 changed files with 197 additions and 162 deletions
76
src/email.rs
76
src/email.rs
|
|
@ -1,76 +0,0 @@
|
|||
// Email content: renders the HTML templates in ../templates/email and pairs each
|
||||
// with a plaintext fallback. The link layer sends both as multipart/alternative.
|
||||
|
||||
pub struct Mail {
|
||||
pub subject: String,
|
||||
pub text: String,
|
||||
pub html: String,
|
||||
}
|
||||
|
||||
const BASE: &str = include_str!("../templates/email/base.html");
|
||||
|
||||
// The masthead: a logo image beside the brand name when a logo URL is set,
|
||||
// otherwise the brand name as an accent eyebrow.
|
||||
fn brand_mark(brand: &str, accent: &str, logo: &str) -> String {
|
||||
let (brand, accent) = (escape(brand), escape(accent));
|
||||
if logo.is_empty() {
|
||||
format!("<span style=\"font-size:12px;font-weight:700;letter-spacing:1.6px;text-transform:uppercase;color:{accent};\">{brand}</span>")
|
||||
} else {
|
||||
format!(
|
||||
"<img src=\"{}\" width=\"40\" height=\"40\" alt=\"\" style=\"display:inline-block;border-radius:9px;vertical-align:middle;\"><span style=\"display:inline-block;margin-left:12px;vertical-align:middle;font-size:18px;font-weight:800;letter-spacing:.2px;color:#0f172a;\">{brand}</span>",
|
||||
escape(logo)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render(brand: &str, accent: &str, logo: &str, title: &str, message: &str, code: &str, note: &str) -> String {
|
||||
BASE.replace("{{brand_mark}}", &brand_mark(brand, accent, logo))
|
||||
.replace("{{brand}}", &escape(brand))
|
||||
.replace("{{accent}}", &escape(accent))
|
||||
.replace("{{title}}", &escape(title))
|
||||
.replace("{{message}}", &escape(message))
|
||||
.replace("{{code}}", &escape(code))
|
||||
.replace("{{note}}", &escape(note))
|
||||
}
|
||||
|
||||
pub fn reset(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail {
|
||||
Mail {
|
||||
subject: format!("Password reset for {account}"),
|
||||
text: format!(
|
||||
"Your password reset code for {account} is: {code}\nIt expires in 15 minutes.\nReset with:\n /msg NickServ RESETPASS {account} {code} <newpassword>\n"
|
||||
),
|
||||
html: render(
|
||||
brand,
|
||||
accent,
|
||||
logo,
|
||||
"Password reset",
|
||||
&format!("Use this code to reset the password for your account {account}."),
|
||||
code,
|
||||
"This code expires in 15 minutes. If you didn't ask to reset it, ignore this email.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirm(brand: &str, accent: &str, logo: &str, account: &str, code: &str) -> Mail {
|
||||
Mail {
|
||||
subject: format!("Confirm your {account} registration"),
|
||||
text: format!(
|
||||
"Confirm your account {account} with:\n /msg NickServ CONFIRM {code}\nThe code expires in 15 minutes.\n"
|
||||
),
|
||||
html: render(
|
||||
brand,
|
||||
accent,
|
||||
logo,
|
||||
"Confirm your account",
|
||||
&format!("Welcome! Confirm the email for your account {account} with the code below."),
|
||||
code,
|
||||
"This code expires in 15 minutes.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Escape the few characters that matter inside HTML text so a value can't break
|
||||
// out of the template.
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||
}
|
||||
|
|
@ -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<PathBuf>, origin: impl Into<String>) -> 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]
|
||||
|
|
|
|||
|
|
@ -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 <code>\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");
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ async fn send(tx: &mpsc::Sender<String>, 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"));
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ pub async fn run(engine: Shared, cfg: GrpcCfg, outbound: broadcast::Sender<LogEn
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::engine::db::Db;
|
||||
use crate::nickserv::NickServ;
|
||||
use fedserv_nickserv::NickServ;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::metadata::MetadataValue;
|
||||
|
||||
|
|
|
|||
16
src/main.rs
16
src/main.rs
|
|
@ -1,26 +1,22 @@
|
|||
// Core lives in src/; pluggable modules live in ../modules/.
|
||||
// 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 email;
|
||||
mod engine;
|
||||
mod gossip;
|
||||
mod grpc;
|
||||
mod link;
|
||||
#[path = "../modules/protocol/mod.rs"]
|
||||
mod proto;
|
||||
#[path = "../modules/nickserv/nickserv.rs"]
|
||||
mod nickserv;
|
||||
#[path = "../modules/chanserv/chanserv.rs"]
|
||||
mod chanserv;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use chanserv::ChanServ;
|
||||
use engine::Engine;
|
||||
use nickserv::NickServ;
|
||||
use proto::inspircd::InspIrcd;
|
||||
use fedserv_chanserv::ChanServ;
|
||||
use fedserv_inspircd::InspIrcd;
|
||||
use fedserv_nickserv::NickServ;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
|
|
|
|||
4
src/proto.rs
Normal file
4
src/proto.rs
Normal file
|
|
@ -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};
|
||||
Loading…
Add table
Add a link
Reference in a new issue