kicknorejoin channel mode (+J <secs>)

This commit is contained in:
Jean Chevronnet 2026-08-09 00:50:55 +00:00
parent 8dc51a281b
commit 37ea5d277d
5 changed files with 66 additions and 3 deletions

View file

@ -152,6 +152,7 @@ pub struct ChanModes {
pub nokicks: bool, // +Q — KICK is disabled on the channel pub nokicks: bool, // +Q — KICK is disabled on the channel
pub allowinvite: bool, // +A — any member (not just ops) may INVITE pub allowinvite: bool, // +A — any member (not just ops) may INVITE
pub permanent: bool, // +P — channel persists with zero members pub permanent: bool, // +P — channel persists with zero members
pub kicknorejoin: Option<u32>, // +J <secs> — block rejoin for N secs after a kick
} }
impl ChanModes { impl ChanModes {
@ -278,6 +279,7 @@ pub struct Channel {
pub joinflood_until: u64, // +j locked out until this unix ts pub joinflood_until: u64, // +j locked out until this unix ts
pub nickflood_hits: Vec<u64>, // +F recent nick-change times pub nickflood_hits: Vec<u64>, // +F recent nick-change times
pub nickflood_until: u64, // +F locked out until this unix ts pub nickflood_until: u64, // +F locked out until this unix ts
pub recent_kicks: HashMap<Uid, u64>, // +J uid -> unix ts of last kick (rejoin delay)
} }
impl Channel { impl Channel {
@ -304,6 +306,7 @@ impl Channel {
joinflood_until: 0, joinflood_until: 0,
nickflood_hits: Vec::new(), nickflood_hits: Vec::new(),
nickflood_until: 0, nickflood_until: 0,
recent_kicks: HashMap::new(),
} }
} }
@ -450,6 +453,19 @@ impl Server {
); );
return; return;
} }
// +J <secs> — 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 // +l full — with +L redirect, bounce the user to the target instead
if let Some(ch) = self.channels.get(&key) { if let Some(ch) = self.channels.get(&key) {
@ -505,6 +521,7 @@ impl Server {
}, },
); );
ch.invites.remove(&uid); // consume any pending invite 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) { if let Some(u) = self.users.get_mut(&uid) {
u.channels.insert(key.clone()); u.channels.insert(key.clone());
} }

View file

@ -451,6 +451,9 @@ impl Command for Kick {
s.propagate_from_user(uid, &format!("KICK {chan} {victim} :{reason}")); // tell links s.propagate_from_user(uid, &format!("KICK {chan} {victim} :{reason}")); // tell links
if let Some(ch) = s.channels.get_mut(&key) { if let Some(ch) = s.channels.get_mut(&key) {
ch.members.remove(&tuid); 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) { if let Some(u) = s.users.get_mut(&tuid) {
u.channels.remove(&key); u.channels.remove(&key);

View file

@ -88,6 +88,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[
&NOKICKS, &NOKICKS,
&ALLOWINVITE, &ALLOWINVITE,
&PERMANENT, &PERMANENT,
&KICKNOREJOIN,
]; ];
// --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ---------- // --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ----------
@ -859,6 +860,47 @@ impl ChanMode for AntiCapsMode {
} }
} }
/// +J `<secs>` — after being kicked, a user can't rejoin for `<secs>` 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::<u32>().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 ============================================================ // === user modes ============================================================
/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the /// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the
@ -1083,7 +1125,7 @@ mod tests {
#[test] #[test]
fn registry_covers_all_channel_modes() { 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(c).is_some(), "missing handler for +{c}");
} }
assert!(chan_mode('y').is_none()); assert!(chan_mode('y').is_none());

View file

@ -29,6 +29,7 @@ pub const ERR_NOSUCHSERVER: u16 = 402;
pub const ERR_WASNOSUCHNICK: u16 = 406; pub const ERR_WASNOSUCHNICK: u16 = 406;
pub const ERR_UNAVAILRESOURCE: u16 = 437; // channel temporarily unavailable (+j) 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_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_ENDOFSPAMFILTER: u16 = 940; // end of the +g word-filter list
pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry pub const RPL_SPAMFILTER: u16 = 941; // one +g word-filter entry
pub const RPL_KNOCK: u16 = 710; // channel gets the knock pub const RPL_KNOCK: u16 = 710; // channel gets the knock

View file

@ -409,7 +409,7 @@ impl Server {
uid, uid,
RPL_MYINFO, RPL_MYINFO,
&format!( &format!(
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAP", "{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJ",
self.name self.name
), ),
); );
@ -417,7 +417,7 @@ impl Server {
uid, uid,
RPL_ISUPPORT, RPL_ISUPPORT,
&format!( &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 self.network
), ),
); );