chanserv/nickserv: resolve access level via caps, so FLAGS-set roles list correctly in ALIST and AOP/SOP/VOP

This commit is contained in:
Jean Chevronnet 2026-07-17 14:15:39 +00:00
parent 7a5f1502a4
commit 2cdc7f820a
No known key found for this signature in database
2 changed files with 24 additions and 6 deletions

View file

@ -1,5 +1,4 @@
use echo_api::Store; use echo_api::{level_caps, Sender, ServiceCtx, Store};
use echo_api::{Sender, ServiceCtx};
// AOP/SOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered // AOP/SOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered
// shortcuts over the access list. `level` is the access level they map to // shortcuts over the access list. `level` is the access level they map to
@ -14,7 +13,14 @@ pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], c
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")),
Some(info) => { Some(info) => {
ctx.notice(me, from.uid, format!("{word} list for \x02{}\x02:", info.name)); ctx.notice(me, from.uid, format!("{word} list for \x02{}\x02:", info.name));
for a in info.access.iter().filter(|a| a.level == level) { // Match by capability, not the raw level string, so entries set via
// granular FLAGS still list under the right tier (VOP = voice-only,
// AOP/SOP = op).
let want_op = level_caps(level).op;
for a in info.access.iter().filter(|a| {
let c = level_caps(&a.level);
if want_op { c.op } else { c.auto == Some("+v") && !c.op }
}) {
ctx.notice(me, from.uid, format!(" \x02{}\x02", a.account)); ctx.notice(me, from.uid, format!(" \x02{}\x02", a.account));
} }
} }

View file

@ -1,5 +1,4 @@
use echo_api::Store; use echo_api::{level_caps, Sender, ServiceCtx, Store};
use echo_api::{Sender, ServiceCtx};
// ALIST: list the channels the sender's account founds or has access on. // ALIST: list the channels the sender's account founds or has access on.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -12,7 +11,20 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
if c.founder.eq_ignore_ascii_case(account) { if c.founder.eq_ignore_ascii_case(account) {
rows.push((c.name.clone(), "founder")); rows.push((c.name.clone(), "founder"));
} else if let Some(a) = c.access.iter().find(|a| a.account.eq_ignore_ascii_case(account)) { } else if let Some(a) = c.access.iter().find(|a| a.account.eq_ignore_ascii_case(account)) {
rows.push((c.name.clone(), if a.level == "voice" { "voice" } else { "op" })); // Resolve the entry's capabilities rather than string-matching the
// level, so a role set via granular FLAGS (e.g. "v") reads correctly
// and isn't mislabelled "op".
let caps = level_caps(&a.level);
let role = if caps.op {
"op"
} else if caps.auto == Some("+v") {
"voice"
} else if caps.auto == Some("+h") {
"halfop"
} else {
"access"
};
rows.push((c.name.clone(), role));
} }
} }
if rows.is_empty() { if rows.is_empty() {