diff --git a/app/bot/src/commands/mod.rs b/app/bot/src/commands/mod.rs new file mode 100644 index 0000000..99ff82c --- /dev/null +++ b/app/bot/src/commands/mod.rs @@ -0,0 +1,107 @@ +use aws_sdk_dynamodb::Client as DynamoDbClient; +use teloxide::{ + payloads::SendMessageSetters, + prelude::{Bot, Requester}, + types::{LinkPreviewOptions, Message, ParseMode}, + utils::command::BotCommands, +}; + +use crate::station; +pub(crate) mod utils; + +#[derive(BotCommands, Clone)] +#[command(rename_rule = "lowercase")] +pub(crate) enum BaseCommand { + /// Visualizza la lista dei comandi + Help, + /// Ottieni informazioni riguardanti il bot + Info, + /// Inizia ad interagire con il bot + Start, + /// Visualizza la lista delle stazioni disponibili + Stazioni, +} + +pub(crate) async fn base_commands_handler( + bot: Bot, + msg: Message, + cmd: BaseCommand, +) -> Result<(), teloxide::RequestError> { + let text = match cmd { + BaseCommand::Help => BaseCommand::descriptions().to_string(), + BaseCommand::Start => { + if msg.chat.is_group() || msg.chat.is_supergroup() { + format!("Ciao {}! Scrivete il nome di una stazione da monitorare (e.g. /Cesena o `/S. Carlo`) + o cercatene una con /stazioni", + msg.chat.title().unwrap_or("")) + } else { + format!("Ciao @{}! Scrivi il nome di una stazione da monitorare (e.g. `Cesena` o `/S. Carlo`) \ + o cercane una con /stazioni", + msg.chat.username().unwrap_or(msg.chat.first_name().unwrap_or(""))) + } + } + BaseCommand::Stazioni => station::stations().join("\n"), + BaseCommand::Info => { + let info = "Bot Telegram che permette di leggere i livello idrometrici dei fiumi dell'Emilia Romagna \ + I dati idrometrici sono ottenuti dalle API messe a disposizione da allertameteo.regione.emilia-romagna.it\n\n\ + Il progetto è completamente open-source (https://github.com/notdodo/erfiume_bot).\n\ + Per donazioni per mantenere il servizio attivo: buymeacoffee.com/d0d0\n\n\ + Inizia con /start o /stazioni"; + info.to_string() + } + }; + + bot.send_message(msg.chat.id, utils::escape_markdown_v2(&text)) + .link_preview_options(LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }) + .parse_mode(ParseMode::MarkdownV2) + .await?; + + Ok(()) +} + +pub(crate) async fn message_handler( + bot: &Bot, + msg: &Message, + dynamodb_client: DynamoDbClient, +) -> Result { + let text = msg.text().unwrap(); + let text = match station::search::get_station( + &dynamodb_client, + text.to_string(), + "Stazioni", + ) + .await + { + Ok(Some(item)) => { + if item.nomestaz != text { + format!("{}\nSe non è la stazione corretta prova ad affinare la ricerca.", item.create_station_message()) + }else { + item.create_station_message().to_string() + } + } + Err(_) | Ok(None) => "Nessuna stazione trovata con la parola di ricerca.\nInserisci esattamente il nome che vedi dalla pagina https://allertameteo.regione.emilia-romagna.it/livello-idrometrico\nAd esempio 'Cesena', 'Lavino di Sopra' o 'S. Carlo'.\nSe non sai quale cercare prova con /stazioni".to_string() + }; + let mut message = text.clone(); + if fastrand::choose_multiple(0..10, 1)[0] == 8 { + message = format!("{}\n\nContribuisci al progetto per mantenerlo attivo e sviluppare nuove funzionalità tramite una donazione: https://buymeacoffee.com/d0d0", text); + } + if fastrand::choose_multiple(0..50, 1)[0] == 8 { + message = format!("{}\n\nEsplora o contribuisci al progetto open-source per sviluppare nuove funzionalità: https://github.com/notdodo/erfiume_bot", text); + } + bot.send_message(msg.chat.id, utils::escape_markdown_v2(&message)) + .link_preview_options(LinkPreviewOptions { + is_disabled: false, + url: None, + prefer_small_media: true, + prefer_large_media: false, + show_above_text: false, + }) + .parse_mode(ParseMode::MarkdownV2) + .await +} diff --git a/app/bot/src/commands/utils.rs b/app/bot/src/commands/utils.rs new file mode 100644 index 0000000..f814bb4 --- /dev/null +++ b/app/bot/src/commands/utils.rs @@ -0,0 +1,20 @@ +pub(crate) fn escape_markdown_v2(text: &str) -> String { + text.replace("\\", "\\\\") + .replace("_", "\\_") + .replace("*", "\\*") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("~", "\\~") + .replace(">", "\\>") + .replace("#", "\\#") + .replace("+", "\\+") + .replace("-", "\\-") + .replace("=", "\\=") + .replace("|", "\\|") + .replace("{", "\\{") + .replace("}", "\\}") + .replace(".", "\\.") + .replace("!", "\\!") +} diff --git a/app/bot/src/main.rs b/app/bot/src/main.rs index b866f27..2b8aa72 100644 --- a/app/bot/src/main.rs +++ b/app/bot/src/main.rs @@ -2,33 +2,18 @@ use aws_config::BehaviorVersion; use aws_sdk_dynamodb::Client as DynamoDbClient; use lambda_runtime::{service_fn, Error as LambdaError, LambdaEvent}; use serde_json::{json, Value}; -use station::search::get_station; use teloxide::{ dispatching::{HandlerExt, UpdateFilterExt}, dptree::deps, - payloads::SendMessageSetters, prelude::{dptree, Bot, Requester, Update}, respond, - types::{LinkPreviewOptions, Me, Message, ParseMode}, - utils::command::BotCommands, + types::{Me, Message}, }; use tracing::{info, instrument}; use tracing_subscriber::EnvFilter; +mod commands; mod station; -#[derive(BotCommands, Clone)] -#[command(rename_rule = "lowercase")] -enum BaseCommand { - /// Visualizza la lista dei comandi - Help, - /// Ottieni informazioni riguardanti il bot - Info, - /// Inizia ad interagire con il bot - Start, - /// Visualizza la lista delle stazioni disponibili - Stazioni, -} - #[tokio::main] async fn main() -> Result<(), LambdaError> { tracing_subscriber::fmt() @@ -49,7 +34,7 @@ async fn main() -> Result<(), LambdaError> { async fn lambda_handler(event: LambdaEvent) -> Result { let bot = Bot::from_env(); let me: Me = bot.get_me().await?; - info!("{:?}", event); + info!("{:?}", event.payload); let outer_json: Value = serde_json::from_value( event @@ -66,21 +51,13 @@ async fn lambda_handler(event: LambdaEvent) -> Result let handler = Update::filter_message() .branch( dptree::entry() - .filter_command::() - .endpoint(base_commands_handler), + .filter_command::() + .endpoint(commands::base_commands_handler), ) .branch(dptree::endpoint(|msg: Message, bot: Bot| async move { - let text = message_handler(&msg); - bot.send_message(msg.chat.id, escape_markdown_v2(&text.await.unwrap())) - .link_preview_options(LinkPreviewOptions { - is_disabled: false, - url: None, - prefer_small_media: true, - prefer_large_media: false, - show_above_text: false, - }) - .parse_mode(ParseMode::MarkdownV2) - .await?; + let shared_config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let dynamodb_client = DynamoDbClient::new(&shared_config); + commands::message_handler(&bot, &msg, dynamodb_client).await?; respond(()) })); @@ -90,97 +67,3 @@ async fn lambda_handler(event: LambdaEvent) -> Result "statusCode": 200, })) } - -fn escape_markdown_v2(text: &str) -> String { - text.replace("\\", "\\\\") - .replace("_", "\\_") - .replace("*", "\\*") - .replace("[", "\\[") - .replace("]", "\\]") - .replace("(", "\\(") - .replace(")", "\\)") - .replace("~", "\\~") - .replace(">", "\\>") - .replace("#", "\\#") - .replace("+", "\\+") - .replace("-", "\\-") - .replace("=", "\\=") - .replace("|", "\\|") - .replace("{", "\\{") - .replace("}", "\\}") - .replace(".", "\\.") - .replace("!", "\\!") -} - -async fn base_commands_handler( - bot: Bot, - msg: Message, - cmd: BaseCommand, -) -> Result<(), teloxide::RequestError> { - let text = match cmd { - BaseCommand::Help => BaseCommand::descriptions().to_string(), - BaseCommand::Start => { - if msg.chat.is_group() || msg.chat.is_supergroup() { - format!("Ciao {}! Scrivete il nome di una stazione da monitorare (e.g. /Cesena o `/S. Carlo`) - o cercatene una con /stazioni", - msg.chat.title().unwrap_or("")) - } else { - format!("Ciao @{}! Scrivi il nome di una stazione da monitorare (e.g. `Cesena` o `/S. Carlo`) \ - o cercane una con /stazioni", - msg.chat.username().unwrap_or(msg.chat.first_name().unwrap_or(""))) - } - } - BaseCommand::Stazioni => station::stations().join("\n"), - BaseCommand::Info => { - let info = "Bot Telegram che permette di leggere i livello idrometrici dei fiumi dell'Emilia Romagna \ - I dati idrometrici sono ottenuti dalle API messe a disposizione da allertameteo.regione.emilia-romagna.it\n\n\ - Il progetto è completamente open-source (https://github.com/notdodo/erfiume_bot).\n\ - Per donazioni per mantenere il servizio attivo: buymeacoffee.com/d0d0\n\n\ - Inizia con /start o /stazioni"; - info.to_string() - } - }; - - bot.send_message(msg.chat.id, escape_markdown_v2(&text)) - .link_preview_options(LinkPreviewOptions { - is_disabled: true, - url: None, - prefer_small_media: false, - prefer_large_media: false, - show_above_text: false, - }) - .parse_mode(ParseMode::MarkdownV2) - .await?; - - Ok(()) -} - -async fn message_handler(message: &Message) -> Result { - let shared_config = aws_config::load_defaults(BehaviorVersion::latest()).await; - let dynamodb_client = DynamoDbClient::new(&shared_config); - let msg = message.text().unwrap(); - let text = match get_station( - &dynamodb_client, - msg.to_string(), - "Stazioni", - ) - .await - { - Ok(Some(item)) => { - if item.nomestaz != msg { - format!("{}\nSe non è la stazione corretta prova ad affinare la ricerca.", item.create_station_message()) - }else { - item.create_station_message().to_string() - } - } - Err(_) | Ok(None) => "Nessuna stazione trovata con la parola di ricerca.\nInserisci esattamente il nome che vedi dalla pagina https://allertameteo.regione.emilia-romagna.it/livello-idrometrico\nAd esempio 'Cesena', 'Lavino di Sopra' o 'S. Carlo'.\nSe non sai quale cercare prova con /stazioni".to_string() - }; - let mut message = text.clone(); - if fastrand::choose_multiple(0..10, 1)[0] == 8 { - message = format!("{}\n\nContribuisci al progetto per mantenerlo attivo e sviluppare nuove funzionalità tramite una donazione: https://buymeacoffee.com/d0d0", text); - } - if fastrand::choose_multiple(0..50, 1)[0] == 8 { - message = format!("{}\n\nEsplora o contribuisci al progetto open-source per sviluppare nuove funzionalità: https://github.com/notdodo/erfiume_bot", text); - } - Ok(message) -} diff --git a/app/bot/src/station/mod.rs b/app/bot/src/station/mod.rs index 7d815e0..8b4486a 100644 --- a/app/bot/src/station/mod.rs +++ b/app/bot/src/station/mod.rs @@ -1,4 +1,4 @@ -pub mod search; +pub(crate) mod search; use chrono::{DateTime, TimeZone}; use chrono_tz::Europe::Rome; diff --git a/app/bot/src/station/search.rs b/app/bot/src/station/search.rs index 6ac38c2..89dbead 100644 --- a/app/bot/src/station/search.rs +++ b/app/bot/src/station/search.rs @@ -6,7 +6,7 @@ use super::{stations, Stazione, UNKNOWN_VALUE}; fn fuzzy_search(search: &str) -> Option { let stations = stations(); - let closest_match = stations + stations .iter() .map(|s: &String| { ( @@ -19,9 +19,7 @@ fn fuzzy_search(search: &str) -> Option { }) .filter(|(_, score)| *score < 4) .min_by_key(|(_, score)| *score) - .map(|(station, _)| station.clone()); // Map to String and clone the station name - - closest_match + .map(|(station, _)| station.clone()) } pub async fn get_station( @@ -175,4 +173,21 @@ mod tests { assert_eq!(fuzzy_search(&message), expected); } + + #[test] + fn parse_string_field_yields_correct_value() { + let expected = "this is a string".to_string(); + let item = HashMap::from([("field".to_string(), AttributeValue::S(expected.clone()))]); + assert_eq!(parse_string_field(&item, "field").unwrap(), expected); + } + + #[test] + fn parse_optional_number_field_yields_correct_value() { + let expected = 4; + let item = HashMap::from([("field".to_string(), AttributeValue::N(expected.to_string()))]); + assert_eq!( + parse_optional_number_field::(&item, "field").unwrap(), + Some(expected) + ); + } } diff --git a/pulumi/__main__.py b/pulumi/__main__.py index 88993a5..ccb5a2d 100644 --- a/pulumi/__main__.py +++ b/pulumi/__main__.py @@ -10,7 +10,6 @@ iam, lambda_, scheduler, - secretsmanager, ) from telegram_provider import Webhook @@ -19,7 +18,7 @@ SYNC_MINUTES_RATE_NORMAL = 24 * 60 # Once a day SYNC_MINUTES_RATE_MEDIUM = 2 * 60 # Every two hours SYNC_MINUTES_RATE_EMERGENCY = 20 -EMERGENCY = True +EMERGENCY = False CUSTOM_DOMAIN_NAME = "erfiume.thedodo.xyz" stazioni_table = dynamodb.Table( @@ -52,13 +51,6 @@ ), ) -telegram_token_secret = secretsmanager.Secret( - f"{RESOURCES_PREFIX}-telegram-bot-token", - name="telegram-bot-token", - description="The Telegram Bot token for erfiume_bot", - recovery_window_in_days=7, -) - fetcher_role = iam.Role( f"{RESOURCES_PREFIX}-fetcher", name=f"{RESOURCES_PREFIX}-fetcher", @@ -141,15 +133,6 @@ ], "Resources": [chats_table.arn], }, - { - "Effect": "Allow", - "Actions": [ - "secretsmanager:GetSecretValue", - ], - "Resources": [ - telegram_token_secret.arn, - ], - }, ], ).json, )