add callerid (+g) with ACCEPT allow-list, plus msgid and extended-monitor
This commit is contained in:
parent
9b12791774
commit
1581bc15d2
7 changed files with 187 additions and 5 deletions
|
|
@ -376,6 +376,50 @@ fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) -> CmdResu
|
||||||
}
|
}
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
|
// +g callerid: a +g user only accepts PMs from users on their ACCEPT list.
|
||||||
|
// Others are blocked; the target is told someone tried (718), and a PRIVMSG
|
||||||
|
// sender is told the target is in +g and has been informed (716 + 717).
|
||||||
|
let sender_nick = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.nick.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let target_g = s
|
||||||
|
.users
|
||||||
|
.get(&tuid)
|
||||||
|
.map(|u| u.flags.callerid)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if target_g && uid != tuid && !s.is_accepted(tuid, &sender_nick) {
|
||||||
|
let (tnick, sident, shost) = {
|
||||||
|
let t = s.users.get(&tuid);
|
||||||
|
let u = s.users.get(&uid);
|
||||||
|
(
|
||||||
|
t.map(|x| x.nick.clone()).unwrap_or_default(),
|
||||||
|
u.map(|x| x.ident.clone()).unwrap_or_default(),
|
||||||
|
u.map(|x| x.host_display().to_string()).unwrap_or_default(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
s.numeric(
|
||||||
|
tuid,
|
||||||
|
RPL_UMODEGMSG,
|
||||||
|
&format!(
|
||||||
|
"{sender_nick} {sident}@{shost} :is messaging you, and you have umode +g."
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if !notice {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
RPL_TARGUMODEG,
|
||||||
|
&format!("{tnick} :is in +g mode (server-side ignore)."),
|
||||||
|
);
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
RPL_TARGNOTIFY,
|
||||||
|
&format!("{tnick} :has been informed that you messaged them."),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CmdResult::Ok;
|
||||||
|
}
|
||||||
// SILENCE: if the recipient silenced the sender, drop it silently — the
|
// SILENCE: if the recipient silenced the sender, drop it silently — the
|
||||||
// sender is never told (that's the point), but still gets their own echo.
|
// sender is never told (that's the point), but still gets their own echo.
|
||||||
let silenced = s.is_silenced(tuid, &prefix);
|
let silenced = s.is_silenced(tuid, &prefix);
|
||||||
|
|
@ -398,6 +442,20 @@ fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) -> CmdResu
|
||||||
s.numeric(uid, RPL_AWAY, &format!("{target} :{msg}"));
|
s.numeric(uid, RPL_AWAY, &format!("{target} :{msg}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// callerid convenience: if the SENDER is +g, auto-accept whoever they
|
||||||
|
// message so that person can reply without being blocked.
|
||||||
|
if s.users.get(&uid).map(|u| u.flags.callerid).unwrap_or(false) {
|
||||||
|
let tnick = s
|
||||||
|
.users
|
||||||
|
.get(&tuid)
|
||||||
|
.map(|u| u.nick.to_ascii_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(su) = s.users.get_mut(&uid) {
|
||||||
|
if !tnick.is_empty() && !su.accept.contains(&tnick) {
|
||||||
|
su.accept.push(tnick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Some((uuid, via)) = s.find_remote(target) {
|
} else if let Some((uuid, via)) = s.find_remote(target) {
|
||||||
// the target is a user on another server — route it across the link
|
// the target is a user on another server — route it across the link
|
||||||
s.send_to_remote(uid, &uuid, via, cmd, text);
|
s.send_to_remote(uid, &uuid, via, cmd, text);
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,16 @@ use crate::channels::normalize_mask;
|
||||||
use crate::command::{CmdResult, Command};
|
use crate::command::{CmdResult, Command};
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use crate::watch::{MONITOR_MAX, SILENCE_MAX, WATCH_MAX};
|
use crate::watch::{ACCEPT_MAX, MONITOR_MAX, SILENCE_MAX, WATCH_MAX};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
pub fn commands() -> Vec<Box<dyn Command>> {
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
vec![Box::new(Watch), Box::new(Monitor), Box::new(Silence)]
|
vec![
|
||||||
|
Box::new(Watch),
|
||||||
|
Box::new(Monitor),
|
||||||
|
Box::new(Silence),
|
||||||
|
Box::new(Accept),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WATCH ------------------------------------------------------------------
|
// --- WATCH ------------------------------------------------------------------
|
||||||
|
|
@ -304,3 +309,87 @@ impl Command for Silence {
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- ACCEPT (callerid +g allow-list) ----------------------------------------
|
||||||
|
|
||||||
|
fn accept_list(s: &Server, uid: Uid) {
|
||||||
|
let list = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.accept.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
for n in list {
|
||||||
|
s.numeric(uid, RPL_ACCEPTLIST, &n);
|
||||||
|
}
|
||||||
|
s.numeric(uid, RPL_ENDOFACCEPT, ":End of ACCEPT list");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Accept;
|
||||||
|
impl Command for Accept {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"ACCEPT"
|
||||||
|
}
|
||||||
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||||
|
let first = params.first().map(|a| a.as_str()).unwrap_or("");
|
||||||
|
if first.is_empty() || first == "*" {
|
||||||
|
accept_list(s, uid);
|
||||||
|
return CmdResult::Ok;
|
||||||
|
}
|
||||||
|
for tok in params
|
||||||
|
.iter()
|
||||||
|
.flat_map(|p| p.split([',', ' ']))
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
{
|
||||||
|
if tok == "*" {
|
||||||
|
accept_list(s, uid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (adding, name) = match tok.strip_prefix('-') {
|
||||||
|
Some(n) => (false, n),
|
||||||
|
None => (true, tok.strip_prefix('+').unwrap_or(tok)),
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let low = name.to_ascii_lowercase();
|
||||||
|
if adding {
|
||||||
|
let (full, exists) = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| (u.accept.len() >= ACCEPT_MAX, u.accept.contains(&low)))
|
||||||
|
.unwrap_or((true, false));
|
||||||
|
if exists {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_ACCEPTEXIST,
|
||||||
|
&format!("{name} :is already on your accept list"),
|
||||||
|
);
|
||||||
|
} else if full {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_ACCEPTFULL,
|
||||||
|
&format!("{name} :Your accept list is full"),
|
||||||
|
);
|
||||||
|
} else if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.accept.push(low);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let existed = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.accept.contains(&low))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !existed {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_ACCEPTNOT,
|
||||||
|
&format!("{name} :is not on your accept list"),
|
||||||
|
);
|
||||||
|
} else if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.accept.retain(|x| x != &low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
10
src/mode.rs
10
src/mode.rs
|
|
@ -781,6 +781,7 @@ static USER_MODES: &[&(dyn UserMode + Sync)] = &[
|
||||||
®ISTERED,
|
®ISTERED,
|
||||||
&SSLPM,
|
&SSLPM,
|
||||||
&SNOMASK,
|
&SNOMASK,
|
||||||
|
&CALLERID,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// A simple boolean user flag (+i / +w).
|
/// A simple boolean user flag (+i / +w).
|
||||||
|
|
@ -820,6 +821,9 @@ fn set_regdeaf(f: &mut UserFlags, v: bool) {
|
||||||
fn set_sslpm(f: &mut UserFlags, v: bool) {
|
fn set_sslpm(f: &mut UserFlags, v: bool) {
|
||||||
f.ssl_pm = v;
|
f.ssl_pm = v;
|
||||||
}
|
}
|
||||||
|
fn set_callerid(f: &mut UserFlags, v: bool) {
|
||||||
|
f.callerid = v;
|
||||||
|
}
|
||||||
static BOT: UFlag = UFlag {
|
static BOT: UFlag = UFlag {
|
||||||
ch: 'B',
|
ch: 'B',
|
||||||
set: set_bot,
|
set: set_bot,
|
||||||
|
|
@ -844,6 +848,10 @@ static SSLPM: UFlag = UFlag {
|
||||||
ch: 'z',
|
ch: 'z',
|
||||||
set: set_sslpm,
|
set: set_sslpm,
|
||||||
};
|
};
|
||||||
|
static CALLERID: UFlag = UFlag {
|
||||||
|
ch: 'g',
|
||||||
|
set: set_callerid,
|
||||||
|
};
|
||||||
|
|
||||||
impl UserMode for UFlag {
|
impl UserMode for UFlag {
|
||||||
fn letter(&self) -> char {
|
fn letter(&self) -> char {
|
||||||
|
|
@ -984,7 +992,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_covers_all_user_modes() {
|
fn registry_covers_all_user_modes() {
|
||||||
for c in "iwoxBDIHrRzs".chars() {
|
for c in "iwoxBDIHrRzsg".chars() {
|
||||||
assert!(user_mode(c).is_some(), "missing umode +{c}");
|
assert!(user_mode(c).is_some(), "missing umode +{c}");
|
||||||
}
|
}
|
||||||
assert!(user_mode('Q').is_none());
|
assert!(user_mode('Q').is_none());
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,16 @@ pub const RPL_SILELIST: u16 = 271;
|
||||||
pub const RPL_ENDOFSILENCE: u16 = 272;
|
pub const RPL_ENDOFSILENCE: u16 = 272;
|
||||||
pub const ERR_SILELISTFULL: u16 = 511;
|
pub const ERR_SILELISTFULL: u16 = 511;
|
||||||
|
|
||||||
|
// ACCEPT / callerid (umode +g)
|
||||||
|
pub const RPL_ACCEPTLIST: u16 = 281;
|
||||||
|
pub const RPL_ENDOFACCEPT: u16 = 282;
|
||||||
|
pub const ERR_ACCEPTFULL: u16 = 456;
|
||||||
|
pub const ERR_ACCEPTEXIST: u16 = 457;
|
||||||
|
pub const ERR_ACCEPTNOT: u16 = 458;
|
||||||
|
pub const RPL_TARGUMODEG: u16 = 716; // "<nick> :is in +g mode (server-side ignore)"
|
||||||
|
pub const RPL_TARGNOTIFY: u16 = 717; // "<nick> :has been informed that you messaged them"
|
||||||
|
pub const RPL_UMODEGMSG: u16 = 718; // to the +g user: "<nick> <user@host> :is messaging you…"
|
||||||
|
|
||||||
// WATCH (notify list)
|
// WATCH (notify list)
|
||||||
pub const RPL_LOGON: u16 = 600;
|
pub const RPL_LOGON: u16 = 600;
|
||||||
pub const RPL_LOGOFF: u16 = 601;
|
pub const RPL_LOGOFF: u16 = 601;
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ impl Server {
|
||||||
watch: Vec::new(),
|
watch: Vec::new(),
|
||||||
monitor: Vec::new(),
|
monitor: Vec::new(),
|
||||||
silence: Vec::new(),
|
silence: Vec::new(),
|
||||||
|
accept: Vec::new(),
|
||||||
quitting: None,
|
quitting: None,
|
||||||
flags: UserFlags::default(),
|
flags: UserFlags::default(),
|
||||||
last_active: now(),
|
last_active: now(),
|
||||||
|
|
@ -491,6 +492,7 @@ mod tests {
|
||||||
watch: Vec::new(),
|
watch: Vec::new(),
|
||||||
monitor: Vec::new(),
|
monitor: Vec::new(),
|
||||||
silence: Vec::new(),
|
silence: Vec::new(),
|
||||||
|
accept: Vec::new(),
|
||||||
quitting: None,
|
quitting: None,
|
||||||
flags: UserFlags::default(),
|
flags: UserFlags::default(),
|
||||||
last_active: 0,
|
last_active: 0,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub struct UserFlags {
|
||||||
pub reg_only_pm: bool, // +R (only accept PMs from logged-in users)
|
pub reg_only_pm: bool, // +R (only accept PMs from logged-in users)
|
||||||
pub ssl_pm: bool, // +z (only accept PMs from TLS users)
|
pub ssl_pm: bool, // +z (only accept PMs from TLS users)
|
||||||
pub snomask: bool, // +s (oper: receive server notices)
|
pub snomask: bool, // +s (oper: receive server notices)
|
||||||
|
pub callerid: bool, // +g (only accept PMs from users on the ACCEPT list)
|
||||||
pub away: Option<String>, // AWAY message, if set
|
pub away: Option<String>, // AWAY message, if set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +71,9 @@ impl UserFlags {
|
||||||
if self.snomask {
|
if self.snomask {
|
||||||
s.push('s');
|
s.push('s');
|
||||||
}
|
}
|
||||||
|
if self.callerid {
|
||||||
|
s.push('g');
|
||||||
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +213,7 @@ pub struct User {
|
||||||
pub watch: Vec<String>, // WATCH list — lowercased nicks
|
pub watch: Vec<String>, // WATCH list — lowercased nicks
|
||||||
pub monitor: Vec<String>, // MONITOR list — lowercased nicks
|
pub monitor: Vec<String>, // MONITOR list — lowercased nicks
|
||||||
pub silence: Vec<String>, // SILENCE masks — nick!user@host globs
|
pub silence: Vec<String>, // SILENCE masks — nick!user@host globs
|
||||||
|
pub accept: Vec<String>, // ACCEPT list — lowercased nicks (callerid +g)
|
||||||
pub quitting: Option<String>, // set by QUIT; drained by the core
|
pub quitting: Option<String>, // set by QUIT; drained by the core
|
||||||
pub flags: UserFlags,
|
pub flags: UserFlags,
|
||||||
pub last_active: u64, // unix secs of the last line we received
|
pub last_active: u64, // unix secs of the last line we received
|
||||||
|
|
@ -355,7 +360,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_MYINFO,
|
RPL_MYINFO,
|
||||||
&format!(
|
&format!(
|
||||||
"{} echoircd-{VERSION} iowxsBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGu",
|
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGu",
|
||||||
self.name
|
self.name
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -363,7 +368,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_ISUPPORT,
|
RPL_ISUPPORT,
|
||||||
&format!(
|
&format!(
|
||||||
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFL,CGMNORSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server",
|
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFL,CGMNORSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server",
|
||||||
self.network
|
self.network
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
10
src/watch.rs
10
src/watch.rs
|
|
@ -18,6 +18,7 @@ use crate::Uid;
|
||||||
pub const WATCH_MAX: usize = 128;
|
pub const WATCH_MAX: usize = 128;
|
||||||
pub const MONITOR_MAX: usize = 128;
|
pub const MONITOR_MAX: usize = 128;
|
||||||
pub const SILENCE_MAX: usize = 32;
|
pub const SILENCE_MAX: usize = 32;
|
||||||
|
pub const ACCEPT_MAX: usize = 64;
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
/// A nick just came online (registered, or someone renamed to it): tell its
|
/// A nick just came online (registered, or someone renamed to it): tell its
|
||||||
|
|
@ -74,6 +75,15 @@ impl Server {
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `sender_nick` is on `target`'s ACCEPT list (callerid +g).
|
||||||
|
pub fn is_accepted(&self, target: Uid, sender_nick: &str) -> bool {
|
||||||
|
let low = sender_nick.to_ascii_lowercase();
|
||||||
|
self.users
|
||||||
|
.get(&target)
|
||||||
|
.map(|u| u.accept.contains(&low))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// True if user `by` has silenced someone whose prefix is `sender_mask`.
|
/// True if user `by` has silenced someone whose prefix is `sender_mask`.
|
||||||
pub fn is_silenced(&self, by: Uid, sender_mask: &str) -> bool {
|
pub fn is_silenced(&self, by: Uid, sender_mask: &str) -> bool {
|
||||||
self.users
|
self.users
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue