xkcdtg/src/main.rs

167 lines
4.9 KiB
Rust
Raw Normal View History

2024-11-27 17:40:02 +04:00
mod types;
use types::*;
2024-11-26 22:45:46 +04:00
use reqwest::{Client, Url};
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";
/// Length of the command substring
const CMD_LEN: usize = COMMAND.len();
2024-11-26 22:45:46 +04:00
/// 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"]"#;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let cfg = {
let token = std::env::var("BOT_TOKEN").expect("invalid BOT_TOKEN env var value");
2024-11-27 18:28:34 +04:00
let client = Client::new();
init_cmds(&client, &token)
.await
.expect("could not make request to API");
2024-11-26 22:45:46 +04:00
Config {
2024-11-27 18:28:34 +04:00
client,
2024-11-26 22:45:46 +04:00
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(())
}
2024-11-27 18:28:34 +04:00
async fn init_cmds(client: &Client, token: &str) -> Result<(), reqwest::Error> {
client
.get(format!("{BOT_URL}{token}/setMyCommands"))
.query(&[(
"commands",
serde_json::json!([
{
"command": COMMAND[1..], // cmd name without leading slash
"description": "Get XKCD comic by id",
}
])
.to_string(),
)])
.send()
.await?
.error_for_status()?;
Ok(())
}
2024-11-26 22:45:46 +04:00
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;
updates
.iter_mut()
.filter_map(|u| {
u.message.take().and_then(|mut msg| {
msg.text.take().and_then(|text| {
// starts with our command
if text.get(..CMD_LEN)? == COMMAND
// check space or @ after cmd name
// to ensure that our command is
// not a part of any other cmd
&& [" ", "@"].contains(&text.get(CMD_LEN..CMD_LEN + 1)?)
{
let mut args = text.split_whitespace();
args.next(); // skip command name
match args.next()? {
"ru" => {
let comic_id = args.next()?.parse::<u16>().ok()?;
Some((msg, format!("https://xkcd.ru/{}/?json=true", comic_id)))
}
"en" => {
let comic_id = args.next()?.parse::<u16>().ok()?;
Some((msg, format!("https://xkcd.com/{}/info.0.json", comic_id)))
}
comic_id => {
let comic_id = comic_id.parse::<u16>().ok()?;
Some((msg, format!("https://xkcd.com/{}/info.0.json", comic_id)))
}
}
} else {
None
}
})
})
})
.for_each(|(msg, url)| {
tokio::spawn(send_comic(cfg.clone(), msg, url));
});
2024-11-26 22:45:46 +04:00
if let Some(u) = updates.last() {
s.offset = u.update_id + 1;
}
Ok(())
}
async fn send_comic(cfg: Config, msg: TgMessage, url: String) -> Result<(), reqwest::Error> {
let info = cfg.client.get(url).send().await?.json::<XkcdInfo>().await?;
2024-11-26 22:45:46 +04:00
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(())
}