From 853be58d18ed0f848f8090b41ce69d68a1ad8d21 Mon Sep 17 00:00:00 2001 From: reverse Date: Tue, 18 Aug 2026 22:37:50 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20channel=20PRIVMSG/NOTICE=20fanout=20bui?= =?UTF-8?q?lds=20at=20most=20one=20line=20per=20capability=20profile=20(se?= =?UTF-8?q?rver-time/account-tag/message-tags)=20and=20shares=20it=20by=20?= =?UTF-8?q?Arc,=20instead=20of=20formatting=20a=20String=20per=20member=20?= =?UTF-8?q?=E2=80=94=20a=20big=20channel=20now=20allocates=20<=3D8=20lines?= =?UTF-8?q?,=20not=20N?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/coremods/core_message.rs | 18 +----- src/server.rs | 104 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 15 deletions(-) diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index 3b59503..00f200b 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -463,21 +463,9 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) if !op_only { record(s, &key, &prefix, cmd, target, &body, &msgid); // for CHATHISTORY } - let members: Vec = s - .channels - .get(&key) - .map(|c| c.members.keys().copied().collect()) - .unwrap_or_default(); - for m in members { - if m == uid || s.users.get(&m).map(|u| u.flags.deaf).unwrap_or(false) { - continue; - } - // +U: an unprivileged sender's message reaches ops (half-op+) only - if op_only && s.rank(m, &key) < RANK_HALFOP { - continue; - } - s.send_tagged(m, uid, &ctags, &msgid, &line); - } + // fan out to members: one shared line per capability profile, +D deaf and + // (+U) op-only filtering applied inside. The sender's own copy is separate. + s.to_channel_tagged(&key, uid, &ctags, &msgid, &line, op_only); // echo-message: give the sender their own copy if they asked for one if s.users .get(&uid) diff --git a/src/server.rs b/src/server.rs index 1490748..129b271 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1033,6 +1033,76 @@ impl Server { } } + /// Fan a channel PRIVMSG/NOTICE out to every eligible member, allocating the + /// line at most once per distinct capability profile (server-time / account-tag + /// / message-tags) and sharing it by `Arc` — instead of formatting a fresh + /// String per member. The tag *values* (time, account, msgid, ctags) are the + /// same for the whole message, so a big channel needs ≤8 lines, not N. Excludes + /// the sender (echo-message is a separate single send) and +D deaf members; + /// `op_only` (+U) limits delivery to half-ops and above. + pub fn to_channel_tagged( + &self, + key: &str, + src: Uid, + ctags: &str, + msgid: &str, + body: &str, + op_only: bool, + ) { + let Some(ch) = self.channels.get(key) else { + return; + }; + let members: Vec = ch.members.keys().copied().collect(); + let time_tag = format!("time={}", iso_time(now())); + let account = self.users.get(&src).and_then(|su| su.account.clone()); + // one cached line per (server_time, account_tag, message_tags) combination + let mut cache: [Option>; 8] = std::array::from_fn(|_| None); + for m in members { + if m == src { + continue; + } + let Some(u) = self.users.get(&m) else { + continue; + }; + if u.flags.deaf { + continue; + } + if op_only && self.rank(m, key) < crate::channels::RANK_HALFOP { + continue; + } + let st = u.caps.server_time; + let at = u.caps.account_tag && account.is_some(); + let mt = u.caps.message_tags; + let idx = st as usize | (at as usize) << 1 | (mt as usize) << 2; + let line = cache[idx] + .get_or_insert_with(|| { + let mut tags: Vec = Vec::new(); + if st { + tags.push(time_tag.clone()); + } + if at { + tags.push(format!("account={}", account.as_deref().unwrap_or_default())); + } + if mt { + if !msgid.is_empty() { + tags.push(format!("msgid={msgid}")); + } + if !ctags.is_empty() { + tags.push(ctags.to_string()); + } + } + let s = if tags.is_empty() { + body.to_string() + } else { + format!("@{} {body}", tags.join(";")) + }; + std::sync::Arc::from(s.as_str()) + }) + .clone(); + self.emit_to(m, LineBuf::Shared(line)); + } + } + /// Mint a unique IRCv3 `msgid` for one message. Generated once per PRIVMSG/ /// NOTICE/TAGMSG and shared across all its recipients so they correlate. /// `-` in hex: unique for this run, distinct across @@ -1421,6 +1491,40 @@ mod tests { assert!(!ann.iter().any(|l| l.contains("PART")), "no fallback on case-only: {ann:?}"); } + #[test] + fn to_channel_tagged_shares_and_tags_by_profile() { + let mut s = srv(); + let arx = add_user(&mut s, 1, "ann"); // plain + let brx = add_user(&mut s, 2, "bob"); // server-time + let crx = add_user(&mut s, 3, "cara"); // message-tags + account-tag + let drx = add_user(&mut s, 4, "dave"); // the sender (has an account) + s.users.get_mut(&2).unwrap().caps.server_time = true; + s.users.get_mut(&3).unwrap().caps.message_tags = true; + s.users.get_mut(&3).unwrap().caps.account_tag = true; + s.users.get_mut(&4).unwrap().account = Some("dv".into()); + for u in [1, 2, 3, 4] { + s.join(u, "#c", None); + } + for rx in [&arx, &brx, &crx, &drx] { + let _ = rx.try_iter().count(); + } + s.to_channel_tagged("#c", 4, "", "abc123", ":dave!u@h PRIVMSG #c :hi", false); + let ann: Vec = arx.try_iter().collect(); + let bob: Vec = brx.try_iter().collect(); + let cara: Vec = crx.try_iter().collect(); + let dave: Vec = drx.try_iter().collect(); + assert!(ann.iter().any(|l| l == ":dave!u@h PRIVMSG #c :hi"), "plain untagged: {ann:?}"); + assert!( + bob.iter().any(|l| l.starts_with("@time=") && l.ends_with(":dave!u@h PRIVMSG #c :hi")), + "server-time tagged: {bob:?}" + ); + assert!( + cara.iter().any(|l| l.contains("account=dv") && l.contains("msgid=abc123")), + "message-tags+account-tag both present: {cara:?}" + ); + assert!(dave.is_empty(), "sender is excluded from the fanout: {dave:?}"); + } + #[test] fn to_channel_shares_line_and_tags_server_time_members() { let mut s = srv();