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::>() .join("|"); Regex::new(format!(r"\s*(?P\d+([\.,]\d+)?)\s+(?P{a})").as_str()).unwrap() }; } #[derive(Clone)] pub struct CurrencyTextFilter; #[derive(Clone)] pub struct CurrencyConverter { client: reqwest::Client, } impl CurrencyConverter { pub fn new() -> anyhow::Result { 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 { 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::() .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::().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 Some(nominal) = valutes .get(cur_name) .and_then(|info| info.get("Nominal")) .map(ToString::to_string) .and_then(|value| value.as_str().parse::().ok()) 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::().ok()) .map(|multiplier| multiplier * num_value / nominal); if let Some(value) = calculated { calucates.push(format!( "
{num_value} {cur_name} = {value:.2} RUB

" )); } } if !calucates.is_empty() { let mut bot_response = String::from("Полагаясь на текущий курс валют могу сказать следующее:\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(()) } }