内容介绍
本专题将带你使用axum实现一个 Webhook 形式的 Telegram 机器人。webhook
Telegram 机器人支持两种方式:轮询和 Webhook。为了节约资源我们将使用 Webhook 的方式开发 Telegram 机器人。处理文本消息
本章将开始与 Telegram 机器人进行交互。首先从最简单的文本消息开始。处理指令
本章我们将学习如何处理 Telegram 的“指令”(command)。开始之前,我们对之前的代码进行必要的封装。发送图片
本章继续完善我们的机器人。收到用户的`/logo`指令,我们需要把我们的 LOGO 图片发送给用户。让我们来看看如何让 Telegram 机器人发送图片信息。发送Markdown
Telegram 还支持 Markdown 和 HTML 类型的文本消息。本章我们将实现`/help`指令,它会将帮助信息以 Markdown 格式发送给用户。总结
本专题带你实现了一个简单的 Telegram 机器人。我们实现的功能是很简单的,其实 Telegram 支持多种消息
处理文本消息
本章将开始与 Telegram 机器人进行交互。首先从最简单的文本消息开始。
本章代码在02/处理文本信息分支。
本章代码在02/处理文本信息分支。
Telegram 向 Webhook 推送了什么数据
开始之前,我们必须清楚,当有新消息的时候,Telegram 向 Webhook 推送了什么数据。
让我们改一下 handler,以便将接收到的数据打印出来:
pub async fn hook(msg: String) -> String {
tracing::debug!("{}", msg);
msg
}
重新运行我们的程序,然后向机器人发送一条信息。之后,我们可以看到控制台打印出了类以下面的内容:
{
"update_id": 999999999,
"message": {
"message_id": 11111,
"from": {
"id": 888888888,
"is_bot": false,
"first_name": "昵称",
"username": "user_name",
"language_code": "zh-hans"
},
"chat": {
"id": 888888888,
"first_name": "昵称",
"username": "user_name",
"type": "private"
},
"date": 1637906678,
"text": "欢迎光临axum中文网"
}
}
完善数据结构
对照上面的打印可以发现,我们定义的数据结构少了很多字段需要把它们补齐:
#[derive(Deserialize, Debug)]
pub struct Update {
pub update_id: u64,
pub message: Message,
}
#[derive(Deserialize, Debug)]
pub struct Message {
pub message_id: u64,
pub from: User,
pub chat: Chat,
pub date: u64,
pub text: String,
}
#[derive(Deserialize, Debug)]
pub struct User {
pub id: u64,
pub is_bot: bool,
pub first_name: String,
pub username: String,
pub language_code: String,
}
#[derive(Deserialize, Debug)]
pub struct Chat {
pub id: u64,
pub first_name: String,
pub username: String,
#[serde(rename(deserialize = "type"))]
pub types: String,
}
即便我们对数据结构进行补充完善,但它还不是完整的,只是从目前我们的需求来看,它是符合要求的。完整的数据结构请查看官方文档。
将 handler 修改回之前接收 JSON 并反序列化为结构体:
pub async fn hook(Json(payload): Json<Update>, Extension(state): Extension<AppState>) -> String {
let msg = format!("{:?}", payload);
tracing::debug!("received: {}", msg);
msg
}
回复文本消息
现在我们实现一个echo
程序:将用户输入的信息原样返回,为了作区分,我们加上ECHO:
字样。
要回复文本信息,我们需要调用 Telegram 的sendMessage
API,它有两个必填项:
-
chat_id
:你要回复的信息的 ID。在我们的Chat
中,有个id
字段,这个chat_id
就是从这个字段中取值。 -
text
:你要回复的内容。
chat_id
:你要回复的信息的 ID。在我们的Chat
中,有个id
字段,这个chat_id
就是从这个字段中取值。
text
:你要回复的内容。
TextMessage
结构体
针对上面的分析,我们定义一个 TextMessage
,它位于 src/types/request.rs
:
#[derive(Serialize, Debug)]
pub struct TextMessage {
pub chat_id: u64,
pub text: String,
}
发送 HTTP 请求来调用 Telegram 的 API
sendMessage
的完整请求地址是https://api.telegram.org/bot{你的TOKEN}/sendMessage
:
// 回复信息
let send_data = request::TextMessage {
chat_id: payload.message.chat.id,
text: format!("ECHO: {}", payload.message.text),
};
let api_addr = format!(
"https://api.telegram.org/bot{}/{}",
state.bot.token.clone(),
"sendMessage"
);
let res = reqwest::Client::new()
.post(&api_addr)
.form(&send_data)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
tracing::debug!("sendMessage: {}", &res);
配置文件与状态共享
我们把机器人 Token 放在了配置文件.env
中:
WEB.ADDR=127.0.0.1:9527
TG_BOT.TOKEN=你的TG机器人TOKEN
TG_BOT.WEBHOOK=https://tg.axum.rs/
对应的结构体及方法:
// src/config.rs
#[derive(Deserialize)]
pub struct WebConfig {
pub addr: String,
}
#[derive(Deserialize, Clone)]
pub struct TgBotConfig {
pub token: String,
pub webhook: String,
}
#[derive(Deserialize)]
pub struct Config {
pub web: WebConfig,
pub tg_bot: TgBotConfig,
}
impl Config {
pub fn from_env() -> Result<Self, config::ConfigError> {
let mut cfg = config::Config::new();
cfg.merge(config::Environment::new())?;
cfg.try_into()
}
}
// src/model.rs
#[derive(Clone)]
pub struct AppState {
pub bot: config::TgBotConfig,
}
最后,在路由定义的时候,加入这个共享状态:
现在,我们给机器人发送一条消息,不但收到了回复,同时控制台也打印出来了 Telegram API 的返回信息:
{
"ok": true,
"result": {
"message_id": 33333,
"from": {
"id": 6666666,
"is_bot": true,
"first_name": "AXUM.RS",
"username": "axum_rs_bot"
},
"chat": {
"id": 888888888,
"first_name": "昵称",
"username": "user_name",
"type": "private"
},
"date": 1637910187,
"text": "ECHO: 欢迎光临axum中文网"
}
}