chanserv: LEVELS grants a capability (op/topic/invite/access) to an access tier and above, additive so it never locks anyone out
This commit is contained in:
parent
0c3e09ea00
commit
d313ca3523
10 changed files with 275 additions and 6 deletions
88
modules/chanserv/src/levels.rs
Normal file
88
modules/chanserv/src/levels.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use echo_api::Store;
|
||||
use echo_api::{AccessRole, LevelCap, Sender, ServiceCtx};
|
||||
|
||||
// LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]
|
||||
// Tune which access tier holds each channel capability (OP, TOPIC, INVITE, ACCESS).
|
||||
// Additive — it grants a capability to a lower tier, so it can never lock anyone
|
||||
// out. Founder only. With no sub-command it shows the current matrix.
|
||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
let Some(&chan) = args.get(1) else {
|
||||
ctx.notice(me, from.uid, "Syntax: LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]");
|
||||
return;
|
||||
};
|
||||
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
None | Some("LIST") | Some("VIEW") => list(me, from, chan, ctx, db),
|
||||
Some("SET") => set(me, from, chan, &args[3..], ctx, db),
|
||||
Some("RESET") | Some("DEL") => reset(me, from, chan, args.get(3).copied(), ctx, db),
|
||||
_ => ctx.notice(me, from.uid, "Syntax: LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]"),
|
||||
}
|
||||
}
|
||||
|
||||
fn list(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||
let Some(info) = db.channel(chan) else {
|
||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
||||
return;
|
||||
};
|
||||
ctx.notice(me, from.uid, format!("Access levels for \x02{}\x02 (capability → minimum tier):", info.name));
|
||||
for cap in LevelCap::ALL {
|
||||
let (tier, tag) = match info.levels.iter().find(|(c, _)| *c == cap) {
|
||||
Some((_, role)) => (*role, " (custom)"),
|
||||
None => (cap.default_role(), ""),
|
||||
};
|
||||
ctx.notice(me, from.uid, format!(" \x02{}\x02 — {}{}", cap.name(), tier_word(tier), tag));
|
||||
}
|
||||
ctx.notice(me, from.uid, "Founder: \x02LEVELS <#chan> SET <capability> <tier>\x02 grants it to that tier and above; \x02RESET\x02 restores the default.");
|
||||
}
|
||||
|
||||
fn set(me: &str, from: &Sender, chan: &str, rest: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
if !super::require_founder(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
let (Some(&cap_s), Some(&tier_s)) = (rest.first(), rest.get(1)) else {
|
||||
ctx.notice(me, from.uid, "Syntax: LEVELS <#channel> SET <capability> <tier>");
|
||||
return;
|
||||
};
|
||||
let Some(cap) = LevelCap::parse(cap_s) else {
|
||||
ctx.notice(me, from.uid, "Unknown capability. One of: \x02OP\x02, \x02TOPIC\x02, \x02INVITE\x02, \x02ACCESS\x02.");
|
||||
return;
|
||||
};
|
||||
let role = match AccessRole::parse(tier_s) {
|
||||
Some(AccessRole::Founder) => {
|
||||
ctx.notice(me, from.uid, "Granting to \x02FOUNDER\x02 is redundant. Pick \x02VOP\x02, \x02HOP\x02, \x02AOP\x02, or \x02SOP\x02.");
|
||||
return;
|
||||
}
|
||||
Some(r) => r,
|
||||
None => {
|
||||
ctx.notice(me, from.uid, "Unknown tier. One of: \x02VOP\x02, \x02HOP\x02, \x02AOP\x02, \x02SOP\x02.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match db.level_set(chan, cap.name(), tier_word(role)) {
|
||||
Ok(()) => ctx.notice(me, from.uid, format!("\x02{}\x02 on \x02{chan}\x02 is now held by \x02{}\x02 and above.", cap.name(), tier_word(role))),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(me: &str, from: &Sender, chan: &str, cap_arg: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||
if !super::require_founder(me, from, chan, ctx, db) {
|
||||
return;
|
||||
}
|
||||
let Some(cap_s) = cap_arg else {
|
||||
ctx.notice(me, from.uid, "Syntax: LEVELS <#channel> RESET <capability>");
|
||||
return;
|
||||
};
|
||||
let Some(cap) = LevelCap::parse(cap_s) else {
|
||||
ctx.notice(me, from.uid, "Unknown capability. One of: \x02OP\x02, \x02TOPIC\x02, \x02INVITE\x02, \x02ACCESS\x02.");
|
||||
return;
|
||||
};
|
||||
match db.level_reset(chan, cap.name()) {
|
||||
Ok(true) => ctx.notice(me, from.uid, format!("\x02{}\x02 on \x02{chan}\x02 is back to its default tier (\x02{}\x02).", cap.name(), tier_word(cap.default_role()))),
|
||||
Ok(false) => ctx.notice(me, from.uid, format!("\x02{}\x02 on \x02{chan}\x02 has no custom level.", cap.name())),
|
||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||
}
|
||||
}
|
||||
|
||||
// The tier word for display / storage (SOP/AOP/HOP/VOP; founder falls back to its label).
|
||||
fn tier_word(role: AccessRole) -> &'static str {
|
||||
role.xop_word().unwrap_or("FOUNDER")
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ mod topic;
|
|||
mod invite;
|
||||
#[path = "akick.rs"]
|
||||
mod akick;
|
||||
mod levels;
|
||||
#[path = "list.rs"]
|
||||
mod list;
|
||||
#[path = "status.rs"]
|
||||
|
|
@ -54,6 +55,7 @@ const TOPICS: &[HelpEntry] = &[
|
|||
HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." },
|
||||
HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD <account|!group> <sop|op|halfop|voice> | DEL <account|!group>\x02\nManages the channel access list. The target may be a GroupServ \x02!group\x02; each of its members holding the group's \x02c\x02 flag then inherits the access." },
|
||||
HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." },
|
||||
HelpEntry { cmd: "LEVELS", summary: "tune which tier holds each capability", detail: "Syntax: \x02LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]\x02\nGrants a channel capability (\x02OP\x02, \x02TOPIC\x02, \x02INVITE\x02, \x02ACCESS\x02) to an access tier (\x02VOP\x02/\x02HOP\x02/\x02AOP\x02/\x02SOP\x02) and above, e.g. let halfops manage AKICK. Additive — it never removes access. Founder only." },
|
||||
HelpEntry { cmd: "SOP/AOP/HOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02SOP|AOP|HOP|VOP <#channel> ADD <account> | DEL <account> | LIST\x02\nTiered shortcuts over ACCESS: SOP grants op plus access-list management, AOP grants op, HOP grants halfop, VOP grants voice." },
|
||||
HelpEntry { cmd: "STATUS", summary: "show a user's access", detail: "Syntax: \x02STATUS <#channel> [nick]\x02\nShows a user's access level on a channel." },
|
||||
HelpEntry { cmd: "OP/DEOP/VOICE/DEVOICE", summary: "give or take op/voice", detail: "Syntax: \x02OP|DEOP|VOICE|DEVOICE <#channel> [nick]\x02\nGives or takes channel op or voice." },
|
||||
|
|
@ -285,6 +287,7 @@ impl Service for ChanServ {
|
|||
Some("TOPIC") => topic::handle(me, from, args, ctx, db),
|
||||
Some("INVITE") => invite::handle(me, from, args, ctx, net, db),
|
||||
Some("AKICK") => akick::handle(me, from, args, ctx, db),
|
||||
Some("LEVELS") => levels::handle(me, from, args, ctx, db),
|
||||
Some("STATUS") => status::handle(me, from, args, ctx, net, db),
|
||||
Some("SUSPEND") => suspend::handle(me, from, args, ctx, net, db, true),
|
||||
Some("UNSUSPEND") => suspend::handle(me, from, args, ctx, net, db, false),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue