diff --git a/.env.example b/.env.example index 7a400ea..6015084 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,8 @@ DATABASE_URL=postgres://dickgrowerbot:dgb4pwd@localhost:5432/dickgrowerbotdb DATABASE_MAX_CONNECTIONS=10 HELP_ADMIN_USERNAME=kozalo -HELP_ADMIN_CHANNEL=kozaloru +HELP_ADMIN_CHANNEL_RU=kozaloru +HELP_ADMIN_CHANNEL_EN=kozalo_blog HELP_GIT_REPO=https://github.com/kozalosev/DickGrowerBot CHATS_MERGING_ENABLED=true diff --git a/.sqlx/query-5208c529f037af3be8532ac3094c728193501939d4edbfcdd28b07487aeb5bb3.json b/.sqlx/query-5208c529f037af3be8532ac3094c728193501939d4edbfcdd28b07487aeb5bb3.json new file mode 100644 index 0000000..a4731ad --- /dev/null +++ b/.sqlx/query-5208c529f037af3be8532ac3094c728193501939d4edbfcdd28b07487aeb5bb3.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT debt, payout_ratio FROM loans WHERE uid = $1 AND chat_id = (SELECT id FROM Chats WHERE chat_id = $2::bigint OR chat_instance = $2::text) AND repaid_at IS NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "debt", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "payout_ratio", + "type_info": "Float4" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "5208c529f037af3be8532ac3094c728193501939d4edbfcdd28b07487aeb5bb3" +} diff --git a/.sqlx/query-62acfff173f54e2079bf5a2a6a79fd90b5ae05e5d7a90c2ffca09230d2871265.json b/.sqlx/query-62acfff173f54e2079bf5a2a6a79fd90b5ae05e5d7a90c2ffca09230d2871265.json new file mode 100644 index 0000000..81cbd8c --- /dev/null +++ b/.sqlx/query-62acfff173f54e2079bf5a2a6a79fd90b5ae05e5d7a90c2ffca09230d2871265.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO Loans (chat_id, uid, debt, payout_ratio) VALUES ((SELECT id FROM Chats WHERE chat_id = $1::bigint OR chat_instance = $1::text),$2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int4", + "Float4" + ] + }, + "nullable": [] + }, + "hash": "62acfff173f54e2079bf5a2a6a79fd90b5ae05e5d7a90c2ffca09230d2871265" +} diff --git a/.sqlx/query-74d2154a6a4e3a7c81756f6dbbd3ac1d445dbc8c6b82d4f46a496f96439ea55b.json b/.sqlx/query-74d2154a6a4e3a7c81756f6dbbd3ac1d445dbc8c6b82d4f46a496f96439ea55b.json deleted file mode 100644 index 64936c6..0000000 --- a/.sqlx/query-74d2154a6a4e3a7c81756f6dbbd3ac1d445dbc8c6b82d4f46a496f96439ea55b.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO Loans (chat_id, uid, debt) VALUES ((SELECT id FROM Chats WHERE chat_id = $1::bigint OR chat_instance = $1::text),$2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "74d2154a6a4e3a7c81756f6dbbd3ac1d445dbc8c6b82d4f46a496f96439ea55b" -} diff --git a/.sqlx/query-f6ec82ea77ac04988817344c222c5deddb93a49097885ec952c08d773aa2a558.json b/.sqlx/query-f6ec82ea77ac04988817344c222c5deddb93a49097885ec952c08d773aa2a558.json deleted file mode 100644 index 046616b..0000000 --- a/.sqlx/query-f6ec82ea77ac04988817344c222c5deddb93a49097885ec952c08d773aa2a558.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT debt FROM loans WHERE uid = $1 AND chat_id = (SELECT id FROM Chats WHERE chat_id = $2::bigint OR chat_instance = $2::text) AND repaid_at IS NULL", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "debt", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "f6ec82ea77ac04988817344c222c5deddb93a49097885ec952c08d773aa2a558" -} diff --git a/Dockerfile b/Dockerfile index 2260f64..ee05138 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,8 @@ ARG WEBHOOK_URL ARG DATABASE_URL ARG DATABASE_MAX_CONNECTIONS ARG HELP_ADMIN_USERNAME -ARG HELP_ADMIN_CHANNEL +ARG HELP_ADMIN_CHANNEL_RU +ARG HELP_ADMIN_CHANNEL_EN ARG HELP_GIT_REPO ARG CHATS_MERGING_ENABLED ARG TOP_UNLIMITED_ENABLED @@ -51,6 +52,8 @@ ARG GROW_SHRINK_RATIO ARG GROWTH_DOD_BONUS_MAX ARG NEWCOMERS_GRACE_DAYS ARG TOP_LIMIT +ARG HELP_PUSSIES_COEF +ARG LOAN_PAYOUT_COEF ENTRYPOINT [ "/usr/local/bin/dickGrowerBot" ] LABEL org.opencontainers.image.source=https://github.com/kozalosev/DickGrowerBot diff --git a/docker-compose.yml b/docker-compose.yml index 564061f..6d9df6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,8 @@ services: - DATABASE_URL=postgres://${POSTGRES_USER:?error}:${POSTGRES_PASSWORD:?error}@${POSTGRES_HOST:?error}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:?error} - DATABASE_MAX_CONNECTIONS - HELP_ADMIN_USERNAME - - HELP_ADMIN_CHANNEL + - HELP_ADMIN_CHANNEL_RU + - HELP_ADMIN_CHANNEL_EN - HELP_GIT_REPO - CHATS_MERGING_ENABLED - TOP_UNLIMITED_ENABLED @@ -23,6 +24,8 @@ services: - GROWTH_DOD_BONUS_MAX - NEWCOMERS_GRACE_DAYS - TOP_LIMIT + - HELP_PUSSIES_COEF + - LOAN_PAYOUT_COEF expose: - 8080 networks: diff --git a/locales/en.yml b/locales/en.yml index 96fda31..0d3c209 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -25,10 +25,11 @@ commands: description: "Fight with your friend's dick!" results: start: "%{name} challenged the chat with a bet of %{bet} cm!" - finish: "The winner is %{winner_name}! His dick is now %{winner_length} cm long. The loser's one is %{loser_length}." + finish: "The winner is %{winner_name}! His dick is now %{winner_length} cm long. The loser's one is %{loser_length}.\nThe bet was %{bet} cm." position: winner: "%{name}'s position in the top is %{pos}." loser: "%{name}'s position in the top is %{pos}." + withheld: "%{payout} cm were withheld from the winner to pay off the loan." button: "Attack!" errors: no_args: "Call the command with a number of centimeters you're willing to bet." @@ -40,13 +41,14 @@ commands: description: "Take a loan if you have a deep cave" debt: "Left to pay %{debt} cm" confirmation: - text: "Your deep hole will be reset to zero, but each growth will be lowered by %{payout_percentage}% until all %{debt} cm is repaid." + text: "Your deep hole will be reset to zero, but each growth will be lowered by %{payout_percentage} until all %{debt} cm is repaid." buttons: agree: "I'm in!" disagree: "Disagree" callback: success: "Perk «Where is my centimeters, Lebowski?» has been assigned successfully!" refused: "The patient refused gender reassignment on credit." + payout_ratio_changed: "The payout rate has been changed since you sent the loan application. Please, invoke the command again." errors: positive_length: "I cannot make your little bro bigger. Only change your gender." import: @@ -99,8 +101,8 @@ titles: some: "\n\nNext attempt in %{hours}h %{minutes}m." perks: top_line: "The following perks affected the result" - help-pussies: "Deep hole" - loan-payout: "Micro-loaner" + help-pussies: "deep hole" + loan-payout: "micro-loaner" errors: not_group_chat: "This bot is supposed to do its mission in group chats only!" feature_disabled: "This feature is currently temporarily disabled." diff --git a/locales/ru.yml b/locales/ru.yml index 8d19819..e1f63b7 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -25,10 +25,11 @@ commands: description: "Сражайся с пипирками друзей!" results: start: "%{name} бросил чату вызов со ставкой %{bet} см!" - finish: "Победитель — %{winner_name}! Его пипирик теперь равен %{winner_length} см, а проигравшего — %{loser_length} см." + finish: "Победитель — %{winner_name}! Его пипирик теперь равен %{winner_length} см, а проигравшего — %{loser_length} см.\nСтавка была %{bet} см." position: winner: "%{name} занимает %{pos} место в топе." loser: "%{name} занимает %{pos} место в топе." + withheld: "%{payout} см было удержано с победителя для погашения задолженности." button: "Атаковать!" errors: no_args: "Вызови команду с числом сантиметров, которые готов поставить." @@ -40,13 +41,14 @@ commands: description: "Возьми кредит, если у тебя глубокая пещера" debt: "Осталось выплатить %{debt} см" confirmation: - text: "Твоя пропасть будет обнулена, но размер каждого прироста снизится на %{payout_percentage}% до выплаты всех %{debt} см." + text: "Твоя пропасть будет обнулена, но размер каждого прироста снизится на %{payout_percentage} до выплаты всех %{debt} см." buttons: agree: "Согласен" disagree: "Я пас" callback: success: "Перк «Где мои сантиметры, Лебовски?» успешно предоставлен!" refused: "Пациент отказался от смены пола в кредит." + payout_ratio_changed: "С момента подачи заявления ставка выплаты изменилась. Пожалуйста, вызовите команду ещё раз." errors: positive_length: "Увеличение братюни недоступно: только смена пола." import: @@ -99,8 +101,8 @@ titles: some: "\n\nСледующая попытка через %{hours} ч. %{minutes} мин." perks: top_line: "На результат повлияли следующие перки" - help-pussies: "Глубокая нора" - loan-payout: "Микрозаймер" + help-pussies: "глубокая нора" + loan-payout: "микрозаймер" errors: not_group_chat: "Бот выполняет свою миссию только в групповых чатах!" feature_disabled: "Данная функция пока временно отключена." diff --git a/migrations/16_add-loans-column-payout-ratio.sql b/migrations/16_add-loans-column-payout-ratio.sql new file mode 100644 index 0000000..d4e60d9 --- /dev/null +++ b/migrations/16_add-loans-column-payout-ratio.sql @@ -0,0 +1,3 @@ +ALTER TABLE Loans ADD COLUMN IF NOT EXISTS payout_ratio real NOT NULL DEFAULT 0.1 + CHECK ( payout_ratio > 0.0 AND payout_ratio < 1.0 ); +ALTER TABLE Loans ALTER COLUMN payout_ratio DROP DEFAULT; diff --git a/src/config.rs b/src/config.rs index ee307ae..5be9ead 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use anyhow::anyhow; use reqwest::Url; use teloxide::types::Me; +use crate::handlers::perks::HelpPussiesPerk; use crate::handlers::utils::Incrementor; use crate::help; @@ -11,6 +12,7 @@ use crate::help; pub struct AppConfig { pub features: FeatureToggles, pub top_limit: u16, + pub loan_payout_ratio: f32, } #[derive(Clone)] @@ -45,6 +47,7 @@ pub struct BattlesFeatureToggles { impl AppConfig { pub fn from_env() -> Self { let top_limit = get_env_value_or_default("TOP_LIMIT", 10); + let loan_payout_ratio = get_env_value_or_default("LOAN_PAYOUT_COEF", 0.0); let chats_merging = get_env_value_or_default("CHATS_MERGING_ENABLED", false); let top_unlimited = get_env_value_or_default("TOP_UNLIMITED_ENABLED", false); let check_acceptor_length = get_env_value_or_default("PVP_CHECK_ACCEPTOR_LENGTH", false); @@ -57,6 +60,7 @@ impl AppConfig { } }, top_limit, + loan_payout_ratio, } } } @@ -84,8 +88,12 @@ pub fn build_context_for_help_messages(me: Me, incr: &Incrementor, competitor_bo grow_max: incr_cfg.growth_range_max().to_string(), other_bots, admin_username: ensure_starts_with_at_sign(get_env_mandatory_value("HELP_ADMIN_USERNAME")?), - admin_channel: ensure_starts_with_at_sign(get_env_mandatory_value("HELP_ADMIN_CHANNEL")?), + admin_channel_ru: ensure_starts_with_at_sign(get_env_mandatory_value("HELP_ADMIN_CHANNEL_RU")?), + admin_channel_en: ensure_starts_with_at_sign(get_env_mandatory_value("HELP_ADMIN_CHANNEL_EN")?), git_repo: get_env_mandatory_value("HELP_GIT_REPO")?, + help_pussies_percentage: incr.find_perk_config::() + .map(|payout_ratio| payout_ratio * 100.0) + .unwrap_or(0.0) }) } diff --git a/src/handlers/inline.rs b/src/handlers/inline.rs index 3b1c335..360486a 100644 --- a/src/handlers/inline.rs +++ b/src/handlers/inline.rs @@ -78,7 +78,7 @@ impl InlineCommand { }, InlineCommand::Loan => { metrics::CMD_LOAN_COUNTER.invoked.inline.inc(); - loan::loan_impl(repos, from_refs, incr) + loan::loan_impl(repos, from_refs, config) .await .map(InlineResult::from) } diff --git a/src/handlers/loan.rs b/src/handlers/loan.rs index 8157b7b..7a8a6a9 100644 --- a/src/handlers/loan.rs +++ b/src/handlers/loan.rs @@ -1,6 +1,7 @@ use std::str::{FromStr, Split}; use anyhow::anyhow; use derive_more::Display; +use num_traits::Zero; use rust_i18n::t; use teloxide::Bot; use teloxide::macros::BotCommands; @@ -9,12 +10,12 @@ use teloxide::requests::Requester; use teloxide::types::ReplyMarkup; use callbacks::{EditMessageReqParamsKind, InvalidCallbackData}; -use crate::{check_invoked_by_owner_and_get_answer_params, config, metrics, repo}; +use crate::{check_invoked_by_owner_and_get_answer_params, metrics, repo}; +use crate::config::AppConfig; use crate::handlers::{CallbackButton, ensure_lang_code, FromRefs, HandlerImplResult, HandlerResult, reply_html, try_resolve_chat_id}; -use crate::handlers::perks::LoanPayoutPerk; -use crate::handlers::utils::{callbacks, Incrementor}; +use crate::handlers::utils::callbacks; use crate::handlers::utils::callbacks::{CallbackDataWithPrefix, InvalidCallbackDataBuilder}; -use crate::repo::ChatIdPartiality; +use crate::repo::{ChatIdPartiality, Loan}; #[derive(BotCommands, Clone)] #[command(rename_rule = "lowercase")] @@ -24,14 +25,14 @@ pub enum LoanCommands { Borrow, } -pub async fn cmd_handler(bot: Bot, msg: Message, repos: repo::Repositories, incr: Incrementor) -> HandlerResult { +pub async fn cmd_handler(bot: Bot, msg: Message, repos: repo::Repositories, config: AppConfig) -> HandlerResult { metrics::CMD_LOAN_COUNTER.invoked.chat.inc(); let from = msg.from().ok_or(anyhow!("unexpected absence of a FROM field"))?; let chat_id = msg.chat.id.into(); let from_refs = FromRefs(from, &chat_id); - let result = loan_impl(&repos, from_refs, incr).await?; + let result = loan_impl(&repos, from_refs, config).await?; let markup = result.keyboard().map(ReplyMarkup::InlineKeyboard); let mut request = reply_html(bot, msg, result.text()); @@ -41,33 +42,35 @@ pub async fn cmd_handler(bot: Bot, msg: Message, repos: repo::Repositories, incr Ok(()) } -pub(crate) async fn loan_impl(repos: &repo::Repositories, from_refs: FromRefs<'_>, incr: Incrementor) -> anyhow::Result> { +pub(crate) async fn loan_impl(repos: &repo::Repositories, from_refs: FromRefs<'_>, config: AppConfig) -> anyhow::Result> { let (from, chat_id_part) = (from_refs.0, from_refs.1); let chat_id_kind = chat_id_part.kind(); let lang_code = ensure_lang_code(Some(from)); - let active_loan = repos.loans.get_active_loan(from.id, &chat_id_kind).await?; - if active_loan > 0 { - let left_to_pay = t!("commands.loan.debt", locale = &lang_code, debt = active_loan); + let maybe_loan = repos.loans.get_active_loan(from.id, &chat_id_kind).await?; + if let Some(Loan { debt, .. }) = maybe_loan { + let left_to_pay = t!("commands.loan.debt", locale = &lang_code, debt = debt); return Ok(HandlerImplResult::OnlyText(left_to_pay)) } - let payout_percentage = if let Some(payout_ratio) = incr.find_perk_config::() { - payout_ratio * 100.0 - } else { + if config.loan_payout_ratio <= 0.0 || config.loan_payout_ratio >= 1.0 { let err_text = t!("errors.feature_disabled", locale = &lang_code); return Ok(HandlerImplResult::OnlyText(err_text)) - }; + } let length = repos.dicks.fetch_length(from.id, &chat_id_kind).await?; let res = if length < 0 { let debt = length.unsigned_abs() as u16; + let payout_percentage = format!("{:.2}%", config.loan_payout_ratio * 100.0); let btn_agree = CallbackButton::new( t!("commands.loan.confirmation.buttons.agree", locale = &lang_code), LoanCallbackData { uid: from.id, - action: LoanCallbackAction::Confirmed { value: debt } + action: LoanCallbackAction::Confirmed { + value: debt, + payout_ratio: config.loan_payout_ratio + } } ); let btn_disagree = CallbackButton::new( @@ -78,7 +81,8 @@ pub(crate) async fn loan_impl(repos: &repo::Repositories, from_refs: FromRefs<'_ } ); HandlerImplResult::WithKeyboard { - text: t!("commands.loan.confirmation.text", locale = &lang_code, debt = debt, payout_percentage = payout_percentage), + text: t!("commands.loan.confirmation.text", locale = &lang_code, + debt = debt, payout_percentage = payout_percentage), buttons: vec![btn_agree, btn_disagree] } } else { @@ -94,13 +98,17 @@ pub fn callback_filter(query: CallbackQuery) -> bool { } pub async fn callback_handler(bot: Bot, query: CallbackQuery, - repos: repo::Repositories, config: config::AppConfig) -> HandlerResult { + repos: repo::Repositories, config: AppConfig) -> HandlerResult { let data = LoanCallbackData::parse(&query)?; - let (answer, lang_code) = check_invoked_by_owner_and_get_answer_params!(bot, query, data.uid); + let (mut answer, lang_code) = check_invoked_by_owner_and_get_answer_params!(bot, query, data.uid); + let edit_msg_params = callbacks::get_params_for_message_edit(&query)?; - match data.action { - LoanCallbackAction::Confirmed { value } => { + LoanCallbackAction::Confirmed { .. } if config.loan_payout_ratio.is_zero() => { + answer.show_alert.replace(true); + answer.text.replace(t!("errors.feature_disabled", locale = &lang_code)); + } + LoanCallbackAction::Confirmed { value, payout_ratio } if payout_ratio == config.loan_payout_ratio => { metrics::CMD_LOAN_COUNTER.finished.inc(); let updated_text = t!("commands.loan.callback.success", locale = &lang_code); match edit_msg_params { @@ -126,6 +134,17 @@ pub async fn callback_handler(bot: Bot, query: CallbackQuery, } } } + LoanCallbackAction::Confirmed { .. } => { + let updated_text = t!("commands.loan.callback.payout_ratio_changed", locale = &lang_code); + match edit_msg_params { + EditMessageReqParamsKind::Chat(chat_id, message_id) => { + bot.edit_message_text(chat_id, message_id, updated_text).await?; + } + EditMessageReqParamsKind::Inline { inline_message_id, .. } => { + bot.edit_message_text_inline(inline_message_id, updated_text).await?; + } + } + } LoanCallbackAction::Refused => { let updated_text = t!("commands.loan.callback.refused", locale = &lang_code); match edit_msg_params { @@ -163,10 +182,10 @@ pub(crate) struct LoanCallbackData { } #[derive(Display)] -#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub(crate) enum LoanCallbackAction { - #[display("confirmed:{value}")] - Confirmed { value: u16 }, + #[display("confirmed:{value}:{payout_ratio}")] + Confirmed { value: u16, payout_ratio: f32 }, #[display("refused")] Refused } @@ -189,7 +208,15 @@ impl TryFrom for LoanCallbackData { let action = match action { "confirmed" => { let value = Self::parse_part(&mut parts, &err, "value")?; - LoanCallbackAction::Confirmed { value } + let payout_ratio = match Self::parse_part(&mut parts, &err, "payout_ratio") { + Ok(ratio) => ratio, + // for backward compatibility; zero ratio disables the loans completely, + // so this value is out of possible ones, thus either the "rate changed" or + // "feature disabled" message will always be sent. + Err(InvalidCallbackData::MissingPart { .. }) => 0.0, + Err(e) => return Err(e) + }; + LoanCallbackAction::Confirmed { value, payout_ratio } } "refused" => LoanCallbackAction::Refused, _ => return Err(err.split_err()) @@ -220,14 +247,14 @@ mod test { #[test] fn test_parse() { - let (uid, value) = get_test_params(); - let [cd_confirmed, cd_refused] = get_strings(uid, value) + let (uid, value, payout_ratio) = get_test_params(); + let [cd_confirmed, cd_refused] = get_strings(uid, value, payout_ratio) .map(build_callback_query); { let lcd_confirmed = LoanCallbackData::parse(&cd_confirmed) .expect("callback data for 'confirmed' must be parsed successfully"); assert_eq!(lcd_confirmed.uid, uid); - assert_eq!(lcd_confirmed.action, LoanCallbackAction::Confirmed { value }); + assert_eq!(lcd_confirmed.action, LoanCallbackAction::Confirmed { value, payout_ratio }); }{ let lcd_refused = LoanCallbackData::parse(&cd_refused) .expect("callback data for 'refused' must be parsed successfully"); @@ -235,31 +262,42 @@ mod test { assert_eq!(lcd_refused.action, LoanCallbackAction::Refused) } } + + #[test] + fn test_parse_old() { + let (uid, value, _) = get_test_params(); + let cd_confirmed = build_callback_query(format!("loan:{uid}:confirmed:{value}")); + + let lcd_confirmed = LoanCallbackData::parse(&cd_confirmed) + .expect("callback data for 'confirmed' must be parsed successfully"); + assert_eq!(lcd_confirmed.uid, uid); + assert_eq!(lcd_confirmed.action, LoanCallbackAction::Confirmed { value, payout_ratio: 0.0 }); + } #[test] fn test_serialize() { - let (uid, value) = get_test_params(); + let (uid, value, payout_ratio) = get_test_params(); let lcd_confirmed = LoanCallbackData { uid, - action: LoanCallbackAction::Confirmed { value } + action: LoanCallbackAction::Confirmed { value, payout_ratio } }; let lcd_refused = LoanCallbackData { uid, action: LoanCallbackAction::Refused }; - let [expected_confirmed, expected_refused] = get_strings(uid, value); + let [expected_confirmed, expected_refused] = get_strings(uid, value, payout_ratio); assert_eq!(lcd_confirmed.to_data_string(), expected_confirmed); assert_eq!(lcd_refused.to_data_string(), expected_refused); } - fn get_test_params() -> (UserId, u16) { - (UserId(123456), 10) + fn get_test_params() -> (UserId, u16, f32) { + (UserId(123456), 10, 0.1) } - fn get_strings(uid: UserId, value: u16) -> [String; 2] {[ - format!("loan:{uid}:confirmed:{value}"), + fn get_strings(uid: UserId, value: u16, payout_ratio: f32) -> [String; 2] {[ + format!("loan:{uid}:confirmed:{value}:{payout_ratio}"), format!("loan:{uid}:refused"), ]} diff --git a/src/handlers/perks.rs b/src/handlers/perks.rs index 38def96..65494a1 100644 --- a/src/handlers/perks.rs +++ b/src/handlers/perks.rs @@ -1,27 +1,22 @@ use async_trait::async_trait; -use num_traits::{ToPrimitive, Zero}; +use num_traits::ToPrimitive; use sqlx::{Pool, Postgres}; -use crate::handlers::utils::{AdditionalChange, ChangeIntent, DickId, ConfigurablePerk, Perk}; +use crate::handlers::utils::{AdditionalChange, ChangeIntent, ConfigurablePerk, DickId, Perk}; use crate::{config, repo}; -use crate::config::FeatureToggles; -pub fn all(pool: &Pool, features: FeatureToggles) -> Vec> { +pub fn all(pool: &Pool, cfg: &config::AppConfig) -> Vec> { let help_pussies_coef = config::get_env_value_or_default("HELP_PUSSIES_COEF", 0.0); - let payout_coefficient = config::get_env_value_or_default("LOAN_PAYOUT_COEF", 0.0); - let loans = repo::Loans::new(pool.clone(), features); + let loans = repo::Loans::new(pool.clone(), cfg.loan_payout_ratio); vec![ Box::new(HelpPussiesPerk { coefficient: help_pussies_coef, }), - Box::new(LoanPayoutPerk { - payout_coefficient, - loans, - }) + Box::new(LoanPayoutPerk { loans }) ] } -struct HelpPussiesPerk { +pub struct HelpPussiesPerk { coefficient: f64 } @@ -31,10 +26,6 @@ impl Perk for HelpPussiesPerk { "help-pussies" } - fn enabled(&self) -> bool { - self.coefficient > 0.0 - } - async fn apply(&self, _: &DickId, change_intent: ChangeIntent) -> AdditionalChange { if change_intent.current_length >= 0 { return AdditionalChange(0) @@ -42,13 +33,24 @@ impl Perk for HelpPussiesPerk { let current_deepness = change_intent.current_length.abs() .to_f64().expect("conversion is always Some"); - let change = (self.coefficient * current_deepness).ceil() as i32; + let change = (self.coefficient * current_deepness).round() as i32; AdditionalChange(change) } + + fn enabled(&self) -> bool { + self.coefficient > 0.0 + } } -pub(super) struct LoanPayoutPerk { - payout_coefficient: f64, +impl ConfigurablePerk for HelpPussiesPerk { + type Config = f64; + + fn get_config(&self) -> Self::Config { + self.coefficient + } +} + +pub struct LoanPayoutPerk { loans: repo::Loans, } @@ -58,22 +60,21 @@ impl Perk for LoanPayoutPerk { "loan-payout" } - fn enabled(&self) -> bool { - self.payout_coefficient > 0.0 && self.payout_coefficient < 1.0 - } - async fn apply(&self, dick_id: &DickId, change_intent: ChangeIntent) -> AdditionalChange { - let debt = self.loans.get_active_loan(dick_id.0, &dick_id.1) + let maybe_loan_components = self.loans.get_active_loan(dick_id.0, &dick_id.1) .await .inspect_err(|e| log::error!("couldn't check if a perk is active: {e}")) - .unwrap_or(0); - if debt.is_zero() { - return AdditionalChange(0) - } + .ok() + .flatten() + .map(|loan| (loan.debt, loan.payout_ratio)); + let (debt, payout_coefficient) = match maybe_loan_components { + Some(x) => x, + None => return AdditionalChange(0) + }; let payout = if change_intent.base_increment.is_positive() { - let base_increment = change_intent.base_increment.to_f64().expect("conversion gives always Some"); - let payout = (base_increment * self.payout_coefficient).ceil() as u16; + let base_increment = change_intent.base_increment as f32; + let payout = (base_increment * payout_coefficient).round() as u16; payout.min(debt) } else { 0 @@ -88,14 +89,6 @@ impl Perk for LoanPayoutPerk { } } -impl ConfigurablePerk for LoanPayoutPerk { - type Config = f64; - - fn get_config(&self) -> Self::Config { - self.payout_coefficient - } -} - #[cfg(test)] mod test { use testcontainers::clients; @@ -127,14 +120,10 @@ mod test { async fn test_loan_payout() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let loans = repo::Loans::new(db.clone(), Default::default()); - - { - let invalid_perk = LoanPayoutPerk { loans: loans.clone(), payout_coefficient: 0.0 }; - assert!(!invalid_perk.enabled()) - } + let loans = repo::Loans::new(db.clone(), 0.1); + { - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); users.create_or_update(USER_ID, "") .await.expect("couldn't create a user"); @@ -143,29 +132,33 @@ mod test { .await.expect("couldn't create a dick"); } - let perk = LoanPayoutPerk { loans: loans.clone(), payout_coefficient: 0.1 }; + let perk = LoanPayoutPerk { loans: loans.clone() }; let dick_id = DickId(USER_ID, CHAT_ID_KIND); let change_intent_positive_increment = ChangeIntent { current_length: 1, base_increment: 10 }; let change_intent_positive_increment_small = ChangeIntent { current_length: 1, base_increment: 2 }; let change_intent_negative_increment = ChangeIntent { current_length: 1, base_increment: -1 }; - + assert!(perk.enabled()); assert_eq!(perk.apply(&dick_id, change_intent_positive_increment).await.0, 0); - + loans.borrow(USER_ID, &CHAT_ID_KIND, 10) .await.expect("couldn't create a loan") .commit() .await.expect("couldn't commit the creation of a loan"); - + assert_eq!(perk.apply(&dick_id, change_intent_positive_increment).await.0, -1); let debt = loans.get_active_loan(USER_ID, &CHAT_ID_KIND) - .await.expect("couldn't fetch the active loan"); + .await.expect("couldn't fetch the active loan") + .expect("loan must be found") + .debt; assert_eq!(debt, 9); - assert_eq!(perk.apply(&dick_id, change_intent_positive_increment_small).await.0, -1); + assert_eq!(perk.apply(&dick_id, change_intent_positive_increment_small).await.0, 0); assert_eq!(perk.apply(&dick_id, change_intent_negative_increment).await.0, 0); let debt = loans.get_active_loan(USER_ID, &CHAT_ID_KIND) - .await.expect("couldn't fetch the active loan"); - assert_eq!(debt, 8); + .await.expect("couldn't fetch the active loan") + .expect("loan must be found") + .debt; + assert_eq!(debt, 9); } } diff --git a/src/handlers/pvp.rs b/src/handlers/pvp.rs index 4cb67e3..737bfb1 100644 --- a/src/handlers/pvp.rs +++ b/src/handlers/pvp.rs @@ -11,7 +11,7 @@ use teloxide::types::{CallbackQuery, ChatId, ChosenInlineResult, InlineKeyboardB use crate::handlers::{CallbackResult, ensure_lang_code, HandlerResult, reply_html, utils}; use crate::{metrics, repo}; use crate::config::{AppConfig, BattlesFeatureToggles}; -use crate::repo::{ChatIdPartiality, Repositories}; +use crate::repo::{ChatIdPartiality, GrowthResult, Repositories}; const CALLBACK_PREFIX: &str = "pvp:"; @@ -221,11 +221,20 @@ async fn pvp_impl_attack(p: BattleParams, initiator: UserId, acceptor: UserInfo, let acceptor_uid = acceptor.clone().into(); let (winner, loser) = choose_winner(initiator, acceptor_uid); let (loser_res, winner_res) = p.repos.dicks.move_length(&p.chat_id, loser, winner, bet).await?; + + let (winner_res, withheld_part) = pay_for_loan_if_needed(&p, winner, bet).await + .inspect_err(|e| log::error!("couldn't pay for a loan from a battle award: {e}")) + .ok().flatten() + .map(|(res, withheld)| { + let withheld_part = format!("\n\n{}", t!("commands.pvp.results.withheld", locale = &p.lang_code, payout = withheld)); + (res, withheld_part) + }) + .unwrap_or((winner_res, String::default())); let winner_info = get_user_info(&p.repos.users, winner, &acceptor).await?; let loser_info = get_user_info(&p.repos.users, loser, &acceptor).await?; let main_part = t!("commands.pvp.results.finish", locale = &p.lang_code, - winner_name = winner_info.name, winner_length = winner_res.new_length, loser_length = loser_res.new_length); + winner_name = winner_info.name, winner_length = winner_res.new_length, loser_length = loser_res.new_length, bet = bet); let text = if let (Some(winner_pos), Some(loser_pos)) = (winner_res.pos_in_top, loser_res.pos_in_top) { let winner_pos = t!("commands.pvp.results.position.winner", locale = &p.lang_code, name = winner_info.name, pos = winner_pos); let loser_pos = t!("commands.pvp.results.position.loser", locale = &p.lang_code, name = loser_info.name, pos = loser_pos); @@ -233,7 +242,7 @@ async fn pvp_impl_attack(p: BattleParams, initiator: UserId, acceptor: UserInfo, } else { main_part }; - CallbackResult::EditMessage(text, None) + CallbackResult::EditMessage(format!("{text}{withheld_part}"), None) } else if enough_acceptor { let text = t!("commands.pvp.errors.not_enough.initiator", locale = &p.lang_code); CallbackResult::EditMessage(text, None) @@ -279,3 +288,19 @@ async fn get_user_info(users: &repo::Users, user_uid: UserId, acceptor: &UserInf }; Ok(user) } + +async fn pay_for_loan_if_needed(p: &BattleParams, winner_id: UserId, award: u16) -> anyhow::Result> { + let chat_id_kind = p.chat_id.kind(); + let loan = match p.repos.loans.get_active_loan(winner_id, &chat_id_kind).await? { + Some(loan) => loan, + None => return Ok(None) + }; + let payout = (loan.payout_ratio * award as f32).round() as u16; + let payout = payout.min(loan.debt); + + p.repos.loans.pay(winner_id, &chat_id_kind, payout).await?; + + let withheld = -(payout as i32); + let growth_res = p.repos.dicks.create_or_grow(winner_id, &p.chat_id, withheld).await?; + Ok(Some((growth_res, payout))) +} diff --git a/src/handlers/utils/incrementor.rs b/src/handlers/utils/incrementor.rs index 69bbcfc..e52e579 100644 --- a/src/handlers/utils/incrementor.rs +++ b/src/handlers/utils/incrementor.rs @@ -31,8 +31,12 @@ pub struct Config { #[async_trait] pub trait Perk: Send + Sync + Downcast { fn name(&self) -> &str; - fn enabled(&self) -> bool; async fn apply(&self, dick_id: &DickId, change_intent: ChangeIntent) -> AdditionalChange; + + fn enabled(&self) -> bool { + let env_key = format!("DISABLE_{}", self.name().to_uppercase().replace('-', "_")); + !config::get_env_value_or_default(&env_key, false) + } } impl_downcast!(Perk); diff --git a/src/help/en.md b/src/help/en.md index c04a1c6..fa7fd2b 100644 --- a/src/help/en.md +++ b/src/help/en.md @@ -12,18 +12,23 @@ This bot has been created as a replacement for all competitors abusing their abi Using the /import command, sent as a reply to a message from another bot, describing positions of the users in its top, any administrator of a chat may import already existing cocks. Currently, the following bots are supported: {other_bots}. -For import to be done successfully, a player must already have a dick in this bot! Both lengths will be summed, so no progress is lost. +For import to be done successfully, a player must already have a dick in this bot! Both lengths will be summed, so no progress is lost. Also, the bot must be granted temporary administrator privileges to be able to read the message of the other bot. This allows us to keep the privacy mode enabled, preventing even theoretical possibility for the bot to read all messages in the chat. -The administrator of the chat, I participate in, doesn't allow to add unknown bots, especially with privacy mode disabled +The administrator of the chat, I participate in, doesn't allow to add unknown bots -Unfortunately, I had to disable the privacy mode to make the import feature work. Otherwise, Telegram doesn't give the access to the messages sent by the other bots. In the future, however, when this command will be less demanded, it may be removed. - -Nevertheless, there is a way to play without the bot being in a chat at all! Just use inline queries! Type the username of the bot after the @ sign and a trailing space to get the same commands! - -NB! The rating used via inline mode is different from the rating used by the bot added to the chat. The process of their synchronization may be implemented later. +There is a way to play without the bot being in a chat at all! Just use inline queries! Type the username of the bot after the @ sign and a trailing space to get the same commands! Contacts and links In case of any problems, write to {admin_username} or create an issue in the source code repository. -The source code of the bot, written in Rust, is available on GitHub under a bit modified MIT license, forbidding the use of the code to create competitor bots or take a commercial profit by any ways: {git_repo} \ No newline at end of file +The source code of the bot, written in Rust, is available on GitHub under a bit modified MIT license, forbidding the use of the code to create competitor bots or take a commercial profit by any ways: {git_repo} + +Subscribe to {admin_channel_en} to stay informed about future updates and other bots of mine. + + +For those who are stuck very deep below minus + +Since release 1.2, there is no way anymore to accept a fight with a bet greater than the size of your weapon. However, many of you are already below zero very much. To smooth out this issue, the following measures were taken: +1️⃣ every growth will be increased by {help_pussies_percentage}% of the "debt"; +2️⃣ as an alternative option, the /loan command has been added, allowing you to reset your little sis to zero on credit, which will be gradually paid out by each subsequent growth. \ No newline at end of file diff --git a/src/help/mod.rs b/src/help/mod.rs index afc498d..966c799 100644 --- a/src/help/mod.rs +++ b/src/help/mod.rs @@ -32,8 +32,10 @@ pub struct Context { pub grow_max: String, pub other_bots: String, pub admin_username: String, - pub admin_channel: String, + pub admin_channel_ru: String, + pub admin_channel_en: String, pub git_repo: String, + pub help_pussies_percentage: f64 } pub fn render_help_messages(context: Context) -> Result { diff --git a/src/help/ru.md b/src/help/ru.md index a0f2c37..30cd1f8 100644 --- a/src/help/ru.md +++ b/src/help/ru.md @@ -12,15 +12,11 @@ С помощью команды /import, отправленной в ответ на сообщение с топом от другого бота, администратор чата может подтянуть уже имеющиеся пипирики. На данный момент поддерживаются следующие боты: {other_bots}. -Чтобы импорт сработал, игрок уже должен иметь гигантусика в данном боте! Значения будут просуммированы, так что прогресс не потеряется. +Чтобы импорт сработал, игрок уже должен иметь гигантусика в данном боте! Значения будут просуммированы, так что прогресс не потеряется. А также перед вызовом команды боту необходимо выдать права администратора в чате, чтобы он мог прочитать сообщение другого бота. Данное ограничение позволяет остальному функционалу бота работать при включённом режиме приватности и не иметь доступа ко всем сообщениям чата. -В чате, где я общаюсь, администратор против добавления каких-либо ботов, особенно читающих все сообщения +В чате, где я общаюсь, администратор против добавления каких-либо ботов -К сожалению, на данный момент режим приватности пришлось отключить ради возможности импортировать данные из других ботов, потому что иначе Telegram не показывает содержимое их сообщений. В последующем, однако, данная команда уже будет не столь актуальна и может быть удалена. - -Однако играть можно и вообще без добавления бота в чат: через использование так называемых «встроенных запросов» (inline queries). Просто введите юзернейм бота через собачку, нажмите пробел и выберите соответствующий вариант команды из предложенных. - -Важно! Топы у бота, добавленного в чат, и через inline-сообщения будут разными! В будущем возможно добавление функции синхронизации, которую сможет выполнить администратор чата. +Играть можно и без добавления бота в чат! Через использование так называемых «встроенных запросов» (inline queries). Просто введите юзернейм бота через собачку, нажмите пробел и выберите соответствующий вариант команды из предложенных. Контакты и ссылки @@ -28,4 +24,11 @@ Весь исходный код бота на языке Rust доступен в свободном доступе под модифицированной лицензией MIT, запрещающей хитрецам поднятие конкурирующих ботов в коммерческих целях: {git_repo} -Подписывайтесь на канал {admin_channel}, чтобы узнавать о новостях и обновлениях, других моих ботах. \ No newline at end of file +Подписывайтесь на канал {admin_channel_ru}, чтобы узнавать о новостях и обновлениях, других моих ботах. + + +Для попавших в глубокие минуса + +Начиная с релиза 1.2, принимать сражения со ставкой, превышающей текущий размер боевого орудия, запрещено. Однако, многие уже успели загнать себя в огромные минуса. Для решения данной проблемы были предприняты следующие меры: +1️⃣ каждый прирост будет увеличен на {help_pussies_percentage}% от размера "задолженности"; +2️⃣ в качестве альтернативного варианта добавлена команда /loan, позволяющая обнулить размер причиндала в "кредит", который будет впоследствии медленно погашаться с каждого положительного прироста. \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ce8a187..41a3f88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,8 +68,8 @@ async fn main() -> Result<(), Box> { } let me = bot.get_me().await?; - let repos = repo::Repositories::new(&db_conn, app_config.features); - let perks = handlers::perks::all(&db_conn, app_config.features); + let repos = repo::Repositories::new(&db_conn, &app_config); + let perks = handlers::perks::all(&db_conn, &app_config); let incrementor = handlers::utils::Incrementor::from_env(&repos.dicks, perks); let help_context = config::build_context_for_help_messages(me, &incrementor, &handlers::ORIGINAL_BOT_USERNAMES)?; let help_container = help::render_help_messages(help_context)?; diff --git a/src/metrics.rs b/src/metrics.rs index d6d471e..5fee4f7 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -36,13 +36,13 @@ pub static CMD_TOP_COUNTER: Lazy = Lazy::new(|| { }); pub static CMD_LOAN_COUNTER: Lazy = Lazy::new(|| { let opts = Opts::new("command_loan_usage_total", "count of /loan invocations"); - let invoked_opts = opts.clone().const_label("state", "query"); + let invoked_opts = opts.clone().const_label("state", "invoked"); BothModesComplexCommandCounters { invoked: BothModesCounters { chat: Counter::new("command_loan (chat)", invoked_opts.clone().const_label("mode", "chat")), inline: Counter::new("command_loan (inline)", invoked_opts.const_label("mode", "inline")) }, - finished: Counter::new("command_loan (finished)", opts.const_label("state", "chosen") + finished: Counter::new("command_loan (finished)", opts.const_label("state", "finished") .const_label("mode", "unknown")) } }); diff --git a/src/repo/chats.rs b/src/repo/chats.rs index d9c471e..4e6b591 100644 --- a/src/repo/chats.rs +++ b/src/repo/chats.rs @@ -28,7 +28,7 @@ impl TryInto for Chat { } } -repository!(Chats, +repository!(Chats, with_feature_toggles, pub async fn get_chat(&self, chat_id: ChatIdKind) -> anyhow::Result> { sqlx::query_as!(Chat, "SELECT id as internal_id, chat_id, chat_instance FROM Chats WHERE chat_id = $1::bigint OR chat_instance = $1::text", diff --git a/src/repo/loans.rs b/src/repo/loans.rs index 6d2ad66..60e786c 100644 --- a/src/repo/loans.rs +++ b/src/repo/loans.rs @@ -1,34 +1,64 @@ use anyhow::anyhow; +use derive_more::Constructor; use sqlx::{Postgres, Transaction}; use teloxide::types::UserId; -use crate::repository; use crate::repo::{ChatIdKind, ensure_only_one_row_updated}; -repository!(Loans, - pub async fn get_active_loan(&self, uid: UserId, chat_id: &ChatIdKind) -> anyhow::Result { - sqlx::query_scalar!("SELECT debt FROM loans \ - WHERE uid = $1 AND \ - chat_id = (SELECT id FROM Chats WHERE chat_id = $2::bigint OR chat_instance = $2::text) \ - AND repaid_at IS NULL", +#[derive(Debug)] +pub struct Loan { + pub debt: u16, + pub payout_ratio: f32, +} + +struct LoanEntity { + debt: i32, + payout_ratio: f32 +} + +impl TryFrom for Loan { + type Error = std::num::TryFromIntError; + + fn try_from(value: LoanEntity) -> Result { + Ok(Self { + debt: value.debt.try_into()?, + payout_ratio: value.payout_ratio + }) + } +} + +#[derive(Clone, Constructor)] +pub struct Loans { + pool: sqlx::Pool, + payout_ratio: f32 +} + +impl Loans { + pub async fn get_active_loan(&self, uid: UserId, chat_id: &ChatIdKind) -> anyhow::Result> { + let maybe_loan = sqlx::query_as!(LoanEntity, + "SELECT debt, payout_ratio FROM loans \ + WHERE uid = $1 AND \ + chat_id = (SELECT id FROM Chats WHERE chat_id = $2::bigint OR chat_instance = $2::text) \ + AND repaid_at IS NULL", uid.0 as i64, chat_id.value() as String) .fetch_optional(&self.pool) - .await - .map(|maybe_loan| maybe_loan.map(|debt| debt as u16).unwrap_or_default()) - .map_err(Into::into) + .await? + .map(Loan::try_from) + .transpose()?; + Ok(maybe_loan) } -, + pub async fn borrow(&self, uid: UserId, chat_id: &ChatIdKind, value: u16) -> anyhow::Result> { let mut tx = self.pool.begin().await?; - sqlx::query!("INSERT INTO Loans (chat_id, uid, debt) VALUES (\ + sqlx::query!("INSERT INTO Loans (chat_id, uid, debt, payout_ratio) VALUES (\ (SELECT id FROM Chats WHERE chat_id = $1::bigint OR chat_instance = $1::text),\ - $2, $3)", - chat_id.value() as String, uid.0 as i64, value as i32) + $2, $3, $4)", + chat_id.value() as String, uid.0 as i64, value as i32, self.payout_ratio) .execute(&mut *tx) .await .map(|_| tx) .map_err(Into::into) } -, + pub async fn pay(&self, uid: UserId, chat_id: &ChatIdKind, value: u16) -> anyhow::Result<()> { sqlx::query!("UPDATE Loans SET debt = debt - $3 \ WHERE uid = $1 AND \ @@ -41,4 +71,4 @@ repository!(Loans, .and_then(ensure_only_one_row_updated)?; Ok(()) } -); +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index b328f86..2cce4bf 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -18,7 +18,8 @@ pub use chats::*; pub use import::*; pub use promo::*; pub use loans::*; -use crate::config::{DatabaseConfig, FeatureToggles}; +use crate::config; +use crate::config::DatabaseConfig; #[derive(Clone)] pub struct Repositories { @@ -31,14 +32,14 @@ pub struct Repositories { } impl Repositories { - pub fn new(db_conn: &Pool, feature_toggles: FeatureToggles) -> Self { + pub fn new(db_conn: &Pool, config: &config::AppConfig) -> Self { Self { - users: Users::new(db_conn.clone(), feature_toggles), - dicks: Dicks::new(db_conn.clone(), feature_toggles), - chats: Chats::new(db_conn.clone(), feature_toggles), - import: Import::new(db_conn.clone(), feature_toggles), - promo: Promo::new(db_conn.clone(), feature_toggles), - loans: Loans::new(db_conn.clone(), feature_toggles), + users: Users::new(db_conn.clone()), + dicks: Dicks::new(db_conn.clone(), config.features), + chats: Chats::new(db_conn.clone(), config.features), + import: Import::new(db_conn.clone()), + promo: Promo::new(db_conn.clone()), + loans: Loans::new(db_conn.clone(), config.loan_payout_ratio), } } } @@ -157,11 +158,11 @@ pub async fn establish_database_connection(config: &DatabaseConfig) -> Result { + ($name:ident, with_feature_toggles, $($methods:item),*) => { #[derive(Clone)] pub struct $name { pool: sqlx::Pool, - #[allow(dead_code)] features: $crate::config::FeatureToggles, + features: $crate::config::FeatureToggles, } impl $name { @@ -172,6 +173,21 @@ macro_rules! repository { $($methods)* } }; + + ($name:ident, $($methods:item),*) => { + #[derive(Clone)] + pub struct $name { + pool: sqlx::Pool, + } + + impl $name { + pub fn new(pool: sqlx::Pool) -> Self { + Self { pool } + } + + $($methods)* + } + }; } fn ensure_only_one_row_updated(res: PgQueryResult) -> Result { diff --git a/src/repo/test/dicks.rs b/src/repo/test/dicks.rs index 0b0a526..246dc17 100644 --- a/src/repo/test/dicks.rs +++ b/src/repo/test/dicks.rs @@ -144,13 +144,13 @@ async fn test_pvp() { } pub async fn create_user(db: &Pool) { - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); users.create_or_update(UserId(UID as u64), NAME) .await.expect("couldn't create a user"); } async fn create_user_and_dick_2(db: &Pool, chat_id: &ChatIdPartiality, name: &str) { - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); let dicks = repo::Dicks::new(db.clone(), Default::default()); let uid2 = UserId((UID + 1) as u64); users.create_or_update(uid2, name) diff --git a/src/repo/test/import.rs b/src/repo/test/import.rs index c4cf072..d217f42 100644 --- a/src/repo/test/import.rs +++ b/src/repo/test/import.rs @@ -9,7 +9,7 @@ use crate::repo::test::dicks::{check_dick, create_dick, create_user}; async fn test_all() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let import = repo::Import::new(db.clone(), Default::default()); + let import = repo::Import::new(db.clone()); let chat_id = ChatId(CHAT_ID); create_user(&db).await; diff --git a/src/repo/test/loans.rs b/src/repo/test/loans.rs index 2e3de58..27f16fb 100644 --- a/src/repo/test/loans.rs +++ b/src/repo/test/loans.rs @@ -9,7 +9,8 @@ use crate::repo::test::{CHAT_ID, start_postgres, UID}; async fn test_all() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let loans = repo::Loans::new(db.clone(), Default::default()); + let payout_ratio = 0.1; + let loans = repo::Loans::new(db.clone(), payout_ratio); create_user(&db).await; create_dick(&db).await; // to create a chat @@ -17,24 +18,28 @@ async fn test_all() { let chat_id = ChatIdKind::ID(ChatId(CHAT_ID)); let value: u16 = 10; - let no_debt = loans.get_active_loan(user_id, &chat_id) + let no_loan = loans.get_active_loan(user_id, &chat_id) .await.expect("couldn't fetch active loans"); - assert_eq!(no_debt, 0); + assert!(no_loan.is_none()); loans.borrow(user_id, &chat_id, value) .await.expect("couldn't apply for a loan") .commit() .await.expect("couldn't commit the application for a loan"); - let debt = loans.get_active_loan(user_id, &chat_id) - .await.expect("couldn't fetch active loans again"); - assert_eq!(debt, value); + let loan = loans.get_active_loan(user_id, &chat_id) + .await.expect("couldn't fetch active loans again") + .expect("the loan must be present"); + assert_eq!(loan.debt, value); + assert_eq!(loan.payout_ratio, payout_ratio); let half_of_debt = value / 2; loans.pay(user_id, &chat_id, half_of_debt) .await.expect("couldn't pay the loan"); let left_to_pay = loans.get_active_loan(user_id, &chat_id) - .await.expect("couldn't fetch how much is left to pay"); + .await.expect("couldn't fetch how much is left to pay") + .expect("the loan, which I left to pay, must be present") + .debt; assert_eq!(left_to_pay, half_of_debt); } diff --git a/src/repo/test/mod.rs b/src/repo/test/mod.rs index 20cb217..0d92c44 100644 --- a/src/repo/test/mod.rs +++ b/src/repo/test/mod.rs @@ -32,6 +32,7 @@ pub async fn start_postgres(docker: &clients::Cli) -> (Container, .with_exposed_port(POSTGRES_PORT) .with_wait_for(WaitFor::message_on_stdout("PostgreSQL init process complete; ready for start up.")) .with_wait_for(WaitFor::message_on_stdout("PostgreSQL init process complete; ready for start up.")) + .with_wait_for(WaitFor::millis(200)) .with_env_var("POSTGRES_USER", POSTGRES_USER) .with_env_var("POSTGRES_PASSWORD", POSTGRES_PASSWORD) .with_env_var("POSTGRES_DB", POSTGRES_DB); @@ -42,7 +43,7 @@ pub async fn start_postgres(docker: &clients::Cli) -> (Container, .expect("invalid database URL"); let conf = DatabaseConfig{ url: db_url, - max_connections: 5, + max_connections: 10, }; let pool = repo::establish_database_connection(&conf) .await.expect("couldn't establish a database connection"); diff --git a/src/repo/test/promo.rs b/src/repo/test/promo.rs index ba65184..64e6512 100644 --- a/src/repo/test/promo.rs +++ b/src/repo/test/promo.rs @@ -13,7 +13,7 @@ async fn activate() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let promo = repo::Promo::new(db.clone(), Default::default()); + let promo = repo::Promo::new(db.clone()); promo.create(PromoCodeParams{ code: PROMO_CODE.to_owned(), bonus_length: PROMO_BONUS, diff --git a/src/repo/test/users.rs b/src/repo/test/users.rs index ed35e73..6151642 100644 --- a/src/repo/test/users.rs +++ b/src/repo/test/users.rs @@ -9,7 +9,7 @@ use crate::repo::test::{CHAT_ID, NAME, start_postgres, UID}; async fn create_or_update() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); let members = users.get_all().await .expect("couldn't fetch the empty list of members"); @@ -38,7 +38,7 @@ async fn create_or_update() { async fn get_chat_members() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); let chat_id = ChatIdKind::ID(ChatId(CHAT_ID)); let members = users.get_chat_members(&chat_id) @@ -56,7 +56,7 @@ async fn get_chat_members() { async fn get_random_active_member() { let docker = clients::Cli::default(); let (_container, db) = start_postgres(&docker).await; - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); let chat_id = ChatIdKind::ID(ChatId(CHAT_ID)); let user = users.get_random_active_member(&chat_id) @@ -85,7 +85,7 @@ fn check_member_with_name(members: &[repo::User], name: &str) { } async fn create_member(db: &Pool) { - let users = repo::Users::new(db.clone(), Default::default()); + let users = repo::Users::new(db.clone()); let dicks = repo::Dicks::new(db.clone(), Default::default()); let chat_id = ChatIdKind::ID(ChatId(CHAT_ID));