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
api/src/email.rs
Normal file
76
api/src/email.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// 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('>', ">")
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
50
api/templates/email/base.html
Normal file
50
api/templates/email/base.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#eef0f4;-webkit-font-smoothing:antialiased;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#eef0f4;padding:40px 12px;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:468px;background:#ffffff;border-radius:16px;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;box-shadow:0 4px 24px rgba(15,23,42,.08);">
|
||||
|
||||
<tr><td style="height:5px;background:{{accent}};font-size:0;line-height:0;"> </td></tr>
|
||||
|
||||
<tr><td style="padding:30px 36px 0;">
|
||||
<div style="margin-bottom:6px;">{{brand_mark}}</div>
|
||||
<h1 style="margin:16px 0 10px;font-size:23px;line-height:1.3;font-weight:800;color:#0f172a;">{{title}}</h1>
|
||||
<p style="margin:0;font-size:15px;line-height:1.65;color:#475569;">{{message}}</p>
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:26px 36px 0;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #e6eaf0;border-radius:14px;">
|
||||
<tr><td align="center" style="padding:24px 16px 22px;">
|
||||
<div style="font-size:11px;font-weight:600;letter-spacing:1.4px;text-transform:uppercase;color:#94a3b8;margin-bottom:12px;">Your code</div>
|
||||
<div style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:36px;font-weight:800;letter-spacing:12px;color:#0f172a;padding-left:12px;">{{code}}</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
<tr><td align="center" style="padding:20px 36px 4px;">
|
||||
<div style="display:inline-block;background:{{accent}};color:#ffffff;font-size:14px;font-weight:600;padding:13px 30px;border-radius:11px;box-shadow:0 2px 8px rgba(15,23,42,.14);">📋 Copy code</div>
|
||||
<div style="margin-top:10px;font-size:12px;color:#a4adba;">Tap and hold the code to copy it.</div>
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:22px 36px 0;">
|
||||
<p style="margin:0;font-size:13px;line-height:1.6;color:#94a3b8;">{{note}}</p>
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:26px 36px 30px;">
|
||||
<div style="border-top:1px solid #eef1f5;padding-top:16px;">
|
||||
<p style="margin:0;font-size:12px;line-height:1.5;color:#b4bbc7;">Sent by {{brand}} services. If you didn't request this, you can safely ignore this email.</p>
|
||||
</div>
|
||||
</td></tr>
|
||||
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue