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::numeric::*;
|
||||
use crate::server::{now, Server};
|
||||
use crate::server::{iso_time, now, HistMsg, Server};
|
||||
use crate::Uid;
|
||||
|
||||
/// 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 nickflood: Option<Rate>, // +F
|
||||
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 {
|
||||
|
|
@ -217,6 +218,9 @@ impl ChanModes {
|
|||
if self.redirect.is_some() {
|
||||
s.push('L');
|
||||
}
|
||||
if self.history.is_some() {
|
||||
s.push('H');
|
||||
}
|
||||
if params {
|
||||
if let Some(k) = &self.key {
|
||||
s.push(' ');
|
||||
|
|
@ -240,6 +244,9 @@ impl ChanModes {
|
|||
s.push(' ');
|
||||
s.push_str(t);
|
||||
}
|
||||
if let Some((n, t)) = &self.history {
|
||||
s.push_str(&format!(" {n}:{t}"));
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
|
@ -518,10 +525,64 @@ impl Server {
|
|||
self.numeric(uid, RPL_TOPIC, &format!("{name} :{text}"));
|
||||
}
|
||||
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.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) {
|
||||
let Some(ch) = self.channels.get(key) else {
|
||||
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,
|
||||
&NICKFLOOD,
|
||||
&REDIRECT,
|
||||
&CHANHISTORY,
|
||||
];
|
||||
|
||||
// --- 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) ----------
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ impl Server {
|
|||
uid,
|
||||
RPL_ISUPPORT,
|
||||
&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
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue