move draft/multiline into its own module file

This commit is contained in:
Jean Chevronnet 2026-08-08 22:49:54 +00:00
parent 768435d810
commit ab7188bb70
6 changed files with 148 additions and 110 deletions

View file

@ -10,6 +10,7 @@ pub mod filter;
pub mod flood;
pub mod markread;
pub mod metadata;
pub mod multiline;
pub mod snoop;
use crate::command::Command;
@ -25,6 +26,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
Box::new(filter::Filter),
Box::new(metadata::Metadata),
Box::new(markread::MarkRead),
Box::new(multiline::Multiline),
]
}
@ -35,5 +37,6 @@ pub fn module_commands() -> Vec<Box<dyn Command>> {
.into_iter()
.chain(metadata::commands())
.chain(markread::commands())
.chain(multiline::commands())
.collect()
}

138
src/modules/multiline.rs Normal file
View file

@ -0,0 +1,138 @@
//! multiline — the server side of IRCv3 draft/multiline. A client wraps one long
//! message in a `BATCH +<ref> draft/multiline <target>`; the `@batch=<ref>`-tagged
//! PRIVMSG/NOTICE lines are buffered here (see `Ircd::dispatch`) and, when
//! `BATCH -<ref>` closes, reassembled (honouring `draft/multiline-concat`) and
//! delivered as normal messages. Self-contained: the in-flight batches live in
//! `Server.ext`, cleaned up by the on_user_quit hook; the BATCH command and the
//! accumulate/close logic are all here.
use std::collections::HashMap;
use crate::command::{CmdResult, Command};
use crate::coremods::core_message::deliver;
use crate::module::Module;
use crate::server::Server;
use crate::Uid;
/// Limits advertised in the `draft/multiline` cap and enforced while buffering.
pub const MAX_BYTES: usize = 4096;
pub const MAX_LINES: usize = 24;
/// An in-progress inbound multiline batch — one long client message being
/// assembled from several `@batch=`-tagged PRIVMSG/NOTICE lines.
pub struct MlineBatch {
pub bref: String,
pub target: String,
pub notice: bool,
pub parts: Vec<(String, bool)>, // (text, concat-with-previous-part)
pub bytes: usize,
}
/// uid -> its open batch. Stored in `Server.ext`.
#[derive(Default)]
pub struct Mline(pub HashMap<Uid, MlineBatch>);
/// Open an inbound batch for `uid` (a client assembling one long message from
/// several tagged PRIVMSG/NOTICE lines).
fn open(s: &mut Server, uid: Uid, bref: &str, target: &str) {
s.ext.get_or_insert_with::<Mline>(Mline::default).0.insert(
uid,
MlineBatch {
bref: bref.to_string(),
target: target.to_string(),
notice: false,
parts: Vec::new(),
bytes: 0,
},
);
}
/// Buffer one PRIVMSG/NOTICE line into `uid`'s open batch when `bref` matches
/// (bounded by the advertised byte/line limits). Returns true if it was part of
/// the batch — i.e. it should not be delivered on its own. Called from dispatch.
pub fn accumulate(
s: &mut Server,
uid: Uid,
bref: &str,
notice: bool,
text: &str,
concat: bool,
) -> bool {
match s.ext.get_mut::<Mline>().and_then(|m| m.0.get_mut(&uid)) {
Some(mb) if mb.bref == bref => {
if mb.parts.len() < MAX_LINES && mb.bytes + text.len() <= MAX_BYTES {
mb.notice = notice;
mb.bytes += text.len();
mb.parts.push((text.to_string(), concat));
}
true
}
_ => false,
}
}
/// Close `uid`'s batch `bref` and return `(target, is_notice, lines)` with `concat`
/// parts joined into single logical lines. `None` if no match.
fn close(s: &mut Server, uid: Uid, bref: &str) -> Option<(String, bool, Vec<String>)> {
let store = s.ext.get_mut::<Mline>()?;
match store.0.get(&uid) {
Some(mb) if mb.bref == bref => {}
_ => return None,
}
let mb = store.0.remove(&uid)?;
let mut lines: Vec<String> = Vec::new();
for (text, concat) in mb.parts {
if concat && !lines.is_empty() {
lines.last_mut().unwrap().push_str(&text);
} else {
lines.push(text);
}
}
Some((mb.target, mb.notice, lines))
}
/// Cleanup hook: drop any half-open batch on disconnect.
pub struct Multiline;
impl Module for Multiline {
fn name(&self) -> &'static str {
"multiline"
}
fn on_user_quit(&mut self, s: &mut Server, uid: Uid, _reason: &str) {
if let Some(m) = s.ext.get_mut::<Mline>() {
m.0.remove(&uid);
}
}
}
pub fn commands() -> Vec<Box<dyn Command>> {
vec![Box::new(Batch)]
}
/// BATCH — the client side of draft/multiline. `BATCH +<ref> draft/multiline
/// <target>` opens a batch; the tagged lines are buffered; `BATCH -<ref>` assembles
/// them and delivers each logical line as a normal PRIVMSG/NOTICE.
struct Batch;
impl Command for Batch {
fn name(&self) -> &'static str {
"BATCH"
}
fn min_params(&self) -> usize {
1
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
let tag = &params[0];
if let Some(bref) = tag.strip_prefix('+') {
if params.get(1).map(|t| t.as_str()) == Some("draft/multiline") {
let target = params.get(2).cloned().unwrap_or_default();
open(s, uid, bref, &target);
}
} else if let Some(bref) = tag.strip_prefix('-') {
if let Some((target, notice, lines)) = close(s, uid, bref) {
for line in lines {
deliver(s, uid, &[target.clone(), line], notice);
}
}
}
CmdResult::Ok
}
}