diff --git a/app/erfiume/apis.py b/app/erfiume/apis.py index ade8eb8..1e93774 100644 --- a/app/erfiume/apis.py +++ b/app/erfiume/apis.py @@ -17,6 +17,272 @@ UNKNOWN_VALUE = -9999.0 +KNOWN_STATIONS = [ + "S. Zeno", + "Spessa Po", + "Parma S. Siro", + "Mercato Saraceno", + "Fiorenzuola d'Arda", + "Fiscaglia Monte", + "Navicello", + "Camposanto", + "Fidenza SIAP", + "Codigoro", + "Casoni", + "Ponte Ronca", + "Gallo", + "Castenaso", + "Correcchio Sillaro", + "Beccara Nuova Reno", + "Salsominore", + "Vigoleno", + "Lonza", + "Morciano di Romagna", + "Pievepelago idro", + "Casse Espansione Enza monte", + "S. Secondo", + "Cassa Crostolo SIAP", + "Tornolo", + "Parma Ovest", + "Rasponi", + "Castel San Pietro", + "Ponte dell'Olio", + "Arcoveggio", + "S. Sofia", + "Lugo SIAP", + "Pieve Cesato", + "Cardinala Idice", + "Ciriano", + "Fossalta", + "Fiorano", + "Puianello", + "Borgo Visignolo", + "Cusercoli Idro", + "Colorno AIPO", + "Ficarolo", + "Fusignano", + "Foscaglia Panaro", + "Teodorano", + "Ponte Sant'Ambrogio", + "Saletto", + "Ponte Val di Sasso", + "Case Bonini", + "Capoponte", + "Ponte Valenza Po", + "Secondo Salto", + "Ponte Vico", + "Sermide", + "Ponte Verucchio", + "Battiferro Bypass", + "Calcara", + "Casalmaggiore", + "Diga di Ridracoli", + "Pontelagoscuro", + "Fiscaglia Valle", + "Molato Diga Monte", + "S. Antonio", + "Ponte Lamberti", + "Linaro", + "Montanaro", + "Lugo", + "Cremona", + "Forcelli", + "S. Agata", + "Modena Naviglio", + "Casalecchio canale", + "Ponte Samone", + "Bagnetto Reno", + "Ponte Alto", + "Ponte Messa", + "Dosso", + "Loiano Ponte Savena", + "S. Carlo", + "Ponte Braldo", + "Vergato", + "Mordano", + "Castiglione", + "Pracchia", + "Ponte Becca Po", + "Ongina", + "Rivergaro", + "Vignola SIAP", + "S. Zaccaria", + "Alseno", + "Ramiola", + "Savignano", + "Strada Casale", + "Rocca San Casciano", + "S. Marco", + "Bobbio", + "Casola Valsenio", + "Fornovo SIAP", + "Pioppa", + "Chiavicone Idice", + "Ponte Veggia", + "La Dozza", + "Fanano", + "Cadelbosco", + "Sostegno Reno", + "S. Bartolo", + "Correcchio canale", + "Canonica Valle", + "Mezzano", + "Saliceto", + "Ponte Nibbiano", + "Gandazzolo Reno", + "S. Ruffillo Savena", + "Farini", + "Ostia Parmense", + "Bova", + "Palesio", + "Modigliana", + "Paltrone Samoggia", + "Ponte Cavola", + "Rimini Ausa", + "Ponte Bacchello", + "Sesto Imolese", + "Pontenure", + "Chiavica Bastia Sillaro", + "Silla", + "Ongina Po", + "Sorbolo", + "Isola S.Antonio PO", + "Chiavicone Reno", + "Parma Ponte Nuovo", + "Rossenna", + "Castellina di Soragna", + "Pontelagoscuro idrometro Boicelli", + "S. Vittoria", + "Sarna", + "Casale Monferrato Po", + "Imola", + "Mignano Diga", + "Polesella SIAP", + "Vetto", + "Borello", + "Ponte Calanca", + "Rivalta RE", + "Opera Reno Panfilia", + "Tebano", + "Parma cassa invaso CAE", + "Bazzano", + "Alfonsine", + "Forli'", + "Casalecchio tiro a volo", + "Matellica", + "Pianoro", + "Porretta Terme", + "Selvanizza", + "Compiano", + "Corniglio", + "Lavino di Sotto", + "Calisese", + "Castell'Arquato Canale", + "Bentivoglio", + "Ponte Felisio", + "S. Bernardino", + "Ponte Dolo", + "Borgoforte", + "Luretta", + "Marzocchina", + "Trebbia Valsigiara", + "S. Donnino", + "Casse Espansione Enza SIAP", + "Bondeno Panaro", + "Carignano Po", + "Borgo Tossignano", + "Accursi Idice", + "Isola Pescaroli SIAP", + "Ravone Via del Chiu", + "Anzola Ghironda", + "Ponte Locatello", + "Villanova", + "Coccolia", + "Sasso Marconi", + "Santarcangelo di Romagna", + "Ponte degli Alpini", + "Centonara", + "Bevano Adriatica", + "Castrocaro", + "Codrignano", + "S. Ilario d'Enza", + "Salsomaggiore sul Ghiara", + "Berceto Baganza", + "Veggiola", + "Vigolo Marchese", + "Cesena", + "Castelmaggiore", + "Casei Gerola Po", + "Suviana", + "Invaso", + "Brocchetti", + "Bonconvento", + "Cento", + "Burana", + "Savio", + "Fornovo", + "Ponte Uso", + "S. Cesario SIAP", + "Piacenza", + "Rubiera casse monte", + "Pianello Val Tidone idro", + "Conca Diga", + "Cavanella SIAP", + "Ponte Bastia", + "Spilamberto", + "Ariano", + "S. Maria Nova", + "Gatta", + "Boretto", + "Marsaglia", + "Gorzano", + "Rimini SS16", + "Lavino di Sopra", + "Castell'Arquato", + "Cotignola", + "Parma Ponte Verdi", + "Ca' de Caroli", + "Fiumalbo", + "Rivalta RA", + "Cedogno", + "Ravone", + "Castelbolognese", + "Ponte Nibbiano Tidoncello", + "Meldola", + "Pizzocalvo", + "Ponte Motta", + "Quarto", + "Ponteceno", + "Noceto", + "Gandazzolo Savena", + "Crescentino Po", + "Rubiera casse valle", + "Monte Cerignone", + "Impianto Forcelli Lavino", + "Bondanello", + "Firenzuola idro", + "Ronco", + "Rottofreno", + "Ferriere Idro", + "Bomporto", + "Pradella", + "Toccalmatto", + "Langhirano idro", + "Ponte Dattaro", + "Marzolara", + "Rubiera Tresinaro", + "Massarolo", + "Opera Po", + "Concordia sulla Secchia", + "Rubiera SS9", + "Marradi", + "Casalecchio chiusa", + "Reda", + "Cabanne", + "Faenza", + "Portonovo", +] + @dataclass class Stazione: diff --git a/app/erfiume/tgbot.py b/app/erfiume/tgbot.py index 65698ab..11fc8b6 100644 --- a/app/erfiume/tgbot.py +++ b/app/erfiume/tgbot.py @@ -17,16 +17,19 @@ MessageHandler, filters, ) +from thefuzz import process # type: ignore[import-untyped] if TYPE_CHECKING: from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities import parameters +from .apis import KNOWN_STATIONS from .logging import logger from .storage import AsyncDynamoDB RANDOM_SEND_LINK = 10 +FUZZ_SCORE_CUTOFF = 80 # UTILS @@ -110,7 +113,7 @@ async def start(update: Update, _: ContextTypes.DEFAULT_TYPE | None) -> None: and update.message ): user = update.effective_user - message = rf"Ciao {user.mention_html()}! Scrivi il nome di una stazione da monitorare per iniziare (e.g. Cesena o /S. Carlo)" + message = rf"Ciao {user.mention_html()}! Scrivi il nome di una stazione da monitorare per iniziare (e.g. Cesena o /S. Carlo) o cercane una con /stazioni" # noqa: E501 await update.message.reply_html(message) elif ( is_from_user(update) @@ -119,7 +122,7 @@ async def start(update: Update, _: ContextTypes.DEFAULT_TYPE | None) -> None: and update.message ): chat = update.effective_chat - message = rf"Ciao {chat.title}! Per iniziare scrivete il nome di una stazione da monitorare (e.g. /Cesena o /S. Carlo)" + message = rf"Ciao {chat.title}! Per iniziare scrivete il nome di una stazione da monitorare (e.g. /Cesena o /S. Carlo) o cercane una con /stazioni" # noqa: E501 await update.message.reply_html(message) @@ -136,6 +139,28 @@ async def cesena(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: ) +async def list_stations(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: + """Send a message when the command /cesena is issued.""" + if update.message: + await update.message.reply_html("\n".join(KNOWN_STATIONS)) + + +async def info(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None: + """Send a message when the command /cesena is issued.""" + message = cleandoc( + """ + Bot Telegram che permette di leggere i livelli idrometrici dei fiumi dell'Emilia Romagna. + I dati idrometrici sono ottenuti dalle API messe a disposizione da allertameteo.regione.emilia-romagna.it. + Il progetto è completamente open-source (https://github.com/notdodo/erfiume_bot). + Per donazioni per mantenere il servizio attivo: buymeacoffee.com/d0d0 + + Inizia con /start o /stazioni + """ + ) + if update.message: + await update.message.reply_html(message, disable_web_page_preview=True) + + async def handle_private_message( update: Update, context: ContextTypes.DEFAULT_TYPE ) -> None: @@ -146,16 +171,22 @@ async def handle_private_message( message = cleandoc( """Stazione non trovata! Inserisci esattamente il nome che vedi dalla pagina https://allertameteo.regione.emilia-romagna.it/livello-idrometrico - Ad esempio 'Cesena', 'Lavino di Sopra' o 'S. Carlo'""" + Ad esempio 'Cesena', 'Lavino di Sopra' o 'S. Carlo'. + Se non sai quale cercare prova con /stazioni""" ) if update.message and update.effective_chat and update.message.text: - logger.info("Received private message: %s", update.message.text) async with AsyncDynamoDB(table_name="Stazioni") as dynamo: - stazione = await dynamo.get_matching_station( - update.message.text.replace("/", "").strip() + query = update.message.text.replace("/", "").strip() + fuzzy_query = process.extractOne( + query, KNOWN_STATIONS, score_cutoff=FUZZ_SCORE_CUTOFF ) - if stazione and update.message: - message = stazione.create_station_message() + logger.info(query) + if fuzzy_query: + stazione = await dynamo.get_matching_station(fuzzy_query[0]) + if stazione and update.message: + message = stazione.create_station_message() + if query != fuzzy_query[0]: + message += "\nSe non è la stazione corretta prova ad affinare la ricerca." await context.bot.send_message( chat_id=update.effective_chat.id, text=message, @@ -173,16 +204,24 @@ async def handle_group_message( message = cleandoc( """Stazione non trovata! Inserisci esattamente il nome che vedi dalla pagina https://allertameteo.regione.emilia-romagna.it/livello-idrometrico - Ad esempio '/Cesena', '/Lavino di Sopra' o '/S. Carlo'""" + Ad esempio '/Cesena', '/Lavino di Sopra' o '/S. Carlo'. + Se non sai quale cercare prova con /stazioni""" ) if update.message and update.effective_chat and update.message.text: - logger.info("Received group message: %s", update.message.text) async with AsyncDynamoDB(table_name="Stazioni") as dynamo: - stazione = await dynamo.get_matching_station( + query = ( update.message.text.replace("/", "").replace("erfiume_bot", "").strip() ) - if stazione and update.message: - message = stazione.create_station_message() + fuzzy_query = process.extractOne( + query, KNOWN_STATIONS, score_cutoff=FUZZ_SCORE_CUTOFF + ) + logger.info(query) + if fuzzy_query: + stazione = await dynamo.get_matching_station(fuzzy_query[0]) + if stazione and update.message: + message = stazione.create_station_message() + if query != fuzzy_query[0]: + message += "\nSe non é la stazione corretta prova ad affinare la ricerca." await context.bot.send_message( chat_id=update.effective_chat.id, text=message, @@ -199,6 +238,8 @@ async def bot(event: dict[str, Any], _context: LambdaContext) -> None: application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("cesena", cesena)) + application.add_handler(CommandHandler("stazioni", list_stations)) + application.add_handler(CommandHandler("info", info)) application.add_handler( MessageHandler( filters.ChatType.PRIVATE & (filters.TEXT | filters.COMMAND), diff --git a/app/erfiume_bot.py b/app/erfiume_bot.py index 100edb2..8f9d4d6 100644 --- a/app/erfiume_bot.py +++ b/app/erfiume_bot.py @@ -25,5 +25,4 @@ def handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]: logger.exception(traceback.format_exc()) return {"statusCode": 501} - logger.info("Successfully processed event") return {"statusCode": 200} diff --git a/app/poetry.lock b/app/poetry.lock index f84d17a..6b430a4 100644 --- a/app/poetry.lock +++ b/app/poetry.lock @@ -2333,6 +2333,20 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "thefuzz" +version = "0.22.1" +description = "Fuzzy string matching in python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481"}, + {file = "thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680"}, +] + +[package.dependencies] +rapidfuzz = ">=3.0.0,<4.0.0" + [[package]] name = "tomlkit" version = "0.13.2" @@ -3520,4 +3534,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "bafd51aacf0a900831407dee4da7a10e7469a7ddfb30c41fa7beaafc9420fac9" +content-hash = "9bb664df4cccdb34bb5af6322b7302ec83748dc5b62911e61894efce2f60e550" diff --git a/app/pyproject.toml b/app/pyproject.toml index 8c30c01..57b2b17 100644 --- a/app/pyproject.toml +++ b/app/pyproject.toml @@ -13,6 +13,7 @@ aioboto3 = "^13.1.1" poetry-dotenv-plugin = "^0.2.0" python = "^3.12" python-telegram-bot = "^21.5" +thefuzz = "^0.22.1" [tool.poetry.group.dev.dependencies] awscli-local = "^0.22.0"