2 Commits

Author SHA1 Message Date
s3rius 8d86ca5fae Fixed currency converter.
/ docker_build (push) Successful in 3m36s
/ deploy_helm (push) Successful in 15s
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
2026-07-16 15:30:41 +02:00
s3rius 2ca20c837c Fixed some dangerous methods.
/ deploy_helm (push) Successful in 15s
/ docker_build (push) Successful in 9m17s
2025-10-24 14:55:27 +02:00
5 changed files with 113 additions and 25 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -38,7 +38,7 @@ jobs:
needs: docker_build needs: docker_build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Setup helm - name: Setup helm
uses: azure/setup-helm@v4.3.0 uses: azure/setup-helm@v4.3.0
- name: Deploy - name: Deploy
+1 -1
View File
@@ -25,7 +25,7 @@ lazy_static = "1.4.0"
log = "0.4.17" log = "0.4.17"
rand = "0.8.5" rand = "0.8.5"
rayon = "1.6.1" rayon = "1.6.1"
regex = "1.7.1" regex = { version = "1.7.1", features = ["unicode"] }
reqwest = { version = "0.11.14", features = ["gzip", "json", "tokio-rustls"] } reqwest = { version = "0.11.14", features = ["gzip", "json", "tokio-rustls"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93" serde_json = "1.0.93"
+3
View File
@@ -92,6 +92,9 @@ pub struct BotConfig {
value_delimiter = ',' value_delimiter = ','
)] )]
pub currency_excluded_chats: Vec<i64>, pub currency_excluded_chats: Vec<i64>,
#[arg(name = "dev-mode", long, default_value_t = false, env = "BOT_DEV_MODE")]
pub dev_mode: bool,
} }
#[derive(Clone, Parser, Debug)] #[derive(Clone, Parser, Debug)]
+96 -21
View File
@@ -1,8 +1,17 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
time::Duration, sync::LazyLock,
time::{Duration, Instant},
}; };
use tokio::sync::Mutex;
use serde_json::Value;
static RATE_CACHE: LazyLock<Mutex<(Value, Instant)>> =
LazyLock::new(|| Mutex::new((Value::Null, Instant::now())));
static CACHE_TTL: Duration = Duration::from_secs(3600);
use grammers_client::{Client, InputMessage, Update}; use grammers_client::{Client, InputMessage, Update};
use regex::Regex; use regex::Regex;
@@ -39,6 +48,7 @@ lazy_static::lazy_static! {
// USD // USD
("бакс", "USD"), ("бакс", "USD"),
("доллар", "USD"), ("доллар", "USD"),
("$", "USD"),
// EUR // EUR
("евро", "EUR"), ("евро", "EUR"),
// JPY // JPY
@@ -53,8 +63,7 @@ lazy_static::lazy_static! {
// CNY // CNY
("юан", "CNY"), ("юан", "CNY"),
// UAH // UAH
("гривна", "UAH"), ("гривн", "UAH"),
("гривны", "UAH"),
("гривен", "UAH"), ("гривен", "UAH"),
("грiвен", "UAH"), ("грiвен", "UAH"),
// KZT // KZT
@@ -80,7 +89,7 @@ lazy_static::lazy_static! {
.chain(SUPPORTED_CURS.iter().copied()) .chain(SUPPORTED_CURS.iter().copied())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("|"); .join("|");
Regex::new(format!(r"\s*(?P<cur_value>\d+([\.,]\d+)?)\s+(?P<cur_name>{a})").as_str()).unwrap() Regex::new(format!(r"\s*(?P<cur_value>\d+([\.,]\d+)?)\s+((?P<cur_name>{a})[\p{{L}}]*)(\s+(в|to|in)\s+(?P<target>{a})[\p{{L}}]*)?").as_str()).unwrap()
}; };
} }
@@ -100,6 +109,31 @@ impl CurrencyConverter {
.build()?; .build()?;
Ok(Self { client }) Ok(Self { client })
} }
async fn fetch_rates(&self) -> anyhow::Result<Value> {
let cached = {
let lock = RATE_CACHE.lock().await;
if lock.1.elapsed() < CACHE_TTL && lock.0 != Value::Null {
Some(lock.0.clone())
} else {
None
}
};
if let Some(resp) = cached {
return Ok(resp);
}
let resp = self
.client
.get("https://www.cbr-xml-daily.ru/daily_json.js")
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
let mut lock = RATE_CACHE.lock().await;
*lock = (resp.clone(), Instant::now());
Ok(resp)
}
} }
/// This filter check if the message matches regex for currencies. /// This filter check if the message matches regex for currencies.
@@ -118,14 +152,14 @@ impl Handler for CurrencyConverter {
let Some(message) = get_message(update) else { let Some(message) = get_message(update) else {
return Ok(()); return Ok(());
}; };
let response = self
.client let response = match self.fetch_rates().await {
.get("https://www.cbr-xml-daily.ru/daily_json.js") Ok(r) => r,
.send() Err(e) => {
.await? log::warn!("Failed to fetch rates: {e}");
.error_for_status()? return Ok(());
.json::<serde_json::Value>() }
.await?; };
let Some(valutes) = response let Some(valutes) = response
.get("Valute") .get("Valute")
@@ -139,6 +173,7 @@ impl Handler for CurrencyConverter {
let mut mapped = HashSet::new(); let mut mapped = HashSet::new();
for capture in CUR_REGEX.captures_iter(message.text()) { for capture in CUR_REGEX.captures_iter(message.text()) {
log::warn!("Captured: {:?}", capture);
// We parse supplied value from message // We parse supplied value from message
let Some(num_value) = capture let Some(num_value) = capture
.name("cur_value") .name("cur_value")
@@ -158,13 +193,6 @@ impl Handler for CurrencyConverter {
else { else {
continue; continue;
}; };
let fingerprint = format!("{num_value:.5} {cur_name}");
// Check if we already processed this value.
if mapped.contains(&fingerprint) {
continue;
}
// Add a value to not calculate it again.
mapped.insert(fingerprint);
// Now we want to know current nominal for this value. // Now we want to know current nominal for this value.
let Some(nominal) = valutes let Some(nominal) = valutes
// We search for it using cur_name. // We search for it using cur_name.
@@ -176,6 +204,7 @@ impl Handler for CurrencyConverter {
else { else {
continue; continue;
}; };
// Now we want to know multiplier. // Now we want to know multiplier.
let Some(multiplier) = valutes let Some(multiplier) = valutes
.get(cur_name) .get(cur_name)
@@ -186,9 +215,55 @@ impl Handler for CurrencyConverter {
continue; continue;
}; };
let mut converted_value = multiplier * num_value / nominal;
let mut target_name = String::from("RUB");
let target_cur_name = capture.name("target").map(|mtch| mtch.as_str());
let target_cur_name = target_cur_name
.and_then(|val| CONVERTION_ALIASES.get(val).copied())
.or(target_cur_name);
if let Some(target_cur) = target_cur_name {
if target_cur == cur_name {
continue;
}
if let Some(info) = valutes.get(target_cur) {
let Some(target_nominal) = info
.get("Nominal")
.map(ToString::to_string)
.and_then(|v| v.as_str().parse::<f64>().ok())
else {
continue;
};
let Some(target_multiplier) = info
.get("Value")
.map(ToString::to_string)
.and_then(|v| v.as_str().parse::<f64>().ok())
else {
continue;
};
let Some(target_code) = info
.get("CharCode")
.and_then(|v| v.as_str())
.map(ToString::to_string)
else {
continue;
};
target_name = target_code;
converted_value = converted_value * target_nominal / target_multiplier;
}
}
let fingerprint = format!("{num_value:.5} {cur_name} {target_name}");
// Check if we already processed this value.
if mapped.contains(&fingerprint) {
continue;
}
// Add a value to not calculate it again.
mapped.insert(fingerprint);
calucates.push(format!( calucates.push(format!(
"<pre>{num_value} {cur_name} = {value:.2} RUB</pre><br>", "<pre>{num_value} {cur_name} = {converted_value:.2} {target_name}</pre><br>",
value = multiplier * num_value / nominal,
)); ));
} }
+11 -1
View File
@@ -100,7 +100,7 @@ async fn handle_with_log(handler: Box<dyn Handler>, client: Client, update_data:
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> { async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
let me = client.get_me().await?; let me = client.get_me().await?;
let handlers: Vec<FilteredHandler> = vec![ let mut handlers: Vec<FilteredHandler> = vec![
// Printing help. // Printing help.
FilteredHandler::new(Help) FilteredHandler::new(Help)
.add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone()))
@@ -132,6 +132,7 @@ async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
.add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone()))
.add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(UpdateTypeFilter(&[UpdateType::New]))
.add_filter(SilentFilter) .add_filter(SilentFilter)
.add_filter(OnlyFromId(me.id()))
.add_filter(TextFilter(&[".bl"], TextMatchMethod::StartsWith)), .add_filter(TextFilter(&[".bl"], TextMatchMethod::StartsWith)),
// Handler for converting currecies. // Handler for converting currecies.
FilteredHandler::new(CurrencyConverter::new()?) FilteredHandler::new(CurrencyConverter::new()?)
@@ -145,6 +146,7 @@ async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
.add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone()))
.add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(UpdateTypeFilter(&[UpdateType::New]))
.add_filter(SilentFilter) .add_filter(SilentFilter)
.add_filter(OnlyFromId(me.id()))
.add_filter(TextFilter(&[".rl"], TextMatchMethod::StartsWith)), .add_filter(TextFilter(&[".rl"], TextMatchMethod::StartsWith)),
// Weather forecast. // Weather forecast.
FilteredHandler::new(WeatherForecaster::new()?) FilteredHandler::new(WeatherForecaster::new()?)
@@ -172,6 +174,7 @@ async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
.add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone()))
.add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(UpdateTypeFilter(&[UpdateType::New]))
.add_filter(SilentFilter) .add_filter(SilentFilter)
.add_filter(OnlyFromId(me.id()))
.add_filter(TextFilter(&[".c"], TextMatchMethod::StartsWith)) .add_filter(TextFilter(&[".c"], TextMatchMethod::StartsWith))
.add_filter(NotFilter(TextFilter( .add_filter(NotFilter(TextFilter(
&[".cid"], &[".cid"],
@@ -191,6 +194,13 @@ async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
.add_filter(TextFilter(&[".t"], TextMatchMethod::StartsWith)), .add_filter(TextFilter(&[".t"], TextMatchMethod::StartsWith)),
]; ];
// If dev mode is enabled only respond to messages from user himself.
if args.dev_mode {
for hander in handlers.iter_mut() {
*hander = hander.clone().add_filter(OnlyFromId(me.id()));
}
}
let mut errors_count = 0; let mut errors_count = 0;
loop { loop {