migrate: carry channel SET flags, noexpire, and account hide-status
All checks were successful
CI / check (push) Successful in 3m45s

Completes the importer: Anope's per-channel bool flags (KEEPTOPIC, PEACE,
SIGNKICK, BS_GREET, and defensively SECUREOPS/RESTRICTED/PRIVATE/TOPICLOCK/
NOAUTOOP/NOBOT) now map to ChanSettings, CS_NO_EXPIRE to channel noexpire, and
NickCore HIDE_MASK to the account's hide-status. PERSIST/SECUREFOUNDER/
KEEP_MODES/FANTASY have no Echo equivalent and are skipped. Verified: #devs
carries its full flag set + noexpire off the real backup.
This commit is contained in:
Jean Chevronnet 2026-07-16 15:03:54 +00:00
parent 062eae703e
commit 3f97a66dbb
No known key found for this signature in database
2 changed files with 34 additions and 7 deletions

View file

@ -52,8 +52,8 @@ async fn main() -> Result<()> {
let node = argv.get(4).map(String::as_str).unwrap_or("services"); let node = argv.get(4).map(String::as_str).unwrap_or("services");
let s = migrate::import_anope(src, out, node)?; let s = migrate::import_anope(src, out, node)?;
println!( println!(
"imported: {} accounts, {} grouped nicks, {} certs, {} vhosts, {} channels, {} access, {} mlocked, {} bots, {} memos", "imported: {} accounts, {} grouped nicks, {} certs, {} vhosts, {} channels ({} with settings), {} access, {} mlocked, {} bots, {} memos",
s.accounts, s.grouped_nicks, s.certs, s.vhosts, s.channels, s.access, s.mlocked, s.bots, s.memos s.accounts, s.grouped_nicks, s.certs, s.vhosts, s.channels, s.settings, s.access, s.mlocked, s.bots, s.memos
); );
for note in &s.skipped { for note in &s.skipped {
println!(" skipped: {note}"); println!(" skipped: {note}");

View file

@ -5,7 +5,7 @@
// that can't become SCRAM verifiers, so migrated accounts authenticate by certfp // that can't become SCRAM verifiers, so migrated accounts authenticate by certfp
// or an external authority (or must reset their password). // or an external authority (or must reset their password).
use crate::engine::db::{Account, Bot, Db, Event, Vhost}; use crate::engine::db::{Account, Bot, ChanSettings, Db, Event, Vhost};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
@ -16,6 +16,7 @@ pub struct Summary {
pub certs: usize, pub certs: usize,
pub vhosts: usize, pub vhosts: usize,
pub channels: usize, pub channels: usize,
pub settings: usize,
pub access: usize, pub access: usize,
pub mlocked: usize, pub mlocked: usize,
pub bots: usize, pub bots: usize,
@ -164,7 +165,7 @@ pub fn import_anope(anope_path: &str, out_path: &str, node: &str) -> std::io::Re
greet: String::new(), greet: String::new(),
no_autoop: !flag(nc, "AUTOOP"), no_autoop: !flag(nc, "AUTOOP"),
no_protect: false, no_protect: false,
hide_status: false, hide_status: flag(nc, "HIDE_MASK"),
vhost, vhost,
vhost_request: None, vhost_request: None,
last_seen: last_seen_of.get(name).copied().unwrap_or(ts), last_seen: last_seen_of.get(name).copied().unwrap_or(ts),
@ -213,6 +214,28 @@ pub fn import_anope(anope_path: &str, out_path: &str, node: &str) -> std::io::Re
db.migrate_append(Event::ChannelBotAssigned { channel: name.to_string(), bot: bot.to_string() })?; db.migrate_append(Event::ChannelBotAssigned { channel: name.to_string(), bot: bot.to_string() })?;
} }
} }
// SET flags (Anope stores each as a present-and-true bool). PERSIST,
// SECUREFOUNDER, KEEP_MODES and FANTASY have no Echo equivalent — skipped.
let any = |names: &[&str]| names.iter().any(|n| flag(ci, n));
let settings = ChanSettings {
signkick: any(&["SIGNKICK"]),
private: any(&["PRIVATE", "CS_PRIVATE"]),
peace: any(&["PEACE"]),
secureops: any(&["SECUREOPS", "CS_SECUREOPS"]),
restricted: any(&["RESTRICTED", "CS_RESTRICTED"]),
noautoop: any(&["NOAUTOOP", "CS_NOAUTOOP"]),
keeptopic: any(&["KEEPTOPIC"]),
topiclock: any(&["TOPICLOCK", "CS_TOPICLOCK"]),
bot_greet: any(&["BS_GREET"]),
nobot: any(&["BS_NOBOT", "NOBOT"]),
};
if settings != ChanSettings::default() {
db.migrate_append(Event::ChannelSettingsSet { channel: name.to_string(), settings })?;
sum.settings += 1;
}
if flag(ci, "CS_NO_EXPIRE") {
db.migrate_append(Event::ChannelNoExpire { channel: name.to_string(), on: true })?;
}
sum.channels += 1; sum.channels += 1;
} }
@ -297,10 +320,10 @@ mod tests {
#[test] #[test]
fn imports_a_minimal_anope_db() { fn imports_a_minimal_anope_db() {
let anope = r##"{"data":{ let anope = r##"{"data":{
"NickCore":[{"uniqueid":100,"display":"alice","email":"a@x.io","registered":1000,"AUTOOP":true,"memomax":20}], "NickCore":[{"uniqueid":100,"display":"alice","email":"a@x.io","registered":1000,"AUTOOP":true,"memomax":20,"HIDE_MASK":true}],
"NickAlias":[{"nick":"alice","ncid":100,"last_seen":2000,"vhost_host":"cool.host","vhost_ident":"al","vhost_creator":"alice","vhost_time":1500}], "NickAlias":[{"nick":"alice","ncid":100,"last_seen":2000,"vhost_host":"cool.host","vhost_ident":"al","vhost_creator":"alice","vhost_time":1500}],
"NSCert":[{"fingerprint":"AABBCC","account":100}], "NSCert":[{"fingerprint":"AABBCC","account":100}],
"ChannelInfo":[{"name":"#chan","founderid":100,"registered":1100,"description":"a place","levels":"AUTOOP 5 AUTOVOICE 3","bi":"Botty"}], "ChannelInfo":[{"name":"#chan","founderid":100,"registered":1100,"description":"a place","levels":"AUTOOP 5 AUTOVOICE 3","bi":"Botty","KEEPTOPIC":true,"PEACE":true,"CS_NO_EXPIRE":true,"PERSIST":true}],
"ChanAccess":[{"ci":"#chan","mask":"bob","data":"5"},{"ci":"#chan","mask":"carol","data":"3"}], "ChanAccess":[{"ci":"#chan","mask":"bob","data":"5"},{"ci":"#chan","mask":"carol","data":"3"}],
"ModeLock":[{"ci":"#chan","name":"NOEXTERNAL","set":true},{"ci":"#chan","name":"TOPIC","set":true},{"ci":"#chan","name":"FLOOD","set":true,"param":"5:10"}], "ModeLock":[{"ci":"#chan","name":"NOEXTERNAL","set":true},{"ci":"#chan","name":"TOPIC","set":true},{"ci":"#chan","name":"FLOOD","set":true,"param":"5:10"}],
"BotInfo":[{"nick":"Botty","user":"b","host":"h","realname":"Bot"},{"nick":"NickServ","user":"n","host":"h","realname":"svc"}], "BotInfo":[{"nick":"Botty","user":"b","host":"h","realname":"Bot"},{"nick":"NickServ","user":"n","host":"h","realname":"svc"}],
@ -312,7 +335,7 @@ mod tests {
std::fs::write(&src, anope).unwrap(); std::fs::write(&src, anope).unwrap();
let s = import_anope(src.to_str().unwrap(), out.to_str().unwrap(), "n1").unwrap(); let s = import_anope(src.to_str().unwrap(), out.to_str().unwrap(), "n1").unwrap();
assert_eq!((s.accounts, s.certs, s.vhosts, s.channels, s.access, s.mlocked, s.bots, s.memos), (1, 1, 1, 1, 2, 1, 1, 1)); assert_eq!((s.accounts, s.certs, s.vhosts, s.channels, s.settings, s.access, s.mlocked, s.bots, s.memos), (1, 1, 1, 1, 1, 2, 1, 1, 1));
assert_eq!((s.loaded_accounts, s.loaded_channels, s.loaded_bots), (1, 1, 1), "produced log replays cleanly"); assert_eq!((s.loaded_accounts, s.loaded_channels, s.loaded_bots), (1, 1, 1), "produced log replays cleanly");
let db = Db::open(out.to_str().unwrap().to_string(), "n1"); let db = Db::open(out.to_str().unwrap().to_string(), "n1");
@ -320,6 +343,7 @@ mod tests {
assert_eq!(acct.email.as_deref(), Some("a@x.io")); assert_eq!(acct.email.as_deref(), Some("a@x.io"));
assert!(acct.scram256.is_none(), "no password migrated (one-way hash)"); assert!(acct.scram256.is_none(), "no password migrated (one-way hash)");
assert!(!acct.no_autoop, "AUTOOP=true keeps auto-op on"); assert!(!acct.no_autoop, "AUTOOP=true keeps auto-op on");
assert!(acct.hide_status, "HIDE_MASK -> hide status");
assert_eq!(acct.certfps, vec!["aabbcc".to_string()]); assert_eq!(acct.certfps, vec!["aabbcc".to_string()]);
assert!(acct.vhost.is_some(), "vhost carried from the nick alias"); assert!(acct.vhost.is_some(), "vhost carried from the nick alias");
let chan = db.channel("#chan").expect("channel migrated"); let chan = db.channel("#chan").expect("channel migrated");
@ -327,6 +351,9 @@ mod tests {
assert_eq!(chan.desc, "a place"); assert_eq!(chan.desc, "a place");
assert_eq!(chan.lock_on, "nt", "param-less mlocks kept, FLOOD skipped"); assert_eq!(chan.lock_on, "nt", "param-less mlocks kept, FLOOD skipped");
assert_eq!(chan.assigned_bot.as_deref(), Some("Botty")); assert_eq!(chan.assigned_bot.as_deref(), Some("Botty"));
assert!(chan.settings.keeptopic && chan.settings.peace, "channel SET flags migrated");
assert!(!chan.settings.signkick, "unset flags stay default (PERSIST has no equivalent, ignored)");
assert!(chan.noexpire, "CS_NO_EXPIRE -> channel noexpire");
let mut roles: Vec<_> = chan.access.iter().map(|a| (a.account.as_str(), a.level.as_str())).collect(); let mut roles: Vec<_> = chan.access.iter().map(|a| (a.account.as_str(), a.level.as_str())).collect();
roles.sort(); roles.sort();
assert_eq!(roles, vec![("bob", "op"), ("carol", "voice")], "levels mapped by threshold"); assert_eq!(roles, vec![("bob", "op"), ("carol", "voice")], "levels mapped by threshold");