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