From 37ea5d277d984aa802f968475ec81ac1367e67a8 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 9 Aug 2026 00:50:55 +0000 Subject: [PATCH] kicknorejoin channel mode (+J ) --- src/channels.rs | 17 ++++++++++++++ src/coremods/core_channel.rs | 3 +++ src/mode.rs | 44 +++++++++++++++++++++++++++++++++++- src/numeric.rs | 1 + src/users.rs | 4 ++-- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/channels.rs b/src/channels.rs index ce720c1..b223f3c 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -152,6 +152,7 @@ pub struct ChanModes { pub nokicks: bool, // +Q — KICK is disabled on the channel pub allowinvite: bool, // +A — any member (not just ops) may INVITE pub permanent: bool, // +P — channel persists with zero members + pub kicknorejoin: Option, // +J — block rejoin for N secs after a kick } impl ChanModes { @@ -278,6 +279,7 @@ pub struct Channel { pub joinflood_until: u64, // +j locked out until this unix ts pub nickflood_hits: Vec, // +F recent nick-change times pub nickflood_until: u64, // +F locked out until this unix ts + pub recent_kicks: HashMap, // +J uid -> unix ts of last kick (rejoin delay) } impl Channel { @@ -304,6 +306,7 @@ impl Channel { joinflood_until: 0, nickflood_hits: Vec::new(), nickflood_until: 0, + recent_kicks: HashMap::new(), } } @@ -450,6 +453,19 @@ impl Server { ); return; } + // +J — can't rejoin within N seconds of being kicked + if let Some(secs) = ch.modes.kicknorejoin { + if let Some(&kt) = ch.recent_kicks.get(&uid) { + if now().saturating_sub(kt) < secs as u64 { + self.numeric( + uid, + ERR_DELAYREJOIN, + &format!("{name} :You must wait {secs}s after a kick to rejoin (+J)"), + ); + return; + } + } + } } // +l full — with +L redirect, bounce the user to the target instead if let Some(ch) = self.channels.get(&key) { @@ -505,6 +521,7 @@ impl Server { }, ); ch.invites.remove(&uid); // consume any pending invite + ch.recent_kicks.remove(&uid); // they got back in; clear any +J rejoin timer if let Some(u) = self.users.get_mut(&uid) { u.channels.insert(key.clone()); } diff --git a/src/coremods/core_channel.rs b/src/coremods/core_channel.rs index 24a96f5..b70b113 100644 --- a/src/coremods/core_channel.rs +++ b/src/coremods/core_channel.rs @@ -451,6 +451,9 @@ impl Command for Kick { s.propagate_from_user(uid, &format!("KICK {chan} {victim} :{reason}")); // tell links if let Some(ch) = s.channels.get_mut(&key) { ch.members.remove(&tuid); + if ch.modes.kicknorejoin.is_some() { + ch.recent_kicks.insert(tuid, now()); // +J rejoin-delay clock + } } if let Some(u) = s.users.get_mut(&tuid) { u.channels.remove(&key); diff --git a/src/mode.rs b/src/mode.rs index a4d3cc2..18a4a64 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -88,6 +88,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[ &NOKICKS, &ALLOWINVITE, &PERMANENT, + &KICKNOREJOIN, ]; // --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ---------- @@ -859,6 +860,47 @@ impl ChanMode for AntiCapsMode { } } +/// +J `` — after being kicked, a user can't rejoin for `` seconds +/// (InspIRCd `m_kicknorejoin`). Enforced in `Server::join`. +struct KickNoRejoinMode; +static KICKNOREJOIN: KickNoRejoinMode = KickNoRejoinMode; +impl ChanMode for KickNoRejoinMode { + fn letter(&self) -> char { + 'J' + } + 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; // needs a positive seconds value + }; + let secs = secs.min(3600); + if let Some(c) = s.channels.get_mut(key) { + c.modes.kicknorejoin = Some(secs); + } + Applied::Yes(Some(secs.to_string())) + } else { + if let Some(c) = s.channels.get_mut(key) { + c.modes.kicknorejoin = None; + } + Applied::Yes(None) + } + } +} + // === user modes ============================================================ /// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the @@ -1083,7 +1125,7 @@ mod tests { #[test] fn registry_covers_all_channel_modes() { - for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAP".chars() { + for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJ".chars() { assert!(chan_mode(c).is_some(), "missing handler for +{c}"); } assert!(chan_mode('y').is_none()); diff --git a/src/numeric.rs b/src/numeric.rs index 9f0b4b0..9df5d0c 100644 --- a/src/numeric.rs +++ b/src/numeric.rs @@ -29,6 +29,7 @@ pub const ERR_NOSUCHSERVER: u16 = 402; pub const ERR_WASNOSUCHNICK: u16 = 406; pub const ERR_UNAVAILRESOURCE: u16 = 437; // channel temporarily unavailable (+j) pub const ERR_LINKCHANNEL: u16 = 470; // +L — you were redirected to another channel +pub const ERR_DELAYREJOIN: u16 = 495; // +J — must wait before rejoining after a kick pub const RPL_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry pub const RPL_KNOCK: u16 = 710; // channel gets the knock diff --git a/src/users.rs b/src/users.rs index aea9ead..527d611 100644 --- a/src/users.rs +++ b/src/users.rs @@ -409,7 +409,7 @@ impl Server { uid, RPL_MYINFO, &format!( - "{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAP", + "{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ", self.name ), ); @@ -417,7 +417,7 @@ impl Server { uid, RPL_ISUPPORT, &format!( - "CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHB,ACGMNOPQRSTcimnpstuz 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,lfjFLHBJ,ACGMNOPQRSTcimnpstuz 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 ), );