From 5f3ae2123d98a8e33d078aa7a76749d6c0dd8e79 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 07:25:42 +0000 Subject: [PATCH] ircv3_metadata_db: persist channel metadata across restart --- .gitignore | 1 + src/ircd.rs | 1 + src/modules/metadata.rs | 47 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/.gitignore b/.gitignore index c590f7b..c6a63bc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ Cargo.lock # persisted x-line db (runtime state) *.xlines +*.metadata diff --git a/src/ircd.rs b/src/ircd.rs index 1a9c353..18b6a2c 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -73,6 +73,7 @@ impl Ircd { ) -> Ircd { let mut server = Server::new(cfg, event_tx, conn_counter); server.load_xlines(); // restore persisted bans (m_xline_db) + crate::modules::metadata::load(&mut server); // restore channel metadata (m_metadata_db) Ircd { server, commands: command_table(), diff --git a/src/modules/metadata.rs b/src/modules/metadata.rs index 7fe304a..79b3680 100644 --- a/src/modules/metadata.rs +++ b/src/modules/metadata.rs @@ -160,6 +160,9 @@ impl Command for MetadataCmd { } } } + if key.starts_with('#') { + save(s); // persist channel metadata (m_ircv3_metadata_db) + } let setter = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); let note = match &value { Some(v) => format!(":{setter} METADATA {disp} {mkey} * :{v}"), @@ -192,6 +195,9 @@ impl Command for MetadataCmd { if let Some(st) = s.ext.get_mut::() { st.0.remove(&key); } + if key.starts_with('#') { + save(s); + } } "SUB" | "UNSUB" => {} // all metadata is public here; subscriptions are a no-op _ => { @@ -202,3 +208,44 @@ impl Command for MetadataCmd { CmdResult::Ok } } + +/// Where channel metadata is persisted (beside the config). +fn db_path(s: &Server) -> String { + format!("{}.metadata", s.conf_path) +} + +/// Persist channel metadata (the `#`-keyed entries) so it survives a restart — +/// InspIRCd `m_ircv3_metadata_db`. Per-user metadata (`u`) is intentionally +/// not saved: uids don't persist across restarts. +pub fn save(s: &Server) { + let mut out = String::new(); + if let Some(st) = s.ext.get::() { + for (key, kv) in &st.0 { + if !key.starts_with('#') { + continue; + } + for (mk, v) in kv { + out.push_str(&format!("{key} {mk} {v}\n")); + } + } + } + let _ = std::fs::write(db_path(s), out); +} + +/// Reload persisted channel metadata at startup. +pub fn load(s: &mut Server) { + let Ok(text) = std::fs::read_to_string(db_path(s)) else { + return; + }; + let store = s.ext.get_or_insert_with::(MetaStore::default); + for line in text.lines() { + let mut it = line.splitn(3, ' '); + if let (Some(key), Some(mk), Some(v)) = (it.next(), it.next(), it.next()) { + store + .0 + .entry(key.to_string()) + .or_default() + .insert(mk.to_string(), v.to_string()); + } + } +}