处理文本消息

699608
2021/11/27 05:18:26

本章将开始与 Telegram 机器人进行交互。首先从最简单的文本消息开始。

本章代码在02/处理文本信息分支。

Telegram 向 Webhook 推送了什么数据

开始之前,我们必须清楚,当有新消息的时候,Telegram 向 Webhook 推送了什么数据。

pub async fn hook(msg: String) -> String {
    tracing::debug!("{}", msg);
    msg
}

重新运行我们的程序,然后向机器人发送一条信息。之后,我们可以看到控制台打印出了类以下面的内容:

完善数据结构

对照上面的打印可以发现,我们定义的数据结构少了很多字段需要把它们补齐:

#[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:你要回复的内容。

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()
    }
}

为了共享这个 Token,我们需要定义 AppState 并将 Token 放到其中:

// src/model.rs

#[derive(Clone)]
pub struct AppState {
    pub bot: config::TgBotConfig,
}

最后,在路由定义的时候,加入这个共享状态:

// src/main.rs

dotenv().ok();
let cfg = config::Config::from_env().expect("初始化配置失败");

let app = Router::new()
    .route("/", routing::post(handler::hook).get(handler::index))
    .layer(AddExtensionLayer::new(model::AppState {
        bot: cfg.tg_bot.clone(),
    }));

测试

现在,我们给机器人发送一条消息,不但收到了回复,同时控制台也打印出来了 Telegram API 的返回信息: