Skip to main content

kcode_telegram_text_delivery/
lib.rs

1use 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}