chanserv: SET SIGNKICK and PRIVATE (typed channel settings)

Introduce a typed ChanSettings struct + ChanSetting enum for ChanServ's on/off
options instead of a bag of string flags, so a new option is one field and the
compiler proves every site handles it. First two, both functional: SIGNKICK
attributes ChanServ kicks with who requested them; PRIVATE hides the channel
from LIST. Folded through the event log like the other channel state (node-local,
no gossip), shown in INFO. One ChannelSettingsSet event, one Store method.
This commit is contained in:
Jean Chevronnet 2026-07-13 02:54:46 +00:00
parent f388e3d650
commit 75019c867a
No known key found for this signature in database
7 changed files with 114 additions and 10 deletions

View file

@ -15,6 +15,10 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here."));
return;
};
let reason = if args.len() > 3 { args[3..].join(" ") } else { "Kicked".to_string() };
let mut reason = if args.len() > 3 { args[3..].join(" ") } else { "Kicked".to_string() };
// SIGNKICK: attribute the kick to whoever asked for it.
if db.channel(chan).is_some_and(|c| c.signkick) {
reason = format!("{reason} (requested by {})", from.nick);
}
ctx.kick(me, chan, target, &reason);
}

View file

@ -101,6 +101,12 @@ impl Service for ChanServ {
ctx.notice(me, from.uid, format!(" Description: {}", info.desc));
}
ctx.notice(me, from.uid, format!(" Registered : {}", fedserv_api::human_time(info.ts)));
let mut opts = Vec::new();
if info.signkick { opts.push("SIGNKICK"); }
if info.private { opts.push("PRIVATE"); }
if !opts.is_empty() {
ctx.notice(me, from.uid, format!(" Options : {}", opts.join(", ")));
}
}
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
}

View file

@ -3,7 +3,8 @@ use fedserv_api::{Sender, ServiceCtx};
// LIST: show all registered channels.
pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
let mut names: Vec<String> = db.channels().into_iter().map(|c| c.name).collect();
// PRIVATE channels are hidden from LIST.
let mut names: Vec<String> = db.channels().into_iter().filter(|c| !c.private).map(|c| c.name).collect();
if names.is_empty() {
ctx.notice(me, from.uid, "No channels are registered.");
return;

View file

@ -1,5 +1,4 @@
use fedserv_api::Store;
use fedserv_api::{Sender, ServiceCtx};
use fedserv_api::{ChanSetting, Sender, ServiceCtx, Store};
// SET <#channel> FOUNDER <account> | DESC <text>: founder-only channel settings.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -40,6 +39,24 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
}
}
_ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text>"),
Some("SIGNKICK") => toggle(me, from, ctx, db, chan, ChanSetting::SignKick, args.get(3).copied(), "SIGNKICK"),
Some("PRIVATE") => toggle(me, from, ctx, db, chan, ChanSetting::Private, args.get(3).copied(), "PRIVATE"),
_ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text> | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF}"),
}
}
// Flip one on/off channel option, reporting the new state.
fn toggle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store, chan: &str, setting: ChanSetting, arg: Option<&str>, name: &str) {
let on = match arg.map(|s| s.to_ascii_uppercase()).as_deref() {
Some("ON") => true,
Some("OFF") => false,
_ => {
ctx.notice(me, from.uid, format!("Syntax: SET <#channel> {name} {{ON|OFF}}"));
return;
}
};
match db.set_channel_setting(chan, setting, on) {
Ok(()) => ctx.notice(me, from.uid, format!("\x02{name}\x02 for \x02{chan}\x02 is now \x02{}\x02.", if on { "on" } else { "off" })),
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
}
}