diff --git a/api/src/lib.rs b/api/src/lib.rs index 3eed88c..30bd37d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -223,3 +223,228 @@ impl ServiceCtx { }); } } + +// --------------------------------------------------------------------------- +// Store vocabulary +// --------------------------------------------------------------------------- +// +// A service reads and writes accounts and channels through the Store / NetView +// traits, never the concrete storage engine: the append-only log, gossip and +// credential material stay out of reach. Reads hand back plain views that carry +// only non-secret fields (never a password hash or SCRAM verifier). + +// A registered account, minus anything credential-shaped. +#[derive(Debug, Clone)] +pub struct AccountView { + pub name: String, + pub email: Option, + pub ts: u64, + pub verified: bool, +} + +// One channel access-list entry (account -> level, e.g. "op" / "voice"). +#[derive(Debug, Clone)] +pub struct ChanAccessView { + pub account: String, + pub level: String, +} + +// One auto-kick entry (a hostmask and the reason shown on kick). +#[derive(Debug, Clone)] +pub struct ChanAkickView { + pub mask: String, + pub reason: String, +} + +// A registered channel and its ops lists. +#[derive(Debug, Clone)] +pub struct ChannelView { + pub name: String, + pub founder: String, + pub ts: u64, + // Mode-lock: chars services keep set / unset (besides the implicit +r). + pub lock_on: String, + pub lock_off: String, + pub access: Vec, + pub akick: Vec, + pub desc: String, + pub entrymsg: String, +} + +impl ChannelView { + // The channel mode this account is entitled to on join (+o founder/op, +v + // voice), or None if it holds no access. + pub fn join_mode(&self, account: &str) -> Option<&'static str> { + if self.founder.eq_ignore_ascii_case(account) { + return Some("+o"); + } + self.access + .iter() + .find(|a| a.account.eq_ignore_ascii_case(account)) + .map(|a| if a.level == "voice" { "+v" } else { "+o" }) + } + + // Whether this account holds channel-operator access (founder or op level). + pub fn is_op(&self, account: &str) -> bool { + self.join_mode(account) == Some("+o") + } + + // The mode-lock rendered as an applyable mode string, e.g. "+rnt-s". + pub fn lock_modes(&self) -> String { + let mut s = format!("+r{}", self.lock_on); + if !self.lock_off.is_empty() { + s.push('-'); + s.push_str(&self.lock_off); + } + s + } + + // The first auto-kick entry whose mask matches the given hostmask. + pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkickView> { + self.akick.iter().find(|k| glob_match(&k.mask, hostmask)) + } +} + +// When a nick was last seen, and doing what. +#[derive(Debug, Clone)] +pub struct SeenView { + pub nick: String, + pub ts: u64, + pub what: String, +} + +#[derive(Debug)] +pub enum RegError { + Exists, + Internal, +} + +#[derive(Debug)] +pub enum CertError { + Invalid, // not a plausible fingerprint + InUse, // already registered (to any account) + NoAccount, // target account does not exist + Internal, // persistence failed +} + +#[derive(Debug)] +pub enum ChanError { + Exists, // channel already registered + NoChannel, // channel is not registered + Internal, // persistence failed +} + +// What an emailed code authorises. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CodeKind { + Reset, + Confirm, +} + +// The account and channel store a service reads and writes. The engine hands a +// service `&mut dyn Store`; the concrete implementation keeps its log, gossip +// and credentials to itself. +pub trait Store { + fn exists(&self, name: &str) -> bool; + fn account(&self, name: &str) -> Option; + // Canonical account name for a nick (following a grouping), if registered. + fn resolve_account(&self, name: &str) -> Option<&str>; + // The canonical account name if the password is correct, else None. + fn authenticate(&self, name: &str, password: &str) -> Option<&str>; + fn grouped_nicks(&self, account: &str) -> Vec; + fn certfps(&self, account: &str) -> &[String]; + fn is_verified(&self, account: &str) -> bool; + fn channel(&self, name: &str) -> Option; + fn channels(&self) -> Vec; + fn channels_owned_by(&self, account: &str) -> Vec; + fn email_enabled(&self) -> bool; + fn email_brand(&self) -> &str; + fn email_accent(&self) -> &str; + fn email_logo(&self) -> &str; + + fn issue_code(&mut self, account: &str, kind: CodeKind) -> String; + fn take_code(&mut self, account: &str, kind: CodeKind, code: &str) -> bool; + fn verify_account(&mut self, account: &str) -> Result<(), RegError>; + fn set_email(&mut self, account: &str, email: Option) -> Result<(), RegError>; + fn group_nick(&mut self, nick: &str, account: &str) -> Result<(), RegError>; + fn ungroup_nick(&mut self, nick: &str) -> Result; + fn drop_account(&mut self, account: &str) -> Result; + fn certfp_add(&mut self, account: &str, fp: &str) -> Result<(), CertError>; + fn certfp_del(&mut self, account: &str, fp: &str) -> Result; + fn register_channel(&mut self, name: &str, founder: &str) -> Result<(), ChanError>; + fn drop_channel(&mut self, name: &str) -> Result<(), ChanError>; + fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError>; + fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError>; + fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError>; + fn set_founder(&mut self, channel: &str, account: &str) -> Result<(), ChanError>; + fn access_add(&mut self, channel: &str, account: &str, level: &str) -> Result<(), ChanError>; + fn access_del(&mut self, channel: &str, account: &str) -> Result; + fn akick_add(&mut self, channel: &str, mask: &str, reason: &str) -> Result<(), ChanError>; + fn akick_del(&mut self, channel: &str, mask: &str) -> Result; +} + +// The live network state a service reads (never mutates directly; changes go out +// as actions on the ServiceCtx). +pub trait NetView { + fn uid_by_nick(&self, nick: &str) -> Option<&str>; + fn nick_of(&self, uid: &str) -> Option<&str>; + fn host_of(&self, uid: &str) -> Option<&str>; + fn account_of(&self, uid: &str) -> Option<&str>; + fn uids_logged_into(&self, account: &str) -> Vec; + fn is_op(&self, channel: &str, uid: &str) -> bool; + fn channel_members(&self, channel: &str) -> Vec; + fn channel_key(&self, channel: &str) -> Option<&str>; + fn last_seen(&self, nick: &str) -> Option; +} + +// A pseudo-client (NickServ, ChanServ, ...). Introduced at burst, receives the +// commands users message it, reads/writes the store, and pushes actions. +pub trait Service: Send { + fn nick(&self) -> &str; + fn uid(&self) -> &str; + fn host(&self) -> &str { + "services.local" + } + fn gecos(&self) -> &str; + // Whether this service owns channel modes (ChanServ), so the engine can source + // channel mode changes from it. + fn manages_channels(&self) -> bool { + false + } + // Whether this is the account service (NickServ), so the engine can source + // account-related notices from it. + fn manages_accounts(&self) -> bool { + false + } + fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, store: &mut dyn Store); +} + +// Case-insensitive hostmask glob: `*` (any run) and `?` (one char). +fn glob_match(pattern: &str, text: &str) -> bool { + let (p, t): (Vec, Vec) = ( + pattern.chars().flat_map(char::to_lowercase).collect(), + text.chars().flat_map(char::to_lowercase).collect(), + ); + let (mut pi, mut ti) = (0, 0); + let (mut star, mut mark) = (None, 0); + while ti < t.len() { + if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) { + pi += 1; + ti += 1; + } else if pi < p.len() && p[pi] == '*' { + star = Some(pi); + mark = ti; + pi += 1; + } else if let Some(s) = star { + pi = s + 1; + mark += 1; + ti = mark; + } else { + return false; + } + } + while pi < p.len() && p[pi] == '*' { + pi += 1; + } + pi == p.len() +} diff --git a/modules/chanserv/access.rs b/modules/chanserv/access.rs index 180893b..d02741a 100644 --- a/modules/chanserv/access.rs +++ b/modules/chanserv/access.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // ACCESS <#channel> LIST | ADD | DEL -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD | DEL "); return; @@ -55,7 +55,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: } // True if `from` is the channel's founder; otherwise notices why and returns false. -fn is_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &Db) -> bool { +fn is_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { match db.channel(chan) { None => { ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); diff --git a/modules/chanserv/akick.rs b/modules/chanserv/akick.rs index 9150a94..d8f45e2 100644 --- a/modules/chanserv/akick.rs +++ b/modules/chanserv/akick.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // AKICK <#channel> ADD [reason] | DEL | LIST // Masks are nick!user@host globs; matching users are banned and kicked on join. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD [reason] | DEL | LIST"); return; diff --git a/modules/chanserv/ban.rs b/modules/chanserv/ban.rs index 2bade20..3c3296a 100644 --- a/modules/chanserv/ban.rs +++ b/modules/chanserv/ban.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; // BAN <#channel> [reason]: ban *!*@host and kick the user. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let (Some(&chan), Some(&nick)) = (args.get(1), args.get(2)) else { ctx.notice(me, from.uid, "Syntax: BAN <#channel> [reason]"); return; diff --git a/modules/chanserv/chanserv.rs b/modules/chanserv/chanserv.rs index 02898f1..193bc3b 100644 --- a/modules/chanserv/chanserv.rs +++ b/modules/chanserv/chanserv.rs @@ -1,6 +1,6 @@ -use crate::engine::db::{ChanError, ChannelInfo, Db}; +use crate::engine::db::{ChanError, ChannelView, Store}; use crate::engine::service::{Sender, Service, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; #[path = "mode.rs"] mod mode; @@ -57,7 +57,7 @@ impl Service for ChanServ { true } - fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &mut Db) { + fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) { let me = self.uid.as_str(); match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { Some("REGISTER") => { @@ -142,7 +142,7 @@ impl Service for ChanServ { Some(info) if info.lock_on.is_empty() && info.lock_off.is_empty() => { ctx.notice(me, from.uid, format!("\x02{}\x02 has no mode lock set.", info.name)); } - Some(info) => ctx.notice(me, from.uid, format!("Mode lock for \x02{}\x02: \x02{}\x02", info.name, show_mlock(info))), + Some(info) => ctx.notice(me, from.uid, format!("Mode lock for \x02{}\x02: \x02{}\x02", info.name, show_mlock(&info))), } return; } @@ -200,7 +200,7 @@ impl Service for ChanServ { } // True if `from` is the channel's founder; otherwise notices why and returns false. -fn require_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &Db) -> bool { +fn require_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { match db.channel(chan) { None => { ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); @@ -215,7 +215,7 @@ fn require_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db } // True if `from` is the founder or an access-list op of `chan`; else notices why. -fn require_op(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &Db) -> bool { +fn require_op(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { match (from.account, db.channel(chan)) { (_, None) => { ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); @@ -255,7 +255,7 @@ fn parse_mlock(spec: &str) -> (String, String) { } // Render a channel's lock as "+on-off" for display. -fn show_mlock(info: &ChannelInfo) -> String { +fn show_mlock(info: &ChannelView) -> String { let mut s = String::new(); if !info.lock_on.is_empty() { s.push('+'); diff --git a/modules/chanserv/clone.rs b/modules/chanserv/clone.rs index 85ea5d5..c4a4fcf 100644 --- a/modules/chanserv/clone.rs +++ b/modules/chanserv/clone.rs @@ -1,14 +1,14 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // CLONE : copy a channel's settings (mode lock, access, // auto-kick, description, entry message) into another. Founder of both. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let (Some(&src), Some(&dest)) = (args.get(1), args.get(2)) else { ctx.notice(me, from.uid, "Syntax: CLONE "); return; }; - let (Some(sinfo), Some(dinfo)) = (db.channel(src).cloned(), db.channel(dest).cloned()) else { + let (Some(sinfo), Some(dinfo)) = (db.channel(src), db.channel(dest)) else { ctx.notice(me, from.uid, "Both channels must be registered."); return; }; diff --git a/modules/chanserv/enforce.rs b/modules/chanserv/enforce.rs index 958dd6d..ab5e0b1 100644 --- a/modules/chanserv/enforce.rs +++ b/modules/chanserv/enforce.rs @@ -1,10 +1,10 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; // ENFORCE <#channel>: re-apply the channel's settings to everyone present — // the mode lock, access status modes, and the auto-kick list. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: ENFORCE <#channel>"); return; @@ -12,11 +12,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: if !super::require_op(me, from, chan, ctx, db) { return; } - let Some(info) = db.channel(chan).cloned() else { + let Some(info) = db.channel(chan) else { return; }; ctx.channel_mode(me, chan, &info.lock_modes()); - let members: Vec = net.channel_members(chan).map(str::to_string).collect(); + let members: Vec = net.channel_members(chan); for uid in members { match net.account_of(&uid).and_then(|a| info.join_mode(a)) { Some(m) => ctx.channel_mode(me, chan, &format!("{m} {uid}")), diff --git a/modules/chanserv/entrymsg.rs b/modules/chanserv/entrymsg.rs index ef0606d..61199c3 100644 --- a/modules/chanserv/entrymsg.rs +++ b/modules/chanserv/entrymsg.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // ENTRYMSG <#channel> [CLEAR | ]: message noticed to users as they join. // With no argument, show the current message. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: ENTRYMSG <#channel> [CLEAR | ]"); return; diff --git a/modules/chanserv/getkey.rs b/modules/chanserv/getkey.rs index 67ea842..0cfc910 100644 --- a/modules/chanserv/getkey.rs +++ b/modules/chanserv/getkey.rs @@ -1,10 +1,10 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; // GETKEY <#channel>: report the channel key (+k), for ops who need to let // someone in. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: GETKEY <#channel>"); return; diff --git a/modules/chanserv/invite.rs b/modules/chanserv/invite.rs index a5dc4d5..75a81cd 100644 --- a/modules/chanserv/invite.rs +++ b/modules/chanserv/invite.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::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: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: INVITE <#channel> [nick]"); return; diff --git a/modules/chanserv/kick.rs b/modules/chanserv/kick.rs index b3b8808..b6c0ff4 100644 --- a/modules/chanserv/kick.rs +++ b/modules/chanserv/kick.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; // KICK <#channel> [reason]: kick a user. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let (Some(&chan), Some(&nick)) = (args.get(1), args.get(2)) else { ctx.notice(me, from.uid, "Syntax: KICK <#channel> [reason]"); return; diff --git a/modules/chanserv/list.rs b/modules/chanserv/list.rs index 33c7190..5672cb7 100644 --- a/modules/chanserv/list.rs +++ b/modules/chanserv/list.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // LIST: show all registered channels. -pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &Db) { - let mut names: Vec<&str> = db.channels().map(|c| c.name.as_str()).collect(); +pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { + let mut names: Vec = db.channels().into_iter().map(|c| c.name).collect(); if names.is_empty() { ctx.notice(me, from.uid, "No channels are registered."); return; diff --git a/modules/chanserv/mode.rs b/modules/chanserv/mode.rs index 4c8713a..60c19b7 100644 --- a/modules/chanserv/mode.rs +++ b/modules/chanserv/mode.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{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 Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: MODE <#channel> , e.g. MODE #chan +nt"); return; diff --git a/modules/chanserv/op.rs b/modules/chanserv/op.rs index 0db1547..014df57 100644 --- a/modules/chanserv/op.rs +++ b/modules/chanserv/op.rs @@ -1,10 +1,10 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::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". -pub fn handle(me: &str, from: &Sender, mode: &str, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, mode: &str, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: OP/DEOP/VOICE/DEVOICE <#channel> [nick]"); return; diff --git a/modules/chanserv/seen.rs b/modules/chanserv/seen.rs index 97df403..60deb84 100644 --- a/modules/chanserv/seen.rs +++ b/modules/chanserv/seen.rs @@ -1,8 +1,8 @@ use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; // SEEN : when a nick was last seen, and doing what. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) { let Some(&nick) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: SEEN "); return; diff --git a/modules/chanserv/set.rs b/modules/chanserv/set.rs index 587ad28..d067f63 100644 --- a/modules/chanserv/set.rs +++ b/modules/chanserv/set.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // SET <#channel> FOUNDER | DESC : founder-only channel settings. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER | DESC "); return; diff --git a/modules/chanserv/status.rs b/modules/chanserv/status.rs index 1982bd1..2ee7af0 100644 --- a/modules/chanserv/status.rs +++ b/modules/chanserv/status.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::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: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: STATUS <#channel> [nick]"); return; diff --git a/modules/chanserv/topic.rs b/modules/chanserv/topic.rs index dfb6253..4b350cc 100644 --- a/modules/chanserv/topic.rs +++ b/modules/chanserv/topic.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // TOPIC <#channel> : set the channel topic (empty clears it). -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: TOPIC <#channel> "); return; diff --git a/modules/chanserv/unban.rs b/modules/chanserv/unban.rs index 0a2965f..82d82bb 100644 --- a/modules/chanserv/unban.rs +++ b/modules/chanserv/unban.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::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: &Network, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: UNBAN <#channel> [nick]"); return; diff --git a/modules/chanserv/xop.rs b/modules/chanserv/xop.rs index f905375..f6790dd 100644 --- a/modules/chanserv/xop.rs +++ b/modules/chanserv/xop.rs @@ -1,10 +1,10 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // AOP/SOP/VOP <#channel> ADD | DEL | LIST — tiered // shortcuts over the access list. `level` is the access level they map to // ("op" for AOP/SOP, "voice" for VOP); `word` is what the user typed. -pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&chan) = args.get(1) else { ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD | DEL | LIST")); return; diff --git a/modules/nickserv/alist.rs b/modules/nickserv/alist.rs index 925c261..7378d7a 100644 --- a/modules/nickserv/alist.rs +++ b/modules/nickserv/alist.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{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: &Db) { +pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { let Some(account) = from.account else { ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); return; diff --git a/modules/nickserv/cert.rs b/modules/nickserv/cert.rs index 85b6849..9dbcb5d 100644 --- a/modules/nickserv/cert.rs +++ b/modules/nickserv/cert.rs @@ -1,11 +1,11 @@ -use crate::engine::db::{CertError, Db}; +use crate::engine::db::{CertError, Store}; use crate::engine::service::{Sender, ServiceCtx}; // CERT ADD|DEL|LIST [fingerprint]: manage the TLS certificate // fingerprints that may log in to your account via SASL EXTERNAL. Each // subcommand is password-gated, so it needs no identified-session state. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { - let auth = |db: &Db, password: &str| db.authenticate(from.nick, password).map(str::to_string); +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { + let auth = |db: &dyn Store, password: &str| db.authenticate(from.nick, password).map(str::to_string); match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() { Some("LIST") => { let Some(password) = args.get(2) else { diff --git a/modules/nickserv/confirm.rs b/modules/nickserv/confirm.rs index 99887c5..63bc234 100644 --- a/modules/nickserv/confirm.rs +++ b/modules/nickserv/confirm.rs @@ -1,8 +1,8 @@ -use crate::engine::db::{CodeKind, Db}; +use crate::engine::db::{CodeKind, Store}; use crate::engine::service::{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 Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(&code) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: CONFIRM "); return; diff --git a/modules/nickserv/drop.rs b/modules/nickserv/drop.rs index a632d87..a16685b 100644 --- a/modules/nickserv/drop.rs +++ b/modules/nickserv/drop.rs @@ -1,10 +1,10 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; // DROP : delete your account. Re-authenticates as confirmation, releases // and drops the channels you found, and logs you out. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) { let Some(account) = from.account else { ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); return; diff --git a/modules/nickserv/ghost.rs b/modules/nickserv/ghost.rs index 14b646b..d79beac 100644 --- a/modules/nickserv/ghost.rs +++ b/modules/nickserv/ghost.rs @@ -1,10 +1,10 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::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. -pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &Db) { +pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { let Some(&target) = args.get(1) else { ctx.notice(me, from.uid, "Syntax: GHOST [password]"); return; diff --git a/modules/nickserv/glist.rs b/modules/nickserv/glist.rs index e1a2221..8272537 100644 --- a/modules/nickserv/glist.rs +++ b/modules/nickserv/glist.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // GLIST: list the nicks grouped to your account. -pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &Db) { +pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { let Some(account) = from.account else { ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); return; diff --git a/modules/nickserv/group.rs b/modules/nickserv/group.rs index daa44f7..70867f9 100644 --- a/modules/nickserv/group.rs +++ b/modules/nickserv/group.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // GROUP : link your current nick to an existing account, so // you can identify to it under this nick too. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let (Some(&account), Some(&password)) = (args.get(1), args.get(2)) else { ctx.notice(me, from.uid, "Syntax: GROUP "); return; diff --git a/modules/nickserv/identify.rs b/modules/nickserv/identify.rs index f191ae2..9b496cb 100644 --- a/modules/nickserv/identify.rs +++ b/modules/nickserv/identify.rs @@ -1,9 +1,9 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, ServiceCtx}; // IDENTIFY [account] : log in. The account defaults to the current // nick, so both the bare-password and account+password forms work. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { let (account_name, password) = match (args.get(1), args.get(2)) { (Some(account), Some(password)) => (*account, *password), (Some(password), None) => (from.nick, *password), diff --git a/modules/nickserv/info.rs b/modules/nickserv/info.rs index 79c090c..50743f7 100644 --- a/modules/nickserv/info.rs +++ b/modules/nickserv/info.rs @@ -1,9 +1,9 @@ -use crate::engine::db::{human_time, Db}; +use crate::engine::db::{human_time, Store}; use crate::engine::service::{Sender, ServiceCtx}; // INFO [account]: show an account's registration details. The email is shown // only to the account's own owner. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { let name = args.get(1).copied().unwrap_or(from.nick); let Some(acct) = db.account(name) else { ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); diff --git a/modules/nickserv/nickserv.rs b/modules/nickserv/nickserv.rs index 8d3e3e0..9ef786f 100644 --- a/modules/nickserv/nickserv.rs +++ b/modules/nickserv/nickserv.rs @@ -1,6 +1,6 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{Sender, Service, ServiceCtx}; -use crate::engine::state::Network; +use crate::engine::state::NetView; #[path = "register.rs"] mod register; @@ -53,7 +53,7 @@ impl Service for NickServ { true } - fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &mut Db) { + fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) { let me = self.uid.as_str(); match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { Some("REGISTER") => register::handle(me, from, args, ctx), diff --git a/modules/nickserv/resetpass.rs b/modules/nickserv/resetpass.rs index 215d872..bb8d96d 100644 --- a/modules/nickserv/resetpass.rs +++ b/modules/nickserv/resetpass.rs @@ -1,9 +1,9 @@ -use crate::engine::db::{CodeKind, Db}; +use crate::engine::db::{CodeKind, Store}; use crate::engine::service::{Sender, ServiceCtx}; // RESETPASS : email a reset code to the address on file. // RESETPASS : complete the reset with that code. -pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { if !db.email_enabled() { ctx.notice(me, from.uid, "Password reset by email isn't available here."); return; diff --git a/modules/nickserv/set.rs b/modules/nickserv/set.rs index 4afab84..535e6f0 100644 --- a/modules/nickserv/set.rs +++ b/modules/nickserv/set.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{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 Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(account) = from.account else { ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); return; diff --git a/modules/nickserv/ungroup.rs b/modules/nickserv/ungroup.rs index 9aea290..364e435 100644 --- a/modules/nickserv/ungroup.rs +++ b/modules/nickserv/ungroup.rs @@ -1,8 +1,8 @@ -use crate::engine::db::Db; +use crate::engine::db::Store; use crate::engine::service::{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 Db) { +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { let Some(account) = from.account else { ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); return; diff --git a/src/engine/db.rs b/src/engine/db.rs index 51af8c8..05580a7 100644 --- a/src/engine/db.rs +++ b/src/engine/db.rs @@ -11,6 +11,13 @@ use tokio::sync::broadcast; use super::scram::{self, Hash}; +// 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 +// modules importing `crate::engine::db::{ChanError, ...}` are unaffected. +pub use fedserv_api::{ + AccountView, ChanAccessView, ChanAkickView, ChanError, ChannelView, CertError, CodeKind, RegError, Store, +}; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Account { pub name: String, @@ -158,11 +165,6 @@ impl ChannelInfo { .map(|a| if a.level == "voice" { "+v" } else { "+o" }) } - /// Whether `account` has operator access (founder or access-list op). - pub fn is_op(&self, account: &str) -> bool { - self.join_mode(account) == Some("+o") - } - /// The matching auto-kick entry for `hostmask` (nick!user@host), if any. pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkick> { self.akick.iter().find(|k| glob_match(&k.mask, hostmask)) @@ -421,12 +423,6 @@ impl EventLog { } } -#[derive(Debug)] -pub enum RegError { - Exists, - Internal, -} - // What an ingested entry did to a locally-known account, so the engine can log // out sessions that were relying on it. pub enum AccountChange { @@ -434,21 +430,6 @@ pub enum AccountChange { Dropped(String), } -#[derive(Debug)] -pub enum CertError { - Invalid, // not a plausible fingerprint - InUse, // already registered (to any account) - NoAccount, // target account does not exist - Internal, // persistence failed -} - -#[derive(Debug)] -pub enum ChanError { - Exists, // channel already registered - NoChannel, // channel is not registered - Internal, // persistence failed -} - // A fingerprint is hex (hash digest), optionally colon-separated. Bound the // length so a junk value can't bloat an account. fn valid_fp(fp: &str) -> bool { @@ -482,13 +463,6 @@ pub struct Db { codes: HashMap, } -// What an emailed code authorises. -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum CodeKind { - Reset, - Confirm, -} - fn key(name: &str) -> String { name.to_ascii_lowercase() } @@ -1210,6 +1184,131 @@ fn verify_password(password: &str, hash: &str) -> bool { .unwrap_or(false) } +// The module-facing account/channel store. Every method forwards to Db's own +// (fully-qualified so it is the inherent method, never this trait method), with +// reads projected into credential-free views. Internal callers keep using the +// richer inherent methods directly. +impl Store for Db { + fn exists(&self, name: &str) -> bool { + Db::exists(self, name) + } + fn account(&self, name: &str) -> Option { + Db::account(self, name).map(|a| AccountView { + name: a.name.clone(), + email: a.email.clone(), + ts: a.ts, + verified: a.verified, + }) + } + fn resolve_account(&self, name: &str) -> Option<&str> { + Db::resolve_account(self, name) + } + fn authenticate(&self, name: &str, password: &str) -> Option<&str> { + Db::authenticate(self, name, password) + } + fn grouped_nicks(&self, account: &str) -> Vec { + Db::grouped_nicks(self, account) + } + fn certfps(&self, account: &str) -> &[String] { + Db::certfps(self, account) + } + fn is_verified(&self, account: &str) -> bool { + Db::is_verified(self, account) + } + fn channel(&self, name: &str) -> Option { + Db::channel(self, name).map(channel_view) + } + fn channels(&self) -> Vec { + Db::channels(self).map(channel_view).collect() + } + fn channels_owned_by(&self, account: &str) -> Vec { + Db::channels_owned_by(self, account) + } + fn email_enabled(&self) -> bool { + Db::email_enabled(self) + } + fn email_brand(&self) -> &str { + Db::email_brand(self) + } + fn email_accent(&self) -> &str { + Db::email_accent(self) + } + fn email_logo(&self) -> &str { + Db::email_logo(self) + } + fn issue_code(&mut self, account: &str, kind: CodeKind) -> String { + Db::issue_code(self, account, kind) + } + fn take_code(&mut self, account: &str, kind: CodeKind, code: &str) -> bool { + Db::take_code(self, account, kind, code) + } + fn verify_account(&mut self, account: &str) -> Result<(), RegError> { + Db::verify_account(self, account) + } + fn set_email(&mut self, account: &str, email: Option) -> Result<(), RegError> { + Db::set_email(self, account, email) + } + fn group_nick(&mut self, nick: &str, account: &str) -> Result<(), RegError> { + Db::group_nick(self, nick, account) + } + fn ungroup_nick(&mut self, nick: &str) -> Result { + Db::ungroup_nick(self, nick) + } + fn drop_account(&mut self, account: &str) -> Result { + Db::drop_account(self, account) + } + fn certfp_add(&mut self, account: &str, fp: &str) -> Result<(), CertError> { + Db::certfp_add(self, account, fp) + } + fn certfp_del(&mut self, account: &str, fp: &str) -> Result { + Db::certfp_del(self, account, fp) + } + fn register_channel(&mut self, name: &str, founder: &str) -> Result<(), ChanError> { + Db::register_channel(self, name, founder) + } + fn drop_channel(&mut self, name: &str) -> Result<(), ChanError> { + Db::drop_channel(self, name) + } + fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError> { + Db::set_mlock(self, name, on, off) + } + fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError> { + Db::set_desc(self, channel, desc) + } + fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError> { + Db::set_entrymsg(self, channel, msg) + } + fn set_founder(&mut self, channel: &str, account: &str) -> Result<(), ChanError> { + Db::set_founder(self, channel, account) + } + fn access_add(&mut self, channel: &str, account: &str, level: &str) -> Result<(), ChanError> { + Db::access_add(self, channel, account, level) + } + fn access_del(&mut self, channel: &str, account: &str) -> Result { + Db::access_del(self, channel, account) + } + fn akick_add(&mut self, channel: &str, mask: &str, reason: &str) -> Result<(), ChanError> { + Db::akick_add(self, channel, mask, reason) + } + fn akick_del(&mut self, channel: &str, mask: &str) -> Result { + Db::akick_del(self, channel, mask) + } +} + +fn channel_view(c: &ChannelInfo) -> ChannelView { + ChannelView { + name: c.name.clone(), + founder: c.founder.clone(), + ts: c.ts, + lock_on: c.lock_on.clone(), + lock_off: c.lock_off.clone(), + access: c.access.iter().map(|a| ChanAccessView { account: a.account.clone(), level: a.level.clone() }).collect(), + akick: c.akick.iter().map(|k| ChanAkickView { mask: k.mask.clone(), reason: k.reason.clone() }).collect(), + desc: c.desc.clone(), + entrymsg: c.entrymsg.clone(), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/engine/service.rs b/src/engine/service.rs index 7b61907..38bba53 100644 --- a/src/engine/service.rs +++ b/src/engine/service.rs @@ -1,29 +1,4 @@ -use crate::engine::db::Db; -use crate::engine::state::Network; - -// Sender + ServiceCtx (the command context a module receives) live in the -// fedserv-api SDK crate; re-exported so modules keep using -// `crate::engine::service::{Sender, ServiceCtx}`. -pub use fedserv_api::{Sender, ServiceCtx}; - -// A pseudo-client (NickServ, ChanServ, ...). Introduced at burst, receives the -// commands users message it, reads/writes the account store, and pushes actions. -pub trait Service: Send { - fn nick(&self) -> &str; - fn uid(&self) -> &str; - fn host(&self) -> &str { - "services.local" - } - fn gecos(&self) -> &str; - // Whether this service owns channel modes (ChanServ), so the engine can source - // channel mode changes from it. - fn manages_channels(&self) -> bool { - false - } - // Whether this is the account service (NickServ), so the engine can source - // account-related notices from it. - fn manages_accounts(&self) -> bool { - false - } - fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &mut Db); -} +// 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 +// modules keep using `crate::engine::service::{...}`. +pub use fedserv_api::{Sender, Service, ServiceCtx}; diff --git a/src/engine/state.rs b/src/engine/state.rs index d0ac4de..c26a2f3 100644 --- a/src/engine/state.rs +++ b/src/engine/state.rs @@ -1,6 +1,10 @@ use std::collections::{HashMap, HashSet}; 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::{NetView, SeenView}; + // Live network view, rebuilt from the uplink's burst each connect (ephemeral — // unlike the account store, which persists). #[derive(Default)] @@ -155,3 +159,39 @@ impl Network { self.seen.get(&lc(nick)) } } + +// The module-facing network view. Reads forward to Network's own accessors, +// with the seen record projected into a plain view. +impl NetView for Network { + fn uid_by_nick(&self, nick: &str) -> Option<&str> { + Network::uid_by_nick(self, nick) + } + fn nick_of(&self, uid: &str) -> Option<&str> { + Network::nick_of(self, uid) + } + fn host_of(&self, uid: &str) -> Option<&str> { + Network::host_of(self, uid) + } + fn account_of(&self, uid: &str) -> Option<&str> { + Network::account_of(self, uid) + } + fn uids_logged_into(&self, account: &str) -> Vec { + Network::uids_logged_into(self, account) + } + fn is_op(&self, channel: &str, uid: &str) -> bool { + Network::is_op(self, channel, uid) + } + fn channel_members(&self, channel: &str) -> Vec { + Network::channel_members(self, channel).map(str::to_string).collect() + } + fn channel_key(&self, channel: &str) -> Option<&str> { + Network::channel_key(self, channel) + } + fn last_seen(&self, nick: &str) -> Option { + Network::last_seen(self, nick).map(|s| SeenView { + nick: s.nick.clone(), + ts: s.ts, + what: s.what.clone(), + }) + } +}