From 768435d8101690e4d170533837cca704297d354c Mon Sep 17 00:00:00 2001 From: reverse Date: Sat, 8 Aug 2026 22:47:10 +0000 Subject: [PATCH] move markread into its own module file --- src/coremods/core_message.rs | 51 ----------------- src/modules/markread.rs | 103 +++++++++++++++++++++++++++++++++++ src/modules/mod.rs | 3 + src/server.rs | 13 ----- 4 files changed, 106 insertions(+), 64 deletions(-) create mode 100644 src/modules/markread.rs diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index 56ed918..a3edab5 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -125,7 +125,6 @@ pub fn commands() -> Vec> { Box::new(TagMsg), Box::new(ChatHistory), Box::new(Redact), - Box::new(MarkRead), Box::new(Batch), ] } @@ -160,56 +159,6 @@ impl Command for Batch { } } -/// MARKREAD — draft/read-marker. `MARKREAD [timestamp=]`. With a -/// timestamp it sets the read marker (only ever advancing) and echoes it to every -/// connection sharing the user's identity (multi-device); without one it returns -/// the stored marker (`*` if unset). -struct MarkRead; -impl Command for MarkRead { - fn name(&self) -> &'static str { - "MARKREAD" - } - fn min_params(&self) -> usize { - 1 - } - fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { - let target = params[0].clone(); - let tkey = target.to_ascii_lowercase(); - let id = s.marker_id(uid); - match params.get(1).and_then(|p| p.strip_prefix("timestamp=")) { - Some(ts_str) => { - let cur = s - .read_markers - .get(&id) - .and_then(|m| m.get(&tkey)) - .copied() - .unwrap_or(0); - let ts = parse_iso(ts_str).unwrap_or(0).max(cur); // markers only advance - s.read_markers - .entry(id.clone()) - .or_default() - .insert(tkey, ts); - let line = format!(":{} MARKREAD {target} timestamp={}", s.name, iso_time(ts)); - let uids: Vec = s.users.keys().copied().collect(); - for p in uids.into_iter().filter(|&p| s.marker_id(p) == id) { - s.send(p, line.clone()); - } - } - None => { - let val = s - .read_markers - .get(&id) - .and_then(|m| m.get(&tkey)) - .copied() - .map(|t| format!("timestamp={}", iso_time(t))) - .unwrap_or_else(|| "*".to_string()); - s.send(uid, format!(":{} MARKREAD {target} {val}", s.name)); - } - } - CmdResult::Ok - } -} - /// REDACT — delete a previously-sent channel message (draft/message-redaction). /// `REDACT <#chan> [:reason]`. Allowed for the message's author, a channel /// half-op-or-above, or an oper. Relayed to channel members who enabled the cap, diff --git a/src/modules/markread.rs b/src/modules/markread.rs new file mode 100644 index 0000000..88e0145 --- /dev/null +++ b/src/modules/markread.rs @@ -0,0 +1,103 @@ +//! markread — InspIRCd's `m_ircv3_read_marker` (draft/read-marker). A client sets +//! or queries the "last read" timestamp per conversation; markers are keyed by +//! account when logged in (so they're shared across a user's devices and survive +//! reconnects) and echoed to every connection sharing that identity. Self-contained: +//! the marker store lives in `Server.ext`, cleaned up by the on_user_quit hook. + +use std::collections::HashMap; + +use crate::command::{CmdResult, Command}; +use crate::module::Module; +use crate::server::{iso_time, parse_iso, Server}; +use crate::Uid; + +/// identity -> target (lowercased) -> read timestamp. Stored in `Server.ext`. +#[derive(Default)] +pub struct ReadMarkers(pub HashMap>); + +/// The read-marker identity for `uid`: their account when logged in (so markers +/// are shared across their devices and survive reconnects), else a per-session +/// key. The on_user_quit hook prunes the session key on disconnect. +pub fn marker_id(s: &Server, uid: Uid) -> String { + s.users + .get(&uid) + .and_then(|u| u.account.clone()) + .unwrap_or_else(|| format!("~{uid}")) +} + +/// Cleanup hook: drop a user's session markers on disconnect (account-keyed +/// markers are intentionally kept so they persist across reconnects). +pub struct MarkRead; +impl Module for MarkRead { + fn name(&self) -> &'static str { + "markread" + } + fn on_user_quit(&mut self, s: &mut Server, uid: Uid, _reason: &str) { + if let Some(m) = s.ext.get_mut::() { + m.0.remove(&format!("~{uid}")); + } + } +} + +pub fn commands() -> Vec> { + vec![Box::new(MarkReadCmd)] +} + +/// MARKREAD — `MARKREAD [timestamp=]`. With a timestamp it sets the +/// read marker (only ever advancing) and echoes it to every connection sharing the +/// user's identity (multi-device); without one it returns the stored marker (`*` if +/// unset). +struct MarkReadCmd; +impl Command for MarkReadCmd { + fn name(&self) -> &'static str { + "MARKREAD" + } + fn min_params(&self) -> usize { + 1 + } + fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { + let target = params[0].clone(); + let tkey = target.to_ascii_lowercase(); + let id = marker_id(s, uid); + match params.get(1).and_then(|p| p.strip_prefix("timestamp=")) { + Some(ts_str) => { + let cur = s + .ext + .get::() + .and_then(|m| m.0.get(&id)) + .and_then(|m| m.get(&tkey)) + .copied() + .unwrap_or(0); + let ts = parse_iso(ts_str).unwrap_or(0).max(cur); // markers only advance + s.ext + .get_or_insert_with::(ReadMarkers::default) + .0 + .entry(id.clone()) + .or_default() + .insert(tkey, ts); + let line = format!(":{} MARKREAD {target} timestamp={}", s.name, iso_time(ts)); + let recips: Vec = s + .users + .keys() + .copied() + .filter(|&p| marker_id(s, p) == id) + .collect(); + for p in recips { + s.send(p, line.clone()); + } + } + None => { + let val = s + .ext + .get::() + .and_then(|m| m.0.get(&id)) + .and_then(|m| m.get(&tkey)) + .copied() + .map(|t| format!("timestamp={}", iso_time(t))) + .unwrap_or_else(|| "*".to_string()); + s.send(uid, format!(":{} MARKREAD {target} {val}", s.name)); + } + } + CmdResult::Ok + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 5fbe4b2..271e973 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -8,6 +8,7 @@ pub mod cloak; pub mod dnsbl; pub mod filter; pub mod flood; +pub mod markread; pub mod metadata; pub mod snoop; @@ -23,6 +24,7 @@ pub fn default_modules() -> Vec> { Box::new(antimixedutf8::AntiMixedUtf8), Box::new(filter::Filter), Box::new(metadata::Metadata), + Box::new(markread::MarkRead), ] } @@ -32,5 +34,6 @@ pub fn module_commands() -> Vec> { filter::commands() .into_iter() .chain(metadata::commands()) + .chain(markread::commands()) .collect() } diff --git a/src/server.rs b/src/server.rs index 02e3de3..dea798d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -164,7 +164,6 @@ pub struct Server { // output primitives are `&self`. pub label_capture: RefCell)>>, pub history: HashMap>, // channel key -> recent messages (CHATHISTORY) - pub read_markers: HashMap>, // identity -> target -> read ts (MARKREAD) pub mline: HashMap, // in-progress inbound multiline batches pub event_tx: Sender, // self-inject events (DNS results) pub conn_counter: Arc, // mints connection uids (for CONNECT dials) @@ -214,7 +213,6 @@ impl Server { webirc: cfg.webirc, label_capture: RefCell::new(None), history: HashMap::new(), - read_markers: HashMap::new(), mline: HashMap::new(), event_tx, conn_counter, @@ -329,16 +327,6 @@ impl Server { Some((mb.target, mb.notice, lines)) } - /// The read-marker identity for `uid`: their account when logged in (so markers - /// are shared across their devices and survive reconnects), else a per-session - /// key. `remove_user` prunes the session key on disconnect. - pub fn marker_id(&self, uid: Uid) -> String { - self.users - .get(&uid) - .and_then(|u| u.account.clone()) - .unwrap_or_else(|| format!("~{uid}")) - } - // --- connection lifecycle ------------------------------------------------ pub fn add_conn( @@ -516,7 +504,6 @@ impl Server { return; }; self.uuid_local.remove(&user.uuid); - self.read_markers.remove(&format!("~{uid}")); // session read-markers (kept if account-keyed) self.mline.remove(&uid); // any half-open multiline batch if user.registered { self.push_whowas(