opers: add users/ignore-privdeaf — reach +D deaf users

A holder's channel messages reach +D (deaf) members: gate the deaf-skip in
to_channel_tagged and the TAGMSG fanout on the sender's users/ignore-privdeaf
(relayed/service messages, which have no local sender, are unaffected). Granted
by the auspex class; verified live (netadmin reaches a deaf user, a restricted
oper does not).
This commit is contained in:
Jean Chevronnet 2026-08-29 13:46:25 +00:00
parent 5dfedb5091
commit 2d42fbd2ca
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
4 changed files with 17 additions and 5 deletions

View file

@ -91,6 +91,7 @@ command. Assign them via `privs=` on a class or type (`*` = all, `-x` removes on
| `users/flood` | exemption from the message- and join-flood limits | | `users/flood` | exemption from the message- and join-flood limits |
| `users/ignore-commonchans` | message a `+c` user without sharing a common channel | | `users/ignore-commonchans` | message a `+c` user without sharing a common channel |
| `users/ignore-callerid` | message a `+g` (caller-ID) user without being on their accept list | | `users/ignore-callerid` | message a `+g` (caller-ID) user without being on their accept list |
| `users/ignore-privdeaf` | reach a `+D` (deaf) user with your channel messages despite their deafness |
| `users/ignore-restrictmsg` | private-message anyone while `restrictmsg` is on | | `users/ignore-restrictmsg` | private-message anyone while `restrictmsg` is on |
| `users/secret-whois` | `/WHOIS` a `+W` (showwhois) user without notifying them | | `users/secret-whois` | `/WHOIS` a `+W` (showwhois) user without notifying them |
| `servers/use-disabled-commands` | use a command turned off by `disabled_commands` | | `servers/use-disabled-commands` | use a command turned off by `disabled_commands` |
@ -100,8 +101,9 @@ command. Assign them via `privs=` on a class or type (`*` = all, `-x` removes on
The built-in `override` class carries the channel/message/anti-spam bypasses The built-in `override` class carries the channel/message/anti-spam bypasses
(`channels/restricted-create`, `channels/ignore-nonicks`, `users/ignore-restrictmsg`, (`channels/restricted-create`, `channels/ignore-nonicks`, `users/ignore-restrictmsg`,
`servers/ignore-securelist`, `servers/ignore-blockamsg`), `auspex` carries the `servers/ignore-securelist`, `servers/ignore-blockamsg`), `auspex` carries the
see-through-privacy set (the three `*/auspex` plus `users/secret-whois` and see-through-privacy set (the three `*/auspex` plus `users/secret-whois`,
`users/ignore-callerid`), and `servers/use-disabled-commands` sits on the `server` class. `users/ignore-callerid` and `users/ignore-privdeaf`), and `servers/use-disabled-commands`
sits on the `server` class.
## Snomasks ## Snomasks

View file

@ -755,8 +755,12 @@ impl Command for TagMsg {
.get(&key) .get(&key)
.map(|c| c.members.keys().copied().collect()) .map(|c| c.members.keys().copied().collect())
.unwrap_or_default(); .unwrap_or_default();
let reach_deaf =
crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::USERS_IGNORE_PRIVDEAF);
for m in members { for m in members {
if (m == uid && !echo) || s.users.get(&m).map(|u| u.flags.deaf).unwrap_or(false) { if (m == uid && !echo)
|| (s.users.get(&m).map(|u| u.flags.deaf).unwrap_or(false) && !reach_deaf)
{
continue; continue;
} }
// only message-tags clients receive a TAGMSG // only message-tags clients receive a TAGMSG

View file

@ -42,6 +42,8 @@ pub mod privs {
pub const CHANNELS_IGNORE_NONICKS: &str = "channels/ignore-nonicks"; pub const CHANNELS_IGNORE_NONICKS: &str = "channels/ignore-nonicks";
/// message a +g (caller-id) user without being on their ACCEPT list /// message a +g (caller-id) user without being on their ACCEPT list
pub const USERS_IGNORE_CALLERID: &str = "users/ignore-callerid"; pub const USERS_IGNORE_CALLERID: &str = "users/ignore-callerid";
/// reach a +D (deaf) user with your channel messages despite their deafness
pub const USERS_IGNORE_PRIVDEAF: &str = "users/ignore-privdeaf";
/// `/WHOIS` a +W (showwhois) user without notifying them /// `/WHOIS` a +W (showwhois) user without notifying them
pub const USERS_SECRET_WHOIS: &str = "users/secret-whois"; pub const USERS_SECRET_WHOIS: &str = "users/secret-whois";
/// private-message anyone while `restrictmsg` is on /// private-message anyone while `restrictmsg` is on
@ -367,7 +369,7 @@ fn builtin() -> (HashMap<String, ClassDef>, HashMap<String, TypeDef>) {
classes.insert("services".into(), cdef(&["SVSNICK", "SVSJOIN", "SVSPART", "SVSMODE", "SVSLOGIN", "SVSLOGOUT"], &[], "")); classes.insert("services".into(), cdef(&["SVSNICK", "SVSJOIN", "SVSPART", "SVSMODE", "SVSLOGIN", "SVSLOGOUT"], &[], ""));
classes.insert("server".into(), cdef(&["CONNECT", "SQUIT", "DIE", "RESTART"], &["servers/use-disabled-commands"], "lr")); classes.insert("server".into(), cdef(&["CONNECT", "SQUIT", "DIE", "RESTART"], &["servers/use-disabled-commands"], "lr"));
// auspex: see through user/channel privacy (real host+IP, geo, secret channels) // auspex: see through user/channel privacy (real host+IP, geo, secret channels)
classes.insert("auspex".into(), cdef(&[], &["users/auspex", "channels/auspex", "servers/auspex", "users/secret-whois", "users/ignore-callerid"], "")); classes.insert("auspex".into(), cdef(&[], &["users/auspex", "channels/auspex", "servers/auspex", "users/secret-whois", "users/ignore-callerid", "users/ignore-privdeaf"], ""));
let mut types: HashMap<String, TypeDef> = HashMap::default(); let mut types: HashMap<String, TypeDef> = HashMap::default();
// The WHOIS title line is bold + colour 4 (red) by default; override per type // The WHOIS title line is bold + colour 4 (red) by default; override per type
@ -685,6 +687,7 @@ mod tests {
assert!(has("override", "users/ignore-restrictmsg")); assert!(has("override", "users/ignore-restrictmsg"));
assert!(has("override", "servers/ignore-securelist") && has("override", "servers/ignore-blockamsg")); assert!(has("override", "servers/ignore-securelist") && has("override", "servers/ignore-blockamsg"));
assert!(has("auspex", "users/secret-whois") && has("auspex", "users/ignore-callerid")); assert!(has("auspex", "users/secret-whois") && has("auspex", "users/ignore-callerid"));
assert!(has("auspex", "users/ignore-privdeaf"));
assert!(has("server", "servers/use-disabled-commands")); assert!(has("server", "servers/use-disabled-commands"));
// netadmin holds every class ⇒ every one of the new privileges resolves in // netadmin holds every class ⇒ every one of the new privileges resolves in
assert!(resolved("netadmin").all_privs); assert!(resolved("netadmin").all_privs);

View file

@ -1354,6 +1354,9 @@ impl Server {
let src_mask = self.users.get(&src).map(|su| su.prefix()).unwrap_or_default(); let src_mask = self.users.get(&src).map(|su| su.prefix()).unwrap_or_default();
let src_signore: Vec<String> = let src_signore: Vec<String> =
self.users.get(&src).map(|su| su.signore.clone()).unwrap_or_default(); self.users.get(&src).map(|su| su.signore.clone()).unwrap_or_default();
// users/ignore-privdeaf: this sender's channel messages reach +D deaf members
let src_reaches_deaf =
crate::modules::opertypes::has_priv(self, src, crate::modules::opertypes::privs::USERS_IGNORE_PRIVDEAF);
// one cached line per (server_time, account_tag, message_tags) combination // one cached line per (server_time, account_tag, message_tags) combination
let mut cache: [Option<std::sync::Arc<str>>; 8] = std::array::from_fn(|_| None); let mut cache: [Option<std::sync::Arc<str>>; 8] = std::array::from_fn(|_| None);
for m in members { for m in members {
@ -1363,7 +1366,7 @@ impl Server {
let Some(u) = self.users.get(&m) else { let Some(u) = self.users.get(&m) else {
continue; continue;
}; };
if u.flags.deaf { if u.flags.deaf && !src_reaches_deaf {
continue; continue;
} }
// SIGNORE: skip a member mutually server-ignored with the sender // SIGNORE: skip a member mutually server-ignored with the sender