protocol: learn channel-mode arity + prefix modes (incl custom Y/ojoin) from CAPAB CHANMODES; services MODE uses the authoritative set

This commit is contained in:
Jean Chevronnet 2026-07-18 01:15:23 +00:00
parent d313ca3523
commit e6f05a3ede
No known key found for this signature in database
9 changed files with 163 additions and 26 deletions

View file

@ -192,6 +192,29 @@ impl Db {
self.live_extbans.clone().unwrap_or_default()
}
/// Record the ircd's live channel-mode set (its CAPAB CHANMODES burst).
pub fn set_live_chanmodes(&mut self, modes: Vec<echo_api::ChanModeCap>) {
self.live_chanmodes = Some(modes);
}
/// Whether channel mode `m` takes a parameter in the given direction — from
/// the ircd's advertised set if known, else the static fallback.
pub fn chanmode_takes_param(&self, m: char, adding: bool) -> bool {
match self.live_chanmodes.as_ref().and_then(|v| v.iter().find(|c| c.letter == m)) {
Some(cap) => cap.takes_param(adding),
None => echo_api::chanmode_takes_param(m, adding),
}
}
/// The channel prefix (status) mode letters the ircd advertises (incl custom
/// ones like ojoin's `Y`), or the static fallback until we've linked.
pub fn status_modes(&self) -> String {
match &self.live_chanmodes {
Some(v) => v.iter().filter(|c| c.is_prefix()).map(|c| c.letter).collect(),
None => echo_api::STATUS_MODES.to_string(),
}
}
/// Resolve an extban token (name, case-insensitive; or single letter, case-
/// sensitive) against the ircd's live set. For extbans echo's static table lacks.
pub fn extban_lookup(&self, token: &str) -> Option<echo_api::ExtbanCap> {

View file

@ -987,6 +987,9 @@ pub struct Db {
// The ircd's live extban set from its CAPAB EXTBANS burst. None until we link;
// once known, it's authoritative for what AKICK/MODE may set.
live_extbans: Option<Vec<echo_api::ExtbanCap>>,
// The ircd's live channel-mode set from its CAPAB CHANMODES burst (param arity
// + prefix modes, incl custom ones like ojoin's Y). None until we link.
live_chanmodes: Option<Vec<echo_api::ChanModeCap>>,
// PBKDF2 cost baked into new SCRAM verifiers; lowered by tests.
pub(crate) scram_iterations: u32,
// Whether outbound email is configured, so email features can gate themselves.
@ -1087,7 +1090,7 @@ impl Db {
apply(&mut accounts, &mut channels, &mut grouped, &mut bots, &mut host_cfg, &mut net, event);
}
tracing::info!(accounts = accounts.len(), channels = channels.len(), "account store loaded");
Self { accounts, channels, grouped, log, extban_enabled: None, live_extbans: None, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), email_logo: String::new(), codes: HashMap::new(), auth_fails: HashMap::new(), vhost_req_times: HashMap::new(), report_times: HashMap::new(), bots, host_cfg, net, ignores: Vec::new(), defcon: 5, external_accounts: false }
Self { accounts, channels, grouped, log, extban_enabled: None, live_extbans: None, live_chanmodes: None, scram_iterations: scram::DEFAULT_ITERATIONS, email_enabled: false, email_brand: "Network Services".to_string(), email_accent: "#4f46e5".to_string(), email_logo: String::new(), codes: HashMap::new(), auth_fails: HashMap::new(), vhost_req_times: HashMap::new(), report_times: HashMap::new(), bots, host_cfg, net, ignores: Vec::new(), defcon: 5, external_accounts: false }
}
/// Fold an entry authored by another node into the store — the services-side

View file

@ -66,6 +66,12 @@ impl Store for Db {
fn extbans(&self) -> Vec<echo_api::ExtbanCap> {
Db::extbans(self)
}
fn chanmode_takes_param(&self, m: char, adding: bool) -> bool {
Db::chanmode_takes_param(self, m, adding)
}
fn status_modes(&self) -> String {
Db::status_modes(self)
}
fn email_enabled(&self) -> bool {
Db::email_enabled(self)
}

View file

@ -1110,6 +1110,12 @@ impl Engine {
self.db.set_live_extbans(entries);
Vec::new()
}
NetEvent::ChanModeRegistry { modes } => {
let prefixes = modes.iter().filter(|c| c.is_prefix()).map(|c| c.letter).collect::<String>();
tracing::info!(count = modes.len(), prefixes = %prefixes, "learned ircd channel-mode set");
self.db.set_live_chanmodes(modes);
Vec::new()
}
NetEvent::UserConnect { uid, nick, host, ip } => {
let arriving_nick = nick.clone();
self.network.user_connect(uid.clone(), nick, host, ip.clone());