Keep a bot-inhabited channel alive when pruning empty channels
This commit is contained in:
parent
071380efae
commit
3c730f7add
3 changed files with 45 additions and 30 deletions
|
|
@ -448,6 +448,7 @@ impl Engine {
|
||||||
// kick the channel's own bot). Any reconcile actions are returned.
|
// kick the channel's own bot). Any reconcile actions are returned.
|
||||||
fn member_left(&mut self, channel: &str, uid: &str) -> Vec<NetAction> {
|
fn member_left(&mut self, channel: &str, uid: &str) -> Vec<NetAction> {
|
||||||
self.network.channel_part(channel, uid);
|
self.network.channel_part(channel, uid);
|
||||||
|
self.prune_channel_if_gone(channel);
|
||||||
self.forget_chatter(channel, uid);
|
self.forget_chatter(channel, uid);
|
||||||
if let Some(bot_lc) = self.bot_uids.iter().find_map(|(lc, u)| (u == uid).then(|| lc.clone())) {
|
if let Some(bot_lc) = self.bot_uids.iter().find_map(|(lc, u)| (u == uid).then(|| lc.clone())) {
|
||||||
if self.bot_channels.remove(&(bot_lc, channel.to_string())) {
|
if self.bot_channels.remove(&(bot_lc, channel.to_string())) {
|
||||||
|
|
@ -738,12 +739,30 @@ impl Engine {
|
||||||
// Forget a user who left the network (QUIT or KILL): drop their membership,
|
// Forget a user who left the network (QUIT or KILL): drop their membership,
|
||||||
// any half-finished SASL exchange, and their pending nick-protection timer.
|
// any half-finished SASL exchange, and their pending nick-protection timer.
|
||||||
fn forget_user(&mut self, uid: &str) {
|
fn forget_user(&mut self, uid: &str) {
|
||||||
|
let chans = self.network.channels_of(uid);
|
||||||
self.network.user_quit(uid);
|
self.network.user_quit(uid);
|
||||||
|
for c in &chans {
|
||||||
|
self.prune_channel_if_gone(c);
|
||||||
|
}
|
||||||
self.sasl_sessions.remove(uid);
|
self.sasl_sessions.remove(uid);
|
||||||
self.pending_enforce.retain(|p| p.uid != uid);
|
self.pending_enforce.retain(|p| p.uid != uid);
|
||||||
self.forget_chatter_everywhere(uid);
|
self.forget_chatter_everywhere(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forget a channel once it's truly gone on the ircd — no members AND no resident
|
||||||
|
// services bot (bots keep a channel alive without being tracked as members).
|
||||||
|
// Pruning it stops a stale key/mode leaking to a later channel of the same name,
|
||||||
|
// while keeping a botted/persistent channel's live state intact.
|
||||||
|
fn prune_channel_if_gone(&mut self, channel: &str) {
|
||||||
|
if self.network.channel_members(channel).next().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.bot_channels.iter().any(|(_, c)| c.eq_ignore_ascii_case(channel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.network.remove_channel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
// Log a single session out of `account`, telling them why (services-sourced).
|
// Log a single session out of `account`, telling them why (services-sourced).
|
||||||
fn logout_uid(&mut self, uid: &str, account: &str, reason: &str) {
|
fn logout_uid(&mut self, uid: &str, account: &str, reason: &str) {
|
||||||
if let Some(action) = self.feed("SESS", format!("{} logged out of \x02{account}\x02 ({reason})", self.who(uid))) {
|
if let Some(action) = self.feed("SESS", format!("{} logged out of \x02{account}\x02 ({reason})", self.who(uid))) {
|
||||||
|
|
|
||||||
|
|
@ -341,16 +341,17 @@ impl Network {
|
||||||
}
|
}
|
||||||
self.users.remove(uid);
|
self.users.remove(uid);
|
||||||
self.accounts.remove(uid);
|
self.accounts.remove(uid);
|
||||||
// Drop the membership, and forget any channel it emptied — an emptied
|
for c in self.channels.values_mut() {
|
||||||
// channel is destroyed by the ircd, so a lingering entry would both grow
|
|
||||||
// unbounded over the process lifetime and hand stale modes (e.g. a `+k`
|
|
||||||
// key) to whoever recreates the name later.
|
|
||||||
self.channels.retain(|_, c| {
|
|
||||||
c.members.remove(uid);
|
c.members.remove(uid);
|
||||||
c.ops.remove(uid);
|
c.ops.remove(uid);
|
||||||
c.voices.remove(uid);
|
c.voices.remove(uid);
|
||||||
!c.members.is_empty()
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Forget a channel outright (the engine calls this once it's confirmed truly
|
||||||
|
// empty on the ircd — no members and no resident services bot).
|
||||||
|
pub fn remove_channel(&mut self, channel: &str) {
|
||||||
|
self.channels.remove(&lc(channel));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nick_of(&self, uid: &str) -> Option<&str> {
|
pub fn nick_of(&self, uid: &str) -> Option<&str> {
|
||||||
|
|
@ -394,19 +395,10 @@ impl Network {
|
||||||
|
|
||||||
// A user left a channel (part/kick): drop membership and record last-seen.
|
// A user left a channel (part/kick): drop membership and record last-seen.
|
||||||
pub fn channel_part(&mut self, channel: &str, uid: &str) {
|
pub fn channel_part(&mut self, channel: &str, uid: &str) {
|
||||||
let key = lc(channel);
|
if let Some(c) = self.channels.get_mut(&lc(channel)) {
|
||||||
let emptied = if let Some(c) = self.channels.get_mut(&key) {
|
|
||||||
c.members.remove(uid);
|
c.members.remove(uid);
|
||||||
c.ops.remove(uid);
|
c.ops.remove(uid);
|
||||||
c.voices.remove(uid);
|
c.voices.remove(uid);
|
||||||
c.members.is_empty()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
// An emptied channel is gone on the ircd; drop it so a later channel of
|
|
||||||
// the same name can't inherit its stale modes (see user_quit).
|
|
||||||
if emptied {
|
|
||||||
self.channels.remove(&key);
|
|
||||||
}
|
}
|
||||||
if let Some(nick) = self.nick_of(uid).map(str::to_string) {
|
if let Some(nick) = self.nick_of(uid).map(str::to_string) {
|
||||||
self.record_seen(lc(&nick), Seen { nick, ts: now(), what: format!("leaving {channel}") });
|
self.record_seen(lc(&nick), Seen { nick, ts: now(), what: format!("leaving {channel}") });
|
||||||
|
|
|
||||||
|
|
@ -2495,29 +2495,33 @@
|
||||||
assert!(parted, "the assigned bot parts the expired channel");
|
assert!(parted, "the assigned bot parts the expired channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
// An emptied channel is destroyed on the ircd; echo must forget it so a later
|
// A truly-empty channel is forgotten (so a stale `+k` key can't leak to a later
|
||||||
// channel of the same name can't inherit its stale `+k` key (or membership).
|
// channel of the same name), but a channel a services bot still sits in is kept
|
||||||
|
// alive (bots aren't tracked as members) so its live key/modes survive.
|
||||||
#[test]
|
#[test]
|
||||||
fn emptied_channel_is_forgotten_so_no_stale_key_leaks() {
|
fn emptied_channel_pruned_but_botted_channel_kept() {
|
||||||
let path = std::env::temp_dir().join("echo-emptykey.jsonl");
|
let path = std::env::temp_dir().join("echo-emptykey.jsonl");
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
let mut e = Engine::new(vec![], Db::open(&path, "42S"));
|
let mut e = Engine::new(vec![], Db::open(&path, "42S"));
|
||||||
|
|
||||||
|
// Bot-less channel: once the last member leaves, it's gone (no stale key).
|
||||||
e.network.channel_join("#x", "000AAAAAB", true);
|
e.network.channel_join("#x", "000AAAAAB", true);
|
||||||
e.network.channel_join("#x", "000AAAAAC", false);
|
|
||||||
e.network.set_channel_key("#x", Some("secret".into()));
|
e.network.set_channel_key("#x", Some("secret".into()));
|
||||||
assert_eq!(e.network.channel_key("#x"), Some("secret"));
|
assert_eq!(e.network.channel_key("#x"), Some("secret"));
|
||||||
|
|
||||||
e.network.channel_part("#x", "000AAAAAB");
|
e.network.channel_part("#x", "000AAAAAB");
|
||||||
e.network.channel_part("#x", "000AAAAAC"); // now empty
|
e.prune_channel_if_gone("#x");
|
||||||
assert_eq!(e.network.channel_key("#x"), None, "stale key must not linger after the channel empties");
|
assert_eq!(e.network.channel_key("#x"), None, "empty bot-less channel is forgotten");
|
||||||
|
e.network.channel_join("#x", "000AAAAAD", true); // recreated, fresh
|
||||||
e.network.channel_join("#x", "000AAAAAD", true); // recreated, no key
|
|
||||||
assert_eq!(e.network.channel_key("#x"), None, "recreated channel starts keyless");
|
assert_eq!(e.network.channel_key("#x"), None, "recreated channel starts keyless");
|
||||||
|
|
||||||
// The quit path prunes an emptied channel too.
|
// Botted channel: emptied of humans, but the bot keeps it alive on the ircd,
|
||||||
e.network.set_channel_key("#x", Some("k2".into()));
|
// so echo must NOT drop its live key.
|
||||||
e.network.user_quit("000AAAAAD");
|
e.network.channel_join("#y", "000AAAAAC", true);
|
||||||
assert_eq!(e.network.channel_key("#x"), None, "a quit that empties a channel forgets it");
|
e.network.set_channel_key("#y", Some("hunter2".into()));
|
||||||
|
e.bot_channels.insert(("bendy".to_string(), "#y".to_string()));
|
||||||
|
e.network.channel_part("#y", "000AAAAAC");
|
||||||
|
e.prune_channel_if_gone("#y");
|
||||||
|
assert_eq!(e.network.channel_key("#y"), Some("hunter2"), "botted channel keeps its key");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The gRPC login path applies the same guards as IRC IDENTIFY: a suspended or
|
// The gRPC login path applies the same guards as IRC IDENTIFY: a suspended or
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue