feat(telegram): add forum topic support

Implement Telegram Forum (topic) support to allow the bot to respond
in the same topic where it was mentioned/called.

Changes:
- parse_update_message(): Extract message_thread_id and format reply_target as 'chat_id:thread_id'
- send(): Parse recipient to extract chat_id and optional thread_id
- All send methods now pass thread_id parameter and include message_thread_id in API requests
- Added test for forum topic message parsing

This ensures bot replies stay within the same forum topic thread.
This commit is contained in:
ZeroClaw Agent 2026-02-17 20:02:47 +03:00 committed by Chummy
parent 3467d34596
commit 36062fb1c2

View file

@ -562,10 +562,23 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.and_then(serde_json::Value::as_i64) .and_then(serde_json::Value::as_i64)
.unwrap_or(0); .unwrap_or(0);
// Extract thread/topic ID for forum support
let thread_id = message
.get("message_thread_id")
.and_then(serde_json::Value::as_i64)
.map(|id| id.to_string());
// reply_target: chat_id or chat_id:thread_id format
let reply_target = if let Some(tid) = thread_id {
format!("{}:{}", chat_id, tid)
} else {
chat_id.clone()
};
Some(ChannelMessage { Some(ChannelMessage {
id: format!("telegram_{chat_id}_{message_id}"), id: format!("telegram_{chat_id}_{message_id}"),
sender: sender_identity, sender: sender_identity,
reply_target: chat_id, reply_target,
content: text.to_string(), content: text.to_string(),
channel: "telegram".to_string(), channel: "telegram".to_string(),
timestamp: std::time::SystemTime::now() timestamp: std::time::SystemTime::now()
@ -575,7 +588,12 @@ Allowlist Telegram username (without '@') or numeric user ID.",
}) })
} }
async fn send_text_chunks(&self, message: &str, chat_id: &str) -> anyhow::Result<()> { async fn send_text_chunks(
&self,
message: &str,
chat_id: &str,
thread_id: Option<&str>,
) -> anyhow::Result<()> {
let chunks = split_message_for_telegram(message); let chunks = split_message_for_telegram(message);
for (index, chunk) in chunks.iter().enumerate() { for (index, chunk) in chunks.iter().enumerate() {
@ -591,12 +609,17 @@ Allowlist Telegram username (without '@') or numeric user ID.",
chunk.to_string() chunk.to_string()
}; };
let markdown_body = serde_json::json!({ let mut markdown_body = serde_json::json!({
"chat_id": chat_id, "chat_id": chat_id,
"text": text, "text": text,
"parse_mode": "Markdown" "parse_mode": "Markdown"
}); });
// Add message_thread_id for forum topic support
if let Some(tid) = thread_id {
markdown_body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
let markdown_resp = self let markdown_resp = self
.client .client
.post(self.api_url("sendMessage")) .post(self.api_url("sendMessage"))
@ -618,10 +641,15 @@ Allowlist Telegram username (without '@') or numeric user ID.",
"Telegram sendMessage with Markdown failed; retrying without parse_mode" "Telegram sendMessage with Markdown failed; retrying without parse_mode"
); );
let plain_body = serde_json::json!({ let mut plain_body = serde_json::json!({
"chat_id": chat_id, "chat_id": chat_id,
"text": text, "text": text,
}); });
// Add message_thread_id for forum topic support
if let Some(tid) = thread_id {
plain_body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
let plain_resp = self let plain_resp = self
.client .client
.post(self.api_url("sendMessage")) .post(self.api_url("sendMessage"))
@ -654,6 +682,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
method: &str, method: &str,
media_field: &str, media_field: &str,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
url: &str, url: &str,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -662,6 +691,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
}); });
body[media_field] = serde_json::Value::String(url.to_string()); body[media_field] = serde_json::Value::String(url.to_string());
if let Some(tid) = thread_id {
body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
body["caption"] = serde_json::Value::String(cap.to_string()); body["caption"] = serde_json::Value::String(cap.to_string());
} }
@ -685,6 +718,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
async fn send_attachment( async fn send_attachment(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
attachment: &TelegramAttachment, attachment: &TelegramAttachment,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let target = attachment.target.trim(); let target = attachment.target.trim();
@ -692,19 +726,24 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if is_http_url(target) { if is_http_url(target) {
return match attachment.kind { return match attachment.kind {
TelegramAttachmentKind::Image => { TelegramAttachmentKind::Image => {
self.send_photo_by_url(chat_id, target, None).await self.send_photo_by_url(chat_id, thread_id, target, None)
.await
} }
TelegramAttachmentKind::Document => { TelegramAttachmentKind::Document => {
self.send_document_by_url(chat_id, target, None).await self.send_document_by_url(chat_id, thread_id, target, None)
.await
} }
TelegramAttachmentKind::Video => { TelegramAttachmentKind::Video => {
self.send_video_by_url(chat_id, target, None).await self.send_video_by_url(chat_id, thread_id, target, None)
.await
} }
TelegramAttachmentKind::Audio => { TelegramAttachmentKind::Audio => {
self.send_audio_by_url(chat_id, target, None).await self.send_audio_by_url(chat_id, thread_id, target, None)
.await
} }
TelegramAttachmentKind::Voice => { TelegramAttachmentKind::Voice => {
self.send_voice_by_url(chat_id, target, None).await self.send_voice_by_url(chat_id, thread_id, target, None)
.await
} }
}; };
} }
@ -715,11 +754,13 @@ Allowlist Telegram username (without '@') or numeric user ID.",
} }
match attachment.kind { match attachment.kind {
TelegramAttachmentKind::Image => self.send_photo(chat_id, path, None).await, TelegramAttachmentKind::Image => self.send_photo(chat_id, thread_id, path, None).await,
TelegramAttachmentKind::Document => self.send_document(chat_id, path, None).await, TelegramAttachmentKind::Document => {
TelegramAttachmentKind::Video => self.send_video(chat_id, path, None).await, self.send_document(chat_id, thread_id, path, None).await
TelegramAttachmentKind::Audio => self.send_audio(chat_id, path, None).await, }
TelegramAttachmentKind::Voice => self.send_voice(chat_id, path, None).await, TelegramAttachmentKind::Video => self.send_video(chat_id, thread_id, path, None).await,
TelegramAttachmentKind::Audio => self.send_audio(chat_id, thread_id, path, None).await,
TelegramAttachmentKind::Voice => self.send_voice(chat_id, thread_id, path, None).await,
} }
} }
@ -727,6 +768,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_document( pub async fn send_document(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_path: &Path, file_path: &Path,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -742,6 +784,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("document", part); .part("document", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -766,6 +812,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_document_bytes( pub async fn send_document_bytes(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_bytes: Vec<u8>, file_bytes: Vec<u8>,
file_name: &str, file_name: &str,
caption: Option<&str>, caption: Option<&str>,
@ -776,6 +823,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("document", part); .part("document", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -800,6 +851,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_photo( pub async fn send_photo(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_path: &Path, file_path: &Path,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -815,6 +867,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("photo", part); .part("photo", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -839,6 +895,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_photo_bytes( pub async fn send_photo_bytes(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_bytes: Vec<u8>, file_bytes: Vec<u8>,
file_name: &str, file_name: &str,
caption: Option<&str>, caption: Option<&str>,
@ -849,6 +906,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("photo", part); .part("photo", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -873,6 +934,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_video( pub async fn send_video(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_path: &Path, file_path: &Path,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -888,6 +950,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("video", part); .part("video", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -912,6 +978,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_audio( pub async fn send_audio(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_path: &Path, file_path: &Path,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -927,6 +994,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("audio", part); .part("audio", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -951,6 +1022,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_voice( pub async fn send_voice(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
file_path: &Path, file_path: &Path,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -966,6 +1038,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.text("chat_id", chat_id.to_string()) .text("chat_id", chat_id.to_string())
.part("voice", part); .part("voice", part);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
form = form.text("caption", cap.to_string()); form = form.text("caption", cap.to_string());
} }
@ -990,6 +1066,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_document_by_url( pub async fn send_document_by_url(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
url: &str, url: &str,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -998,6 +1075,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
"document": url "document": url
}); });
if let Some(tid) = thread_id {
body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
body["caption"] = serde_json::Value::String(cap.to_string()); body["caption"] = serde_json::Value::String(cap.to_string());
} }
@ -1022,6 +1103,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_photo_by_url( pub async fn send_photo_by_url(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
url: &str, url: &str,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -1030,6 +1112,10 @@ Allowlist Telegram username (without '@') or numeric user ID.",
"photo": url "photo": url
}); });
if let Some(tid) = thread_id {
body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
if let Some(cap) = caption { if let Some(cap) = caption {
body["caption"] = serde_json::Value::String(cap.to_string()); body["caption"] = serde_json::Value::String(cap.to_string());
} }
@ -1054,10 +1140,11 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_video_by_url( pub async fn send_video_by_url(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
url: &str, url: &str,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.send_media_by_url("sendVideo", "video", chat_id, url, caption) self.send_media_by_url("sendVideo", "video", chat_id, thread_id, url, caption)
.await .await
} }
@ -1065,10 +1152,11 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_audio_by_url( pub async fn send_audio_by_url(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
url: &str, url: &str,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.send_media_by_url("sendAudio", "audio", chat_id, url, caption) self.send_media_by_url("sendAudio", "audio", chat_id, thread_id, url, caption)
.await .await
} }
@ -1076,10 +1164,11 @@ Allowlist Telegram username (without '@') or numeric user ID.",
pub async fn send_voice_by_url( pub async fn send_voice_by_url(
&self, &self,
chat_id: &str, chat_id: &str,
thread_id: Option<&str>,
url: &str, url: &str,
caption: Option<&str>, caption: Option<&str>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.send_media_by_url("sendVoice", "voice", chat_id, url, caption) self.send_media_by_url("sendVoice", "voice", chat_id, thread_id, url, caption)
.await .await
} }
} }
@ -1094,28 +1183,34 @@ impl Channel for TelegramChannel {
// Strip tool_call tags before processing to prevent Markdown parsing failures // Strip tool_call tags before processing to prevent Markdown parsing failures
let content = strip_tool_call_tags(&message.content); let content = strip_tool_call_tags(&message.content);
// Parse recipient: "chat_id" or "chat_id:thread_id" format
let (chat_id, thread_id) = match message.recipient.split_once(':') {
Some((chat, thread)) => (chat, Some(thread)),
None => (message.recipient.as_str(), None),
};
let (text_without_markers, attachments) = parse_attachment_markers(&content); let (text_without_markers, attachments) = parse_attachment_markers(&content);
if !attachments.is_empty() { if !attachments.is_empty() {
if !text_without_markers.is_empty() { if !text_without_markers.is_empty() {
self.send_text_chunks(&text_without_markers, &message.recipient) self.send_text_chunks(&text_without_markers, chat_id, thread_id)
.await?; .await?;
} }
for attachment in &attachments { for attachment in &attachments {
self.send_attachment(&message.recipient, attachment).await?; self.send_attachment(chat_id, thread_id, attachment).await?;
} }
return Ok(()); return Ok(());
} }
if let Some(attachment) = parse_path_only_attachment(&content) { if let Some(attachment) = parse_path_only_attachment(&content) {
self.send_attachment(&message.recipient, &attachment) self.send_attachment(chat_id, thread_id, &attachment)
.await?; .await?;
return Ok(()); return Ok(());
} }
self.send_text_chunks(&content, &message.recipient).await self.send_text_chunks(&content, chat_id, thread_id).await
} }
async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> { async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
@ -1453,6 +1548,35 @@ mod tests {
assert_eq!(msg.reply_target, "12345"); assert_eq!(msg.reply_target, "12345");
} }
#[test]
fn parse_update_message_extracts_thread_id_for_forum_topic() {
let ch = TelegramChannel::new("token".into(), vec!["*".into()]);
let update = serde_json::json!({
"update_id": 3,
"message": {
"message_id": 42,
"text": "hello from topic",
"from": {
"id": 555,
"username": "alice"
},
"chat": {
"id": -100_200_300
},
"message_thread_id": 789
}
});
let msg = ch
.parse_update_message(&update)
.expect("message with thread_id should parse");
assert_eq!(msg.sender, "alice");
assert_eq!(msg.reply_target, "-100200300:789");
assert_eq!(msg.content, "hello from topic");
assert_eq!(msg.id, "telegram_-100200300_42");
}
// ── File sending API URL tests ────────────────────────────────── // ── File sending API URL tests ──────────────────────────────────
#[test] #[test]
@ -1511,7 +1635,7 @@ mod tests {
// The actual API call will fail (no real server), but we verify the method exists // The actual API call will fail (no real server), but we verify the method exists
// and handles the input correctly up to the network call // and handles the input correctly up to the network call
let result = ch let result = ch
.send_document_bytes("123456", file_bytes, "test.txt", Some("Test caption")) .send_document_bytes("123456", None, file_bytes, "test.txt", Some("Test caption"))
.await; .await;
// Should fail with network error, not a panic or type error // Should fail with network error, not a panic or type error
@ -1531,7 +1655,7 @@ mod tests {
let file_bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let file_bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
let result = ch let result = ch
.send_photo_bytes("123456", file_bytes, "test.png", None) .send_photo_bytes("123456", None, file_bytes, "test.png", None)
.await; .await;
assert!(result.is_err()); assert!(result.is_err());
@ -1542,7 +1666,12 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let result = ch let result = ch
.send_document_by_url("123456", "https://example.com/file.pdf", Some("PDF doc")) .send_document_by_url(
"123456",
None,
"https://example.com/file.pdf",
Some("PDF doc"),
)
.await; .await;
assert!(result.is_err()); assert!(result.is_err());
@ -1553,7 +1682,7 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let result = ch let result = ch
.send_photo_by_url("123456", "https://example.com/image.jpg", None) .send_photo_by_url("123456", None, "https://example.com/image.jpg", None)
.await; .await;
assert!(result.is_err()); assert!(result.is_err());
@ -1566,7 +1695,7 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let path = Path::new("/nonexistent/path/to/file.txt"); let path = Path::new("/nonexistent/path/to/file.txt");
let result = ch.send_document("123456", path, None).await; let result = ch.send_document("123456", None, path, None).await;
assert!(result.is_err()); assert!(result.is_err());
let err = result.unwrap_err().to_string(); let err = result.unwrap_err().to_string();
@ -1582,7 +1711,7 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let path = Path::new("/nonexistent/path/to/photo.jpg"); let path = Path::new("/nonexistent/path/to/photo.jpg");
let result = ch.send_photo("123456", path, None).await; let result = ch.send_photo("123456", None, path, None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -1592,7 +1721,7 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let path = Path::new("/nonexistent/path/to/video.mp4"); let path = Path::new("/nonexistent/path/to/video.mp4");
let result = ch.send_video("123456", path, None).await; let result = ch.send_video("123456", None, path, None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -1602,7 +1731,7 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let path = Path::new("/nonexistent/path/to/audio.mp3"); let path = Path::new("/nonexistent/path/to/audio.mp3");
let result = ch.send_audio("123456", path, None).await; let result = ch.send_audio("123456", None, path, None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -1612,7 +1741,7 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let path = Path::new("/nonexistent/path/to/voice.ogg"); let path = Path::new("/nonexistent/path/to/voice.ogg");
let result = ch.send_voice("123456", path, None).await; let result = ch.send_voice("123456", None, path, None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -1702,13 +1831,19 @@ mod tests {
// With caption // With caption
let result = ch let result = ch
.send_document_bytes("123456", file_bytes.clone(), "test.txt", Some("My caption")) .send_document_bytes(
"123456",
None,
file_bytes.clone(),
"test.txt",
Some("My caption"),
)
.await; .await;
assert!(result.is_err()); // Network error expected assert!(result.is_err()); // Network error expected
// Without caption // Without caption
let result = ch let result = ch
.send_document_bytes("123456", file_bytes, "test.txt", None) .send_document_bytes("123456", None, file_bytes, "test.txt", None)
.await; .await;
assert!(result.is_err()); // Network error expected assert!(result.is_err()); // Network error expected
} }
@ -1722,6 +1857,7 @@ mod tests {
let result = ch let result = ch
.send_photo_bytes( .send_photo_bytes(
"123456", "123456",
None,
file_bytes.clone(), file_bytes.clone(),
"test.png", "test.png",
Some("Photo caption"), Some("Photo caption"),
@ -1731,7 +1867,7 @@ mod tests {
// Without caption // Without caption
let result = ch let result = ch
.send_photo_bytes("123456", file_bytes, "test.png", None) .send_photo_bytes("123456", None, file_bytes, "test.png", None)
.await; .await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -1744,7 +1880,7 @@ mod tests {
let file_bytes: Vec<u8> = vec![]; let file_bytes: Vec<u8> = vec![];
let result = ch let result = ch
.send_document_bytes("123456", file_bytes, "empty.txt", None) .send_document_bytes("123456", None, file_bytes, "empty.txt", None)
.await; .await;
// Should not panic, will fail at API level // Should not panic, will fail at API level
@ -1756,7 +1892,9 @@ mod tests {
let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]);
let file_bytes = b"content".to_vec(); let file_bytes = b"content".to_vec();
let result = ch.send_document_bytes("123456", file_bytes, "", None).await; let result = ch
.send_document_bytes("123456", None, file_bytes, "", None)
.await;
// Should not panic // Should not panic
assert!(result.is_err()); assert!(result.is_err());
@ -1768,7 +1906,7 @@ mod tests {
let file_bytes = b"content".to_vec(); let file_bytes = b"content".to_vec();
let result = ch let result = ch
.send_document_bytes("", file_bytes, "test.txt", None) .send_document_bytes("", None, file_bytes, "test.txt", None)
.await; .await;
// Should not panic // Should not panic