api: typed Flag/Flags bitset for channel access (mirrors Privs); level_caps resolves every level through it, collapsing 40 lines to 5
All checks were successful
CI / check (push) Successful in 4m0s

This commit is contained in:
Jean Chevronnet 2026-07-17 18:30:45 +00:00
parent 65323caaeb
commit 2a12ebd6e3
No known key found for this signature in database
2 changed files with 187 additions and 76 deletions

View file

@ -1,10 +1,11 @@
use echo_api::{apply_flags, Sender, ServiceCtx, Store, ACCESS_FLAGS};
use echo_api::{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.
// changing needs the founder or the \x02a\x02 flag. Every stored level — a tier
// preset ("op"/"sop"/…) or a raw flag string — resolves through `Flags`.
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."));
@ -22,17 +23,17 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
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!(" \x02{}\x02: \x02{}\x02", a.account, Flags::from_level(&a.level).to_letters()));
}
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));
let entry = info.access.iter().find(|a| a.account.eq_ignore_ascii_case(target)).map(|a| a.level.clone());
// FLAGS <#chan> <account> — show one.
let Some(&delta) = args.get(3) else {
match current {
match entry.as_deref().map(|l| Flags::from_level(l).to_letters()) {
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.")),
}
@ -48,8 +49,7 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
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, ACCESS_FLAGS) {
let updated = match Flags::from_level(entry.as_deref().unwrap_or("")).apply_delta(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."));
@ -63,23 +63,9 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
}
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.")),
let letters = updated.to_letters();
match db.access_add(chan, target, &letters) {
Ok(()) => ctx.notice(me, from.uid, format!("\x02{target}\x02 on \x02{chan}\x02 now holds \x02{letters}\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 tier presets to their
// equivalent flags so FLAGS can round-trip and edit them. Each mapping resolves
// to the same capabilities as the preset (see `level_caps`): sop = op + topic +
// invite + access (auto +ao), op = op + topic + invite, halfop = auto +h.
fn display_flags(level: &str) -> String {
match level {
"founder" => "f".to_string(),
"sop" => "otia".to_string(),
"op" => "oti".to_string(),
"halfop" => "h".to_string(),
"voice" => "v".to_string(),
flags => flags.to_string(),
}
}