Compare commits
No commits in common. "787988d9c804cd617392dee56e3055c3a3fb2005" and "7b4dc5d9a76a10188b9398fdb8f587de5af07976" have entirely different histories.
787988d9c8
...
7b4dc5d9a7
2547
Cargo.lock
generated
2547
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@ -2,21 +2,15 @@
|
||||
name = "telegram-leetbot"
|
||||
version = "0.1.0"
|
||||
authors = ["Andreas Larsen <andreas@northcode.no>"]
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.19"
|
||||
pretty_env_logger = "0.4.0"
|
||||
regex = "1.6.0"
|
||||
# frankenstein = { version = "0.19.0", features = ["async-http-client"] }
|
||||
sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "sqlite", "any", "chrono" ] }
|
||||
teloxide = { version = "0.10.1", features = [ "macros" ] }
|
||||
thiserror = "1.0.31"
|
||||
# futures = "*"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
# serde = "1.0"
|
||||
# serde_json = "1.0"
|
||||
# chrono = "0.4.19"
|
||||
telegram-bot = "0.6.1"
|
||||
futures = "*"
|
||||
tokio-core = "*"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4.6"
|
||||
|
||||
# [patch.crates-io]
|
||||
# openssl = { git = "https://github.com/ishitatsuyuki/rust-openssl", branch = "0.9.x" }
|
||||
[patch.crates-io]
|
||||
openssl = { git = "https://github.com/ishitatsuyuki/rust-openssl", branch = "0.9.x" }
|
||||
@ -4,4 +4,3 @@ metadata:
|
||||
name: {{ .Release.Name }}-token
|
||||
data:
|
||||
TELEGRAM_BOT_TOKEN: {{ .Values.app.token | b64enc }}
|
||||
DB_URL: {{ .Values.app.db | b64enc }}
|
||||
|
||||
@ -3,7 +3,6 @@ image:
|
||||
|
||||
app:
|
||||
token: blabla
|
||||
db: sqlite:leet.db
|
||||
tz: Europe/Oslo
|
||||
|
||||
storage:
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
podman run -d -p 5432:5432 -e POSTGRES_USER=leetbot -e POSTGRES_PASSWORD=leetbot docker.io/postgres
|
||||
17
src/lib.rs
17
src/lib.rs
@ -1,17 +0,0 @@
|
||||
use chrono::{Local, NaiveTime};
|
||||
|
||||
|
||||
pub fn text_is_leet(text: &str) -> bool {
|
||||
text.eq_ignore_ascii_case("leet") || text == "l33t" || text == "1337"
|
||||
}
|
||||
|
||||
pub fn time_is_leet() -> bool {
|
||||
let now = Local::now().time();
|
||||
let leet = NaiveTime::from_hms(13,37,30);
|
||||
|
||||
let allowed_timegap = 30;
|
||||
|
||||
let distance = now - leet;
|
||||
|
||||
distance.num_seconds().abs() <= allowed_timegap
|
||||
}
|
||||
339
src/main.rs
339
src/main.rs
@ -1,225 +1,142 @@
|
||||
mod lib;
|
||||
use crate::lib::*;
|
||||
extern crate telegram_bot;
|
||||
extern crate futures;
|
||||
extern crate tokio_core;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate chrono;
|
||||
|
||||
use chrono::{NaiveDateTime, Local};
|
||||
use sqlx::AnyPool;
|
||||
use sqlx::any::AnyPoolOptions;
|
||||
use teloxide::{prelude::*, RequestError, dispatching::UpdateFilterExt, utils::command::BotCommands};
|
||||
use thiserror::Error;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use crate::futures::Stream;
|
||||
use tokio_core::reactor::Core;
|
||||
use std::env;
|
||||
use telegram_bot::*;
|
||||
|
||||
// DB
|
||||
async fn init_db_pool(url: &str) -> Result<AnyPool, sqlx::Error> {
|
||||
let pool = AnyPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(url).await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
use chrono::prelude::*;
|
||||
|
||||
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?;
|
||||
use std::sync::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
const SCORE_FILE_PATH : &str = "score.json";
|
||||
|
||||
#[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)
|
||||
}
|
||||
fn main() {
|
||||
let mut core = Core::new().unwrap();
|
||||
|
||||
async fn check_leet(username: &str, pool: &AnyPool) -> Result<(), LeetError> {
|
||||
if ! time_is_leet() {
|
||||
return Err(LeetError::NotLeet);
|
||||
let token = env::var("TELEGRAM_BOT_TOKEN").unwrap();
|
||||
let api = Api::configure(token).build(core.handle()).unwrap();
|
||||
|
||||
let leet_time = NaiveTime::from_hms(13, 37, 30);
|
||||
|
||||
let scores : Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
let time_scored : Arc<Mutex<HashMap<String, DateTime<Local>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
{
|
||||
if let Ok(mut score_file) = std::fs::File::open(SCORE_FILE_PATH) {
|
||||
|
||||
let mut json_str = String::new();
|
||||
|
||||
score_file.read_to_string(&mut json_str).expect("Failed to read score file");
|
||||
|
||||
let mut map = scores.lock().unwrap();
|
||||
|
||||
*map = serde_json::from_str(&json_str).expect("Failed to deserialize scores");
|
||||
}
|
||||
}
|
||||
|
||||
let now = Local::now().naive_local();
|
||||
{
|
||||
let scores = scores.clone();
|
||||
|
||||
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?;
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
{
|
||||
let map = scores.lock().unwrap();
|
||||
|
||||
if let Some(latest_leet) = latest_leet {
|
||||
let distance = now - latest_leet.2;
|
||||
let json_str = serde_json::to_string(&*map).unwrap();
|
||||
|
||||
if distance.num_hours() < 24 {
|
||||
return Err(LeetError::AlreadyHitLeet);
|
||||
}
|
||||
let mut score_file = std::fs::File::create(SCORE_FILE_PATH).expect("Unable to open or create file");
|
||||
|
||||
score_file.write_all(json_str.as_bytes()).expect("unable to write to score file!");
|
||||
}
|
||||
|
||||
println!("written score file");
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(5*60));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_leet(username: &str, pool: &AnyPool) -> Result<(), sqlx::Error> {
|
||||
let sql_now = match pool.any_kind() {
|
||||
sqlx::any::AnyKind::Postgres => "now()",
|
||||
sqlx::any::AnyKind::Sqlite => "datetime()"
|
||||
};
|
||||
|
||||
let query_sql = format!("insert into leet_log (username, log_time) values ($1, {})", sql_now);
|
||||
|
||||
sqlx::query(&query_sql)
|
||||
.bind(username)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
Ok(score)
|
||||
}
|
||||
|
||||
|
||||
// Bot
|
||||
|
||||
fn message_ok() -> Result<(), MessageHandlerError> {
|
||||
Ok(respond(())?)
|
||||
}
|
||||
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename = "lowercase", description = "score commands")]
|
||||
enum LeetCommand {
|
||||
#[command(description = "get scores")]
|
||||
Scores
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
fn filter_leet(message: Message) -> bool {
|
||||
let text = match message.text() {
|
||||
Some(t) => t,
|
||||
None => { return false; }
|
||||
};
|
||||
|
||||
text_is_leet(text)
|
||||
}
|
||||
|
||||
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 leet_check = check_leet(&username, &pool).await;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
log_leet(&username, &pool).await?;
|
||||
|
||||
let scores = get_scores(&pool).await?;
|
||||
|
||||
let user_score = scores.iter().find(|(u,_)| u == &username).ok_or(MessageHandlerError::NoScore)?.1;
|
||||
|
||||
let msg = format!("{} just hit leet! New score: {}", username, user_score);
|
||||
|
||||
bot.send_message(message.chat.id, msg).await?;
|
||||
|
||||
message_ok()
|
||||
}
|
||||
|
||||
async fn leet_command_handler(message: Message, cmd: LeetCommand, bot: AutoSend<Bot>, pool: AnyPool) -> Result<(), MessageHandlerError> {
|
||||
|
||||
match cmd {
|
||||
LeetCommand::Scores => {
|
||||
let scores = get_scores(&pool).await?;
|
||||
|
||||
let scores_str = scores.into_iter().map(|(name,score)| format!(" {}: {}\n", name, score)).collect::<String>();
|
||||
|
||||
let msg = format!("Leet scores:\n{}", scores_str);
|
||||
|
||||
bot.send_message(message.chat.id, msg).await?;
|
||||
}
|
||||
}
|
||||
|
||||
message_ok()
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let db_url = std::env::var("DB_URL")
|
||||
.unwrap_or_else(|_| "sqlite::memory:".to_string());
|
||||
|
||||
let db_pool = init_db_pool(&db_url).await?;
|
||||
|
||||
setup_tables(&db_pool).await?;
|
||||
|
||||
let telegram_token = std::env::var("TELEGRAM_BOT_TOKEN")
|
||||
.expect("TELEGRAM_TOKEN env not set!");
|
||||
|
||||
let bot = Bot::new(&telegram_token).auto_send();
|
||||
|
||||
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;
|
||||
|
||||
Ok(())
|
||||
|
||||
let future = api.stream().for_each(|update| {
|
||||
if let UpdateKind::Message(message) = update.kind {
|
||||
if let MessageKind::Text {ref data, ..} = message.kind {
|
||||
println!("<{}>: {}", message.from.first_name, data);
|
||||
|
||||
let current_time = Local::now().time();
|
||||
|
||||
let allowed_timegap = 30;
|
||||
|
||||
if data.to_lowercase() == "leet" || data == "1337" {
|
||||
dbg!(data);
|
||||
|
||||
let leet_distance = (current_time - leet_time).num_seconds().abs();
|
||||
|
||||
if leet_distance > allowed_timegap {
|
||||
api.spawn(message.text_reply(
|
||||
"It is not leet right now".to_string()
|
||||
));
|
||||
} else {
|
||||
|
||||
let mut time_scored_map = time_scored.lock().unwrap();
|
||||
|
||||
let time_scored = time_scored_map.entry(message.from.first_name.clone()).or_insert(Local.timestamp(0,0));
|
||||
|
||||
let time_scored_distance = (Local::now() - *time_scored).num_seconds().abs();
|
||||
|
||||
if time_scored_distance < allowed_timegap * 4 {
|
||||
api.spawn(message.text_reply(
|
||||
"You already hit leet today you dumbass, don't try to cheat!".to_string()
|
||||
));
|
||||
} else {
|
||||
let mut map = scores.lock().unwrap();
|
||||
|
||||
let entry = map.entry(message.from.first_name.clone()).or_insert(0);
|
||||
|
||||
*entry += 1;
|
||||
*time_scored = Local::now();
|
||||
|
||||
api.spawn(message.text_reply(
|
||||
format!("{} just hit leet! New score: {}", message.from.first_name, *entry)
|
||||
));
|
||||
|
||||
let current_leet_count = time_scored_map.iter().filter(|(_,t)| (**t - Local::now()).num_seconds().abs() < allowed_timegap * 2).count();
|
||||
|
||||
if current_leet_count == 3 {
|
||||
api.spawn(message.chat.text("OH BABY A TRIPPLE!!!"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} else if data == "-score" {
|
||||
let map = scores.lock().unwrap();
|
||||
|
||||
let json = serde_json::to_string(&*map).unwrap();
|
||||
|
||||
api.spawn(message.text_reply(
|
||||
format!("Score json: {}", json)
|
||||
));
|
||||
} else if data == "-time" {
|
||||
|
||||
api.spawn(message.text_reply(
|
||||
format!("local time: {}, leet time: {}, time distance to leet: {:#?}", current_time, leet_time, (current_time - leet_time).num_seconds())
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
core.run(future).unwrap();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user