From a70ee42aeeed5894a6776963658a581db164591f Mon Sep 17 00:00:00 2001 From: reverse Date: Sat, 8 Aug 2026 21:41:54 +0000 Subject: [PATCH] markread (draft/read-marker, identity-shared) + advertise UTF8ONLY --- src/coremods/core_message.rs | 51 ++++++++++++++++++++++++++++++++++++ src/server.rs | 17 ++++++++++-- src/users.rs | 2 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index aae9f3d..1f4a934 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -125,9 +125,60 @@ pub fn commands() -> Vec> { Box::new(TagMsg), Box::new(ChatHistory), Box::new(Redact), + Box::new(MarkRead), ] } +/// 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/server.rs b/src/server.rs index 9b67f29..9b4c154 100644 --- a/src/server.rs +++ b/src/server.rs @@ -150,8 +150,9 @@ pub struct Server { // output primitives are `&self`. pub label_capture: RefCell)>>, pub history: HashMap>, // channel key -> recent messages (CHATHISTORY) - pub event_tx: Sender, // self-inject events (DNS results) - pub conn_counter: Arc, // mints connection uids (for CONNECT dials) + pub read_markers: HashMap>, // identity -> target -> read ts (MARKREAD) + pub event_tx: Sender, // self-inject events (DNS results) + pub conn_counter: Arc, // mints connection uids (for CONNECT dials) } impl Server { @@ -194,6 +195,7 @@ impl Server { webirc: cfg.webirc, label_capture: RefCell::new(None), history: HashMap::new(), + read_markers: HashMap::new(), event_tx, conn_counter, } @@ -248,6 +250,16 @@ impl Server { } } + /// 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( @@ -425,6 +437,7 @@ impl Server { return; }; self.uuid_local.remove(&user.uuid); + self.read_markers.remove(&format!("~{uid}")); // session read-markers (kept if account-keyed) if user.registered { self.push_whowas( &user.nick, diff --git a/src/users.rs b/src/users.rs index 4435840..b976dbe 100644 --- a/src/users.rs +++ b/src/users.rs @@ -400,7 +400,7 @@ impl Server { uid, RPL_ISUPPORT, &format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFL,CGMNORSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", + "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFL,CGMNORSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server", self.network ), );