botserv: introduce bots as live pseudo-clients (slice 2)
The engine now reconciles the bot registry against the network: at burst and after each command it introduces any bot not yet live (minting a SID-prefixed uid) and quits (QuitUser) any removed from the registry. So BOT ADD makes the bot appear immediately and BOT DEL removes it; a reconnect re-introduces all registered bots fresh. Adds set_sid + per-connection bot-uid tracking. Next: ASSIGN a bot to a channel, then in-channel fantasy commands.
This commit is contained in:
parent
577c05ad2e
commit
f94b0afcd9
2 changed files with 92 additions and 8 deletions
|
|
@ -91,6 +91,9 @@ pub struct Engine {
|
||||||
nick_service: Option<String>, // uid of the account service (NickServ), for its notices
|
nick_service: Option<String>, // uid of the account service (NickServ), for its notices
|
||||||
irc_out: Option<mpsc::UnboundedSender<NetAction>>, // services-initiated actions -> the uplink
|
irc_out: Option<mpsc::UnboundedSender<NetAction>>, // services-initiated actions -> the uplink
|
||||||
opers: HashMap<String, Privs>, // casefolded account -> privileges (from [[oper]] config)
|
opers: HashMap<String, Privs>, // casefolded account -> privileges (from [[oper]] config)
|
||||||
|
sid: String, // our services SID, for minting bot uids
|
||||||
|
bot_uids: HashMap<String, String>, // casefolded bot nick -> live uid (per connection)
|
||||||
|
next_bot_index: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
|
@ -107,6 +110,9 @@ impl Engine {
|
||||||
nick_service,
|
nick_service,
|
||||||
irc_out: None,
|
irc_out: None,
|
||||||
opers: HashMap::new(),
|
opers: HashMap::new(),
|
||||||
|
sid: String::new(),
|
||||||
|
bot_uids: HashMap::new(),
|
||||||
|
next_bot_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,6 +133,36 @@ impl Engine {
|
||||||
self.opers.get(&account.to_ascii_lowercase()).copied().unwrap_or_default()
|
self.opers.get(&account.to_ascii_lowercase()).copied().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Our services SID, needed to mint valid bot uids.
|
||||||
|
pub fn set_sid(&mut self, sid: String) {
|
||||||
|
self.sid = sid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bring the network's bot pseudo-clients in line with the registry: introduce
|
||||||
|
// any new bots, quit any that were deleted. Run at burst and after commands.
|
||||||
|
fn reconcile_bots(&mut self) -> Vec<NetAction> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let live: Vec<(String, String, String, String)> =
|
||||||
|
self.db.bots().map(|b| (b.nick.clone(), b.user.clone(), b.host.clone(), b.gecos.clone())).collect();
|
||||||
|
for (nick, user, host, gecos) in &live {
|
||||||
|
let k = nick.to_ascii_lowercase();
|
||||||
|
if !self.bot_uids.contains_key(&k) {
|
||||||
|
let uid = format!("{}B{:05}", self.sid, self.next_bot_index);
|
||||||
|
self.next_bot_index += 1;
|
||||||
|
out.push(NetAction::IntroduceUser { uid: uid.clone(), nick: nick.clone(), ident: user.clone(), host: host.clone(), gecos: gecos.clone() });
|
||||||
|
self.bot_uids.insert(k, uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let live_keys: std::collections::HashSet<String> = live.iter().map(|(n, ..)| n.to_ascii_lowercase()).collect();
|
||||||
|
let removed: Vec<String> = self.bot_uids.keys().filter(|k| !live_keys.contains(*k)).cloned().collect();
|
||||||
|
for k in removed {
|
||||||
|
if let Some(uid) = self.bot_uids.remove(&k) {
|
||||||
|
out.push(NetAction::QuitUser { uid, reason: "Bot removed".to_string() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
// Finish a SASL login, unless the account is suspended.
|
// Finish a SASL login, unless the account is suspended.
|
||||||
fn sasl_login(&self, agent: &str, client: &str, account: String) -> Vec<NetAction> {
|
fn sasl_login(&self, agent: &str, client: &str, account: String) -> Vec<NetAction> {
|
||||||
if self.db.is_suspended(&account) {
|
if self.db.is_suspended(&account) {
|
||||||
|
|
@ -387,7 +423,10 @@ impl Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||||
pub fn startup_actions(&self) -> Vec<NetAction> {
|
pub fn startup_actions(&mut self) -> Vec<NetAction> {
|
||||||
|
// Fresh link: forget bot uids minted on any previous connection.
|
||||||
|
self.bot_uids.clear();
|
||||||
|
self.next_bot_index = 0;
|
||||||
let mut out = vec![NetAction::Burst];
|
let mut out = vec![NetAction::Burst];
|
||||||
for svc in &self.services {
|
for svc in &self.services {
|
||||||
out.push(NetAction::IntroduceUser {
|
out.push(NetAction::IntroduceUser {
|
||||||
|
|
@ -400,6 +439,8 @@ impl Engine {
|
||||||
}
|
}
|
||||||
// Advertise our SASL mechanisms so the uplink can offer `sasl=PLAIN` to
|
// Advertise our SASL mechanisms so the uplink can offer `sasl=PLAIN` to
|
||||||
// clients in CAP LS (IRCv3 SASL 3.2).
|
// clients in CAP LS (IRCv3 SASL 3.2).
|
||||||
|
// Introduce the registered bots too.
|
||||||
|
out.extend(self.reconcile_bots());
|
||||||
out.push(NetAction::Metadata {
|
out.push(NetAction::Metadata {
|
||||||
target: "*".to_string(),
|
target: "*".to_string(),
|
||||||
key: "saslmechlist".to_string(),
|
key: "saslmechlist".to_string(),
|
||||||
|
|
@ -780,6 +821,7 @@ impl Engine {
|
||||||
let privs = account.as_deref().map(|a| self.oper_privs(a)).unwrap_or_default();
|
let privs = account.as_deref().map(|a| self.oper_privs(a)).unwrap_or_default();
|
||||||
let mut ctx = ServiceCtx::default();
|
let mut ctx = ServiceCtx::default();
|
||||||
let sender = Sender { uid: from, nick: &nick, account: account.as_deref(), privs };
|
let sender = Sender { uid: from, nick: &nick, account: account.as_deref(), privs };
|
||||||
|
{
|
||||||
let Self { services, network, db, .. } = self;
|
let Self { services, network, db, .. } = self;
|
||||||
for svc in services.iter_mut() {
|
for svc in services.iter_mut() {
|
||||||
if to.eq_ignore_ascii_case(svc.uid()) || to.eq_ignore_ascii_case(svc.nick()) {
|
if to.eq_ignore_ascii_case(svc.uid()) || to.eq_ignore_ascii_case(svc.nick()) {
|
||||||
|
|
@ -788,7 +830,11 @@ impl Engine {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.actions
|
}
|
||||||
|
// A command may have changed the bot registry (BotServ BOT ADD/DEL).
|
||||||
|
let mut out = ctx.actions;
|
||||||
|
out.extend(self.reconcile_bots());
|
||||||
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1591,6 +1637,43 @@ mod tests {
|
||||||
assert!(notice(&bs(&mut e, "000AAAAAC", "BOT LIST"), "Bendy"));
|
assert!(notice(&bs(&mut e, "000AAAAAC", "BOT LIST"), "Bendy"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bot is introduced on the network when added, and quit when deleted.
|
||||||
|
#[test]
|
||||||
|
fn botserv_introduces_and_quits_bots() {
|
||||||
|
use fedserv_botserv::BotServ;
|
||||||
|
use fedserv_nickserv::NickServ;
|
||||||
|
let path = std::env::temp_dir().join("fedserv-botintro.jsonl");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
let mut db = Db::open(&path, "42S");
|
||||||
|
db.scram_iterations = 4096;
|
||||||
|
db.register("staff", "password1", None).unwrap();
|
||||||
|
let mut e = Engine::new(
|
||||||
|
vec![
|
||||||
|
Box::new(NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }),
|
||||||
|
Box::new(BotServ { uid: "42SAAAAAD".into() }),
|
||||||
|
],
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
e.set_sid("42S".into());
|
||||||
|
let mut opers = std::collections::HashMap::new();
|
||||||
|
opers.insert("staff".to_string(), Privs::default().with(fedserv_api::Priv::Admin));
|
||||||
|
e.set_opers(opers);
|
||||||
|
let bs = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAD".into(), text: t.into() });
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "staff".into(), host: "h".into() });
|
||||||
|
e.handle(NetEvent::Privmsg { from: "000AAAAAC".into(), to: "42SAAAAAA".into(), text: "IDENTIFY password1".into() });
|
||||||
|
|
||||||
|
// Adding a bot introduces it on the network with a SID-prefixed uid.
|
||||||
|
let out = bs(&mut e, "000AAAAAC", "BOT ADD Bendy bot serv.host A Helper");
|
||||||
|
assert!(out.iter().any(|a| matches!(a, NetAction::IntroduceUser { nick, uid, .. } if nick == "Bendy" && uid.starts_with("42SB"))), "introduced: {out:?}");
|
||||||
|
// A later command doesn't re-introduce an already-live bot.
|
||||||
|
assert!(!bs(&mut e, "000AAAAAC", "BOT LIST").iter().any(|a| matches!(a, NetAction::IntroduceUser { .. })), "no duplicate introduce");
|
||||||
|
// Deleting it quits the pseudo-client.
|
||||||
|
assert!(bs(&mut e, "000AAAAAC", "BOT DEL Bendy").iter().any(|a| matches!(a, NetAction::QuitUser { .. })), "quit on delete");
|
||||||
|
// A registered bot is introduced at burst.
|
||||||
|
bs(&mut e, "000AAAAAC", "BOT ADD Roger svc host Bot");
|
||||||
|
assert!(e.startup_actions().iter().any(|a| matches!(a, NetAction::IntroduceUser { nick, .. } if nick == "Roger")), "introduced at burst");
|
||||||
|
}
|
||||||
|
|
||||||
// MemoServ delivers a memo to an offline account and notifies them on login.
|
// MemoServ delivers a memo to an offline account and notifies them on login.
|
||||||
#[test]
|
#[test]
|
||||||
fn memoserv_delivers_and_notifies_on_login() {
|
fn memoserv_delivers_and_notifies_on_login() {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ async fn main() -> Result<()> {
|
||||||
let (irc_tx, irc_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (irc_tx, irc_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
engine.lock().await.set_irc_out(irc_tx);
|
engine.lock().await.set_irc_out(irc_tx);
|
||||||
engine.lock().await.set_opers(cfg.opers());
|
engine.lock().await.set_opers(cfg.opers());
|
||||||
|
engine.lock().await.set_sid(cfg.server.sid.clone());
|
||||||
|
|
||||||
if let Some(gossip) = cfg.gossip.clone() {
|
if let Some(gossip) = cfg.gossip.clone() {
|
||||||
tracing::info!(peers = cfg.peer.len(), "starting gossip");
|
tracing::info!(peers = cfg.peer.len(), "starting gossip");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue