Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8d86ca5fae
|
|||
|
2ca20c837c
|
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user