Compare commits

..

2 Commits

Author SHA1 Message Date
787988d9c8
update persistence
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 13:21:13 +02:00
5ac12eb97b
wip
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 02:25:28 +02:00
7 changed files with 1958 additions and 967 deletions

2519
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,21 @@
name = "telegram-leetbot"
version = "0.1.0"
authors = ["Andreas Larsen <andreas@northcode.no>"]
edition = "2018"
edition = "2021"
[dependencies]
telegram-bot = "0.6.1"
futures = "*"
tokio-core = "*"
serde = "1.0"
serde_json = "1.0"
chrono = "0.4.6"
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"
[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" }

View File

@ -4,3 +4,4 @@ metadata:
name: {{ .Release.Name }}-token
data:
TELEGRAM_BOT_TOKEN: {{ .Values.app.token | b64enc }}
DB_URL: {{ .Values.app.db | b64enc }}

View File

@ -3,6 +3,7 @@ image:
app:
token: blabla
db: sqlite:leet.db
tz: Europe/Oslo
storage:

2
run-db.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
podman run -d -p 5432:5432 -e POSTGRES_USER=leetbot -e POSTGRES_PASSWORD=leetbot docker.io/postgres

View File

@ -0,0 +1,17 @@
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
}

View File

@ -1,142 +1,225 @@
extern crate telegram_bot;
extern crate futures;
extern crate tokio_core;
extern crate serde;
extern crate serde_json;
extern crate chrono;
mod lib;
use crate::lib::*;
use std::io::Read;
use std::io::Write;
use crate::futures::Stream;
use tokio_core::reactor::Core;
use std::env;
use telegram_bot::*;
use chrono::{NaiveDateTime, Local};
use sqlx::AnyPool;
use sqlx::any::AnyPoolOptions;
use teloxide::{prelude::*, RequestError, dispatching::UpdateFilterExt, utils::command::BotCommands};
use thiserror::Error;
use chrono::prelude::*;
// DB
async fn init_db_pool(url: &str) -> Result<AnyPool, sqlx::Error> {
let pool = AnyPoolOptions::new()
.max_connections(5)
.connect(url).await?;
use std::sync::*;
use std::collections::HashMap;
Ok(pool)
}
const SCORE_FILE_PATH : &str = "score.json";
async fn setup_tables(pool: &sqlx::AnyPool) -> Result<(), sqlx::Error> {
fn main() {
let mut core = Core::new().unwrap();
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
)"
};
let token = env::var("TELEGRAM_BOT_TOKEN").unwrap();
let api = Api::configure(token).build(core.handle()).unwrap();
sqlx::query(query_sql)
.execute(pool)
.await?;
let leet_time = NaiveTime::from_hms(13, 37, 30);
Ok(())
}
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()));
#[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)
}
{
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");
}
async fn check_leet(username: &str, pool: &AnyPool) -> Result<(), LeetError> {
if ! time_is_leet() {
return Err(LeetError::NotLeet);
}
{
let scores = scores.clone();
let now = Local::now().naive_local();
std::thread::spawn(move || {
loop {
{
let map = scores.lock().unwrap();
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?;
let json_str = serde_json::to_string(&*map).unwrap();
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));
}
});
}
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())
));
}
if let Some(latest_leet) = latest_leet {
let distance = now - latest_leet.2;
if distance.num_hours() < 24 {
return Err(LeetError::AlreadyHitLeet);
}
}
Ok(())
});
core.run(future).unwrap();
}
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(())
}