whois: render the oper-type title line in bold + colour (per-type color=<name|0-15|none>, default red) so it stands out
This commit is contained in:
parent
bc520d59a5
commit
2c9ce18cd7
2 changed files with 57 additions and 17 deletions
|
|
@ -308,16 +308,11 @@ impl Command for Whois {
|
||||||
&format!("{nick} :is an IRC Operator"),
|
&format!("{nick} :is an IRC Operator"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 320: the oper type's title on its own line, e.g. "is a Network
|
// 320: the oper type's title on its own line (bold + its colour), e.g. "is a
|
||||||
// Administrator" — from the oper's type (hidden with +H like the 313 line).
|
// Network Administrator" — from the oper's type (hidden with +H like 313).
|
||||||
if oper && (!hideoper || asker_oper) {
|
if oper && (!hideoper || asker_oper) {
|
||||||
if let Some(title) = crate::modules::opertypes::title_of(s, tuid) {
|
if let Some(line) = crate::modules::opertypes::whois_line(s, tuid) {
|
||||||
let article = if title.chars().next().is_some_and(|c| "aeiouAEIOU".contains(c)) {
|
s.numeric(uid, RPL_WHOISSPECIAL, &format!(":{line}"));
|
||||||
"an"
|
|
||||||
} else {
|
|
||||||
"a"
|
|
||||||
};
|
|
||||||
s.numeric(uid, RPL_WHOISSPECIAL, &format!(":is {article} {title}"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 320: oper-set SWHOIS line. No redundant target-nick param — just the
|
// 320: oper-set SWHOIS line. No redundant target-nick param — just the
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use crate::Uid;
|
||||||
/// oper; absent ⇒ a legacy oper with full access. Read by WHOIS for the title.
|
/// oper; absent ⇒ a legacy oper with full access. Read by WHOIS for the title.
|
||||||
pub struct OperType {
|
pub struct OperType {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub color: Option<u8>, // mIRC colour for the WHOIS title line (None = plain)
|
||||||
pub all_commands: bool,
|
pub all_commands: bool,
|
||||||
pub commands: HashSet<String>,
|
pub commands: HashSet<String>,
|
||||||
pub all_privs: bool,
|
pub all_privs: bool,
|
||||||
|
|
@ -71,11 +72,28 @@ impl Module for OperTypes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The WHOIS title of a typed oper, if any (read by core_info's 313).
|
/// The WHOIS title of a typed oper, if any (used in the denial message).
|
||||||
pub fn title_of(s: &Server, uid: Uid) -> Option<String> {
|
pub fn title_of(s: &Server, uid: Uid) -> Option<String> {
|
||||||
s.users.get(&uid).and_then(|u| u.ext.get::<OperType>()).map(|t| t.title.clone())
|
s.users.get(&uid).and_then(|u| u.ext.get::<OperType>()).map(|t| t.title.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The formatted WHOIS special line for a typed oper, if any — "is a/an <title>",
|
||||||
|
/// bold + the type's colour (mIRC code, e.g. 4 = red) so it stands out. core_info
|
||||||
|
/// emits it on its own 320 line.
|
||||||
|
pub fn whois_line(s: &Server, uid: Uid) -> Option<String> {
|
||||||
|
let t = s.users.get(&uid).and_then(|u| u.ext.get::<OperType>())?;
|
||||||
|
let article = if t.title.chars().next().is_some_and(|c| "aeiouAEIOU".contains(c)) {
|
||||||
|
"an"
|
||||||
|
} else {
|
||||||
|
"a"
|
||||||
|
};
|
||||||
|
let body = format!("is {article} {}", t.title);
|
||||||
|
Some(match t.color {
|
||||||
|
Some(c) => format!("\x02\x03{c:02}{body}\x0f"), // bold + colour, reset after
|
||||||
|
None => body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply the oper's type at oper-up: auto usermodes / snomasks / vhost / level, then
|
/// Apply the oper's type at oper-up: auto usermodes / snomasks / vhost / level, then
|
||||||
/// store the grant + title. A missing type (or an unknown id) leaves the oper with
|
/// store the grant + title. A missing type (or an unknown id) leaves the oper with
|
||||||
/// full access, so `oper` blocks without `type=` keep working.
|
/// full access, so `oper` blocks without `type=` keep working.
|
||||||
|
|
@ -104,6 +122,7 @@ pub fn apply(s: &mut Server, uid: Uid, type_id: Option<&str>) {
|
||||||
if let Some(u) = s.users.get_mut(&uid) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
u.ext.set(OperType {
|
u.ext.set(OperType {
|
||||||
title: r.title.clone(),
|
title: r.title.clone(),
|
||||||
|
color: r.color,
|
||||||
all_commands: r.all_commands,
|
all_commands: r.all_commands,
|
||||||
commands: r.commands.clone(),
|
commands: r.commands.clone(),
|
||||||
all_privs: r.all_privs,
|
all_privs: r.all_privs,
|
||||||
|
|
@ -125,6 +144,7 @@ fn set_snomask(s: &mut Server, uid: Uid, letters: &str) {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Resolved {
|
struct Resolved {
|
||||||
title: String,
|
title: String,
|
||||||
|
color: Option<u8>,
|
||||||
all_commands: bool,
|
all_commands: bool,
|
||||||
commands: HashSet<String>,
|
commands: HashSet<String>,
|
||||||
all_privs: bool,
|
all_privs: bool,
|
||||||
|
|
@ -175,6 +195,7 @@ struct TypeDef {
|
||||||
snomasks: String,
|
snomasks: String,
|
||||||
vhost: Option<String>,
|
vhost: Option<String>,
|
||||||
level: Option<u32>,
|
level: Option<u32>,
|
||||||
|
color: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cdef(commands: &[&str], privs: &[&str], sno: &str) -> ClassDef {
|
fn cdef(commands: &[&str], privs: &[&str], sno: &str) -> ClassDef {
|
||||||
|
|
@ -186,7 +207,8 @@ fn cdef(commands: &[&str], privs: &[&str], sno: &str) -> ClassDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tdef(title: &str, classes: &[&str], all_classes: bool, modes: &str, sno: &str, all_sno: bool, level: u32) -> TypeDef {
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn tdef(title: &str, classes: &[&str], all_classes: bool, modes: &str, sno: &str, all_sno: bool, level: u32, color: Option<u8>) -> TypeDef {
|
||||||
TypeDef {
|
TypeDef {
|
||||||
title: title.to_string(),
|
title: title.to_string(),
|
||||||
all_classes,
|
all_classes,
|
||||||
|
|
@ -195,6 +217,7 @@ fn tdef(title: &str, classes: &[&str], all_classes: bool, modes: &str, sno: &str
|
||||||
snomasks: sno.to_string(),
|
snomasks: sno.to_string(),
|
||||||
all_snomasks: all_sno,
|
all_snomasks: all_sno,
|
||||||
level: Some(level),
|
level: Some(level),
|
||||||
|
color,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,12 +233,15 @@ fn builtin() -> (HashMap<String, ClassDef>, HashMap<String, TypeDef>) {
|
||||||
classes.insert("server".into(), cdef(&["CONNECT", "SQUIT", "DIE", "RESTART"], &[], "lr"));
|
classes.insert("server".into(), cdef(&["CONNECT", "SQUIT", "DIE", "RESTART"], &[], "lr"));
|
||||||
|
|
||||||
let mut types: HashMap<String, TypeDef> = HashMap::default();
|
let mut types: HashMap<String, TypeDef> = HashMap::default();
|
||||||
// title classes all modes sno all* level
|
// The WHOIS title line is bold + colour 4 (red) by default; override per type
|
||||||
types.insert("helpop".into(), tdef("Help Operator", &[], false, "+ih", "o", false, 10));
|
// with `color=<name|0-15|none>`.
|
||||||
types.insert("globop".into(), tdef("GlobOp", &["announce"], false, "+iw", "acgoq", false, 20));
|
let red = Some(4);
|
||||||
types.insert("admin".into(), tdef("Administrator", &["announce", "ban", "override", "host"], false, "+iw", "", true, 50));
|
// title classes all modes sno all* level color
|
||||||
types.insert("servadmin".into(), tdef("Services Administrator", &["announce", "ban", "override", "host", "services"], false, "+iw", "", true, 70));
|
types.insert("helpop".into(), tdef("Help Operator", &[], false, "+ih", "o", false, 10, red));
|
||||||
types.insert("netadmin".into(), tdef("Network Administrator", &[], true, "+iw", "", true, 100));
|
types.insert("globop".into(), tdef("GlobOp", &["announce"], false, "+iw", "acgoq", false, 20, red));
|
||||||
|
types.insert("admin".into(), tdef("Administrator", &["announce", "ban", "override", "host"], false, "+iw", "", true, 50, red));
|
||||||
|
types.insert("servadmin".into(), tdef("Services Administrator", &["announce", "ban", "override", "host", "services"], false, "+iw", "", true, 70, red));
|
||||||
|
types.insert("netadmin".into(), tdef("Network Administrator", &[], true, "+iw", "", true, 100, red));
|
||||||
(classes, types)
|
(classes, types)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +340,24 @@ fn apply_type_kv(td: &mut TypeDef, k: &str, v: &str) {
|
||||||
td.level = Some(l);
|
td.level = Some(l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"color" | "colour" => {
|
||||||
|
td.color = match v.to_ascii_lowercase().as_str() {
|
||||||
|
"none" | "off" | "no" | "plain" => None,
|
||||||
|
"white" => Some(0),
|
||||||
|
"black" => Some(1),
|
||||||
|
"blue" => Some(2),
|
||||||
|
"green" => Some(3),
|
||||||
|
"red" => Some(4),
|
||||||
|
"brown" => Some(5),
|
||||||
|
"magenta" | "purple" => Some(6),
|
||||||
|
"orange" => Some(7),
|
||||||
|
"yellow" => Some(8),
|
||||||
|
"cyan" | "teal" => Some(10),
|
||||||
|
"pink" => Some(13),
|
||||||
|
"grey" | "gray" => Some(14),
|
||||||
|
n => n.parse::<u8>().ok().filter(|c| *c <= 15).or(td.color),
|
||||||
|
};
|
||||||
|
}
|
||||||
_ => {} // maxchans etc.: accepted, not yet enforced
|
_ => {} // maxchans etc.: accepted, not yet enforced
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -356,6 +400,7 @@ fn resolve(td: &TypeDef, classes: &HashMap<String, ClassDef>) -> Resolved {
|
||||||
|
|
||||||
Resolved {
|
Resolved {
|
||||||
title: td.title.clone(),
|
title: td.title.clone(),
|
||||||
|
color: td.color,
|
||||||
all_commands,
|
all_commands,
|
||||||
commands,
|
commands,
|
||||||
all_privs,
|
all_privs,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue