use teloxide::{
payloads::SendMessageSetters,
prelude::{Bot, Requester},
requests::Request,
types::{ChatId, Message, MessageId, ReplyParameters},
};
const TELEGRAM_MESSAGE_LIMIT: usize = 4_000;
pub async fn send_telegram_text(
bot: &Bot,
chat_id: i64,
text: &str,
reply_to_message_id: Option<i64>,
) -> Result<Vec<Message>, teloxide::RequestError> {
let mut sent = Vec::new();
for (index, chunk) in telegram_chunks(text, TELEGRAM_MESSAGE_LIMIT)
.into_iter()
.enumerate()
{
let mut request = bot.send_message(ChatId(chat_id), chunk);
if index == 0
&& let Some(message_id) =
reply_to_message_id.and_then(|value| i32::try_from(value).ok())
{
request = request.reply_parameters(
ReplyParameters::new(MessageId(message_id)).allow_sending_without_reply(),
);
}
let message =
kcode_telegram_request_policy::retry_request("send_message", || request.clone().send())
.await?;
sent.push(message);
}
Ok(sent)
}
pub async fn send_telegram_message(
bot: &Bot,
chat_id: ChatId,
text: impl Into<String>,
) -> Result<Message, teloxide::RequestError> {
let request = bot.send_message(chat_id, text.into());
kcode_telegram_request_policy::retry_request("send_message", || request.clone().send()).await
}
fn telegram_chunks(text: &str, max_utf16_units: usize) -> Vec<String> {
assert!(max_utf16_units >= 2);
if text.encode_utf16().count() <= max_utf16_units {
return vec![text.to_owned()];
}
let mut chunks = Vec::new();
let mut start = 0;
let mut units = 0;
for (index, character) in text.char_indices() {
let character_units = character.len_utf16();
if units > 0 && units + character_units > max_utf16_units {
chunks.push(text[start..index].to_owned());
start = index;
units = 0;
}
units += character_units;
}
if start < text.len() {
chunks.push(text[start..].to_owned());
}
chunks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splitting_preserves_exact_whitespace_and_bytes() {
let text = format!(" {}\n{}\t ", "a".repeat(10), "😀".repeat(10));
let chunks = telegram_chunks(&text, 12);
assert!(chunks.len() > 1);
assert!(
chunks
.iter()
.all(|chunk| chunk.encode_utf16().count() <= 12)
);
assert_eq!(chunks.concat().as_bytes(), text.as_bytes());
}
#[test]
fn astral_characters_are_kept_whole_at_utf16_boundaries() {
let text = format!("{}😀b", "a".repeat(3_999));
let chunks = telegram_chunks(&text, TELEGRAM_MESSAGE_LIMIT);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].encode_utf16().count(), 3_999);
assert_eq!(chunks[1], "😀b");
assert_eq!(chunks[1].encode_utf16().count(), 3);
assert_eq!(chunks.concat(), text);
}
#[test]
fn an_astral_character_can_end_an_exact_utf16_chunk() {
let text = format!("{}😀z", "a".repeat(3_998));
let chunks = telegram_chunks(&text, TELEGRAM_MESSAGE_LIMIT);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].encode_utf16().count(), TELEGRAM_MESSAGE_LIMIT);
assert!(chunks[0].ends_with('😀'));
assert_eq!(chunks[1], "z");
assert_eq!(chunks.concat(), text);
}
}