kcode_telegram_text_delivery/
lib.rs1use teloxide::{
2 payloads::SendMessageSetters,
3 prelude::{Bot, Requester},
4 requests::Request,
5 types::{ChatId, Message, MessageId, ReplyParameters},
6};
7
8const TELEGRAM_MESSAGE_LIMIT: usize = 4_000;
9
10pub async fn send_telegram_text(
11 bot: &Bot,
12 chat_id: i64,
13 text: &str,
14 reply_to_message_id: Option<i64>,
15) -> Result<Vec<Message>, teloxide::RequestError> {
16 let mut sent = Vec::new();
17 for (index, chunk) in telegram_chunks(text, TELEGRAM_MESSAGE_LIMIT)
18 .into_iter()
19 .enumerate()
20 {
21 let mut request = bot.send_message(ChatId(chat_id), chunk);
22 if index == 0
23 && let Some(message_id) =
24 reply_to_message_id.and_then(|value| i32::try_from(value).ok())
25 {
26 request = request.reply_parameters(
27 ReplyParameters::new(MessageId(message_id)).allow_sending_without_reply(),
28 );
29 }
30 let message =
31 kcode_telegram_request_policy::retry_request("send_message", || request.clone().send())
32 .await?;
33 sent.push(message);
34 }
35 Ok(sent)
36}
37
38pub async fn send_telegram_message(
39 bot: &Bot,
40 chat_id: ChatId,
41 text: impl Into<String>,
42) -> Result<Message, teloxide::RequestError> {
43 let request = bot.send_message(chat_id, text.into());
44 kcode_telegram_request_policy::retry_request("send_message", || request.clone().send()).await
45}
46
47fn telegram_chunks(text: &str, max_utf16_units: usize) -> Vec<String> {
48 assert!(max_utf16_units >= 2);
49 if text.encode_utf16().count() <= max_utf16_units {
50 return vec![text.to_owned()];
51 }
52
53 let mut chunks = Vec::new();
54 let mut start = 0;
55 let mut units = 0;
56 for (index, character) in text.char_indices() {
57 let character_units = character.len_utf16();
58 if units > 0 && units + character_units > max_utf16_units {
59 chunks.push(text[start..index].to_owned());
60 start = index;
61 units = 0;
62 }
63 units += character_units;
64 }
65 if start < text.len() {
66 chunks.push(text[start..].to_owned());
67 }
68 chunks
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn splitting_preserves_exact_whitespace_and_bytes() {
77 let text = format!(" {}\n{}\t ", "a".repeat(10), "😀".repeat(10));
78 let chunks = telegram_chunks(&text, 12);
79 assert!(chunks.len() > 1);
80 assert!(
81 chunks
82 .iter()
83 .all(|chunk| chunk.encode_utf16().count() <= 12)
84 );
85 assert_eq!(chunks.concat().as_bytes(), text.as_bytes());
86 }
87
88 #[test]
89 fn astral_characters_are_kept_whole_at_utf16_boundaries() {
90 let text = format!("{}😀b", "a".repeat(3_999));
91 let chunks = telegram_chunks(&text, TELEGRAM_MESSAGE_LIMIT);
92 assert_eq!(chunks.len(), 2);
93 assert_eq!(chunks[0].encode_utf16().count(), 3_999);
94 assert_eq!(chunks[1], "😀b");
95 assert_eq!(chunks[1].encode_utf16().count(), 3);
96 assert_eq!(chunks.concat(), text);
97 }
98
99 #[test]
100 fn an_astral_character_can_end_an_exact_utf16_chunk() {
101 let text = format!("{}😀z", "a".repeat(3_998));
102 let chunks = telegram_chunks(&text, TELEGRAM_MESSAGE_LIMIT);
103 assert_eq!(chunks.len(), 2);
104 assert_eq!(chunks[0].encode_utf16().count(), TELEGRAM_MESSAGE_LIMIT);
105 assert!(chunks[0].ends_with('😀'));
106 assert_eq!(chunks[1], "z");
107 assert_eq!(chunks.concat(), text);
108 }
109}