move draft/multiline into its own module file
This commit is contained in:
parent
768435d810
commit
ab7188bb70
6 changed files with 148 additions and 110 deletions
|
|
@ -125,40 +125,9 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
Box::new(TagMsg),
|
Box::new(TagMsg),
|
||||||
Box::new(ChatHistory),
|
Box::new(ChatHistory),
|
||||||
Box::new(Redact),
|
Box::new(Redact),
|
||||||
Box::new(Batch),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BATCH — the client side of draft/multiline. `BATCH +<ref> draft/multiline
|
|
||||||
/// <target>` opens a batch; the `@batch=<ref>`-tagged PRIVMSG/NOTICE lines are
|
|
||||||
/// buffered (see `Ircd::dispatch`); `BATCH -<ref>` assembles them (honouring
|
|
||||||
/// `draft/multiline-concat`) and delivers each logical line normally.
|
|
||||||
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 = ¶ms[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();
|
|
||||||
s.multiline_open(uid, bref, &target);
|
|
||||||
}
|
|
||||||
} else if let Some(bref) = tag.strip_prefix('-') {
|
|
||||||
if let Some((target, notice, lines)) = s.multiline_close(uid, bref) {
|
|
||||||
for line in lines {
|
|
||||||
deliver(s, uid, &[target.clone(), line], notice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CmdResult::Ok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// REDACT — delete a previously-sent channel message (draft/message-redaction).
|
/// REDACT — delete a previously-sent channel message (draft/message-redaction).
|
||||||
/// `REDACT <#chan> <msgid> [:reason]`. Allowed for the message's author, a channel
|
/// `REDACT <#chan> <msgid> [:reason]`. Allowed for the message's author, a channel
|
||||||
/// half-op-or-above, or an oper. Relayed to channel members who enabled the cap,
|
/// half-op-or-above, or an oper. Relayed to channel members who enabled the cap,
|
||||||
|
|
@ -463,7 +432,8 @@ impl Command for ChatHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared PRIVMSG/NOTICE delivery. NOTICE never generates automatic replies.
|
/// Shared PRIVMSG/NOTICE delivery. NOTICE never generates automatic replies.
|
||||||
fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) -> CmdResult {
|
/// `pub(crate)` so the multiline module can replay an assembled batch through it.
|
||||||
|
pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) -> CmdResult {
|
||||||
let cmd = if notice { "NOTICE" } else { "PRIVMSG" };
|
let cmd = if notice { "NOTICE" } else { "PRIVMSG" };
|
||||||
if params.is_empty() {
|
if params.is_empty() {
|
||||||
if !notice {
|
if !notice {
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,8 @@ impl Ircd {
|
||||||
// not delivered on its own — it's assembled and sent when the BATCH closes.
|
// not delivered on its own — it's assembled and sent when the BATCH closes.
|
||||||
if let Some(bref) = &msg.batch {
|
if let Some(bref) = &msg.batch {
|
||||||
if matches!(cmd, "PRIVMSG" | "NOTICE") && msg.params.len() >= 2 {
|
if matches!(cmd, "PRIVMSG" | "NOTICE") && msg.params.len() >= 2 {
|
||||||
let consumed = self.server.multiline_accumulate(
|
let consumed = crate::modules::multiline::accumulate(
|
||||||
|
&mut self.server,
|
||||||
uid,
|
uid,
|
||||||
bref,
|
bref,
|
||||||
cmd == "NOTICE",
|
cmd == "NOTICE",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub mod filter;
|
||||||
pub mod flood;
|
pub mod flood;
|
||||||
pub mod markread;
|
pub mod markread;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
pub mod multiline;
|
||||||
pub mod snoop;
|
pub mod snoop;
|
||||||
|
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
|
|
@ -25,6 +26,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
||||||
Box::new(filter::Filter),
|
Box::new(filter::Filter),
|
||||||
Box::new(metadata::Metadata),
|
Box::new(metadata::Metadata),
|
||||||
Box::new(markread::MarkRead),
|
Box::new(markread::MarkRead),
|
||||||
|
Box::new(multiline::Multiline),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,5 +37,6 @@ pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(metadata::commands())
|
.chain(metadata::commands())
|
||||||
.chain(markread::commands())
|
.chain(markread::commands())
|
||||||
|
.chain(multiline::commands())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
138
src/modules/multiline.rs
Normal file
138
src/modules/multiline.rs
Normal 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 = ¶ms[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -97,20 +97,6 @@ pub struct HistMsg {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Limits advertised in the `draft/multiline` cap and enforced while buffering.
|
|
||||||
pub const MLINE_MAX_BYTES: usize = 4096;
|
|
||||||
pub const MLINE_MAX_LINES: usize = 24;
|
|
||||||
|
|
||||||
/// An in-progress inbound draft/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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A recently-departed identity, kept for WHOWAS.
|
/// A recently-departed identity, kept for WHOWAS.
|
||||||
pub struct WhowasEntry {
|
pub struct WhowasEntry {
|
||||||
pub nick: String,
|
pub nick: String,
|
||||||
|
|
@ -164,7 +150,6 @@ pub struct Server {
|
||||||
// output primitives are `&self`.
|
// output primitives are `&self`.
|
||||||
pub label_capture: RefCell<Option<(Uid, Vec<String>)>>,
|
pub label_capture: RefCell<Option<(Uid, Vec<String>)>>,
|
||||||
pub history: HashMap<String, VecDeque<HistMsg>>, // channel key -> recent messages (CHATHISTORY)
|
pub history: HashMap<String, VecDeque<HistMsg>>, // channel key -> recent messages (CHATHISTORY)
|
||||||
pub mline: HashMap<Uid, MlineBatch>, // in-progress inbound multiline batches
|
|
||||||
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||||
pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials)
|
pub conn_counter: Arc<AtomicU64>, // mints connection uids (for CONNECT dials)
|
||||||
/// Module-owned server state, keyed by type — the InspIRCd `ExtensionItem`
|
/// Module-owned server state, keyed by type — the InspIRCd `ExtensionItem`
|
||||||
|
|
@ -213,7 +198,6 @@ impl Server {
|
||||||
webirc: cfg.webirc,
|
webirc: cfg.webirc,
|
||||||
label_capture: RefCell::new(None),
|
label_capture: RefCell::new(None),
|
||||||
history: HashMap::new(),
|
history: HashMap::new(),
|
||||||
mline: HashMap::new(),
|
|
||||||
event_tx,
|
event_tx,
|
||||||
conn_counter,
|
conn_counter,
|
||||||
ext: Extensible::default(),
|
ext: Extensible::default(),
|
||||||
|
|
@ -269,64 +253,6 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open an inbound draft/multiline batch for `uid` (a client assembling one
|
|
||||||
/// long message from several tagged PRIVMSG/NOTICE lines).
|
|
||||||
pub fn multiline_open(&mut self, uid: Uid, bref: &str, target: &str) {
|
|
||||||
self.mline.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 multiline 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.
|
|
||||||
pub fn multiline_accumulate(
|
|
||||||
&mut self,
|
|
||||||
uid: Uid,
|
|
||||||
bref: &str,
|
|
||||||
notice: bool,
|
|
||||||
text: &str,
|
|
||||||
concat: bool,
|
|
||||||
) -> bool {
|
|
||||||
match self.mline.get_mut(&uid) {
|
|
||||||
Some(mb) if mb.bref == bref => {
|
|
||||||
if mb.parts.len() < MLINE_MAX_LINES && mb.bytes + text.len() <= MLINE_MAX_BYTES {
|
|
||||||
mb.notice = notice;
|
|
||||||
mb.bytes += text.len();
|
|
||||||
mb.parts.push((text.to_string(), concat));
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close `uid`'s multiline batch `bref` and return `(target, is_notice, lines)`
|
|
||||||
/// with `concat` parts joined into single logical lines. `None` if no match.
|
|
||||||
pub fn multiline_close(&mut self, uid: Uid, bref: &str) -> Option<(String, bool, Vec<String>)> {
|
|
||||||
match self.mline.get(&uid) {
|
|
||||||
Some(mb) if mb.bref == bref => {}
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
let mb = self.mline.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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- connection lifecycle ------------------------------------------------
|
// --- connection lifecycle ------------------------------------------------
|
||||||
|
|
||||||
pub fn add_conn(
|
pub fn add_conn(
|
||||||
|
|
@ -504,7 +430,6 @@ impl Server {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.uuid_local.remove(&user.uuid);
|
self.uuid_local.remove(&user.uuid);
|
||||||
self.mline.remove(&uid); // any half-open multiline batch
|
|
||||||
if user.registered {
|
if user.registered {
|
||||||
self.push_whowas(
|
self.push_whowas(
|
||||||
&user.nick,
|
&user.nick,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ use std::net::{SocketAddr, TcpStream};
|
||||||
use crate::extensible::Extensible;
|
use crate::extensible::Extensible;
|
||||||
use crate::module::Hook;
|
use crate::module::Hook;
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::{Server, MLINE_MAX_BYTES, MLINE_MAX_LINES, VERSION};
|
use crate::modules::multiline::{MAX_BYTES as MLINE_MAX_BYTES, MAX_LINES as MLINE_MAX_LINES};
|
||||||
|
use crate::server::{Server, VERSION};
|
||||||
use crate::socketengine::OutSink;
|
use crate::socketengine::OutSink;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue