初步集成消息通道,支持Telegram + Lark机器人
This commit is contained in:
255
src-tauri/src/chat_channel/backends/telegram.rs
Normal file
255
src-tauri/src/chat_channel/backends/telegram.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use crate::chat_channel::error::ChatChannelError;
|
||||
use crate::chat_channel::traits::ChatChannelBackend;
|
||||
use crate::chat_channel::types::*;
|
||||
|
||||
pub struct TelegramBackend {
|
||||
bot_token: String,
|
||||
chat_id: String,
|
||||
client: reqwest::Client,
|
||||
status: Arc<Mutex<ChannelConnectionStatus>>,
|
||||
channel_id: i32,
|
||||
shutdown_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>,
|
||||
}
|
||||
|
||||
impl TelegramBackend {
|
||||
pub fn new(channel_id: i32, bot_token: String, chat_id: String) -> Self {
|
||||
Self {
|
||||
bot_token,
|
||||
chat_id,
|
||||
client: reqwest::Client::new(),
|
||||
status: Arc::new(Mutex::new(ChannelConnectionStatus::Disconnected)),
|
||||
channel_id,
|
||||
shutdown_tx: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn api_url(&self, method: &str) -> String {
|
||||
format!(
|
||||
"https://api.telegram.org/bot{}/{}",
|
||||
self.bot_token, method
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ChatChannelBackend for TelegramBackend {
|
||||
fn channel_type(&self) -> ChannelType {
|
||||
ChannelType::Telegram
|
||||
}
|
||||
|
||||
async fn start(
|
||||
&self,
|
||||
command_tx: mpsc::Sender<IncomingCommand>,
|
||||
) -> Result<(), ChatChannelError> {
|
||||
*self.status.lock().await = ChannelConnectionStatus::Connecting;
|
||||
|
||||
// Verify bot token by calling getMe
|
||||
let resp = self
|
||||
.client
|
||||
.get(&self.api_url("getMe"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ChatChannelError::ConnectionFailed(e.to_string()))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
*self.status.lock().await = ChannelConnectionStatus::Error;
|
||||
return Err(ChatChannelError::AuthenticationFailed(
|
||||
"Invalid bot token".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
*self.status.lock().await = ChannelConnectionStatus::Connected;
|
||||
|
||||
// Start long-polling loop
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
|
||||
*self.shutdown_tx.lock().await = Some(shutdown_tx);
|
||||
|
||||
let client = self.client.clone();
|
||||
let bot_token = self.bot_token.clone();
|
||||
let channel_id = self.channel_id;
|
||||
let status = self.status.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut offset: i64 = 0;
|
||||
loop {
|
||||
if *shutdown_rx.borrow() {
|
||||
break;
|
||||
}
|
||||
|
||||
let url = format!(
|
||||
"https://api.telegram.org/bot{}/getUpdates?timeout=30&offset={}",
|
||||
bot_token, offset
|
||||
);
|
||||
|
||||
let result = tokio::select! {
|
||||
r = client.get(&url).send() => r,
|
||||
_ = shutdown_rx.changed() => break,
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(resp) => {
|
||||
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||
if let Some(updates) = body.get("result").and_then(|r| r.as_array()) {
|
||||
for update in updates {
|
||||
if let Some(uid) =
|
||||
update.get("update_id").and_then(|u| u.as_i64())
|
||||
{
|
||||
offset = uid + 1;
|
||||
}
|
||||
if let Some(text) = update
|
||||
.pointer("/message/text")
|
||||
.and_then(|t| t.as_str())
|
||||
{
|
||||
let sender_id = update
|
||||
.pointer("/message/from/id")
|
||||
.and_then(|i| i.as_i64())
|
||||
.map(|i| i.to_string())
|
||||
.unwrap_or_default();
|
||||
let _ = command_tx
|
||||
.send(IncomingCommand {
|
||||
channel_id,
|
||||
sender_id,
|
||||
command_text: text.to_string(),
|
||||
metadata: update.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[Telegram] polling error: {e}");
|
||||
*status.lock().await = ChannelConnectionStatus::Error;
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
*status.lock().await = ChannelConnectionStatus::Connected;
|
||||
}
|
||||
}
|
||||
}
|
||||
*status.lock().await = ChannelConnectionStatus::Disconnected;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&self) -> Result<(), ChatChannelError> {
|
||||
if let Some(tx) = self.shutdown_tx.lock().await.take() {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
*self.status.lock().await = ChannelConnectionStatus::Disconnected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(&self) -> ChannelConnectionStatus {
|
||||
*self.status.lock().await
|
||||
}
|
||||
|
||||
async fn send_message(&self, text: &str) -> Result<SentMessageId, ChatChannelError> {
|
||||
let body = serde_json::json!({
|
||||
"chat_id": self.chat_id,
|
||||
"text": text,
|
||||
"parse_mode": "Markdown",
|
||||
});
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.post(&self.api_url("sendMessage"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ChatChannelError::SendFailed(e.to_string()))?;
|
||||
|
||||
let result: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ChatChannelError::SendFailed(e.to_string()))?;
|
||||
|
||||
let message_id = result
|
||||
.pointer("/result/message_id")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(SentMessageId(message_id))
|
||||
}
|
||||
|
||||
async fn send_rich_message(
|
||||
&self,
|
||||
message: &RichMessage,
|
||||
) -> Result<SentMessageId, ChatChannelError> {
|
||||
let text = format_telegram_markdown(message);
|
||||
self.send_message(&text).await
|
||||
}
|
||||
|
||||
async fn test_connection(&self) -> Result<(), ChatChannelError> {
|
||||
let resp = self
|
||||
.client
|
||||
.get(&self.api_url("getMe"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ChatChannelError::ConnectionFailed(e.to_string()))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChatChannelError::AuthenticationFailed(
|
||||
"Invalid bot token".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_telegram_markdown(msg: &RichMessage) -> String {
|
||||
let mut text = String::new();
|
||||
|
||||
let level_emoji = match msg.level {
|
||||
MessageLevel::Info => "ℹ️",
|
||||
MessageLevel::Warning => "⚠️",
|
||||
MessageLevel::Error => "❌",
|
||||
};
|
||||
|
||||
if let Some(title) = &msg.title {
|
||||
text.push_str(&format!("{} *{}*\n", level_emoji, escape_markdown(title)));
|
||||
}
|
||||
|
||||
text.push_str(&escape_markdown(&msg.body));
|
||||
|
||||
if !msg.fields.is_empty() {
|
||||
text.push('\n');
|
||||
for (key, value) in &msg.fields {
|
||||
text.push_str(&format!(
|
||||
"\n*{}*: {}",
|
||||
escape_markdown(key),
|
||||
escape_markdown(value)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
fn escape_markdown(text: &str) -> String {
|
||||
text.replace('_', "\\_")
|
||||
.replace('*', "\\*")
|
||||
.replace('[', "\\[")
|
||||
.replace(']', "\\]")
|
||||
.replace('(', "\\(")
|
||||
.replace(')', "\\)")
|
||||
.replace('~', "\\~")
|
||||
.replace('`', "\\`")
|
||||
.replace('>', "\\>")
|
||||
.replace('#', "\\#")
|
||||
.replace('+', "\\+")
|
||||
.replace('-', "\\-")
|
||||
.replace('=', "\\=")
|
||||
.replace('|', "\\|")
|
||||
.replace('{', "\\{")
|
||||
.replace('}', "\\}")
|
||||
.replace('.', "\\.")
|
||||
.replace('!', "\\!")
|
||||
}
|
||||
Reference in New Issue
Block a user