Add a shared channel-access FLAGS model
A granular access system, replacing the op/voice-only levels with per-user flag letters (f full, o auto-op, O op, h auto-halfop, v auto-voice, t topic, i invite, a modify-access, s settings, g greet). One shared primitive in the api — level_caps() resolves a stored level to capabilities (recognising the legacy op/voice/founder presets so nothing breaks) and apply_flags() applies +/- deltas — so join_mode/is_op/access_rank all route through it and a flag-granted +o auto-ops exactly like a legacy op entry. ChanServ FLAGS <#chan> [account [+/-flags]] lists or edits access; changing needs the founder or the 'a' flag. This is the foundation GroupServ will reuse for group membership flags.
This commit is contained in:
parent
5b023c22a4
commit
c7a4d5be27
5 changed files with 269 additions and 22 deletions
80
chanserv/src/flags.rs
Normal file
80
chanserv/src/flags.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use fedserv_api::{apply_flags, Sender, ServiceCtx, Store, ACCESS_FLAGS};
|
||||
|
||||
// FLAGS <#channel> [account [+/-flags]]: the granular access model. With no
|
||||
// account, list the access entries and their flags; with an account, show or
|
||||
// change its flags. Letters: f full o auto-op O op h auto-halfop v auto-voice
|
||||
// t topic i invite a access-list s settings g greet. Viewing needs op access;
|
||||
// changing needs the founder or the \x02a\x02 flag.
|
||||
pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let Some(info) = db.channel(chan) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
let is_founder = from.account == Some(info.founder.as_str());
|
||||
let caps = info.caps_of(from.account);
|
||||
|
||||
// FLAGS <#chan> — list.
|
||||
let Some(&target) = args.get(2) else {
|
||||
if !is_founder && !caps.op {
|
||||
ctx.notice(me, from.uid, format!("You need access to \x02{chan}\x02 to view its flags."));
|
||||
return;
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("Access flags for \x02{chan}\x02:"));
|
||||
ctx.notice(me, from.uid, format!(" \x02{}\x02 (founder): \x02f\x02", info.founder));
|
||||
for a in &info.access {
|
||||
ctx.notice(me, from.uid, format!(" \x02{}\x02: \x02{}\x02", a.account, display_flags(&a.level)));
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("End of flags ({} entr{}).", info.access.len() + 1, if info.access.is_empty() { "y" } else { "ies" }));
|
||||
return;
|
||||
};
|
||||
|
||||
let current = info.access.iter().find(|a| a.account.eq_ignore_ascii_case(target)).map(|a| display_flags(&a.level));
|
||||
|
||||
// FLAGS <#chan> <account> — show one.
|
||||
let Some(&delta) = args.get(3) else {
|
||||
match current {
|
||||
Some(f) if !f.is_empty() => ctx.notice(me, from.uid, format!("\x02{target}\x02 on \x02{chan}\x02: \x02{f}\x02")),
|
||||
_ => ctx.notice(me, from.uid, format!("\x02{target}\x02 has no access to \x02{chan}\x02.")),
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
// FLAGS <#chan> <account> <+/-flags> — modify.
|
||||
if !is_founder && !caps.access {
|
||||
ctx.notice(me, from.uid, format!("You need the founder or the \x02a\x02 flag to change access on \x02{chan}\x02."));
|
||||
return;
|
||||
}
|
||||
if info.founder.eq_ignore_ascii_case(target) {
|
||||
ctx.notice(me, from.uid, "The founder's access is set with \x02SET FOUNDER\x02, not flags.");
|
||||
return;
|
||||
}
|
||||
let base = current.unwrap_or_default();
|
||||
let updated = match apply_flags(&base, delta) {
|
||||
Ok(f) => f,
|
||||
Err(bad) => {
|
||||
ctx.notice(me, from.uid, format!("\x02{bad}\x02 isn't a valid flag. Valid flags: \x02{ACCESS_FLAGS}\x02."));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if updated.is_empty() {
|
||||
match db.access_del(chan, target) {
|
||||
Ok(true) => ctx.notice(me, from.uid, format!("Cleared \x02{target}\x02's access to \x02{chan}\x02.")),
|
||||
_ => ctx.notice(me, from.uid, format!("\x02{target}\x02 had no access to \x02{chan}\x02.")),
|
||||
}
|
||||
return;
|
||||
}
|
||||
match db.access_add(chan, target, &updated) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("\x02{target}\x02 on \x02{chan}\x02 now holds \x02{updated}\x02.")),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
|
||||
// Show a stored level as flag letters (mapping the legacy presets).
|
||||
fn display_flags(level: &str) -> String {
|
||||
match level {
|
||||
"op" => "oti".to_string(),
|
||||
"voice" => "v".to_string(),
|
||||
"founder" => "f".to_string(),
|
||||
flags => flags.to_string(),
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ use fedserv_api::NetView;
|
|||
mod mode;
|
||||
#[path = "access.rs"]
|
||||
mod access;
|
||||
mod flags;
|
||||
#[path = "op.rs"]
|
||||
mod op;
|
||||
#[path = "kick.rs"]
|
||||
|
|
@ -205,6 +206,13 @@ impl Service for ChanServ {
|
|||
}
|
||||
Some("MODE") => mode::handle(me, from, args, ctx, db),
|
||||
Some("ACCESS") => access::handle(me, from, args, ctx, db),
|
||||
Some("FLAGS") => {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: FLAGS <#channel> [account [+/-flags]]");
|
||||
return;
|
||||
};
|
||||
flags::handle(me, from, chan, args, ctx, db);
|
||||
}
|
||||
Some("OP") => op::handle(me, from, "+o", args, ctx, net, db),
|
||||
Some("DEOP") => op::handle(me, from, "-o", args, ctx, net, db),
|
||||
Some("VOICE") => op::handle(me, from, "+v", args, ctx, net, db),
|
||||
|
|
@ -229,7 +237,7 @@ impl Service for ChanServ {
|
|||
Some("AOP") => xop::handle(me, from, "AOP", "op", args, ctx, db),
|
||||
Some("SOP") => xop::handle(me, from, "SOP", "op", args, ctx, db),
|
||||
Some("VOP") => xop::handle(me, from, "VOP", "voice", args, ctx, db),
|
||||
Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02, \x02INFO\x02, \x02LIST\x02, \x02SET\x02, \x02ACCESS\x02, \x02AOP\x02/\x02SOP\x02/\x02VOP\x02, \x02STATUS\x02, \x02OP\x02/\x02DEOP\x02, \x02VOICE\x02/\x02DEVOICE\x02, \x02KICK\x02, \x02BAN\x02/\x02UNBAN\x02, \x02AKICK\x02, \x02ENFORCE\x02, \x02TOPIC\x02, \x02ENTRYMSG\x02, \x02INVITE\x02, \x02GETKEY\x02, \x02SEEN\x02, \x02CLONE\x02, \x02MODE\x02, \x02MLOCK\x02, \x02DROP\x02. Operators also have \x02SUSPEND\x02/\x02UNSUSPEND\x02 and \x02NOEXPIRE\x02 <#channel> {ON|OFF}."),
|
||||
Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02, \x02INFO\x02, \x02LIST\x02, \x02SET\x02, \x02ACCESS\x02, \x02FLAGS\x02, \x02AOP\x02/\x02SOP\x02/\x02VOP\x02, \x02STATUS\x02, \x02OP\x02/\x02DEOP\x02, \x02VOICE\x02/\x02DEVOICE\x02, \x02KICK\x02, \x02BAN\x02/\x02UNBAN\x02, \x02AKICK\x02, \x02ENFORCE\x02, \x02TOPIC\x02, \x02ENTRYMSG\x02, \x02INVITE\x02, \x02GETKEY\x02, \x02SEEN\x02, \x02CLONE\x02, \x02MODE\x02, \x02MLOCK\x02, \x02DROP\x02. Operators also have \x02SUSPEND\x02/\x02UNSUSPEND\x02 and \x02NOEXPIRE\x02 <#channel> {ON|OFF}."),
|
||||
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
|
||||
None => {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue