chanserv: SET PEACE (block acting against equal-or-higher access)

PEACE stops a channel op from using ChanServ KICK/BAN/DEOP/DEVOICE against
someone whose access rank (founder>op>voice) is equal to or above their own.
Adds a typed access_rank on ChannelView and a peace_blocks guard in those
commands. Same typed-settings path as SIGNKICK/PRIVATE; shown in INFO.
This commit is contained in:
Jean Chevronnet 2026-07-13 03:06:58 +00:00
parent 75019c867a
commit 3bf6b2be77
No known key found for this signature in database
7 changed files with 62 additions and 2 deletions

View file

@ -157,6 +157,9 @@ pub struct ChanSettings {
// Hide the channel from ChanServ LIST.
#[serde(default)]
pub private: bool,
// Forbid using ChanServ to act against someone with equal-or-higher access.
#[serde(default)]
pub peace: bool,
}
// A registered channel and who owns it.
@ -586,7 +589,7 @@ impl Db {
if !c.entrymsg.is_empty() {
snapshot.push(Event::ChannelEntryMsgSet { channel: c.name.clone(), msg: c.entrymsg.clone() });
}
if c.settings.signkick || c.settings.private {
if c.settings.signkick || c.settings.private || c.settings.peace {
snapshot.push(Event::ChannelSettingsSet { channel: c.name.clone(), settings: c.settings });
}
}
@ -1115,6 +1118,7 @@ impl Db {
match setting {
ChanSetting::SignKick => settings.signkick = on,
ChanSetting::Private => settings.private = on,
ChanSetting::Peace => settings.peace = on,
}
self.log
.append(Event::ChannelSettingsSet { channel: channel.to_string(), settings })
@ -1479,6 +1483,7 @@ fn channel_view(c: &ChannelInfo) -> ChannelView {
entrymsg: c.entrymsg.clone(),
signkick: c.settings.signkick,
private: c.settings.private,
peace: c.settings.peace,
}
}
@ -1613,6 +1618,21 @@ mod tests {
assert!(s.signkick && !s.private, "settings replay from the log");
}
// Access ranks order founder > op > voice > none for PEACE comparisons.
#[test]
fn access_rank_orders_founder_op_voice() {
let mut db = Db::open(&tmp("rank"), "N1");
db.register_channel("#c", "boss").unwrap();
db.access_add("#c", "op1", "op").unwrap();
db.access_add("#c", "v1", "voice").unwrap();
let cv = Store::channel(&db, "#c").unwrap();
assert_eq!(cv.access_rank(Some("boss")), 3);
assert_eq!(cv.access_rank(Some("OP1")), 2, "case-insensitive");
assert_eq!(cv.access_rank(Some("v1")), 1);
assert_eq!(cv.access_rank(Some("nobody")), 0);
assert_eq!(cv.access_rank(None), 0);
}
// A wrong code is tolerated a few times, then the code is burned so it can't
// be ground down online even though it is short.
#[test]