delayjoin +D: hide JOINs until a member speaks/reveals (NAMES + part/quit/nick aware)
This commit is contained in:
parent
80cc61e7f8
commit
97a35338fa
6 changed files with 134 additions and 7 deletions
|
|
@ -20,6 +20,7 @@ pub struct Member {
|
||||||
pub voice: bool, // +v (+)
|
pub voice: bool, // +v (+)
|
||||||
pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown)
|
pub joined: u64, // unix ts this member joined (for +d delaymsg; 0 = unknown)
|
||||||
pub recent_msgs: Vec<String>, // +K repeat: this member's last few lines here
|
pub recent_msgs: Vec<String>, // +K repeat: this member's last few lines here
|
||||||
|
pub hidden: bool, // +D delayjoin: JOIN withheld until they reveal themselves
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom.
|
/// Prefix ranks, high→low — gate who may grant a prefix / kick whom.
|
||||||
|
|
@ -159,6 +160,7 @@ pub struct ChanModes {
|
||||||
pub opmoderated: bool, // +U — unprivileged users' messages go to ops only
|
pub opmoderated: bool, // +U — unprivileged users' messages go to ops only
|
||||||
pub delaymsg: Option<u32>, // +d <secs> — new joiners can't speak for N secs
|
pub delaymsg: Option<u32>, // +d <secs> — new joiners can't speak for N secs
|
||||||
pub repeat: Option<u32>, // +K <n> — block a line repeated within your last n
|
pub repeat: Option<u32>, // +K <n> — block a line repeated within your last n
|
||||||
|
pub delayjoin: bool, // +D — hide JOINs until the user speaks/reveals
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChanModes {
|
impl ChanModes {
|
||||||
|
|
@ -186,6 +188,7 @@ impl ChanModes {
|
||||||
'A' => self.allowinvite = on,
|
'A' => self.allowinvite = on,
|
||||||
'P' => self.permanent = on,
|
'P' => self.permanent = on,
|
||||||
'U' => self.opmoderated = on,
|
'U' => self.opmoderated = on,
|
||||||
|
'D' => self.delayjoin = on,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -357,6 +360,66 @@ impl Server {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// +D delayjoin: announce a hidden member's withheld JOIN now (they revealed
|
||||||
|
/// themselves by speaking / being opped / changing nick). No-op if not hidden.
|
||||||
|
pub fn reveal_member(&mut self, uid: Uid, key: &str) {
|
||||||
|
let hidden = self
|
||||||
|
.channels
|
||||||
|
.get(key)
|
||||||
|
.and_then(|c| c.members.get(&uid))
|
||||||
|
.map(|m| m.hidden)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !hidden {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(m) = self
|
||||||
|
.channels
|
||||||
|
.get_mut(key)
|
||||||
|
.and_then(|c| c.members.get_mut(&uid))
|
||||||
|
{
|
||||||
|
m.hidden = false;
|
||||||
|
}
|
||||||
|
let name = self
|
||||||
|
.channels
|
||||||
|
.get(key)
|
||||||
|
.map(|c| c.name.clone())
|
||||||
|
.unwrap_or_else(|| key.to_string());
|
||||||
|
let aud = self
|
||||||
|
.channels
|
||||||
|
.get(key)
|
||||||
|
.map(|c| c.modes.auditorium)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let (prefix, acct, realname) = match self.users.get(&uid) {
|
||||||
|
Some(u) => (
|
||||||
|
u.prefix(),
|
||||||
|
u.account.clone().unwrap_or_else(|| "*".to_string()),
|
||||||
|
u.realname.clone(),
|
||||||
|
),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let plain = format!(":{prefix} JOIN {name}");
|
||||||
|
let extended = format!(":{prefix} JOIN {name} {acct} :{realname}");
|
||||||
|
let members: Vec<Uid> = self
|
||||||
|
.channels
|
||||||
|
.get(key)
|
||||||
|
.map(|c| c.members.keys().copied().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
for m in members {
|
||||||
|
if m == uid {
|
||||||
|
continue; // they already saw their own JOIN
|
||||||
|
}
|
||||||
|
if aud && self.rank(m, key) < RANK_OP {
|
||||||
|
continue; // +u auditorium still hides them from non-ops
|
||||||
|
}
|
||||||
|
let ext = self
|
||||||
|
.users
|
||||||
|
.get(&m)
|
||||||
|
.map(|u| u.caps.extended_join)
|
||||||
|
.unwrap_or(false);
|
||||||
|
self.send(m, if ext { extended.clone() } else { plain.clone() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// +X exemptchanops — is `uid` exempt from `restriction` in this channel? True
|
/// +X exemptchanops — is `uid` exempt from `restriction` in this channel? True
|
||||||
/// when a `+X <restriction>:<rankchar>` entry names a rank they meet or exceed.
|
/// when a `+X <restriction>:<rankchar>` entry names a rank they meet or exceed.
|
||||||
pub fn chanop_exempt(&self, uid: Uid, key: &str, restriction: &str) -> bool {
|
pub fn chanop_exempt(&self, uid: Uid, key: &str, restriction: &str) -> bool {
|
||||||
|
|
@ -412,7 +475,19 @@ impl Server {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let prefix = self.users[&uid].prefix();
|
let prefix = self.users[&uid].prefix();
|
||||||
self.to_channel(&key, &format!(":{prefix} PART {chan} :{reason}"), None);
|
// +D delayjoin: a still-hidden member's PART is shown only to themselves
|
||||||
|
let hidden = self
|
||||||
|
.channels
|
||||||
|
.get(&key)
|
||||||
|
.and_then(|c| c.members.get(&uid))
|
||||||
|
.map(|m| m.hidden)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let line = format!(":{prefix} PART {chan} :{reason}");
|
||||||
|
if hidden {
|
||||||
|
self.send(uid, line);
|
||||||
|
} else {
|
||||||
|
self.to_channel(&key, &line, None);
|
||||||
|
}
|
||||||
self.propagate_part(uid, chan, reason);
|
self.propagate_part(uid, chan, reason);
|
||||||
if let Some(ch) = self.channels.get_mut(&key) {
|
if let Some(ch) = self.channels.get_mut(&key) {
|
||||||
ch.members.remove(&uid);
|
ch.members.remove(&uid);
|
||||||
|
|
@ -639,12 +714,27 @@ impl Server {
|
||||||
let plain = format!(":{prefix} JOIN {name}");
|
let plain = format!(":{prefix} JOIN {name}");
|
||||||
let extended = format!(":{prefix} JOIN {name} {acct} :{realname}");
|
let extended = format!(":{prefix} JOIN {name} {acct} :{realname}");
|
||||||
let aud = self.channels[&key].modes.auditorium;
|
let aud = self.channels[&key].modes.auditorium;
|
||||||
|
// +D delayjoin: withhold the JOIN from everyone else until they speak/reveal
|
||||||
|
let delayjoin = self.channels[&key].modes.delayjoin;
|
||||||
|
if delayjoin {
|
||||||
|
if let Some(m) = self
|
||||||
|
.channels
|
||||||
|
.get_mut(&key)
|
||||||
|
.and_then(|c| c.members.get_mut(&uid))
|
||||||
|
{
|
||||||
|
m.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
let members: Vec<Uid> = self.channels[&key].members.keys().copied().collect();
|
let members: Vec<Uid> = self.channels[&key].members.keys().copied().collect();
|
||||||
for m in members {
|
for m in members {
|
||||||
// +u auditorium: non-op members don't see other users join
|
// +u auditorium: non-op members don't see other users join
|
||||||
if aud && m != uid && self.rank(m, &key) < RANK_OP {
|
if aud && m != uid && self.rank(m, &key) < RANK_OP {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// +D: only the joining user sees their own JOIN for now
|
||||||
|
if delayjoin && m != uid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ext = self
|
let ext = self
|
||||||
.users
|
.users
|
||||||
.get(&m)
|
.get(&m)
|
||||||
|
|
@ -747,6 +837,10 @@ impl Server {
|
||||||
if hide && *m != uid && flags.rank() < RANK_OP {
|
if hide && *m != uid && flags.rank() < RANK_OP {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// +D delayjoin: a still-hidden member isn't shown to anyone but themselves
|
||||||
|
if flags.hidden && *m != uid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let p = if multi {
|
let p = if multi {
|
||||||
flags.all_prefixes()
|
flags.all_prefixes()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -434,7 +434,18 @@ impl Command for Part {
|
||||||
} else {
|
} else {
|
||||||
format!(":{prefix} PART {target} :{reason}")
|
format!(":{prefix} PART {target} :{reason}")
|
||||||
};
|
};
|
||||||
s.to_channel_vis(&key, &line, uid); // +u: only ops + self see the part
|
// +D delayjoin: a still-hidden member's PART is shown only to themselves
|
||||||
|
let hidden = s
|
||||||
|
.channels
|
||||||
|
.get(&key)
|
||||||
|
.and_then(|c| c.members.get(&uid))
|
||||||
|
.map(|m| m.hidden)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if hidden {
|
||||||
|
s.send(uid, line.clone());
|
||||||
|
} else {
|
||||||
|
s.to_channel_vis(&key, &line, uid); // +u: only ops + self see the part
|
||||||
|
}
|
||||||
s.propagate_part(uid, target, &reason); // tell linked servers
|
s.propagate_part(uid, target, &reason); // tell linked servers
|
||||||
if let Some(ch) = s.channels.get_mut(&key) {
|
if let Some(ch) = s.channels.get_mut(&key) {
|
||||||
ch.members.remove(&uid);
|
ch.members.remove(&uid);
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,8 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// +D delayjoin: speaking reveals a hidden member (their JOIN goes out first)
|
||||||
|
s.reveal_member(uid, &key);
|
||||||
// deliver to every member except the sender and +D (deaf) users, tagging
|
// deliver to every member except the sender and +D (deaf) users, tagging
|
||||||
// per-recipient (server-time + any client-only tags on the line)
|
// per-recipient (server-time + any client-only tags on the line)
|
||||||
let line = format!(":{prefix} {cmd} {target} :{body}");
|
let line = format!(":{prefix} {cmd} {target} :{body}");
|
||||||
|
|
|
||||||
14
src/mode.rs
14
src/mode.rs
|
|
@ -93,6 +93,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[
|
||||||
&DELAYMSG,
|
&DELAYMSG,
|
||||||
&REPEAT,
|
&REPEAT,
|
||||||
&EXEMPTCHANOPS,
|
&EXEMPTCHANOPS,
|
||||||
|
&DELAYJOIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ----------
|
// --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ----------
|
||||||
|
|
@ -180,6 +181,10 @@ impl ChanMode for Prefix {
|
||||||
_ => m.voice = adding,
|
_ => m.voice = adding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// +D delayjoin: gaining a prefix reveals a hidden member
|
||||||
|
if adding {
|
||||||
|
s.reveal_member(tuid, key);
|
||||||
|
}
|
||||||
Applied::Yes(Some(pn.to_string()))
|
Applied::Yes(Some(pn.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -314,6 +319,9 @@ fn set_permanent(m: &mut ChanModes, v: bool) {
|
||||||
fn set_opmoderated(m: &mut ChanModes, v: bool) {
|
fn set_opmoderated(m: &mut ChanModes, v: bool) {
|
||||||
m.opmoderated = v;
|
m.opmoderated = v;
|
||||||
}
|
}
|
||||||
|
fn set_delayjoin(m: &mut ChanModes, v: bool) {
|
||||||
|
m.delayjoin = v;
|
||||||
|
}
|
||||||
static NOKICKS: Flag = Flag {
|
static NOKICKS: Flag = Flag {
|
||||||
ch: 'Q',
|
ch: 'Q',
|
||||||
set: set_nokicks,
|
set: set_nokicks,
|
||||||
|
|
@ -330,6 +338,10 @@ static OPMODERATED: Flag = Flag {
|
||||||
ch: 'U',
|
ch: 'U',
|
||||||
set: set_opmoderated,
|
set: set_opmoderated,
|
||||||
};
|
};
|
||||||
|
static DELAYJOIN: Flag = Flag {
|
||||||
|
ch: 'D',
|
||||||
|
set: set_delayjoin,
|
||||||
|
};
|
||||||
|
|
||||||
impl ChanMode for Flag {
|
impl ChanMode for Flag {
|
||||||
fn letter(&self) -> char {
|
fn letter(&self) -> char {
|
||||||
|
|
@ -1247,7 +1259,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_covers_all_channel_modes() {
|
fn registry_covers_all_channel_modes() {
|
||||||
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJUdKX".chars() {
|
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAPJUdKXD".chars() {
|
||||||
assert!(chan_mode(c).is_some(), "missing handler for +{c}");
|
assert!(chan_mode(c).is_some(), "missing handler for +{c}");
|
||||||
}
|
}
|
||||||
assert!(chan_mode('y').is_none());
|
assert!(chan_mode('y').is_none());
|
||||||
|
|
|
||||||
|
|
@ -451,9 +451,13 @@ impl Server {
|
||||||
let mut seen: HashSet<Uid> = HashSet::new();
|
let mut seen: HashSet<Uid> = HashSet::new();
|
||||||
for key in &user.channels {
|
for key in &user.channels {
|
||||||
if let Some(ch) = self.channels.get_mut(key) {
|
if let Some(ch) = self.channels.get_mut(key) {
|
||||||
|
// +D delayjoin: if their JOIN here was never announced, no QUIT either
|
||||||
|
let hidden = ch.members.get(&uid).map(|m| m.hidden).unwrap_or(false);
|
||||||
ch.members.remove(&uid);
|
ch.members.remove(&uid);
|
||||||
for &m in ch.members.keys() {
|
if !hidden {
|
||||||
seen.insert(m);
|
for &m in ch.members.keys() {
|
||||||
|
seen.insert(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,10 @@ impl Server {
|
||||||
let chans: Vec<String> = self.users[&uid].channels.iter().cloned().collect();
|
let chans: Vec<String> = self.users[&uid].channels.iter().cloned().collect();
|
||||||
for key in &chans {
|
for key in &chans {
|
||||||
if let Some(ch) = self.channels.get(key) {
|
if let Some(ch) = self.channels.get(key) {
|
||||||
|
// +D delayjoin: a still-hidden member's NICK isn't shown here
|
||||||
|
if ch.members.get(&uid).map(|m| m.hidden).unwrap_or(false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for &m in ch.members.keys() {
|
for &m in ch.members.keys() {
|
||||||
targets.insert(m);
|
targets.insert(m);
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +422,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_MYINFO,
|
RPL_MYINFO,
|
||||||
&format!(
|
&format!(
|
||||||
"{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKX",
|
"{} echoircd-{VERSION} iowxsgBDIHrRzWc qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAPJUdKXD",
|
||||||
self.name
|
self.name
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -426,7 +430,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_ISUPPORT,
|
RPL_ISUPPORT,
|
||||||
&format!(
|
&format!(
|
||||||
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACGMNOPQRSTUcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server",
|
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIgX,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server",
|
||||||
self.network
|
self.network
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue