channel +H chanhistory: replay recent messages to joiners (m_chanhistory), reuses the history store
This commit is contained in:
parent
d4c4c1a7d0
commit
7bf9e4c0b8
3 changed files with 130 additions and 25 deletions
|
|
@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::module::Hook;
|
use crate::module::Hook;
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::{now, Server};
|
use crate::server::{iso_time, now, HistMsg, Server};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`].
|
/// Per-member prefix modes (+q/+a/+o/+h/+v). Flag modes live in [`ChanModes`].
|
||||||
|
|
@ -146,6 +146,7 @@ pub struct ChanModes {
|
||||||
pub joinflood: Option<Rate>, // +j
|
pub joinflood: Option<Rate>, // +j
|
||||||
pub nickflood: Option<Rate>, // +F
|
pub nickflood: Option<Rate>, // +F
|
||||||
pub redirect: Option<String>, // +L <#target> — when full, send there
|
pub redirect: Option<String>, // +L <#target> — when full, send there
|
||||||
|
pub history: Option<(u32, u64)>, // +H <lines>:<secs> — replay recent messages to joiners
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChanModes {
|
impl ChanModes {
|
||||||
|
|
@ -217,6 +218,9 @@ impl ChanModes {
|
||||||
if self.redirect.is_some() {
|
if self.redirect.is_some() {
|
||||||
s.push('L');
|
s.push('L');
|
||||||
}
|
}
|
||||||
|
if self.history.is_some() {
|
||||||
|
s.push('H');
|
||||||
|
}
|
||||||
if params {
|
if params {
|
||||||
if let Some(k) = &self.key {
|
if let Some(k) = &self.key {
|
||||||
s.push(' ');
|
s.push(' ');
|
||||||
|
|
@ -240,6 +244,9 @@ impl ChanModes {
|
||||||
s.push(' ');
|
s.push(' ');
|
||||||
s.push_str(t);
|
s.push_str(t);
|
||||||
}
|
}
|
||||||
|
if let Some((n, t)) = &self.history {
|
||||||
|
s.push_str(&format!(" {n}:{t}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
@ -518,10 +525,64 @@ impl Server {
|
||||||
self.numeric(uid, RPL_TOPIC, &format!("{name} :{text}"));
|
self.numeric(uid, RPL_TOPIC, &format!("{name} :{text}"));
|
||||||
}
|
}
|
||||||
self.send_names(uid, &key);
|
self.send_names(uid, &key);
|
||||||
|
self.replay_chanhistory(uid, &key); // +H: replay recent messages to the joiner
|
||||||
self.propagate_join(uid, name); // tell linked servers this user joined
|
self.propagate_join(uid, name); // tell linked servers this user joined
|
||||||
self.events.push_back(Hook::Join(uid, key));
|
self.events.push_back(Hook::Join(uid, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// +H chanhistory: replay a channel's recent messages to a user who just
|
||||||
|
/// joined — the last `<lines>` (within `<secs>`, 0 = no limit) from the store,
|
||||||
|
/// wrapped in a `chathistory` batch for batch-capable clients.
|
||||||
|
fn replay_chanhistory(&mut self, uid: Uid, key: &str) {
|
||||||
|
let Some((lines, secs)) = self.channels.get(key).and_then(|c| c.modes.history) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(name) = self.channels.get(key).map(|c| c.name.clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let batch = self.users.get(&uid).map(|u| u.caps.batch).unwrap_or(false);
|
||||||
|
let cutoff = if secs > 0 {
|
||||||
|
now().saturating_sub(secs)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let bref = if batch {
|
||||||
|
Some(self.next_msgid().replace('-', ""))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let out: Vec<String> = match self.history.get(key) {
|
||||||
|
Some(buf) => {
|
||||||
|
let mut recent: Vec<&HistMsg> = buf.iter().filter(|m| m.ts >= cutoff).collect();
|
||||||
|
let start = recent.len().saturating_sub(lines as usize);
|
||||||
|
recent.drain(..start);
|
||||||
|
recent
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
let mut tags = format!("time={};msgid={}", iso_time(m.ts), m.msgid);
|
||||||
|
if let Some(b) = &bref {
|
||||||
|
tags.push_str(&format!(";batch={b}"));
|
||||||
|
}
|
||||||
|
format!("@{tags} :{} {} {name} :{}", m.prefix, m.verb, m.text)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
if out.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(b) = &bref {
|
||||||
|
self.send(uid, format!(":{} BATCH +{b} chathistory {name}", self.name));
|
||||||
|
}
|
||||||
|
for l in out {
|
||||||
|
self.send(uid, l);
|
||||||
|
}
|
||||||
|
if let Some(b) = &bref {
|
||||||
|
self.send(uid, format!(":{} BATCH -{b}", self.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_names(&self, uid: Uid, key: &str) {
|
pub fn send_names(&self, uid: Uid, key: &str) {
|
||||||
let Some(ch) = self.channels.get(key) else {
|
let Some(ch) = self.channels.get(key) else {
|
||||||
self.numeric(uid, RPL_ENDOFNAMES, &format!("{key} :End of /NAMES list"));
|
self.numeric(uid, RPL_ENDOFNAMES, &format!("{key} :End of /NAMES list"));
|
||||||
|
|
|
||||||
44
src/mode.rs
44
src/mode.rs
|
|
@ -83,6 +83,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[
|
||||||
&JOINFLOOD,
|
&JOINFLOOD,
|
||||||
&NICKFLOOD,
|
&NICKFLOOD,
|
||||||
&REDIRECT,
|
&REDIRECT,
|
||||||
|
&CHANHISTORY,
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- 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 ----------
|
||||||
|
|
@ -390,6 +391,49 @@ impl ChanMode for Limit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- +H chanhistory: replay recent messages to joiners ----------------------
|
||||||
|
|
||||||
|
struct ChanHistory;
|
||||||
|
static CHANHISTORY: ChanHistory = ChanHistory;
|
||||||
|
impl ChanMode for ChanHistory {
|
||||||
|
fn letter(&self) -> char {
|
||||||
|
'H'
|
||||||
|
}
|
||||||
|
fn wants_param(&self, adding: bool) -> bool {
|
||||||
|
adding // +H <lines>[:<secs>]; -H takes nothing
|
||||||
|
}
|
||||||
|
fn apply(
|
||||||
|
&self,
|
||||||
|
s: &mut Server,
|
||||||
|
_chan: &str,
|
||||||
|
key: &str,
|
||||||
|
_uid: Uid,
|
||||||
|
adding: bool,
|
||||||
|
param: Option<&str>,
|
||||||
|
) -> Applied {
|
||||||
|
if adding {
|
||||||
|
let Some(p) = param else {
|
||||||
|
return Applied::No;
|
||||||
|
};
|
||||||
|
let (lines_s, secs_s) = p.split_once(':').unwrap_or((p, "0"));
|
||||||
|
let Some(lines) = lines_s.parse::<u32>().ok().filter(|&n| n > 0) else {
|
||||||
|
return Applied::No;
|
||||||
|
};
|
||||||
|
let lines = lines.min(crate::server::HISTORY_CAP as u32);
|
||||||
|
let secs = secs_s.parse::<u64>().unwrap_or(0);
|
||||||
|
if let Some(c) = s.channels.get_mut(key) {
|
||||||
|
c.modes.history = Some((lines, secs));
|
||||||
|
}
|
||||||
|
Applied::Yes(Some(format!("{lines}:{secs}")))
|
||||||
|
} else {
|
||||||
|
if let Some(c) = s.channels.get_mut(key) {
|
||||||
|
c.modes.history = None;
|
||||||
|
}
|
||||||
|
Applied::Yes(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- list modes (+b bans, +e ban exceptions, +I invite exceptions) ----------
|
// --- list modes (+b bans, +e ban exceptions, +I invite exceptions) ----------
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,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 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=beIg,k,lfjFLH,CGMNORSTcimnpstuz 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