use std::fmt::Write;
use anyhow::{Context, Result};
use reqwest::Client;
use super::types::{SendMessageRequest, TelegramApiResponse, TelegramSentMessage, TelegramUpdate};
pub struct TelegramBot {
token: String,
http: Client,
}
impl TelegramBot {
pub fn new(token: String) -> Self {
Self {
token,
http: Client::new(),
}
}
fn api_url(&self, endpoint: &str) -> String {
format!("https://api.telegram.org/bot{}/{}", self.token, endpoint)
}
pub fn markdown_to_telegram_html(text: &str) -> String {
let lines: Vec<&str> = text.split('\n').collect();
let mut result_lines: Vec<String> = Vec::new();
for line in &lines {
let trimmed_line = line.trim_start();
if trimmed_line.starts_with("```") {
result_lines.push(trimmed_line.to_string());
continue;
}
let mut line_out = String::new();
let stripped = line.trim_start_matches('#');
let header_level = line.len() - stripped.len();
if header_level > 0 && line.starts_with('#') && stripped.starts_with(' ') {
let title = Self::escape_html(stripped.trim());
result_lines.push(format!("<b>{title}</b>"));
continue;
}
let mut i = 0;
let bytes = line.as_bytes();
let len = bytes.len();
while i < len {
if i + 1 < len
&& bytes[i] == b'*'
&& bytes[i + 1] == b'*'
&& let Some(end) = line[i + 2..].find("**")
{
let inner = Self::escape_html(&line[i + 2..i + 2 + end]);
let _ = write!(line_out, "<b>{inner}</b>");
i += 4 + end;
continue;
}
if i + 1 < len
&& bytes[i] == b'_'
&& bytes[i + 1] == b'_'
&& let Some(end) = line[i + 2..].find("__")
{
let inner = Self::escape_html(&line[i + 2..i + 2 + end]);
let _ = write!(line_out, "<b>{inner}</b>");
i += 4 + end;
continue;
}
if bytes[i] == b'*'
&& (i == 0 || bytes[i - 1] != b'*')
&& let Some(end) = line[i + 1..].find('*')
&& end > 0
{
let inner = Self::escape_html(&line[i + 1..i + 1 + end]);
let _ = write!(line_out, "<i>{inner}</i>");
i += 2 + end;
continue;
}
if bytes[i] == b'_'
&& let Some(end) = line[i + 1..].find('_')
&& end > 0
{
let inner = Self::escape_html(&line[i + 1..i + 1 + end]);
let _ = write!(line_out, "<i>{inner}</i>");
i += 2 + end;
continue;
}
if i + 1 < len
&& bytes[i] == b'~'
&& bytes[i + 1] == b'~'
&& let Some(end) = line[i + 2..].find("~~")
{
let inner = Self::escape_html(&line[i + 2..i + 2 + end]);
let _ = write!(line_out, "<s>{inner}</s>");
i += 4 + end;
continue;
}
let ch = line[i..].chars().next().unwrap();
match ch {
'<' => line_out.push_str("<"),
'>' => line_out.push_str(">"),
'&' => line_out.push_str("&"),
'"' => line_out.push_str("""),
'\'' => line_out.push_str("'"),
_ => line_out.push(ch),
}
i += ch.len_utf8();
}
result_lines.push(line_out);
}
let joined = result_lines.join("\n");
let mut final_out = String::with_capacity(joined.len());
let mut in_code_block = false;
let mut code_buf = String::new();
for line in joined.split('\n') {
let trimmed = line.trim();
if trimmed.starts_with("```") {
if in_code_block {
in_code_block = false;
let escaped = code_buf.trim_end_matches('\n');
let _ = writeln!(final_out, "<pre><code>{escaped}</code></pre>");
code_buf.clear();
} else {
in_code_block = true;
code_buf.clear();
}
} else if in_code_block {
code_buf.push_str(line);
code_buf.push('\n');
} else {
final_out.push_str(line);
final_out.push('\n');
}
}
if in_code_block && !code_buf.is_empty() {
let _ = writeln!(final_out, "<pre><code>{}</code></pre>", code_buf.trim_end());
}
final_out.trim_end_matches('\n').to_string()
}
fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub async fn get_updates(&self, offset: i64, timeout_secs: u64) -> Result<Vec<TelegramUpdate>> {
let body = serde_json::json!({
"offset": offset,
"timeout": timeout_secs,
"allowed_updates": ["message"],
});
let response = self
.http
.post(self.api_url("getUpdates"))
.json(&body)
.send()
.await
.context("failed to call Telegram getUpdates")?;
let payload: TelegramApiResponse<Vec<TelegramUpdate>> = response
.json()
.await
.context("failed to parse Telegram getUpdates response")?;
payload.into_result("getUpdates")
}
pub async fn send_message(
&self,
chat_id: i64,
message_thread_id: Option<i64>,
text: &str,
reply_to_message_id: Option<i64>,
) -> Result<TelegramSentMessage> {
let body = SendMessageRequest {
chat_id,
text,
parse_mode: None,
message_thread_id,
reply_to_message_id,
};
let response = self
.http
.post(self.api_url("sendMessage"))
.json(&body)
.send()
.await
.context("failed to call Telegram sendMessage")?;
let payload: TelegramApiResponse<TelegramSentMessage> = response
.json()
.await
.context("failed to parse Telegram sendMessage response")?;
payload.into_result("sendMessage")
}
pub async fn send_message_html(
&self,
chat_id: i64,
message_thread_id: Option<i64>,
text: &str,
reply_to_message_id: Option<i64>,
) -> Result<TelegramSentMessage> {
let html_text = Self::markdown_to_telegram_html(text);
let body = SendMessageRequest {
chat_id,
text: &html_text,
parse_mode: Some("HTML".to_string()),
message_thread_id,
reply_to_message_id,
};
let response = self
.http
.post(self.api_url("sendMessage"))
.json(&body)
.send()
.await
.context("failed to call Telegram sendMessage (HTML)")?;
let payload: TelegramApiResponse<TelegramSentMessage> = response
.json()
.await
.context("failed to parse Telegram sendMessage response")?;
payload.into_result("sendMessage")
}
pub async fn edit_message_text_html(
&self,
chat_id: i64,
message_id: i64,
text: &str,
) -> Result<()> {
let html_text = Self::markdown_to_telegram_html(text);
let body = serde_json::json!({
"chat_id": chat_id,
"message_id": message_id,
"text": html_text,
"parse_mode": "HTML",
});
let response = self
.http
.post(self.api_url("editMessageText"))
.json(&body)
.send()
.await
.context("failed to call Telegram editMessageText")?;
let payload: TelegramApiResponse<bool> = response
.json()
.await
.context("failed to parse Telegram editMessageText response")?;
payload.into_result("editMessageText").map(|_| ())
}
pub async fn delete_message(&self, chat_id: i64, message_id: i64) -> Result<()> {
let body = serde_json::json!({
"chat_id": chat_id,
"message_id": message_id,
});
let response = self
.http
.post(self.api_url("deleteMessage"))
.json(&body)
.send()
.await
.context("failed to call Telegram deleteMessage")?;
let payload: TelegramApiResponse<bool> = response
.json()
.await
.context("failed to parse Telegram deleteMessage response")?;
payload.into_result("deleteMessage").map(|_| ())
}
pub async fn set_my_commands(&self, commands: Vec<(String, String)>) -> Result<()> {
let body = serde_json::json!({
"commands": commands.into_iter().map(|(cmd, desc)| {
serde_json::json!({
"command": cmd,
"description": desc
})
}).collect::<Vec<_>>()
});
let response = self
.http
.post(self.api_url("setMyCommands"))
.json(&body)
.send()
.await
.context("failed to call Telegram setMyCommands")?;
let payload: TelegramApiResponse<bool> = response
.json()
.await
.context("failed to parse Telegram setMyCommands response")?;
payload.into_result("setMyCommands").map(|_| ())
}
pub async fn set_message_reaction(
&self,
chat_id: i64,
message_id: i64,
reaction: &str,
) -> Result<()> {
let body = serde_json::json!({
"chat_id": chat_id,
"message_id": message_id,
"reaction": [serde_json::json!({ "type": "emoji", "emoji": reaction })]
});
let response = self
.http
.post(self.api_url("setMessageReaction"))
.json(&body)
.send()
.await
.context("failed to call Telegram setMessageReaction")?;
let payload: TelegramApiResponse<bool> = response
.json()
.await
.context("failed to parse Telegram setMessageReaction response")?;
payload.into_result("setMessageReaction").map(|_| ())
}
}