Skip to content

Commit

Permalink
enh: handle group membership (#23)
Browse files Browse the repository at this point in the history
* enh: handle group membership

* udp: update ghas
  • Loading branch information
notdodo authored Sep 24, 2024
1 parent 70902f1 commit e6f29b1
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 105 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ concurrency:

jobs:
python-ci-app:
uses: notdodo/github-actions/.github/workflows/[email protected].2
uses: notdodo/github-actions/.github/workflows/[email protected].3
with:
poetry-version: latest
python-version: 3.12
working-directory: "./app"

python-ci-pulumi:
uses: notdodo/github-actions/.github/workflows/[email protected].2
uses: notdodo/github-actions/.github/workflows/[email protected].3
with:
poetry-version: latest
python-version: 3.12
Expand Down
38 changes: 38 additions & 0 deletions app/erfiume/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

import asyncio
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from inspect import cleandoc

import httpx
from zoneinfo import ZoneInfo

from .logging import logger

Expand Down Expand Up @@ -50,6 +53,41 @@ def to_dict(self) -> dict[str, str | Decimal | int]:
"value": Decimal(str(self.value)),
}

def create_station_message(self) -> str:
"""
Create and format the answer from the bot.
"""
timestamp = (
datetime.fromtimestamp(
int(self.timestamp) / 1000, tz=ZoneInfo("Europe/Rome")
)
.replace(tzinfo=None)
.strftime("%d-%m-%Y %H:%M")
)
value = float(self.value) # type: ignore [arg-type]
yellow = self.soglia1
orange = self.soglia2
red = self.soglia3
alarm = "🔴"
if value <= yellow:
alarm = "🟢"
elif value > yellow and value <= orange:
alarm = "🟡"
elif value >= orange and value <= red:
alarm = "🟠"

if value == UNKNOWN_VALUE:
value = "non disponibile" # type: ignore[assignment]
alarm = ""
return cleandoc(
f"""Stazione: {self.nomestaz}
Valore: {value!r} {alarm}
Soglia Gialla: {yellow}
Soglia Arancione: {orange}
Soglia Rossa: {red}
Ultimo rilevamento: {timestamp}"""
)


@dataclass
class Valore:
Expand Down
146 changes: 97 additions & 49 deletions app/erfiume/tgbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

import json
from datetime import datetime
import random
from inspect import cleandoc
from typing import TYPE_CHECKING, Any

Expand All @@ -17,69 +17,110 @@
MessageHandler,
filters,
)
from zoneinfo import ZoneInfo

if TYPE_CHECKING:
from .apis import Stazione
from aws_lambda_powertools.utilities.typing import LambdaContext

from aws_lambda_powertools.utilities import parameters

from .logging import logger
from .storage import AsyncDynamoDB

UNKNOWN_VALUE = -9999.0
RANDOM_SEND_LINK = 10


# UTILS
async def fetch_bot_token() -> str:
"""
Fetch the Telegram Bot token from AWS SM
"""
return parameters.get_secret("telegram-bot-token")


async def start(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
"""Send a message when the command /start is issued."""
user = update.effective_user
if update.message and user:
await update.message.reply_html(
rf"Ciao {user.mention_html()}! Scrivi il nome di una stazione da monitorare per iniziare (e.g. <b>Cesena</b>)",
)
def is_from_group(update: Update) -> bool:
"""Check if the update is from a group."""
chat = update.effective_chat
return chat is not None and chat.type in [
"group",
"supergroup",
]


def is_from_user(update: Update) -> bool:
"""Check if the update is from a real user."""
return update.effective_user is not None


def create_station_message(station: Stazione) -> str:
def is_from_private_chat(update: Update) -> bool:
"""Check if the update is from a private chat with the bot."""
return update.effective_chat is not None and update.effective_chat.type == "private"


def has_joined_group(update: Update) -> bool:
"""
Create and format the answer from the bot.
Handle event when the bot is add to a group chat
"""
timestamp = (
datetime.fromtimestamp(
int(station.timestamp) / 1000, tz=ZoneInfo("Europe/Rome")
if is_from_group(update) and update.message and update.effective_chat:
for new_user in update.message.new_chat_members:
if new_user.username == "erfiume_bot":
return True
return False


async def send_donation_link(
update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
"""Randomnly send a donation link."""
if random.randint(1, 10) == RANDOM_SEND_LINK and update.effective_chat: # noqa: S311
message = """Contribuisci al progetto per mantenerlo attivo e sviluppare nuove funzionalità tramite una donazione: https://buymeacoffee.com/d0d0"""
await context.bot.send_message(
chat_id=update.effective_chat.id,
text=message,
)
.replace(tzinfo=None)
.strftime("%d-%m-%Y %H:%M")
)
value = float(station.value) # type: ignore [arg-type]
yellow = station.soglia1
orange = station.soglia2
red = station.soglia3
alarm = "🔴"
if value <= yellow:
alarm = "🟢"
elif value > yellow and value <= orange:
alarm = "🟡"
elif value >= orange and value <= red:
alarm = "🟠"

if value == UNKNOWN_VALUE:
value = "non disponibile" # type: ignore[assignment]
alarm = ""
return cleandoc(
f"""Stazione: {station.nomestaz}
Valore: {value!r} {alarm}
Soglia Gialla: {yellow}
Soglia Arancione: {orange}
Soglia Rossa: {red}
Ultimo rilevamento: {timestamp}"""
)


async def send_project_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Randomnly send a link to the GitHub repository."""
if random.randint(1, 50) == RANDOM_SEND_LINK and update.effective_chat: # noqa: S311
message = """Esplora o contribuisci al progetto open-source per sviluppare nuove funzionalità: https://github.com/notdodo/erfiume_bot"""
await context.bot.send_message(
chat_id=update.effective_chat.id,
text=message,
)


async def send_random_messages(
update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
"""Handle the send of random messages."""
await send_donation_link(update, context)
await send_project_link(update, context)


# END UTILS


# HANDLERS
async def start(update: Update, _: ContextTypes.DEFAULT_TYPE | None) -> None:
"""Send a message when the command /start is issued."""
if (
is_from_user(update)
and is_from_private_chat(update)
and update.effective_user
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. <b>Cesena</b> o <b>/S. Carlo</b>)"
await update.message.reply_html(message)
elif (
is_from_user(update)
and is_from_group(update)
and update.effective_chat
and update.message
):
chat = update.effective_chat
message = rf"Ciao {chat.title}! Per iniziare scrivete il nome di una stazione da monitorare (e.g. <b>/Cesena</b> o <b>/S. Carlo</b>)"
await update.message.reply_html(message)


async def cesena(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
Expand All @@ -88,7 +129,7 @@ async def cesena(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
stazione = await dynamo.get_matching_station("Cesena")
if stazione:
if update.message:
await update.message.reply_html(create_station_message(stazione))
await update.message.reply_html(stazione.create_station_message())
elif update.message:
await update.message.reply_html(
"Nessun stazione trovata!",
Expand All @@ -99,7 +140,7 @@ async def handle_private_message(
update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
"""
Handle messages writte from private chat to match a specific station
Handle messages written from private chat to match a specific station
"""

message = cleandoc(
Expand All @@ -114,11 +155,12 @@ async def handle_private_message(
update.message.text.replace("/", "").strip()
)
if stazione and update.message:
message = create_station_message(stazione)
message = stazione.create_station_message()
await context.bot.send_message(
chat_id=update.effective_chat.id,
text=message,
)
await send_random_messages(update, context)


async def handle_group_message(
Expand All @@ -131,7 +173,7 @@ 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'"""
)
if update.message and update.effective_chat and update.message.text:
logger.info("Received group message: %s", update.message.text)
Expand All @@ -140,14 +182,18 @@ async def handle_group_message(
update.message.text.replace("/", "").replace("erfiume_bot", "").strip()
)
if stazione and update.message:
message = create_station_message(stazione)
message = stazione.create_station_message()
await context.bot.send_message(
chat_id=update.effective_chat.id,
text=message,
)
await send_random_messages(update, context)


# END HANDLERS


async def bot(event: dict[str, Any]) -> None:
async def bot(event: dict[str, Any], _context: LambdaContext) -> None:
"""Run entry point for the bot"""
application = Application.builder().token(await fetch_bot_token()).build()

Expand All @@ -162,7 +208,7 @@ async def bot(event: dict[str, Any]) -> None:
application.add_handler(
MessageHandler(
(filters.ChatType.SUPERGROUP | filters.ChatType.GROUP)
& (filters.COMMAND | filters.Regex("@erfiume_bot")),
& (filters.COMMAND | filters.Mention("erfiume_bot")),
handle_group_message,
)
)
Expand All @@ -172,4 +218,6 @@ async def bot(event: dict[str, Any]) -> None:
update_dict = json.loads(event["body"])
async with application:
update = Update.de_json(update_dict, application.bot)
if update and has_joined_group(update):
await start(update, None)
await application.process_update(update)
4 changes: 2 additions & 2 deletions app/erfiume_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@


@logger.inject_lambda_context
def handler(event: dict[str, Any], _context: LambdaContext) -> dict[str, Any]:
def handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]:
"""Run entry point for the bot."""
logger.info("Received event: %s", event)
try:
asyncio.run(bot(event))
asyncio.run(bot(event, context))
except Exception as e: # noqa: BLE001
logger.exception("An error occurred: %s", e)
logger.exception(traceback.format_exc())
Expand Down
51 changes: 0 additions & 51 deletions app/standalone.py

This file was deleted.

1 change: 0 additions & 1 deletion pulumi/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,6 @@
react_on=[
"message",
"inline_query",
"my_chat_member",
],
url=f"https://{CUSTOM_DOMAIN_NAME}/erfiume_bot",
)

0 comments on commit e6f29b1

Please sign in to comment.