leetbot/src/main.rs

248 lines
6.6 KiB
Rust
Raw Normal View History

2022-07-29 19:32:35 +00:00
mod lib;
use crate::lib::{text_is_leet, time_is_leet};
2022-07-29 19:32:35 +00:00
use chrono::{NaiveDateTime, NaiveTime, Local};
2022-07-29 19:32:35 +00:00
use sqlx::AnyPool;
use sqlx::any::AnyPoolOptions;
use teloxide::{prelude::*, RequestError, dispatching::UpdateFilterExt, utils::command::BotCommands};
use thiserror::Error;
// DB
async fn init_db_pool(url: &str) -> Result<AnyPool, sqlx::Error> {
let pool = AnyPoolOptions::new()
.max_connections(5)
.connect(url).await?;
Ok(pool)
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
async fn setup_tables(pool: &sqlx::AnyPool) -> Result<(), sqlx::Error> {
let query_sql = match pool.any_kind() {
sqlx::any::AnyKind::Postgres => "create table if not exists leet_log (
id int generated always as identity,
username varchar(255) not null,
log_time timestamp not null
)" ,
sqlx::any::AnyKind::Sqlite => "create table if not exists leet_log (
id integer primary key autoincrement not null,
username varchar(255) not null,
log_time timestamp not null
)"
};
sqlx::query(query_sql)
.execute(pool)
.await?;
Ok(())
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
#[derive(Debug,Error)]
enum LeetError {
#[error("It's not leet right now")]
NotLeet,
#[error("Already hit leet for today")]
AlreadyHitLeet,
#[error("Sql error while checking for leet status: {0}")]
DbError(#[from] sqlx::Error)
}
2019-05-07 18:00:23 +00:00
async fn check_leet(username: &str, sent_time: NaiveDateTime, pool: &AnyPool) -> Result<(), LeetError> {
if ! time_is_leet(sent_time.time()) {
2022-07-29 19:32:35 +00:00
return Err(LeetError::NotLeet);
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let latest_leet : Option<(i32, String, NaiveDateTime)> = sqlx::query_as("select * from leet_log where username = $1 order by log_time desc")
.bind(username)
.fetch_optional(pool)
.await?;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
if let Some(latest_leet) = latest_leet {
let distance = sent_time - latest_leet.2;
2019-05-07 18:00:23 +00:00
if distance.num_hours() < 1 {
2022-07-29 19:32:35 +00:00
return Err(LeetError::AlreadyHitLeet);
}
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
Ok(())
}
2019-05-07 18:00:23 +00:00
async fn log_leet(username: &str, sent_time: NaiveDateTime, pool: &AnyPool) -> Result<(), sqlx::Error> {
sqlx::query("insert into leet_log (username, log_time) values ($1, $2)")
2022-07-29 19:32:35 +00:00
.bind(username)
.bind(sent_time)
2022-07-29 19:32:35 +00:00
.execute(pool)
.await?;
2022-07-29 19:32:35 +00:00
Ok(())
}
2022-07-29 19:32:35 +00:00
async fn get_scores(pool: &AnyPool) -> Result<Vec<(String,i64)>, sqlx::Error> {
let score: Vec<_> = sqlx::query_as("select username, count(username) as score from leet_log group by username")
.fetch_all(pool)
.await?;
2022-07-29 19:32:35 +00:00
Ok(score)
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
// Bot
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
fn message_ok() -> Result<(), MessageHandlerError> {
Ok(respond(())?)
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
#[derive(BotCommands, Clone)]
#[command(rename = "lowercase", description = "score commands")]
enum LeetCommand {
#[command(description = "get scores")]
Scores,
#[command(description = "get current time info")]
2022-08-04 16:30:09 +00:00
Time,
#[command(description = "show help about leetbot")]
Help
2022-07-29 19:32:35 +00:00
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
#[derive(Error,Debug)]
enum MessageHandlerError {
#[error("Telegram request error {0}")]
Request(#[from] RequestError),
#[error("No sender for message!")]
NoFrom,
#[error("Sender has no username!")]
NoUsername,
#[error("Internal sql error {0}")]
Db(#[from] sqlx::Error),
#[error("No score for user in db")]
NoScore,
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
fn filter_leet(message: Message) -> bool {
let text = match message.text() {
Some(t) => t,
None => { return false; }
};
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
text_is_leet(text)
}
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
async fn leet_message_handler(message: Message, bot: AutoSend<Bot>, pool: AnyPool) -> Result<(), MessageHandlerError> {
let username = message.from()
.ok_or(MessageHandlerError::NoFrom)?
.username.clone()
.ok_or(MessageHandlerError::NoUsername)?;
let message_sent_time = message.date
.with_timezone(&Local)
.naive_local();
let leet_check = check_leet(&username, message_sent_time, &pool).await;
2022-07-29 19:32:35 +00:00
if let Err(e) = leet_check {
match e {
LeetError::NotLeet => bot.send_message(message.chat.id, "it is not leet right now").await?,
LeetError::AlreadyHitLeet => bot.send_message(message.chat.id, "you already hit leet today!").await?,
LeetError::DbError(e) => {
bot.send_message(message.chat.id, "internal sql error").await?;
return Err(MessageHandlerError::Db(e));
}
};
return message_ok();
}
2019-05-07 18:00:23 +00:00
log_leet(&username, message_sent_time, &pool).await?;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let scores = get_scores(&pool).await?;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let user_score = scores.iter().find(|(u,_)| u == &username).ok_or(MessageHandlerError::NoScore)?.1;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let msg = format!("{} just hit leet! New score: {}", username, user_score);
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
bot.send_message(message.chat.id, msg).await?;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
message_ok()
}
async fn leet_command_handler(message: Message, cmd: LeetCommand, bot: AutoSend<Bot>, pool: AnyPool) -> Result<(), MessageHandlerError> {
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
match cmd {
LeetCommand::Scores => {
let scores = get_scores(&pool).await?;
2022-07-29 19:32:35 +00:00
let scores_str = scores.into_iter().map(|(name,score)| format!(" {}: {}\n", name, score)).collect::<String>();
2022-07-29 19:32:35 +00:00
let msg = format!("Leet scores:\n{}", scores_str);
bot.send_message(message.chat.id, msg).await?;
},
LeetCommand::Time => {
let message_time = message.date
.with_timezone(&Local)
.naive_local();
let current_time = Local::now().time();
let leet_time = NaiveTime::from_hms(13,37,30);
let distance_to_leet = leet_time - current_time;
let msg = format!("My current time is: {}
message was sent at: {}
leet is at: {}, in {} seconds",
current_time,
message_time,
leet_time,
distance_to_leet.num_seconds()
);
2022-07-29 19:32:35 +00:00
bot.send_message(message.chat.id, msg).await?;
2022-08-04 16:30:09 +00:00
},
LeetCommand::Help => {
bot.send_message(message.chat.id, LeetCommand::descriptions().to_string()).await?;
2022-07-29 19:32:35 +00:00
}
}
2022-07-29 19:32:35 +00:00
message_ok()
}
2019-07-02 19:09:10 +00:00
2022-07-29 19:32:35 +00:00
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
pretty_env_logger::init();
2019-07-02 19:09:10 +00:00
2022-07-29 19:32:35 +00:00
let db_url = std::env::var("DB_URL")
.unwrap_or_else(|_| "sqlite::memory:".to_string());
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let db_pool = init_db_pool(&db_url).await?;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
setup_tables(&db_pool).await?;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let telegram_token = std::env::var("TELEGRAM_BOT_TOKEN")
.expect("TELEGRAM_TOKEN env not set!");
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let bot = Bot::new(&telegram_token).auto_send();
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
let handler = Update::filter_message()
.branch(dptree::entry()
.filter_command::<LeetCommand>()
.endpoint(leet_command_handler))
.branch(dptree::entry()
.filter(filter_leet)
.endpoint(leet_message_handler))
;
Dispatcher::builder(bot, handler)
.dependencies(dptree::deps![db_pool])
.default_handler(|upd| async move {
println!("unhandled update: {:?}", upd);
})
.error_handler(LoggingErrorHandler::with_custom_text("error during dispatch"))
.enable_ctrlc_handler()
.build()
.dispatch()
.await;
2019-05-07 18:00:23 +00:00
2022-07-29 19:32:35 +00:00
Ok(())
2019-05-07 18:00:23 +00:00
}