Skip to content

Commit 152c323

Browse files
committed
Allow long lines when IRCv3 multiline is available, using multiline-concat to send via a multiline batch.
1 parent b911745 commit 152c323

File tree

4 files changed

+235
-93
lines changed

4 files changed

+235
-93
lines changed

data/src/capabilities.rs

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::collections::HashSet;
22
use std::str::FromStr;
3+
use std::string::ToString;
34

4-
use crate::config;
5+
use irc::proto::{self, Tags, command, format};
6+
7+
use crate::{Target, User, config, message};
58

69
// This is not an exhaustive list of IRCv3 capabilities, just the ones that
710
// Halloy will request when available. When adding new IRCv3 capabilities to
@@ -62,11 +65,78 @@ impl FromStr for Capability {
6265
}
6366

6467
#[derive(Debug, Clone, Copy)]
65-
pub struct Multiline {
68+
pub struct MultilineLimits {
6669
pub max_bytes: usize,
6770
pub max_lines: Option<usize>,
6871
}
6972

73+
impl MultilineLimits {
74+
pub fn concat_bytes(
75+
&self,
76+
relay_bytes: usize,
77+
batch_kind: MultilineBatchKind,
78+
target: &Target,
79+
) -> usize {
80+
// Message byte limit - relay bytes - space - command - space - target - message separator - crlf
81+
format::BYTE_LIMIT.saturating_sub(
82+
match batch_kind {
83+
MultilineBatchKind::PRIVMSG => 7,
84+
MultilineBatchKind::NOTICE => 6,
85+
} + target.as_str().len()
86+
+ relay_bytes
87+
+ 6,
88+
)
89+
}
90+
}
91+
92+
pub fn multiline_concat_lines(concat_bytes: usize, text: &str) -> Vec<&str> {
93+
let mut lines = Vec::new();
94+
let mut last_line_start = 0;
95+
let mut prev_char_index = 0;
96+
97+
for (char_index, _) in text.char_indices() {
98+
if char_index.saturating_sub(last_line_start) > concat_bytes {
99+
lines.push(&text[last_line_start..prev_char_index]);
100+
last_line_start = prev_char_index;
101+
}
102+
103+
prev_char_index = char_index;
104+
}
105+
106+
lines.push(&text[last_line_start..]);
107+
108+
lines
109+
}
110+
111+
pub fn multiline_encoded(
112+
user: Option<&User>,
113+
batch_kind: MultilineBatchKind,
114+
target: &Target,
115+
text: &str,
116+
tags: Tags,
117+
) -> message::Encoded {
118+
let mut encoded = command!(
119+
match batch_kind {
120+
MultilineBatchKind::PRIVMSG => "PRIVMSG",
121+
MultilineBatchKind::NOTICE => "NOTICE",
122+
},
123+
target.as_str(),
124+
text,
125+
);
126+
127+
if let Some(user) = user {
128+
encoded.source = Some(proto::Source::User(proto::User {
129+
nickname: user.nickname().to_string(),
130+
username: user.username().map(ToString::to_string),
131+
hostname: user.hostname().map(ToString::to_string),
132+
}));
133+
}
134+
135+
encoded.tags = tags;
136+
137+
message::Encoded(encoded)
138+
}
139+
70140
#[derive(Debug, Clone, Copy, PartialEq)]
71141
pub enum MultilineBatchKind {
72142
PRIVMSG,
@@ -78,7 +148,7 @@ pub struct Capabilities {
78148
listed: HashSet<String>,
79149
pending: HashSet<String>,
80150
acknowledged: HashSet<Capability>,
81-
multiline: Option<Multiline>,
151+
multiline: Option<MultilineLimits>,
82152
}
83153

84154
impl Capabilities {
@@ -234,7 +304,7 @@ impl Capabilities {
234304
.strip_prefix("max-bytes=")
235305
.and_then(|value| value.parse::<usize>().ok())
236306
}) {
237-
self.multiline = Some(Multiline {
307+
self.multiline = Some(MultilineLimits {
238308
max_bytes,
239309
max_lines: dictionary.iter().find_map(|key_value| {
240310
key_value
@@ -272,7 +342,7 @@ impl Capabilities {
272342
}
273343
}
274344

275-
pub fn multiline(&self) -> Option<Multiline> {
345+
pub fn multiline_limits(&self) -> Option<MultilineLimits> {
276346
if self.acknowledged(Capability::Multiline) {
277347
self.multiline
278348
} else {

data/src/client.rs

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use tokio::fs;
1717
pub use self::on_connect::on_connect;
1818
use crate::bouncer::{self, BouncerNetwork};
1919
use crate::capabilities::{
20-
Capabilities, Capability, Multiline, MultilineBatchKind,
20+
Capabilities, Capability, MultilineBatchKind, MultilineLimits,
21+
multiline_concat_lines, multiline_encoded,
2122
};
2223
use crate::environment::{SOURCE_WEBSITE, VERSION};
2324
use crate::history::ReadMarker;
@@ -576,16 +577,22 @@ impl Client {
576577
mut messages: Vec<message::Encoded>,
577578
priority: TokenPriority,
578579
) {
579-
if let Some(target) =
580-
buffer.target().as_ref().map(Target::as_normalized_str)
580+
if let Some(multiline_limits) = self.multiline_limits()
581+
&& let Some(batch_kind) =
582+
messages.first().and_then(|message| match message.command {
583+
Command::PRIVMSG(_, _) => Some(MultilineBatchKind::PRIVMSG),
584+
Command::NOTICE(_, _) => Some(MultilineBatchKind::NOTICE),
585+
_ => None,
586+
})
587+
&& let Some(target) = buffer.target().as_ref()
581588
{
582589
let reference_tag = generate_batch_reference_tag();
583590

584591
let mut opening_batch: message::Encoded = command!(
585592
"BATCH",
586593
format!("+{reference_tag}"),
587594
"draft/multiline",
588-
target
595+
target.as_str()
589596
)
590597
.into();
591598

@@ -608,8 +615,51 @@ impl Client {
608615
let closing_batch: message::Encoded =
609616
command!("BATCH", format!("-{reference_tag}")).into();
610617

618+
let multiline_concat_bytes = multiline_limits.concat_bytes(
619+
self.relay_bytes(),
620+
batch_kind,
621+
target,
622+
);
623+
611624
let messages = iter::once(opening_batch)
612-
.chain(messages)
625+
.chain(messages.into_iter().flat_map(|message| {
626+
match &message.command {
627+
Command::PRIVMSG(_, text)
628+
| Command::NOTICE(_, text) => {
629+
let lines = multiline_concat_lines(
630+
multiline_concat_bytes,
631+
text,
632+
);
633+
634+
if lines.len() < 2 {
635+
vec![message]
636+
} else {
637+
let mut lines = lines
638+
.into_iter()
639+
.map(|text| {
640+
multiline_encoded(
641+
None,
642+
batch_kind,
643+
target,
644+
text,
645+
message.tags.clone(),
646+
)
647+
})
648+
.collect::<Vec<_>>();
649+
650+
for line in lines.iter_mut().skip(1) {
651+
line.tags.insert(
652+
"draft/multiline-concat".to_string(),
653+
String::new(),
654+
);
655+
}
656+
657+
lines
658+
}
659+
}
660+
_ => vec![],
661+
}
662+
}))
613663
.chain(iter::once(closing_batch))
614664
.collect::<Vec<message::Encoded>>();
615665

@@ -819,54 +869,29 @@ impl Client {
819869
tags,
820870
user,
821871
target,
822-
kind,
872+
Some(batch_kind),
823873
text,
824874
)) => {
825-
if let Some(user) = user
826-
&& let Some(kind) = kind
827-
{
828-
let mut encoded = command!(
829-
match kind {
830-
MultilineBatchKind::PRIVMSG => "PRIVMSG",
831-
MultilineBatchKind::NOTICE => "NOTICE",
832-
},
833-
target.to_string(),
834-
text,
835-
);
836-
837-
encoded.source =
838-
Some(proto::Source::User(
839-
proto::User {
840-
nickname: user
841-
.nickname()
842-
.to_string(),
843-
username: user
844-
.username()
845-
.map(|username| {
846-
username
847-
.to_string()
848-
}),
849-
hostname: user
850-
.hostname()
851-
.map(|hostname| {
852-
hostname
853-
.to_string()
854-
}),
855-
},
856-
));
857-
858-
encoded.tags = tags.clone();
875+
let encoded = multiline_encoded(
876+
user.as_ref(),
877+
*batch_kind,
878+
target,
879+
text,
880+
tags.clone(),
881+
);
859882

860-
finished.events.extend(
861-
self.handle(
862-
message::Encoded(encoded),
863-
context,
864-
config,
865-
)?,
866-
);
867-
}
883+
finished.events.extend(self.handle(
884+
encoded, context, config,
885+
)?);
868886
}
869-
None => (),
887+
Some(BatchKind::Multiline(
888+
_,
889+
_,
890+
_,
891+
None,
892+
_,
893+
))
894+
| None => (),
870895
};
871896

872897
return Ok(finished.events);
@@ -3822,8 +3847,8 @@ impl Client {
38223847
+ 14
38233848
}
38243849

3825-
pub fn multiline(&self) -> Option<Multiline> {
3826-
self.capabilities.multiline()
3850+
pub fn multiline_limits(&self) -> Option<MultilineLimits> {
3851+
self.capabilities.multiline_limits()
38273852
}
38283853
}
38293854

@@ -4253,8 +4278,11 @@ impl Map {
42534278
self.client(server).map_or(144, Client::relay_bytes)
42544279
}
42554280

4256-
pub fn get_multiline(&self, server: &Server) -> Option<Multiline> {
4257-
self.client(server).and_then(Client::multiline)
4281+
pub fn get_multiline_limits(
4282+
&self,
4283+
server: &Server,
4284+
) -> Option<MultilineLimits> {
4285+
self.client(server).and_then(Client::multiline_limits)
42584286
}
42594287

42604288
pub fn get_server_supports_multiline(&self, server: &Server) -> bool {

0 commit comments

Comments
 (0)