177 lines
5.1 KiB
Rust
177 lines
5.1 KiB
Rust
use std::{collections::HashMap, time::Duration};
|
||
|
||
use grammers_client::{Client, InputMessage, Update};
|
||
use regex::Regex;
|
||
|
||
use crate::{
|
||
bot::{filters::base::Filter, handlers::Handler},
|
||
utils::messages::get_message,
|
||
};
|
||
|
||
lazy_static::lazy_static! {
|
||
static ref SUPPORTED_CURS: Vec<&'static str> = vec![
|
||
"GBP",
|
||
"HUF",
|
||
"USD",
|
||
"EUR",
|
||
"CNY",
|
||
"NOK",
|
||
"UAH",
|
||
"SEK",
|
||
"CHF",
|
||
"KRW",
|
||
"JPY",
|
||
"KZT",
|
||
"PLN",
|
||
"TRY",
|
||
"AMD",
|
||
"RSD",
|
||
];
|
||
|
||
static ref CONVERTION_ALIASES: HashMap<&'static str, &'static str> = HashMap::from(
|
||
[
|
||
// GBP
|
||
("фунт", "GBP"),
|
||
// USD
|
||
("бакс", "USD"),
|
||
("доллар", "USD"),
|
||
// EUR
|
||
("евро", "EUR"),
|
||
// JPY
|
||
("иен", "JPY"),
|
||
("йен", "JPY"),
|
||
// KRW
|
||
("вон", "KRW"),
|
||
// CHF
|
||
("франк", "CHF"),
|
||
// SEK
|
||
("крон", "CHF"),
|
||
// CNY
|
||
("юан", "CNY"),
|
||
// UAH
|
||
("гривна", "UAH"),
|
||
("гривны", "UAH"),
|
||
("гривен", "UAH"),
|
||
("грiвен", "UAH"),
|
||
// KZT
|
||
("тенге", "KZT"),
|
||
("тэнге", "KZT"),
|
||
// PLN
|
||
("злот", "PLN"),
|
||
// TRY
|
||
("лир", "TRY"),
|
||
// AMD
|
||
("драм", "AMD"),
|
||
// RSD
|
||
("динар", "RSD"),
|
||
]
|
||
);
|
||
|
||
static ref CUR_REGEX: Regex = {
|
||
#[allow(clippy::clone_double_ref)]
|
||
let a = CONVERTION_ALIASES.keys()
|
||
.copied()
|
||
.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()
|
||
};
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct CurrencyTextFilter;
|
||
|
||
#[derive(Clone)]
|
||
pub struct CurrencyConverter {
|
||
client: reqwest::Client,
|
||
}
|
||
|
||
impl CurrencyConverter {
|
||
pub fn new() -> anyhow::Result<Self> {
|
||
let client = reqwest::ClientBuilder::new()
|
||
.timeout(Duration::from_secs(2))
|
||
.gzip(true)
|
||
.build()?;
|
||
Ok(Self { client })
|
||
}
|
||
}
|
||
|
||
/// This filter check if the message matches regex for currencies.
|
||
impl Filter for CurrencyTextFilter {
|
||
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
|
||
let Some(message) = get_message(update) else {
|
||
return Ok(false);
|
||
};
|
||
Ok(CUR_REGEX.is_match(message.text()))
|
||
}
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Handler for CurrencyConverter {
|
||
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
|
||
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 Some(valutes) = response
|
||
.get("Valute")
|
||
.and_then(serde_json::Value::as_object) else{
|
||
log::warn!("Can't get valutes fom response.");
|
||
return Ok(());
|
||
};
|
||
|
||
let mut calucates = Vec::new();
|
||
|
||
for capture in CUR_REGEX.captures_iter(message.text()) {
|
||
// We parse supplied value from message
|
||
let Some(num_value) = capture
|
||
.name("cur_value")
|
||
// Convert match to string.
|
||
.map(|mtch| mtch.as_str())
|
||
// Parse it.
|
||
.and_then(|val| val.parse::<f64>().ok()) else{
|
||
continue;
|
||
};
|
||
let cur_name = capture.name("cur_name").map(|mtch| mtch.as_str());
|
||
let Some(cur_name) = cur_name
|
||
// We check if the value is an alias.
|
||
.and_then(|val| CONVERTION_ALIASES.get(val).copied())
|
||
// get previous value if not.
|
||
.or(cur_name) else{
|
||
continue;
|
||
};
|
||
let calculated = valutes
|
||
.get(cur_name)
|
||
.and_then(|info| info.get("Value"))
|
||
.map(ToString::to_string)
|
||
.and_then(|value| value.as_str().parse::<f64>().ok())
|
||
.map(|multiplier| multiplier * num_value);
|
||
if let Some(value) = calculated {
|
||
calucates.push(format!(
|
||
"<pre>{num_value} {cur_name} = {value:.2} RUB</pre><br>"
|
||
));
|
||
}
|
||
}
|
||
|
||
if !calucates.is_empty() {
|
||
let mut bot_response =
|
||
String::from("<b>Полагаясь на текущий курс валют могу сказать следующее:</b>\n\n");
|
||
for calc in calucates {
|
||
bot_response.push_str(calc.as_str());
|
||
}
|
||
message
|
||
// We send it as silent, so we can filter this message later.
|
||
.reply(InputMessage::html(bot_response).silent(true))
|
||
.await?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|