Fixed currency converter.
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
use std::{
|
||||
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 regex::Regex;
|
||||
|
||||
@@ -39,6 +48,7 @@ lazy_static::lazy_static! {
|
||||
// USD
|
||||
("бакс", "USD"),
|
||||
("доллар", "USD"),
|
||||
("$", "USD"),
|
||||
// EUR
|
||||
("евро", "EUR"),
|
||||
// JPY
|
||||
@@ -53,8 +63,7 @@ lazy_static::lazy_static! {
|
||||
// CNY
|
||||
("юан", "CNY"),
|
||||
// UAH
|
||||
("гривна", "UAH"),
|
||||
("гривны", "UAH"),
|
||||
("гривн", "UAH"),
|
||||
("гривен", "UAH"),
|
||||
("грiвен", "UAH"),
|
||||
// KZT
|
||||
@@ -80,7 +89,7 @@ lazy_static::lazy_static! {
|
||||
.chain(SUPPORTED_CURS.iter().copied())
|
||||
.collect::<Vec<_>>()
|
||||
.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()?;
|
||||
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.
|
||||
@@ -118,14 +152,14 @@ impl Handler for CurrencyConverter {
|
||||
let Some(message) = get_message(update) else {
|
||||
return Ok(());
|
||||
};
|
||||
let response = self
|
||||
.client
|
||||
.get("https://www.cbr-xml-daily.ru/daily_json.js")
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
|
||||
let response = match self.fetch_rates().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to fetch rates: {e}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let Some(valutes) = response
|
||||
.get("Valute")
|
||||
@@ -139,6 +173,7 @@ impl Handler for CurrencyConverter {
|
||||
|
||||
let mut mapped = HashSet::new();
|
||||
for capture in CUR_REGEX.captures_iter(message.text()) {
|
||||
log::warn!("Captured: {:?}", capture);
|
||||
// We parse supplied value from message
|
||||
let Some(num_value) = capture
|
||||
.name("cur_value")
|
||||
@@ -158,13 +193,6 @@ impl Handler for CurrencyConverter {
|
||||
else {
|
||||
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.
|
||||
let Some(nominal) = valutes
|
||||
// We search for it using cur_name.
|
||||
@@ -176,6 +204,7 @@ impl Handler for CurrencyConverter {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Now we want to know multiplier.
|
||||
let Some(multiplier) = valutes
|
||||
.get(cur_name)
|
||||
@@ -186,9 +215,55 @@ impl Handler for CurrencyConverter {
|
||||
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!(
|
||||
"<pre>{num_value} {cur_name} = {value:.2} RUB</pre><br>",
|
||||
value = multiplier * num_value / nominal,
|
||||
"<pre>{num_value} {cur_name} = {converted_value:.2} {target_name}</pre><br>",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user