Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/0.1.3 #4

Merged
merged 3 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions locales/en.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
commands:
grow:
result: "Your dick has been grown by <b>%{incr} cm</b> and now it is <b>%{length} cm</b> long"
tomorrow: "Your dick has been already grown today. Come back tomorrow!"
result: "Your dick has been grown by <b>%{incr} cm</b> and now it is <b>%{length} cm</b> long.\nYour position in the top is <b>%{pos}</b>."
tomorrow: "Your dick has been already grown today."
top:
title: "Top of the biggest dicks:"
line: "%{n}|<b>%{name}</b> — <b>%{length}</b> cm"
empty: "No one is in the game yet :("
dod:
result: "The Dick of the Day is <b>%{name}</b>!\n\nHis dick has become longer for <b>%{growth} cm</b> and is <b>%{length} cm</b> long now"
already_chosen: "The Dick of the Day has been already chosen for today! It's <b>%{name}</b>"
result: "The Dick of the Day is <b>%{name}</b>!\n\nHis dick has become longer for <b>%{growth} cm</b> and is <b>%{length}</b> cm long now.\nHis position in the top is <b>%{pos}</b>."
already_chosen: "The Dick of the Day has been already chosen for today! It's <b>%{name}</b>."
no_candidates: "There is no candidates for election. In this chat nobody is in the game yet 😢"
import:
result:
Expand Down Expand Up @@ -37,5 +37,8 @@ commands:
no_dicks: "It seems you don't have any dicks yet. 🤔 Right now is the time to add me into a chat and execute the <code>/grow</code> command!"
titles:
greeting: "Hello"
time_till_next_day:
none: " Come back tomorrow!"
some: "\n\nThe next attempt in <b>%{hours}</b>h <b>%{minutes}</b>m."
errors:
not_group_chat: "This bot is supposed to do its mission in group chats only! <i>(for the testing period: in a specific list of chats)</i>"
11 changes: 7 additions & 4 deletions locales/ru.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
commands:
grow:
result: "Ваша пися выросла на %{incr} см и теперь её длина составляет %{length} см"
tomorrow: "Вы уже растили пиписю сегодня. Возвращайтесь завтра!"
result: "Твоя пися выросла на %{incr} см и теперь её длина составляет %{length} см.\nТы занимаешь %{pos} место в топе."
tomorrow: "Ты уже растил пиписю сегодня."
top:
title: "Топ самых больших пиписек:"
line: "%{n}|<b>%{name}</b> — <b>%{length}</b> см"
empty: "Никто пока не участвует в игре :("
dod:
result: "Пам-пам-пам! Писюн Дня — <b>%{name}</b>!\n\nЕго пиписик вырос на <b>%{growth} см</b> и теперь длиной <b>%{length} см</b>"
already_chosen: "Писюн Дня уже был выбран на сегодня! Это <b>%{name}</b>"
result: "Пам-пам-пам! Писюн Дня — <b>%{name}</b>!\n\nЕго пиписик вырос на <b>%{growth} см</b> и теперь длиной <b>%{length}</b> см.\nОн занимает %{pos} место в топе."
already_chosen: "Писюн Дня уже был выбран на сегодня! Это <b>%{name}</b>."
no_candidates: "Не из кого выбирать: в этом чате ещё никто не участвует в игре 😢"
import:
result:
Expand Down Expand Up @@ -37,5 +37,8 @@ commands:
no_dicks: "Кажется, ты ещё не начал растить ни одного писюна? 🤔 Сейчас самое время добавить меня в какой-либо чат и выполнить команду <code>/grow</code>!"
titles:
greeting: "Приветствую"
time_till_next_day:
none: " Возвращайся завтра!"
some: "\n\nСледующая попытка через <b>%{hours}</b> ч. <b>%{minutes}</b> мин."
errors:
not_group_chat: "Бот выполняет свою миссию только в групповых чатах! <i>(на время тестового периода: только в строго определённом списке чатов)</i>"
25 changes: 14 additions & 11 deletions src/handlers/dick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use rust_i18n::t;
use teloxide::Bot;
use teloxide::macros::BotCommands;
use teloxide::types::Message;
use crate::handlers::{ensure_lang_code, HandlerResult, reply_html};
use crate::handlers::{ensure_lang_code, HandlerResult, reply_html, utils};
use crate::{config, metrics, repo};

const TOMORROW_SQL_CODE: &str = "GD0E1";
Expand Down Expand Up @@ -38,42 +38,45 @@ pub async fn dick_cmd_handler(bot: Bot, msg: Message, cmd: DickCommands,
};
let increment = gen_increment(config.growth_range, grow_shrink_ratio);
let grow_result = repos.dicks.create_or_grow(from.id, msg.chat.id, increment).await;
let lang_code = ensure_lang_code(Some(from));

match grow_result {
Ok(new_length) => {
let lang_code = ensure_lang_code(Some(from));
t!("commands.grow.result", locale = lang_code.as_str(), incr = increment, length = new_length)
let main_part = match grow_result {
Ok(repo::GrowthResult { new_length, pos_in_top }) => {
t!("commands.grow.result", locale = &lang_code,
incr = increment, length = new_length, pos = pos_in_top)
},
Err(e) => {
let db_err = e.downcast::<sqlx::Error>()?;
if let sqlx::Error::Database(e) = db_err {
e.code()
.filter(|c| c == TOMORROW_SQL_CODE)
.map(|_| t!("commands.grow.tomorrow"))
.map(|_| t!("commands.grow.tomorrow", locale = &lang_code))
.ok_or(anyhow!(e))?
} else {
Err(db_err)?
}
}
}
};
let time_left_part = utils::date::get_time_till_next_day_string(&lang_code);
format!("{main_part}{time_left_part}")
},
DickCommands::Grow => Err("unexpected absence of a FROM field for the /grow command")?,
DickCommands::Top => {
metrics::CMD_TOP_COUNTER.inc();

let lang_code = ensure_lang_code(msg.from());
let title = t!("commands.top.title", locale = lang_code.as_str());
let title = t!("commands.top.title", locale = &lang_code);
let lines = repos.dicks.get_top(msg.chat.id)
.await?
.iter().enumerate()
.map(|(i, d)| {
let name = teloxide::utils::html::escape(d.owner_name.as_str());
t!("commands.top.line", locale = lang_code.as_str(), n = i+1, name = name, length = d.length)
let name = teloxide::utils::html::escape(&d.owner_name);
t!("commands.top.line", locale = &lang_code, n = i+1, name = name, length = d.length)
})
.collect::<Vec<String>>();

if lines.is_empty() {
t!("commands.top.empty", locale = lang_code.as_str())
t!("commands.top.empty", locale = &lang_code)
} else {
format!("{}\n\n{}", title, lines.join("\n"))
}
Expand Down
14 changes: 9 additions & 5 deletions src/handlers/dod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use teloxide::Bot;
use teloxide::macros::BotCommands;
use teloxide::types::Message;
use crate::{config, metrics, repo};
use crate::handlers::{ensure_lang_code, HandlerResult, reply_html};
use crate::handlers::{ensure_lang_code, HandlerResult, reply_html, utils};

const DOD_ALREADY_CHOSEN_SQL_CODE: &str = "GD0E2";

Expand All @@ -27,9 +27,11 @@ pub async fn dod_cmd_handler(bot: Bot, msg: Message,
Some(winner) => {
let bonus: u32 = OsRng::default().gen_range(config.dod_bonus_range);
let dod_result = repos.dicks.set_dod_winner(chat_id, repo::UID(winner.uid), bonus).await;
match dod_result {
Ok(new_length) => t!("commands.dod.result", locale = &lang_code,
name = winner.name, growth = bonus, length = new_length),
let main_part = match dod_result {
Ok(repo::GrowthResult{ new_length, pos_in_top }) => {
t!("commands.dod.result", locale = &lang_code,
name = winner.name, growth = bonus, length = new_length, pos = pos_in_top)
},
Err(e) => {
match e.downcast::<sqlx::Error>()? {
sqlx::Error::Database(e)
Expand All @@ -39,7 +41,9 @@ pub async fn dod_cmd_handler(bot: Bot, msg: Message,
e => Err(e)?
}
}
}
};
let time_left_part = utils::date::get_time_till_next_day_string(&lang_code);
format!("{main_part}{time_left_part}")
},
None => t!("commands.dod.no_candidates", locale = &lang_code)
};
Expand Down
57 changes: 46 additions & 11 deletions src/handlers/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub enum ImportCommands {
Import
}

#[cfg_attr(test, derive(PartialEq, Eq))]
enum OriginalBotKind {
PIPISA,
KRAFT28,
Expand All @@ -33,22 +34,20 @@ impl OriginalBotKind {
fn convert_name(&self, name: &str) -> String {
match self {
OriginalBotKind::PIPISA => {
if name.len() > 13 {
&name[0..13]
} else {
name
}
name.chars()
.take(13)
.collect()
},
OriginalBotKind::KRAFT28 => name
}.to_owned()
OriginalBotKind::KRAFT28 => name.to_owned()
}
}
}

impl TryFrom<&String> for OriginalBotKind {
impl TryFrom<&str> for OriginalBotKind {
type Error = String;

fn try_from(value: &String) -> Result<Self, Self::Error> {
match value.as_str().trim_start_matches('@') {
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.trim_start_matches('@') {
"pipisabot" => Ok(OriginalBotKind::PIPISA),
"kraft28_bot" => Ok(OriginalBotKind::KRAFT28),
_ => Err("Unknown OriginalBotKind".to_owned())
Expand Down Expand Up @@ -205,7 +204,7 @@ fn check_reply_source_and_text(reply: &Message) -> Option<ParseResult> {
.and_then(|u| u.username.as_ref())
.filter(|name| ORIGINAL_BOT_USERNAMES.contains(&name.as_ref()))
.and_then(|name| {
let name = name.try_into()
let name = name.as_str().try_into()
.map_err(|name| log::error!("couldn't convert name: {name}"))
.ok();
if let (Some(name), Some(text)) = (name, reply.text()) {
Expand Down Expand Up @@ -296,3 +295,39 @@ fn map_user(pos: Captures) -> Option<OriginalUser> {
}
None
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn original_bot_kind_convert_name_pipisa() {
let (p, short) = (OriginalBotKind::PIPISA, "SadBot #incel".to_owned());
assert_eq!(p.convert_name("SadBot #incel..."), short);
assert_eq!(p.convert_name("SadBot #incel>suicide"), short);
}

#[test]
fn original_bot_kind_try_from() {
let check = |variant: &str, kind| {
let second_variant = variant.strip_prefix("@").expect("no '@' prefix");
let valid_variants = [variant, &second_variant];
assert!(valid_variants.into_iter()
.all(|v| OriginalBotKind::try_from(v)
.is_ok_and(|k| k == kind)));

let invalid_variants = valid_variants.into_iter()
.map(|v| {
v.strip_suffix("_bot")
.or_else (|| v.strip_suffix("bot"))
.expect("no 'bot' suffix")
});
assert!(invalid_variants.into_iter()
.all(|v| OriginalBotKind::try_from(v)
.is_err()));
} ;

check("@pipisabot", OriginalBotKind::PIPISA);
check("@kraft28_bot", OriginalBotKind::KRAFT28);
}
}
14 changes: 10 additions & 4 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ mod help;
mod dod;
mod import;
mod promo;
mod utils;

use std::borrow::ToOwned;
use teloxide::Bot;
use teloxide::requests::Requester;
use teloxide::types::{Message, User};
Expand All @@ -19,15 +21,19 @@ pub type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;

pub fn ensure_lang_code(user: Option<&User>) -> String {
user
.map(|u| {
u.language_code.clone()
.and_then(|u| {
u.language_code.as_ref()
.or_else(|| {
log::warn!("no language_code for {}, using the default", u.id);
None
})
})
.flatten()
.unwrap_or("en".to_owned())
.map(|code| match &code[..2] {
"uk" | "be" => "ru",
_ => code
})
.unwrap_or("en")
.to_owned()
}

pub async fn reply_html(bot: Bot, msg: Message, answer: String) -> HandlerResult {
Expand Down
39 changes: 39 additions & 0 deletions src/handlers/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
pub mod date {
use chrono::{DateTime, Duration, Timelike, Utc};
use rust_i18n::t;

pub fn get_time_till_next_day_string(lang_code: &str) -> String {
let now = if cfg!(test) {
DateTime::parse_from_rfc3339("2023-10-21T22:10:57+00:00")
.expect("invalid datetime string")
.into()
} else {
Utc::now()
};
Some(now + Duration::days(1))
.and_then(|d| d.with_hour(0))
.and_then(|d| d.with_minute(0))
.and_then(|d| d.with_second(0))
.map(|tomorrow| tomorrow - now)
.map(|time_left| {
let hrs = time_left.num_hours();
let mins = time_left.num_minutes() - hrs * 60;
t!("titles.time_till_next_day.some", locale = lang_code,
hours = hrs, minutes = mins)
})
.unwrap_or(t!("titles.time_till_next_day.none", locale = lang_code))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn get_time_till_next_day_string() {
let expected = "<b>1</b>h <b>49</b>m.";
let actual = date::get_time_till_next_day_string("en");
let actual = &actual[expected.len()+1..];
assert_eq!(expected, actual)
}
}
37 changes: 31 additions & 6 deletions src/repo/dicks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@ pub struct Dick {
pub owner_name: String,
}

pub struct GrowthResult {
pub new_length: i32,
pub pos_in_top: u64,
}

repository!(Dicks,
pub async fn create_or_grow(&self, uid: UserId, chat_id: ChatId, increment: i32) -> anyhow::Result<i32> {
pub async fn create_or_grow(&self, uid: UserId, chat_id: ChatId, increment: i32) -> anyhow::Result<GrowthResult> {
let uid: i64 = uid.0.try_into()?;
sqlx::query("INSERT INTO dicks(uid, chat_id, length) VALUES ($1, $2, $3) ON CONFLICT (uid, chat_id) DO UPDATE SET length = (dicks.length + $3) RETURNING length")
let new_length = sqlx::query("INSERT INTO dicks(uid, chat_id, length) VALUES ($1, $2, $3)
ON CONFLICT (uid, chat_id) DO UPDATE SET length = (dicks.length + $3)
RETURNING length")
.bind(uid)
.bind(chat_id.0)
.bind(increment)
.fetch_one(&self.pool)
.await?
.try_get("length")
.map_err(|e| e.into())
.try_get("length")?;
let pos_in_top = self.get_position_in_top(chat_id, uid).await? as u64;
Ok(GrowthResult { new_length, pos_in_top })
}
,
pub async fn get_top(&self, chat_id: ChatId) -> anyhow::Result<Vec<Dick>, sqlx::Error> {
Expand All @@ -29,12 +37,29 @@ repository!(Dicks,
.await
}
,
pub async fn set_dod_winner(&self, chat_id: ChatId, user_id: UID, bonus: u32) -> anyhow::Result<i32> {
pub async fn set_dod_winner(&self, chat_id: ChatId, user_id: UID, bonus: u32) -> anyhow::Result<GrowthResult> {
let mut tx = self.pool.begin().await?;
let new_length = Self::grow_dods_dick(&mut tx, chat_id, user_id, bonus.try_into()?).await?;
Self::insert_to_dod_table(&mut tx, chat_id, user_id).await?;
let pos_in_top = self.get_position_in_top(chat_id, user_id.0).await? as u64;
tx.commit().await?;
Ok(new_length)
Ok(GrowthResult { new_length, pos_in_top })
}
,
async fn get_position_in_top(&self, chat_id: ChatId, uid: i64) -> anyhow::Result<i64> {
sqlx::query("
WITH top AS (
SELECT chat_id, uid, length FROM dicks WHERE chat_id = $1
) SELECT
ROW_NUMBER() OVER(ORDER BY top.length DESC) as position
FROM top
WHERE uid = $2")
.bind(chat_id.0)
.bind(uid)
.fetch_one(&self.pool)
.await?
.try_get::<i64, _>("position")
.map_err(|e| e.into())
}
,
async fn grow_dods_dick(tx: &mut Transaction<'_, Postgres>, chat_id: ChatId, user_id: UID, bonus: i32) -> anyhow::Result<i32> {
Expand Down