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";
|
2024-11-27 17:35:34 +04:00
|
|
|
/// 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
|
2024-11-27 18:36:18 +04:00
|
|
|
.post(format!("{BOT_URL}{token}/setMyCommands"))
|
|
|
|
.form(&[(
|
2024-11-27 18:28:34 +04:00
|
|
|
"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?
|
2024-11-27 18:39:25 +04:00
|
|
|
.error_for_status()?
|
2024-11-26 22:45:46 +04:00
|
|
|
.json::<TgResponse>()
|
|
|
|
.await?
|
|
|
|
.result;
|
|
|
|
|
2024-11-27 17:35:34 +04:00
|
|
|
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
|
2024-11-27 18:39:25 +04:00
|
|
|
|
2024-11-27 17:35:34 +04:00
|
|
|
match args.next()? {
|
|
|
|
"ru" => {
|
|
|
|
let comic_id = args.next()?.parse::<u16>().ok()?;
|
2024-11-27 18:49:39 +04:00
|
|
|
let url = format!("https://xkcd.ru/{}/?json=true", comic_id);
|
|
|
|
Some(SenderCtx { msg, url, comic_id })
|
2024-11-27 17:35:34 +04:00
|
|
|
}
|
|
|
|
"en" => {
|
|
|
|
let comic_id = args.next()?.parse::<u16>().ok()?;
|
2024-11-27 18:49:39 +04:00
|
|
|
let url = format!("https://xkcd.com/{}/info.0.json", comic_id);
|
|
|
|
Some(SenderCtx { msg, url, comic_id })
|
2024-11-27 17:35:34 +04:00
|
|
|
}
|
|
|
|
comic_id => {
|
|
|
|
let comic_id = comic_id.parse::<u16>().ok()?;
|
2024-11-27 18:49:39 +04:00
|
|
|
let url = format!("https://xkcd.com/{}/info.0.json", comic_id);
|
|
|
|
Some(SenderCtx { msg, url, comic_id })
|
2024-11-27 17:35:34 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
2024-11-27 18:49:39 +04:00
|
|
|
.for_each(|ctx| {
|
|
|
|
tokio::spawn(send_comic(cfg.clone(), ctx));
|
2024-11-27 17:35:34 +04:00
|
|
|
});
|
2024-11-26 22:45:46 +04:00
|
|
|
|
|
|
|
if let Some(u) = updates.last() {
|
|
|
|
s.offset = u.update_id + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:49:39 +04:00
|
|
|
async fn send_comic(cfg: Config, ctx: SenderCtx) -> Result<(), reqwest::Error> {
|
|
|
|
let info = cfg
|
|
|
|
.client
|
|
|
|
.get(ctx.url)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.error_for_status()?
|
|
|
|
.json::<XkcdInfo>()
|
|
|
|
.await?;
|
2024-11-26 22:45:46 +04:00
|
|
|
|
|
|
|
cfg.client
|
|
|
|
.post((*cfg.send_url).clone())
|
|
|
|
.form(&[
|
2024-11-27 18:49:39 +04:00
|
|
|
("chat_id", ctx.msg.chat.id.to_string()),
|
2024-11-26 22:45:46 +04:00
|
|
|
(
|
|
|
|
"message_thread_id",
|
2024-11-27 18:49:39 +04:00
|
|
|
ctx.msg
|
|
|
|
.thread
|
2024-11-26 22:45:46 +04:00
|
|
|
.map(|t| t.to_string())
|
|
|
|
.unwrap_or("null".to_owned()),
|
|
|
|
),
|
|
|
|
("photo", info.img),
|
|
|
|
("caption", info.alt),
|
|
|
|
])
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|