engine: a takeover keeps the account's founded channels (the account still exists) — only a genuine drop or expiry releases them
Some checks failed
CI / check (push) Failing after 3m56s
Some checks failed
CI / check (push) Failing after 3m56s
This commit is contained in:
parent
426fc694a2
commit
47dfa76fd5
3 changed files with 29 additions and 23 deletions
|
|
@ -574,11 +574,13 @@ impl Engine {
|
||||||
// relied on (lost a conflict, or dropped elsewhere), clean up after it.
|
// relied on (lost a conflict, or dropped elsewhere), clean up after it.
|
||||||
let gone = match self.db.ingest(entry)? {
|
let gone = match self.db.ingest(entry)? {
|
||||||
Some(db::IngestEffect::TakenOver(a)) => {
|
Some(db::IngestEffect::TakenOver(a)) => {
|
||||||
self.handle_account_gone(&a, "collided with another network and no longer belongs to you");
|
// The account still exists (its home moved) — end local sessions
|
||||||
true
|
// using the old credential, but keep the channels it founded.
|
||||||
|
self.handle_account_gone(&a, "collided with another network and no longer belongs to you", false);
|
||||||
|
false
|
||||||
}
|
}
|
||||||
Some(db::IngestEffect::Dropped(a)) => {
|
Some(db::IngestEffect::Dropped(a)) => {
|
||||||
self.handle_account_gone(&a, "was dropped");
|
self.handle_account_gone(&a, "was dropped", true);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Some(db::IngestEffect::Suspended(a)) => {
|
Some(db::IngestEffect::Suspended(a)) => {
|
||||||
|
|
@ -610,13 +612,19 @@ impl Engine {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// An account a local session was using is gone (lost a conflict, or dropped on
|
// An account a local session was using is gone or moved. Log out those
|
||||||
// another node). Log out those sessions (their credential is gone) and drop the
|
// sessions (their credential may have changed). Only release the channels it
|
||||||
// channels it founded locally — their founder identity no longer exists here.
|
// founded when `release_channels` — i.e. the account is genuinely gone (dropped
|
||||||
fn handle_account_gone(&mut self, account: &str, reason: &str) {
|
// or expired). A takeover leaves the account in place (its home just moved), so
|
||||||
|
// its channels keep a valid founder by name and must NOT be dropped.
|
||||||
|
fn handle_account_gone(&mut self, account: &str, reason: &str, release_channels: bool) {
|
||||||
let victims = self.network.uids_logged_into(account);
|
let victims = self.network.uids_logged_into(account);
|
||||||
// A channel with a valid successor is inherited rather than released.
|
// A channel with a valid successor is inherited rather than released.
|
||||||
let (inherited, orphaned) = self.db.release_founded_channels(account);
|
let (inherited, orphaned) = if release_channels {
|
||||||
|
self.db.release_founded_channels(account)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
let (cs, ns) = (self.chan_service.clone(), self.nick_service.clone());
|
let (cs, ns) = (self.chan_service.clone(), self.nick_service.clone());
|
||||||
// Release the registered mode on each dropped (not inherited) channel.
|
// Release the registered mode on each dropped (not inherited) channel.
|
||||||
for chan in &orphaned {
|
for chan in &orphaned {
|
||||||
|
|
@ -726,7 +734,7 @@ impl Engine {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if self.db.drop_account(&account).unwrap_or(false) {
|
if self.db.drop_account(&account).unwrap_or(false) {
|
||||||
self.handle_account_gone(&account, "expired after a long period of inactivity");
|
self.handle_account_gone(&account, "expired after a long period of inactivity", true);
|
||||||
self.audit(format!("Account \x02{account}\x02 expired after inactivity."));
|
self.audit(format!("Account \x02{account}\x02 expired after inactivity."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ impl Engine {
|
||||||
pub fn authority_drop(&mut self, account: &str) -> AuthorityStatus {
|
pub fn authority_drop(&mut self, account: &str) -> AuthorityStatus {
|
||||||
match self.db.drop_account(account) {
|
match self.db.drop_account(account) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
self.handle_account_gone(account, "was dropped");
|
self.handle_account_gone(account, "was dropped", true);
|
||||||
AuthorityStatus::Ok
|
AuthorityStatus::Ok
|
||||||
}
|
}
|
||||||
Ok(false) => AuthorityStatus::NotFound,
|
Ok(false) => AuthorityStatus::NotFound,
|
||||||
|
|
|
||||||
|
|
@ -475,7 +475,7 @@
|
||||||
// account founded — and a bot assigned to one must part, not linger, even
|
// account founded — and a bot assigned to one must part, not linger, even
|
||||||
// though this cleanup runs outside the dispatch path.
|
// though this cleanup runs outside the dispatch path.
|
||||||
#[test]
|
#[test]
|
||||||
fn gossip_takeover_parts_orphaned_channel_bot() {
|
fn gossip_account_drop_parts_orphaned_channel_bot() {
|
||||||
use echo_botserv::BotServ;
|
use echo_botserv::BotServ;
|
||||||
use echo_nickserv::NickServ;
|
use echo_nickserv::NickServ;
|
||||||
let path = std::env::temp_dir().join("echo-gossipbot.jsonl");
|
let path = std::env::temp_dir().join("echo-gossipbot.jsonl");
|
||||||
|
|
@ -501,16 +501,13 @@
|
||||||
let out = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAD".into(), text: "ASSIGN #hers Bendy".into() });
|
let out = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAD".into(), text: "ASSIGN #hers Bendy".into() });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ServiceJoin { channel, .. } if channel == "#hers")), "bot joins the channel");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ServiceJoin { channel, .. } if channel == "#hers")), "bot joins the channel");
|
||||||
|
|
||||||
// An earlier remote claim on alice arrives by gossip and wins the name.
|
// alice is dropped on another node; the drop arrives by gossip and, being a
|
||||||
|
// genuine removal, releases her founderless channel.
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
e.set_irc_out(tx);
|
e.set_irc_out(tx);
|
||||||
let winner = db::Account {
|
e.gossip_ingest(LogEntry::for_test("peer", 1, 1, db::Event::AccountDropped { account: "alice".into() })).unwrap();
|
||||||
name: "alice".into(), email: None,
|
|
||||||
ts: 0, home: "peer".into(), scram256: None, scram512: None, certfps: vec![], verified: true, ajoin: vec![], suspension: None, memos: vec![], memo_ignore: vec![], memo_notify: true, memo_limit: None, greet: String::new(), no_autoop: false, no_protect: false, hide_status: false, vhost: None, vhost_request: None, last_seen: 0, noexpire: false, expiry_warned: false, oper_note: None,
|
|
||||||
};
|
|
||||||
e.gossip_ingest(LogEntry::for_test("peer", 0, 1, db::Event::AccountRegistered(Box::new(winner)))).unwrap();
|
|
||||||
|
|
||||||
assert!(e.db.channel("#hers").is_none(), "orphaned channel released");
|
assert!(e.db.channel("#hers").is_none(), "the dropped account's channel is released");
|
||||||
let parted = std::iter::from_fn(|| rx.try_recv().ok())
|
let parted = std::iter::from_fn(|| rx.try_recv().ok())
|
||||||
.any(|a| matches!(a, NetAction::ServicePart { channel, .. } if channel == "#hers"));
|
.any(|a| matches!(a, NetAction::ServicePart { channel, .. } if channel == "#hers"));
|
||||||
assert!(parted, "the assigned bot parts the released channel");
|
assert!(parted, "the assigned bot parts the released channel");
|
||||||
|
|
@ -858,10 +855,11 @@
|
||||||
let entry = LogEntry::for_test("peer", 0, 1, db::Event::AccountRegistered(Box::new(winner)));
|
let entry = LogEntry::for_test("peer", 0, 1, db::Event::AccountRegistered(Box::new(winner)));
|
||||||
e.gossip_ingest(entry).unwrap();
|
e.gossip_ingest(entry).unwrap();
|
||||||
|
|
||||||
// The local session is logged out and its orphaned channel is dropped.
|
// The local session is logged out (its credential moved to the winner), but
|
||||||
|
// the channel it founded is KEPT — the account still exists, home just moved.
|
||||||
assert!(e.network.account_of("000AAAAAB").is_none(), "session cleared after losing the name");
|
assert!(e.network.account_of("000AAAAAB").is_none(), "session cleared after losing the name");
|
||||||
assert!(e.db.channel("#hers").is_none(), "orphaned channel dropped");
|
assert!(e.db.channel("#hers").is_some(), "the channel is kept on a takeover (the account still exists)");
|
||||||
// The outbound path got a logout, a notice, a -r on the channel, and a release notice.
|
// The outbound path got a logout and a notice, but NO -r and NO release notice.
|
||||||
let (mut logout, mut notice, mut unreg, mut released) = (false, false, false, false);
|
let (mut logout, mut notice, mut unreg, mut released) = (false, false, false, false);
|
||||||
while let Ok(a) = rx.try_recv() {
|
while let Ok(a) = rx.try_recv() {
|
||||||
match a {
|
match a {
|
||||||
|
|
@ -874,8 +872,8 @@
|
||||||
}
|
}
|
||||||
assert!(logout, "a logout must be pushed to the uplink");
|
assert!(logout, "a logout must be pushed to the uplink");
|
||||||
assert!(notice, "the user must be told why");
|
assert!(notice, "the user must be told why");
|
||||||
assert!(unreg, "the orphaned channel must be de-registered (-r)");
|
assert!(!unreg, "a takeover must NOT de-register the channel");
|
||||||
assert!(released, "the user must be told which channels were released");
|
assert!(!released, "no channels are released on a takeover");
|
||||||
}
|
}
|
||||||
|
|
||||||
// NickServ INFO shows registration details (email only to the owner); ALIST
|
// NickServ INFO shows registration details (email only to the owner); ALIST
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue