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:
parent
3467d34596
commit
36062fb1c2
1 changed files with 174 additions and 36 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue