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"
|
name = "telegram-leetbot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Andreas Larsen <andreas@northcode.no>"]
|
authors = ["Andreas Larsen <andreas@northcode.no>"]
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.19"
|
telegram-bot = "0.6.1"
|
||||||
pretty_env_logger = "0.4.0"
|
futures = "*"
|
||||||
regex = "1.6.0"
|
tokio-core = "*"
|
||||||
# frankenstein = { version = "0.19.0", features = ["async-http-client"] }
|
serde = "1.0"
|
||||||
sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "sqlite", "any", "chrono" ] }
|
serde_json = "1.0"
|
||||||
teloxide = { version = "0.10.1", features = [ "macros" ] }
|
chrono = "0.4.6"
|
||||||
thiserror = "1.0.31"
|
|
||||||
# futures = "*"
|
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
|
||||||
# serde = "1.0"
|
|
||||||
# serde_json = "1.0"
|
|
||||||
# chrono = "0.4.19"
|
|
||||||
|
|
||||||
# [patch.crates-io]
|
[patch.crates-io]
|
||||||
# openssl = { git = "https://github.com/ishitatsuyuki/rust-openssl", branch = "0.9.x" }
|
openssl = { git = "https://github.com/ishitatsuyuki/rust-openssl", branch = "0.9.x" }
|
||||||
@ -4,4 +4,3 @@ metadata:
|
|||||||
name: {{ .Release.Name }}-token
|
name: {{ .Release.Name }}-token
|
||||||
data:
|
data:
|
||||||
TELEGRAM_BOT_TOKEN: {{ .Values.app.token | b64enc }}
|
TELEGRAM_BOT_TOKEN: {{ .Values.app.token | b64enc }}
|
||||||
DB_URL: {{ .Values.app.db | b64enc }}
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ image:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
token: blabla
|
token: blabla
|
||||||
db: sqlite:leet.db
|
|
||||||
tz: Europe/Oslo
|
tz: Europe/Oslo
|
||||||
|
|
||||||
storage:
|
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
|
|
||||||
}
|
|
||||||
329
src/main.rs
329
src/main.rs
@ -1,225 +1,142 @@
|
|||||||
mod lib;
|
extern crate telegram_bot;
|
||||||
use crate::lib::*;
|
extern crate futures;
|
||||||
|
extern crate tokio_core;
|
||||||
|
extern crate serde;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate chrono;
|
||||||
|
|
||||||
use chrono::{NaiveDateTime, Local};
|
use std::io::Read;
|
||||||
use sqlx::AnyPool;
|
use std::io::Write;
|
||||||
use sqlx::any::AnyPoolOptions;
|
use crate::futures::Stream;
|
||||||
use teloxide::{prelude::*, RequestError, dispatching::UpdateFilterExt, utils::command::BotCommands};
|
use tokio_core::reactor::Core;
|
||||||
use thiserror::Error;
|
use std::env;
|
||||||
|
use telegram_bot::*;
|
||||||
|
|
||||||
// DB
|
use chrono::prelude::*;
|
||||||
async fn init_db_pool(url: &str) -> Result<AnyPool, sqlx::Error> {
|
|
||||||
let pool = AnyPoolOptions::new()
|
|
||||||
.max_connections(5)
|
|
||||||
.connect(url).await?;
|
|
||||||
|
|
||||||
Ok(pool)
|
use std::sync::*;
|
||||||
}
|
use std::collections::HashMap;
|
||||||
|
|
||||||
async fn setup_tables(pool: &sqlx::AnyPool) -> Result<(), sqlx::Error> {
|
const SCORE_FILE_PATH : &str = "score.json";
|
||||||
|
|
||||||
let query_sql = match pool.any_kind() {
|
fn main() {
|
||||||
sqlx::any::AnyKind::Postgres => "create table if not exists leet_log (
|
let mut core = Core::new().unwrap();
|
||||||
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)
|
let token = env::var("TELEGRAM_BOT_TOKEN").unwrap();
|
||||||
.execute(pool)
|
let api = Api::configure(token).build(core.handle()).unwrap();
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
let leet_time = NaiveTime::from_hms(13, 37, 30);
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug,Error)]
|
let scores : Arc<Mutex<HashMap<String, usize>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||||
enum LeetError {
|
let time_scored : Arc<Mutex<HashMap<String, DateTime<Local>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||||
#[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)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_leet(username: &str, pool: &AnyPool) -> Result<(), LeetError> {
|
{
|
||||||
if ! time_is_leet() {
|
if let Ok(mut score_file) = std::fs::File::open(SCORE_FILE_PATH) {
|
||||||
return Err(LeetError::NotLeet);
|
|
||||||
|
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")
|
std::thread::spawn(move || {
|
||||||
.bind(username)
|
loop {
|
||||||
.fetch_optional(pool)
|
{
|
||||||
.await?;
|
let map = scores.lock().unwrap();
|
||||||
|
|
||||||
if let Some(latest_leet) = latest_leet {
|
let json_str = serde_json::to_string(&*map).unwrap();
|
||||||
let distance = now - latest_leet.2;
|
|
||||||
|
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 distance.num_hours() < 24 {
|
|
||||||
return Err(LeetError::AlreadyHitLeet);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
});
|
||||||
|
|
||||||
async fn log_leet(username: &str, pool: &AnyPool) -> Result<(), sqlx::Error> {
|
core.run(future).unwrap();
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user