modules: capability-scoped Store/NetView boundary (Tier 2)

Services no longer receive the concrete account store. on_command now takes
&mut dyn Store + &dyn NetView (both defined in fedserv-api); the engine's Db
and Network implement them. Reads hand back plain views (AccountView,
ChannelView, ...) carrying only non-secret fields, so the event log, gossip,
and credential material (password hash, SCRAM verifiers) are unreachable from
a module. All command modules ported; behaviour unchanged.
This commit is contained in:
Jean Chevronnet 2026-07-13 01:04:07 +00:00
parent 931b8727e9
commit 8ed1a9ab70
No known key found for this signature in database
36 changed files with 487 additions and 148 deletions

View file

@ -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<String>,
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<ChanAccessView>,
pub akick: Vec<ChanAkickView>,
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<AccountView>;
// 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<String>;
fn certfps(&self, account: &str) -> &[String];
fn is_verified(&self, account: &str) -> bool;
fn channel(&self, name: &str) -> Option<ChannelView>;
fn channels(&self) -> Vec<ChannelView>;
fn channels_owned_by(&self, account: &str) -> Vec<String>;
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<String>) -> Result<(), RegError>;
fn group_nick(&mut self, nick: &str, account: &str) -> Result<(), RegError>;
fn ungroup_nick(&mut self, nick: &str) -> Result<bool, RegError>;
fn drop_account(&mut self, account: &str) -> Result<bool, RegError>;
fn certfp_add(&mut self, account: &str, fp: &str) -> Result<(), CertError>;
fn certfp_del(&mut self, account: &str, fp: &str) -> Result<bool, CertError>;
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<bool, ChanError>;
fn akick_add(&mut self, channel: &str, mask: &str, reason: &str) -> Result<(), ChanError>;
fn akick_del(&mut self, channel: &str, mask: &str) -> Result<bool, ChanError>;
}
// 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<String>;
fn is_op(&self, channel: &str, uid: &str) -> bool;
fn channel_members(&self, channel: &str) -> Vec<String>;
fn channel_key(&self, channel: &str) -> Option<&str>;
fn last_seen(&self, nick: &str) -> Option<SeenView>;
}
// 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<char>, Vec<char>) = (
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()
}

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account> // ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>
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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>"); ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>");
return; 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. // 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) { match db.channel(chan) {
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST // AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST
// Masks are nick!user@host globs; matching users are banned and kicked on join. // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST"); ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
// BAN <#channel> <nick> [reason]: ban *!*@host and kick the user. // BAN <#channel> <nick> [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 { let (Some(&chan), Some(&nick)) = (args.get(1), args.get(2)) else {
ctx.notice(me, from.uid, "Syntax: BAN <#channel> <nick> [reason]"); ctx.notice(me, from.uid, "Syntax: BAN <#channel> <nick> [reason]");
return; return;

View file

@ -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::service::{Sender, Service, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
#[path = "mode.rs"] #[path = "mode.rs"]
mod mode; mod mode;
@ -57,7 +57,7 @@ impl Service for ChanServ {
true 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(); let me = self.uid.as_str();
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
Some("REGISTER") => { Some("REGISTER") => {
@ -142,7 +142,7 @@ impl Service for ChanServ {
Some(info) if info.lock_on.is_empty() && info.lock_off.is_empty() => { 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)); 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; return;
} }
@ -200,7 +200,7 @@ impl Service for ChanServ {
} }
// True if `from` is the channel's founder; otherwise notices why and returns false. // 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) { match db.channel(chan) {
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); 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. // 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)) { match (from.account, db.channel(chan)) {
(_, None) => { (_, None) => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); 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. // 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(); let mut s = String::new();
if !info.lock_on.is_empty() { if !info.lock_on.is_empty() {
s.push('+'); s.push('+');

View file

@ -1,14 +1,14 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// CLONE <source> <target>: copy a channel's settings (mode lock, access, // CLONE <source> <target>: copy a channel's settings (mode lock, access,
// auto-kick, description, entry message) into another. Founder of both. // 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 { let (Some(&src), Some(&dest)) = (args.get(1), args.get(2)) else {
ctx.notice(me, from.uid, "Syntax: CLONE <source> <target>"); ctx.notice(me, from.uid, "Syntax: CLONE <source> <target>");
return; 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."); ctx.notice(me, from.uid, "Both channels must be registered.");
return; return;
}; };

View file

@ -1,10 +1,10 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; 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 — // ENFORCE <#channel>: re-apply the channel's settings to everyone present —
// the mode lock, access status modes, and the auto-kick list. // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: ENFORCE <#channel>"); ctx.notice(me, from.uid, "Syntax: ENFORCE <#channel>");
return; 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) { if !super::require_op(me, from, chan, ctx, db) {
return; return;
} }
let Some(info) = db.channel(chan).cloned() else { let Some(info) = db.channel(chan) else {
return; return;
}; };
ctx.channel_mode(me, chan, &info.lock_modes()); ctx.channel_mode(me, chan, &info.lock_modes());
let members: Vec<String> = net.channel_members(chan).map(str::to_string).collect(); let members: Vec<String> = net.channel_members(chan);
for uid in members { for uid in members {
match net.account_of(&uid).and_then(|a| info.join_mode(a)) { match net.account_of(&uid).and_then(|a| info.join_mode(a)) {
Some(m) => ctx.channel_mode(me, chan, &format!("{m} {uid}")), Some(m) => ctx.channel_mode(me, chan, &format!("{m} {uid}")),

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// ENTRYMSG <#channel> [CLEAR | <text>]: message noticed to users as they join. // ENTRYMSG <#channel> [CLEAR | <text>]: message noticed to users as they join.
// With no argument, show the current message. // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: ENTRYMSG <#channel> [CLEAR | <text>]"); ctx.notice(me, from.uid, "Syntax: ENTRYMSG <#channel> [CLEAR | <text>]");
return; return;

View file

@ -1,10 +1,10 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; 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 // GETKEY <#channel>: report the channel key (+k), for ops who need to let
// someone in. // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: GETKEY <#channel>"); ctx.notice(me, from.uid, "Syntax: GETKEY <#channel>");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; 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. // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: INVITE <#channel> [nick]"); ctx.notice(me, from.uid, "Syntax: INVITE <#channel> [nick]");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
// KICK <#channel> <nick> [reason]: kick a user. // KICK <#channel> <nick> [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 { let (Some(&chan), Some(&nick)) = (args.get(1), args.get(2)) else {
ctx.notice(me, from.uid, "Syntax: KICK <#channel> <nick> [reason]"); ctx.notice(me, from.uid, "Syntax: KICK <#channel> <nick> [reason]");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// LIST: show all registered channels. // LIST: show all registered channels.
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 mut names: Vec<&str> = db.channels().map(|c| c.name.as_str()).collect(); let mut names: Vec<String> = db.channels().into_iter().map(|c| c.name).collect();
if names.is_empty() { if names.is_empty() {
ctx.notice(me, from.uid, "No channels are registered."); ctx.notice(me, from.uid, "No channels are registered.");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// MODE <#channel> <modes>: the founder sets channel modes via ChanServ. // MODE <#channel> <modes>: 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: MODE <#channel> <modes>, e.g. MODE #chan +nt"); ctx.notice(me, from.uid, "Syntax: MODE <#channel> <modes>, e.g. MODE #chan +nt");
return; return;

View file

@ -1,10 +1,10 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; 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 // 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". // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: OP/DEOP/VOICE/DEVOICE <#channel> [nick]"); ctx.notice(me, from.uid, "Syntax: OP/DEOP/VOICE/DEVOICE <#channel> [nick]");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
// SEEN <nick>: when a nick was last seen, and doing what. // SEEN <nick>: 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 { let Some(&nick) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: SEEN <nick>"); ctx.notice(me, from.uid, "Syntax: SEEN <nick>");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// SET <#channel> FOUNDER <account> | DESC <text>: founder-only channel settings. // SET <#channel> FOUNDER <account> | DESC <text>: 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text>"); ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text>");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; 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). // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: STATUS <#channel> [nick]"); ctx.notice(me, from.uid, "Syntax: STATUS <#channel> [nick]");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// TOPIC <#channel> <text>: set the channel topic (empty clears it). // TOPIC <#channel> <text>: 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: TOPIC <#channel> <text>"); ctx.notice(me, from.uid, "Syntax: TOPIC <#channel> <text>");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; 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). // 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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: UNBAN <#channel> [nick]"); ctx.notice(me, from.uid, "Syntax: UNBAN <#channel> [nick]");
return; return;

View file

@ -1,10 +1,10 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// AOP/SOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered // AOP/SOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered
// shortcuts over the access list. `level` is the access level they map to // 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. // ("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 { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST")); ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST"));
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// ALIST: list the channels the sender's account founds or has access on. // 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 { let Some(account) = from.account else {
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
return; return;

View file

@ -1,11 +1,11 @@
use crate::engine::db::{CertError, Db}; use crate::engine::db::{CertError, Store};
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// CERT ADD|DEL|LIST <password> [fingerprint]: manage the TLS certificate // CERT ADD|DEL|LIST <password> [fingerprint]: manage the TLS certificate
// fingerprints that may log in to your account via SASL EXTERNAL. Each // fingerprints that may log in to your account via SASL EXTERNAL. Each
// subcommand is password-gated, so it needs no identified-session state. // 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) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
let auth = |db: &Db, password: &str| db.authenticate(from.nick, password).map(str::to_string); 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() { match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
Some("LIST") => { Some("LIST") => {
let Some(password) = args.get(2) else { let Some(password) = args.get(2) else {

View file

@ -1,8 +1,8 @@
use crate::engine::db::{CodeKind, Db}; use crate::engine::db::{CodeKind, Store};
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// CONFIRM <code>: confirm your account's email with the code you were emailed. // CONFIRM <code>: 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 { let Some(&code) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: CONFIRM <code>"); ctx.notice(me, from.uid, "Syntax: CONFIRM <code>");
return; return;

View file

@ -1,10 +1,10 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
// DROP <password>: delete your account. Re-authenticates as confirmation, releases // DROP <password>: delete your account. Re-authenticates as confirmation, releases
// and drops the channels you found, and logs you out. // 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 { let Some(account) = from.account else {
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
return; return;

View file

@ -1,10 +1,10 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
// GHOST/RECOVER <nick> [password]: rename off a session using a nick you own, // GHOST/RECOVER <nick> [password]: rename off a session using a nick you own,
// either by being identified to its account or giving that account's password. // 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 { let Some(&target) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: GHOST <nick> [password]"); ctx.notice(me, from.uid, "Syntax: GHOST <nick> [password]");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// GLIST: list the nicks grouped to your account. // 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 { let Some(account) = from.account else {
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// GROUP <account> <password>: link your current nick to an existing account, so // GROUP <account> <password>: link your current nick to an existing account, so
// you can identify to it under this nick too. // 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 { let (Some(&account), Some(&password)) = (args.get(1), args.get(2)) else {
ctx.notice(me, from.uid, "Syntax: GROUP <account> <password>"); ctx.notice(me, from.uid, "Syntax: GROUP <account> <password>");
return; return;

View file

@ -1,9 +1,9 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// IDENTIFY [account] <password>: log in. The account defaults to the current // IDENTIFY [account] <password>: log in. The account defaults to the current
// nick, so both the bare-password and account+password forms work. // 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)) { let (account_name, password) = match (args.get(1), args.get(2)) {
(Some(account), Some(password)) => (*account, *password), (Some(account), Some(password)) => (*account, *password),
(Some(password), None) => (from.nick, *password), (Some(password), None) => (from.nick, *password),

View file

@ -1,9 +1,9 @@
use crate::engine::db::{human_time, Db}; use crate::engine::db::{human_time, Store};
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// INFO [account]: show an account's registration details. The email is shown // INFO [account]: show an account's registration details. The email is shown
// only to the account's own owner. // 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 name = args.get(1).copied().unwrap_or(from.nick);
let Some(acct) = db.account(name) else { let Some(acct) = db.account(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered."));

View file

@ -1,6 +1,6 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, Service, ServiceCtx}; use crate::engine::service::{Sender, Service, ServiceCtx};
use crate::engine::state::Network; use crate::engine::state::NetView;
#[path = "register.rs"] #[path = "register.rs"]
mod register; mod register;
@ -53,7 +53,7 @@ impl Service for NickServ {
true 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(); let me = self.uid.as_str();
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
Some("REGISTER") => register::handle(me, from, args, ctx), Some("REGISTER") => register::handle(me, from, args, ctx),

View file

@ -1,9 +1,9 @@
use crate::engine::db::{CodeKind, Db}; use crate::engine::db::{CodeKind, Store};
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// RESETPASS <account>: email a reset code to the address on file. // RESETPASS <account>: email a reset code to the address on file.
// RESETPASS <account> <code> <newpassword>: complete the reset with that code. // RESETPASS <account> <code> <newpassword>: 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() { if !db.email_enabled() {
ctx.notice(me, from.uid, "Password reset by email isn't available here."); ctx.notice(me, from.uid, "Password reset by email isn't available here.");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// SET PASSWORD <newpassword> | SET EMAIL [address]: change your account settings. // SET PASSWORD <newpassword> | 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 { let Some(account) = from.account else {
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
return; return;

View file

@ -1,8 +1,8 @@
use crate::engine::db::Db; use crate::engine::db::Store;
use crate::engine::service::{Sender, ServiceCtx}; use crate::engine::service::{Sender, ServiceCtx};
// UNGROUP [nick]: remove a nick grouped to your account (defaults to your current nick). // 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 { let Some(account) = from.account else {
ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first."); ctx.notice(me, from.uid, "You need to be logged in. Identify to NickServ first.");
return; return;

View file

@ -11,6 +11,13 @@ use tokio::sync::broadcast;
use super::scram::{self, Hash}; 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account { pub struct Account {
pub name: String, pub name: String,
@ -158,11 +165,6 @@ impl ChannelInfo {
.map(|a| if a.level == "voice" { "+v" } else { "+o" }) .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. /// The matching auto-kick entry for `hostmask` (nick!user@host), if any.
pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkick> { pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkick> {
self.akick.iter().find(|k| glob_match(&k.mask, hostmask)) 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 // What an ingested entry did to a locally-known account, so the engine can log
// out sessions that were relying on it. // out sessions that were relying on it.
pub enum AccountChange { pub enum AccountChange {
@ -434,21 +430,6 @@ pub enum AccountChange {
Dropped(String), 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 // A fingerprint is hex (hash digest), optionally colon-separated. Bound the
// length so a junk value can't bloat an account. // length so a junk value can't bloat an account.
fn valid_fp(fp: &str) -> bool { fn valid_fp(fp: &str) -> bool {
@ -482,13 +463,6 @@ pub struct Db {
codes: HashMap<String, (CodeKind, String, Instant)>, codes: HashMap<String, (CodeKind, String, Instant)>,
} }
// What an emailed code authorises.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CodeKind {
Reset,
Confirm,
}
fn key(name: &str) -> String { fn key(name: &str) -> String {
name.to_ascii_lowercase() name.to_ascii_lowercase()
} }
@ -1210,6 +1184,131 @@ fn verify_password(password: &str, hash: &str) -> bool {
.unwrap_or(false) .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<AccountView> {
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<String> {
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<ChannelView> {
Db::channel(self, name).map(channel_view)
}
fn channels(&self) -> Vec<ChannelView> {
Db::channels(self).map(channel_view).collect()
}
fn channels_owned_by(&self, account: &str) -> Vec<String> {
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<String>) -> 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<bool, RegError> {
Db::ungroup_nick(self, nick)
}
fn drop_account(&mut self, account: &str) -> Result<bool, RegError> {
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<bool, CertError> {
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<bool, ChanError> {
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<bool, ChanError> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,29 +1,4 @@
use crate::engine::db::Db; // Sender, ServiceCtx, and the Service trait a pseudo-client implements all live
use crate::engine::state::Network; // in the fedserv-api SDK crate; re-exported so the engine and the service
// modules keep using `crate::engine::service::{...}`.
// Sender + ServiceCtx (the command context a module receives) live in the pub use fedserv_api::{Sender, Service, ServiceCtx};
// 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);
}

View file

@ -1,6 +1,10 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::time::{SystemTime, UNIX_EPOCH}; 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 — // Live network view, rebuilt from the uplink's burst each connect (ephemeral —
// unlike the account store, which persists). // unlike the account store, which persists).
#[derive(Default)] #[derive(Default)]
@ -155,3 +159,39 @@ impl Network {
self.seen.get(&lc(nick)) 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<String> {
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<String> {
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<SeenView> {
Network::last_seen(self, nick).map(|s| SeenView {
nick: s.nick.clone(),
ts: s.ts,
what: s.what.clone(),
})
}
}