From 668f2735b28d66b3bc94d9281bac80c1e1dc7abe Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 01:22:27 +0000 Subject: [PATCH] delaymsg channel mode (+d ) --- src/channels.rs | 3 +++ src/coremods/core_message.rs | 21 +++++++++++++++++ src/mode.rs | 44 +++++++++++++++++++++++++++++++++++- src/users.rs | 4 ++-- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index 9b7c677..f881fc6 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -18,6 +18,7 @@ pub struct Member { pub op: bool, // +o (@) pub halfop: bool, // +h (%) pub voice: bool, // +v (+) + pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown) } /// Prefix ranks, high→low — gate who may grant a prefix / kick whom. @@ -155,6 +156,7 @@ pub struct ChanModes { pub permanent: bool, // +P — channel persists with zero members pub kicknorejoin: Option, // +J — block rejoin for N secs after a kick pub opmoderated: bool, // +U — unprivileged users' messages go to ops only + pub delaymsg: Option, // +d — new joiners can't speak for N secs } impl ChanModes { @@ -551,6 +553,7 @@ impl Server { uid, Member { op: is_new, + joined: now(), ..Default::default() }, ); diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index d522fff..f15e87f 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -216,6 +216,27 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) } return CmdResult::Fail; } + // +d delaymsg — a just-joined unprivileged user must wait before speaking + if let Some(secs) = s.channels.get(&key).and_then(|c| c.modes.delaymsg) { + if s.rank(uid, &key) < RANK_VOICE { + let joined = s + .channels + .get(&key) + .and_then(|c| c.members.get(&uid)) + .map(|m| m.joined) + .unwrap_or(0); + if joined != 0 && crate::server::now().saturating_sub(joined) < secs as u64 { + if !notice { + s.numeric( + uid, + ERR_CANNOTSENDTOCHAN, + &format!("{target} :You must wait {secs}s after joining to speak (+d)"), + ); + } + return CmdResult::Fail; + } + } + } // extban `m:` mute — matched users can't speak unless voiced-or-above if s.extban_active(uid, &key, 'm') && s.rank(uid, &key) < RANK_VOICE { if !notice { diff --git a/src/mode.rs b/src/mode.rs index be7a1af..6186cb8 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -90,6 +90,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[ &PERMANENT, &KICKNOREJOIN, &OPMODERATED, + &DELAYMSG, ]; // --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ---------- @@ -910,6 +911,47 @@ impl ChanMode for KickNoRejoinMode { } } +/// +d `` — a newly-joined member can't speak for `` seconds (InspIRCd +/// `m_delaymsg`). Enforced in the message path; voiced-or-above are exempt. +struct DelayMsgMode; +static DELAYMSG: DelayMsgMode = DelayMsgMode; +impl ChanMode for DelayMsgMode { + fn letter(&self) -> char { + 'd' + } + fn wants_param(&self, adding: bool) -> bool { + adding + } + fn apply( + &self, + s: &mut Server, + _chan: &str, + key: &str, + _uid: Uid, + adding: bool, + param: Option<&str>, + ) -> Applied { + if adding { + let Some(secs) = param + .and_then(|p| p.parse::().ok()) + .filter(|&n| n >= 1) + else { + return Applied::No; + }; + let secs = secs.min(3600); + if let Some(c) = s.channels.get_mut(key) { + c.modes.delaymsg = Some(secs); + } + Applied::Yes(Some(secs.to_string())) + } else { + if let Some(c) = s.channels.get_mut(key) { + c.modes.delaymsg = None; + } + Applied::Yes(None) + } + } +} + // === user modes ============================================================ /// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the @@ -1150,7 +1192,7 @@ mod tests { #[test] fn registry_covers_all_channel_modes() { - for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJU".chars() { + for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJUd".chars() { assert!(chan_mode(c).is_some(), "missing handler for +{c}"); } assert!(chan_mode('y').is_none()); diff --git a/src/users.rs b/src/users.rs index 1eb5159..257afbf 100644 --- a/src/users.rs +++ b/src/users.rs @@ -418,7 +418,7 @@ impl Server { uid, RPL_MYINFO, &format!( - "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJU", + "{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUd", self.name ), ); @@ -426,7 +426,7 @@ impl Server { uid, RPL_ISUPPORT, &format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHBJ,ACGMNOPQRSTUcimnpstuz 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", + "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHBJd,ACGMNOPQRSTUcimnpstuz 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 ), );