initial commit
This commit is contained in:
commit
b98dd28fcd
5 changed files with 1565 additions and 0 deletions
183
src/main.rs
Normal file
183
src/main.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use reqwest::{Client, Url};
|
||||
use serde::Deserialize;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
|
||||
const BOT_URL: &str = "https://api.telegram.org/bot";
|
||||
/// Command to search for in a message text
|
||||
const COMMAND: &str = "/xkcd";
|
||||
/// Long polling timeout in seconds
|
||||
const POLL_TIMEOUT: &str = "300";
|
||||
/// Update types to receive (filtered out on tg backend)
|
||||
const POLL_TYPES: &str = r#"["message"]"#;
|
||||
|
||||
/// Shared mutable state object
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
offset: u32,
|
||||
}
|
||||
|
||||
/// Shared config object
|
||||
#[derive(Debug, Clone)]
|
||||
struct Config {
|
||||
client: Client,
|
||||
upd_url: Arc<Url>,
|
||||
send_url: Arc<Url>,
|
||||
}
|
||||
|
||||
/// Telegram Bot API response:
|
||||
/// these idiots use `{ok:true,response:...}` schema
|
||||
/// for some reason
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TgResponse {
|
||||
result: Vec<TgUpdate>,
|
||||
}
|
||||
|
||||
/// Telegram Bot API update event schema
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TgUpdate {
|
||||
update_id: u32,
|
||||
message: Option<TgMessage>,
|
||||
}
|
||||
|
||||
/// Telegram Bot API message object schema
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TgMessage {
|
||||
#[serde(rename = "message_thread_id")]
|
||||
thread: Option<i32>,
|
||||
chat: TgChat,
|
||||
text: Option<String>,
|
||||
}
|
||||
|
||||
/// Telegram Bot API chat object schema
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TgChat {
|
||||
id: i64,
|
||||
// #[serde(default)]
|
||||
// is_forum: bool,
|
||||
}
|
||||
|
||||
/// XKCD.com API comic info schema
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct XkcdInfo {
|
||||
alt: String,
|
||||
img: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let cfg = {
|
||||
let token = std::env::var("BOT_TOKEN").expect("invalid BOT_TOKEN env var value");
|
||||
Config {
|
||||
client: Client::new(),
|
||||
upd_url: Url::parse(&format!("{BOT_URL}{token}/getUpdates"))
|
||||
.unwrap()
|
||||
.into(),
|
||||
send_url: Url::parse(&format!("{BOT_URL}{token}/sendPhoto"))
|
||||
.unwrap()
|
||||
.into(),
|
||||
}
|
||||
};
|
||||
|
||||
let mut s = State { offset: 0 };
|
||||
|
||||
let mut sigint = signal(SignalKind::interrupt())?;
|
||||
let mut sigterm = signal(SignalKind::terminate())?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = sigint.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = sigterm.recv() => {
|
||||
break;
|
||||
}
|
||||
out = handler(&cfg, &mut s) => {
|
||||
match out {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("[handler] {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handler(cfg: &Config, s: &mut State) -> Result<(), reqwest::Error> {
|
||||
let mut updates = cfg
|
||||
.client
|
||||
.get((*cfg.upd_url).clone())
|
||||
.query(&[
|
||||
("offset", s.offset.to_string().as_str()),
|
||||
("timeout", POLL_TIMEOUT),
|
||||
("allowed_updates", POLL_TYPES),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<TgResponse>()
|
||||
.await?
|
||||
.result;
|
||||
|
||||
for u in &mut updates {
|
||||
let Some(mut msg) = u.message.take() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(text) = msg.text.take() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !text.starts_with(COMMAND) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(comic_id) = text
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.next()
|
||||
.and_then(|id| id.parse::<u16>().ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
tokio::spawn(send_comic(cfg.clone(), msg, comic_id));
|
||||
}
|
||||
|
||||
if let Some(u) = updates.last() {
|
||||
s.offset = u.update_id + 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_comic(cfg: Config, msg: TgMessage, comic_id: u16) -> Result<(), reqwest::Error> {
|
||||
let info = cfg
|
||||
.client
|
||||
.get(format!("https://xkcd.com/{}/info.0.json", comic_id))
|
||||
.send()
|
||||
.await?
|
||||
.json::<XkcdInfo>()
|
||||
.await?;
|
||||
|
||||
cfg.client
|
||||
.post((*cfg.send_url).clone())
|
||||
.form(&[
|
||||
("chat_id", msg.chat.id.to_string()),
|
||||
(
|
||||
"message_thread_id",
|
||||
msg.thread
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or("null".to_owned()),
|
||||
),
|
||||
("photo", info.img),
|
||||
("caption", info.alt),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Reference in a new issue