diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000..019da46 --- /dev/null +++ b/logging.conf @@ -0,0 +1,30 @@ +[loggers] +keys=root + +[handlers] +keys=stream,file + +[logger_root] +handlers=stream,file +level=NOTSET + +[formatters] +keys=simple,complex + +[formatter_simple] +format=%(asctime)s - %(levelname)-8s > %(message)s + +[formatter_complex] +format=%(asctime)s - %(levelname)-8s > [file = %(filename)s ; func = %(funcName)s ; line = %(lineno)d] > %(message)s + +[handler_stream] +class=StreamHandler +level=INFO +formatter=simple +args=(sys.stdout,) + +[handler_file] +class=FileHandler +level=DEBUG +formatter=complex +args=('user_bot.log',) diff --git a/main.py b/main.py index 8aeeccc..9c217c3 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,8 @@ """ import asyncio +import logging +import logging.config from configparser import ConfigParser, ExtendedInterpolation from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -15,13 +17,25 @@ from src import worker +logging.config.fileConfig("logging.conf") + +# Hidding non-critical logs from other modules +logging.getLogger("telethon").setLevel(logging.CRITICAL) +logging.getLogger("apscheduler").setLevel(logging.CRITICAL) + +logging.info("User bot is connecting...") + +logging.debug("Trying to read the configuration file...") config = ConfigParser(interpolation=ExtendedInterpolation()) config.read("config.ini") +logging.debug("The configuration file was read successfully.") +logging.debug("Trying to connect the user bot client to Telegram...") client = TelegramClient( "src/data/bot", config.get("Telegram", "api_id"), config.get("Telegram", "api_hash") ).start() -print("Telegram user bot is now running...") +logging.debug("User bot client connected to Telegram successfully.") +logging.info("User bot connected!") @client.on(events.NewMessage("me", pattern="/health")) @@ -38,7 +52,18 @@ async def handle_health(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the reply. """ - await event.reply(worker.health()) + logging.debug("Running method `handle_health`...") + try: + health_status = worker.health() + logging.info("Sending health status response: %s", health_status) + await event.reply(health_status) + logging.info("Health status sent successfully.") + + except Exception as e: + logging.error("Error while sending health status response: %s | event = %s", e, event) + raise e + + logging.debug("Method `handle_health` finished.") @client.on(events.NewMessage("me", pattern="/greeting_info")) @@ -55,7 +80,19 @@ async def handle_greeting_info(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the reply. """ - await event.reply(f"Next greeting at {worker.next_greeting_time}") + logging.debug("Running method `handle_greeting_info`...") + try: + next_greeting_time = worker.next_greeting_time + logging.info("Next greeting time retrieved: %s", next_greeting_time) + + await event.reply(f"Next greeting at {next_greeting_time}") + logging.info("Greeting info sent successfully.") + + except Exception as e: + logging.error("Error while sending greeting info response: %s | event = %s", e, event) + raise e + + logging.debug("Method `handle_greeting_info` finished.") @client.on(events.NewMessage("me", pattern="/send_greeting")) @@ -72,8 +109,20 @@ async def handle_send_greeting(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the greeting or the reply. """ - await worker.send_morning_greeting(client) - await event.reply("Done!") + logging.debug("Running method `handle_send_greeting`...") + try: + logging.info("Triggering the sending of the morning greeting...") + await worker.send_morning_greeting(client) + logging.info("Morning greeting sent successfully.") + + await event.reply("Done!") + logging.info("Confirmation reply sent to user.") + + except Exception as e: + logging.error("Error while sending morning greeting or reply: %s | event = %s", e, event) + raise e + + logging.debug("Method `handle_send_greeting` finished.") @client.on(events.NewMessage("me", pattern="/test_greeting")) @@ -90,8 +139,22 @@ async def handle_test_greeting(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the greeting or the reply. """ - await worker.send_morning_greeting(client, user_id="me", set_as_used=False) - await event.reply("Done!") + logging.debug("Running method `handle_test_greeting`...") + try: + logging.info("Triggering the sending of a test morning greeting...") + await worker.send_morning_greeting(client, user_id="me", set_as_used=False) + logging.info("Test morning greeting sent successfully.") + + await event.reply("Done!") + logging.info("Confirmation reply sent to user.") + + except Exception as e: + logging.error( + "Error while sending test morning greeting or reply: %s | event = %s", e, event + ) + raise e + + logging.debug("Method `handle_test_greeting` finished.") @client.on(events.NewMessage("me", pattern="/afternoon_media_info")) @@ -108,7 +171,21 @@ async def handle_afternoon_media(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the reply. """ - await event.reply(f"Next greeting at {worker.next_afternoon_media_time}") + logging.debug("Running method `handle_afternoon_media`...") + try: + next_afternoon_media_time = worker.next_afternoon_media_time + logging.info("Next afternoon media time retrieved: %s", next_afternoon_media_time) + + await event.reply(f"Next media at {next_afternoon_media_time}") + logging.info("Afternoon media info sent successfully.") + + except Exception as e: + logging.error( + "Error while sending afternoon media info response: %s | event = %s", e, event + ) + raise e + + logging.debug("Method `handle_afternoon_media` finished.") @client.on(events.NewMessage("me", pattern="/send_afternoon_media")) @@ -125,8 +202,20 @@ async def handle_send_afternoon_media(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the media or the reply. """ - await worker.send_afternoon_media(client) - await event.reply("Done!") + logging.debug("Running method `handle_send_afternoon_media`...") + try: + logging.info("Triggering the sending of the afternoon media item...") + await worker.send_afternoon_media(client) + logging.info("Afternoon media sent successfully.") + + await event.reply("Done!") + logging.info("Confirmation reply sent to user.") + + except Exception as e: + logging.error("Error while sending afternoon media or reply: %s | event = %s", e, event) + raise e + + logging.debug("Method `handle_send_afternoon_media` finished.") @client.on(events.NewMessage("me", pattern="/test_afternoon_media")) @@ -143,8 +232,22 @@ async def handle_test_afternoon_media(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the media or the reply. """ - await worker.send_afternoon_media(client, user_id="me", set_as_used=False) - await event.reply("Done!") + logging.debug("Running method `handle_test_afternoon_media`...") + try: + logging.info("Triggering the sending of a test afternoon media item...") + await worker.send_afternoon_media(client, user_id="me", set_as_used=False) + logging.info("Test afternoon media sent successfully.") + + await event.reply("Done!") + logging.info("Confirmation reply sent to user.") + + except Exception as e: + logging.error( + "Error while sending test afternoon media or reply: %ss | event = %s", e, event + ) + raise e + + logging.debug("Method `handle_test_afternoon_media` finished.") @client.on(events.NewMessage("me", pattern="/stats")) @@ -161,7 +264,17 @@ async def handle_stats(event: Message | events.NewMessage): Raises: Exception: If there is an error while sending the statistics. """ - await worker.send_stats(client, user_id="me") + logging.debug("Running method `handle_stats`...") + try: + logging.info("Triggering the sending of application statistics...") + await worker.send_stats(client, user_id="me") + logging.info("Statistics sent successfully.") + + except Exception as e: + logging.error("Error while sending statistics: %ss | event = %s", e, event) + raise e + + logging.debug("Method `handle_stats` finished.") async def main(): @@ -171,11 +284,32 @@ async def main(): morning greetings, afternoon media, and pill reminders. It then enters an infinite loop to keep the application running and responsive. """ + logging.info("Starting the scheduler...") + logging.debug("Initializing the scheduler...") scheduler = AsyncIOScheduler() + logging.debug("Scheduler initialized successfully.") + + logging.debug("Scheduling morning greeting task...") worker.start_sending_morning_greeting(scheduler, client, try_today=True) + logging.debug("Morning greeting task scheduled successfully.") + + logging.debug("Scheduling afternoon media task...") worker.start_sending_afternoon_media(scheduler, client, try_today=True) + logging.debug("Afternoon media task scheduled successfully.") + + logging.debug("Scheduling pill reminder task...") worker.start_sending_pills_reminder(scheduler, client) + logging.debug("Pill reminder task scheduled successfully.") + + logging.debug("Setting up handler to stop pill reminders for today...") + worker.handle_stop_sending_pill_reminder_for_today(client) + logging.debug("Handler to stop pill reminders for today set up successfully.") + + logging.debug("Starting the scheduler...") scheduler.start() + logging.debug("Scheduler started successfully.") + + logging.info("User bot is now running!") while True: await asyncio.sleep(1) diff --git a/src/worker.py b/src/worker.py index 64543c3..f16f0bb 100644 --- a/src/worker.py +++ b/src/worker.py @@ -5,19 +5,25 @@ configuration and data storage, and it integrates with the APScheduler for scheduling tasks. """ +import asyncio import bisect +import logging +import logging.config import random from datetime import datetime, timedelta from pathlib import Path import yaml from apscheduler.schedulers.asyncio import AsyncIOScheduler -from telethon import TelegramClient +from telethon import TelegramClient, events +from telethon.tl.custom.message import Message from telethon.tl.types import InputDocument from src.sentence_generator import morning from src.utils.random import random_time +logging.config.fileConfig("logging.conf") + DATA_PATH = Path("src/data") MEDIA_PATH = DATA_PATH / "media" MEDIA_YAML_PATH = DATA_PATH / "media.yaml" @@ -27,6 +33,7 @@ next_greeting_time: datetime = datetime.now() next_afternoon_media_time: datetime = datetime.now() +keep_sending_pill_reminder = False class CustomYamlDumper(yaml.Dumper): @@ -37,7 +44,7 @@ class CustomYamlDumper(yaml.Dumper): `increase_indent` method to customize the indentation settings. """ - def increase_indent(self, flow=False, *args, **kwargs): + def increase_indent(self, flow: bool = False, *args, **kwargs): """Increase the indentation level for YAML output. This method modifies the behavior of indentation to ensure that it is not indentless, @@ -68,8 +75,26 @@ def read_yaml(path: Path, *, encoding: str = "utf-8") -> dict: FileNotFoundError: If the specified YAML file does not exist. yaml.YAMLError: If there is an error parsing the YAML file. """ - with open(path, encoding=encoding) as f: - return yaml.safe_load(f) + logging.debug("Running method `read_yaml`...") + try: + with open(path, encoding=encoding) as f: + data = yaml.safe_load(f) + logging.info("YAML file read successfully.") + return data + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", path) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file at %s: %s", path, e) + raise e + + except Exception as e: + logging.error("Error while reading YAML file at %s: %s", path, e) + raise e + + logging.debug("Method `read_yaml` finished.") def save_yaml(data: dict, path: Path, *, encoding: str = "utf-8"): @@ -84,11 +109,20 @@ def save_yaml(data: dict, path: Path, *, encoding: str = "utf-8"): encoding (str, optional): The character encoding to use when writing the file. Defaults to "utf-8". """ - with open(path, "w", encoding=encoding) as f: - yaml.dump(data, f, Dumper=CustomYamlDumper) + logging.debug("Running method `save_yaml`...") + try: + with open(path, "w", encoding=encoding) as f: + yaml.dump(data, f, Dumper=CustomYamlDumper) + logging.info("Data saved successfully to %s", path) + + except Exception as e: + logging.error("Error while saving data to YAML file at %s: %s", path, e) + raise e + logging.debug("Method `save_yaml` finished.") -def filter_by_register(data: dict | list, register: list["int | str"], *, key="uid") -> list: + +def filter_by_register(data: dict | list, register: list["int | str"], *, key: str = "uid") -> list: """Filters the data to exclude items present in the register. This function checks the provided data against a list of identifiers in the register and @@ -104,8 +138,14 @@ def filter_by_register(data: dict | list, register: list["int | str"], *, key="u Returns: list: A list of items from the data that are not present in the register. """ + logging.debug("Running method `filter_by_register`...") + logging.info( + "Starting filtering process with data %s, register %s and key %s.", data, register, key + ) _register = set(register) - return [d for d in data if (d if key is None else d[key]) not in _register] + filtered_data = [d for d in data if (d if key is None else d[key]) not in _register] + logging.debug("Method `filter_by_register` finished.") + return filtered_data def text_to_timedelta(text: str) -> timedelta: @@ -124,11 +164,25 @@ def text_to_timedelta(text: str) -> timedelta: ValueError: If the input string is not in the expected format or cannot be converted to integers. """ - _time: list[int] = [int(x) for x in text.split(":")] - return timedelta(hours=_time[0], minutes=_time[1], seconds=_time[2]) + logging.debug("Running method `text_to_timedelta`...") + try: + _time: list[int] = [int(x) for x in text.split(":")] + if len(_time) != 3: + logging.error("Input must be in the format 'HH:MM:SS'. Input: %s", text) + raise ValueError("Input must be in the format 'HH:MM:SS'") + result = timedelta(hours=_time[0], minutes=_time[1], seconds=_time[2]) + logging.info("Conversion successful: %s", result) + return result -def get_next_time(tm: timedelta, timespan: timedelta, try_now=False) -> datetime: + except ValueError as e: + logging.error("Error converting text to timedelta: %s", e) + raise e + + logging.debug("Method `text_to_timedelta` finished.") + + +def get_next_time(tm: timedelta, timespan: timedelta, try_now: bool = False) -> datetime: """Calculates the next occurrence of a specified time based on the current time. This function determines the next datetime by adding a specified time duration to the current @@ -144,6 +198,10 @@ def get_next_time(tm: timedelta, timespan: timedelta, try_now=False) -> datetime Returns: datetime: The next occurrence of the specified time as a datetime object. """ + logging.debug("Running method `get_next_time`...") + logging.info( + "Calculating next time with tm: %s, timespan: %s, try_now: %s", tm, timespan, try_now + ) now = datetime.now() if not try_now: now += timespan @@ -157,9 +215,14 @@ def get_next_time(tm: timedelta, timespan: timedelta, try_now=False) -> datetime second=tm.seconds % 60, ) + logging.debug("Calculated datetime before adjustment: %s", dt) + if dt < datetime.now(): + logging.debug("Calculated time is in the past. Adding timespan: %s", timespan) dt += timespan + logging.info("Next occurrence of time is: %s", dt) + logging.debug("Method `get_next_time` finished.") return dt @@ -172,7 +235,11 @@ def health() -> str: Returns: str: A message indicating that the application is alive. """ - return "Alive" + logging.debug("Running method `health`...") + status = "Alive" + logging.info("Application health status: %s", status) + logging.debug("Method `health` finished.") + return status def get_morning_greeting() -> str: @@ -184,7 +251,12 @@ def get_morning_greeting() -> str: Returns: str: A morning greeting message. """ - return morning.get_morning_greeting() + logging.debug("Running method `get_morning_greeting`...") + logging.info("Retrieving morning greeting message...") + greeting = morning.get_morning_greeting() + logging.info("Morning greeting retrieved: %s", greeting) + logging.debug("Method `get_morning_greeting` finished.") + return greeting def get_morning_sticker() -> dict: @@ -195,22 +267,43 @@ def get_morning_sticker() -> dict: for use by encoding its file reference. Returns: - dict: A dictionary representing the selected morning sticker, - including its file reference and other associated data. + dict: A dictionary representing the selected morning sticker, including its file reference + and other associated data. Raises: FileNotFoundError: If the specified YAML files cannot be found. yaml.YAMLError: If there is an error parsing the YAML files. ValueError: If there are no available morning stickers to choose from. """ - morning_stickers_sent: list = read_yaml(REGISTER_YAML_PATH).get("morning_stickers", []) - morning_stickers: list = filter_by_register( - data=read_yaml(STICKERS_YAML_PATH, encoding="latin-1")["morning_stickers"], - register=morning_stickers_sent, - ) - morning_sticker: dict = random.choice(morning_stickers) - morning_sticker["file_reference"] = morning_sticker["file_reference"].encode("latin-1") - return morning_sticker + logging.debug("Running method `get_morning_sticker`...") + try: + logging.info("Retrieving morning stickers...") + morning_stickers_sent: list = read_yaml(REGISTER_YAML_PATH).get("morning_stickers", []) + logging.debug("Sent morning stickers: %s", morning_stickers_sent) + + morning_stickers: list = filter_by_register( + data=read_yaml(STICKERS_YAML_PATH, encoding="latin-1")["morning_stickers"], + register=morning_stickers_sent, + ) + logging.debug("Available morning stickers after filtering: %d", len(morning_stickers)) + + if not morning_stickers: + logging.error("No available morning stickers to choose from.") + raise ValueError("No available morning stickers to choose from.") + + morning_sticker: dict = random.choice(morning_stickers) + morning_sticker["file_reference"] = morning_sticker["file_reference"].encode("latin-1") + logging.info("Selected morning sticker: %s", morning_sticker) + logging.debug("Method `get_morning_sticker` finished.") + return morning_sticker + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e def get_morning_media() -> dict: @@ -228,12 +321,34 @@ def get_morning_media() -> dict: yaml.YAMLError: If there is an error parsing the YAML files. ValueError: If there are no available morning media items to choose from. """ - morning_media_sent = read_yaml(REGISTER_YAML_PATH).get("morning_media", []) - morning_media = filter_by_register( - data=read_yaml(MEDIA_YAML_PATH)["morning_media"], - register=morning_media_sent, - ) - return random.choice(morning_media) + logging.debug("Running method `get_morning_media`...") + try: + logging.info("Retrieving morning media items...") + morning_media_sent = read_yaml(REGISTER_YAML_PATH).get("morning_media", []) + logging.debug("Sent morning media items: %s", morning_media_sent) + + morning_media = filter_by_register( + data=read_yaml(MEDIA_YAML_PATH)["morning_media"], + register=morning_media_sent, + ) + logging.debug("Available morning media items after filtering: %d", len(morning_media)) + + if not morning_media: + logging.error("No available morning media items to choose from.") + raise ValueError("No available morning media items to choose from.") + + selected_media = random.choice(morning_media) + logging.info("Selected morning media item: %s", selected_media) + logging.debug("Method `get_morning_media` finished.") + return selected_media + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e def get_afternoon_media() -> dict: @@ -251,12 +366,34 @@ def get_afternoon_media() -> dict: yaml.YAMLError: If there is an error parsing the YAML files. ValueError: If there are no available afternoon media items to choose from. """ - afternoon_media_sent = read_yaml(REGISTER_YAML_PATH).get("afternoon_media", []) - afternoon_media = filter_by_register( - data=read_yaml(MEDIA_YAML_PATH)["afternoon_media"], - register=afternoon_media_sent, - ) - return random.choice(afternoon_media) + logging.debug("Running method `get_afternoon_media`...") + try: + logging.info("Retrieving afternoon media items...") + afternoon_media_sent = read_yaml(REGISTER_YAML_PATH).get("afternoon_media", []) + logging.debug("Sent afternoon media items: %s", afternoon_media_sent) + + afternoon_media = filter_by_register( + data=read_yaml(MEDIA_YAML_PATH)["afternoon_media"], + register=afternoon_media_sent, + ) + logging.debug("Available afternoon media items after filtering: %d", len(afternoon_media)) + + if not afternoon_media: + logging.error("No available afternoon media items to choose from.") + raise ValueError("No available afternoon media items to choose from.") + + selected_media = random.choice(afternoon_media) + logging.info("Selected afternoon media item: %s", selected_media) + logging.debug("Method `get_afternoon_media` finished.") + return selected_media + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e def set_as_used(entry: str, uid: int, db_yaml: Path): @@ -275,14 +412,36 @@ def set_as_used(entry: str, uid: int, db_yaml: Path): FileNotFoundError: If the specified YAML files cannot be found. yaml.YAMLError: If there is an error parsing the YAML files. """ - register = read_yaml(REGISTER_YAML_PATH) - if uid not in register[entry]: - bisect.insort(register[entry], uid) - data = read_yaml(db_yaml) - if len(register[entry]) == len(data[entry]): - register[entry] = [] + logging.debug("Running method `set_as_used`...") + try: + logging.info("Marking entry '%s' as used for UID: %d", entry, uid) + register = read_yaml(REGISTER_YAML_PATH) + logging.debug("Current register before update: %s", register) + + if uid not in register[entry]: + bisect.insort(register[entry], uid) + logging.info("UID %d added to register for entry '%s'.", uid, entry) + + data = read_yaml(db_yaml) + if len(register[entry]) == len(data[entry]): + register[entry] = [] + logging.info("All entries for '%s' have been used. Clearing the register.", entry) + + save_yaml(register, REGISTER_YAML_PATH) + logging.info("Register updated and saved successfully.") - save_yaml(register, REGISTER_YAML_PATH) + else: + logging.info("UID %d is already marked as used for entry '%s'.", uid, entry) + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e + + logging.debug("Method `set_as_used` finished.") def set_morning_sticker_as_used(uid: int): @@ -299,10 +458,24 @@ def set_morning_sticker_as_used(uid: int): FileNotFoundError: If the specified YAML files cannot be found. yaml.YAMLError: If there is an error parsing the YAML files. """ - set_as_used("morning_stickers", uid, STICKERS_YAML_PATH) + logging.debug("Running method `set_morning_sticker_as_used`...") + try: + logging.info("Marking morning sticker as used for UID: %d", uid) + set_as_used("morning_stickers", uid, STICKERS_YAML_PATH) + logging.info("Morning sticker marked as used for UID: %d", uid) + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e -def set_morning_media_as_used(uid): + logging.debug("Method `set_morning_sticker_as_used` finished.") + + +def set_morning_media_as_used(uid: int): """Marks a morning media item as used for a specified unique ID. This function updates the register to indicate that the morning media item has been used by the @@ -316,10 +489,24 @@ def set_morning_media_as_used(uid): FileNotFoundError: If the specified YAML files cannot be found. yaml.YAMLError: If there is an error parsing the YAML files. """ - set_as_used("morning_media", uid, MEDIA_YAML_PATH) + logging.debug("Running method `set_morning_media_as_used`...") + try: + logging.info("Marking morning media as used for UID: %d", uid) + set_as_used("morning_media", uid, MEDIA_YAML_PATH) + logging.info("Morning media marked as used for UID: %d", uid) + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e + logging.debug("Method `set_morning_media_as_used` finished.") -def set_afternoon_media_as_used(uid): + +def set_afternoon_media_as_used(uid: int): """Marks an afternoon media item as used for a specified unique ID. This function updates the register to indicate that the afternoon media item has been used by @@ -333,7 +520,21 @@ def set_afternoon_media_as_used(uid): FileNotFoundError: If the specified YAML files cannot be found. yaml.YAMLError: If there is an error parsing the YAML files. """ - set_as_used("afternoon_media", uid, MEDIA_YAML_PATH) + logging.debug("Running method `set_afternoon_media_as_used`...") + try: + logging.info("Marking afternoon media as used for UID: %d", uid) + set_as_used("afternoon_media", uid, MEDIA_YAML_PATH) + logging.info("Afternoon media marked as used for UID: %d", uid) + + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e + + logging.debug("Method `set_afternoon_media_as_used` finished.") async def send_morning_greeting( @@ -347,8 +548,8 @@ async def send_morning_greeting( Args: client (TelegramClient): The Telegram client used to send messages. - user_id (str, optional): The identifier for the user to whom the greeting is sent. - Defaults to "nathy". + user_id (str, optional): The identifier for the user to whom the greeting is sent. Defaults + to "nathy". set_as_used (bool, optional): Indicates whether to mark the sticker and media as used after sending. Defaults to True. @@ -357,31 +558,64 @@ async def send_morning_greeting( yaml.YAMLError: If there is an error parsing the YAML configuration. Exception: If there is an error sending messages through the Telegram client. """ - tconfig = read_yaml(TELEGRAM_CONFIG_PATH) - user_id = tconfig.get(user_id, {}).get("chat_id") - - msg = get_morning_greeting() - sticker = get_morning_sticker() - media = get_morning_media() - - await client.send_message(user_id, msg) - await client.send_message( - user_id, - file=InputDocument( - id=sticker["id"], - access_hash=sticker["access_hash"], - file_reference=sticker["file_reference"], - ), - ) - await client.send_file(user_id, MEDIA_PATH / media["path"]) + logging.debug("Running method `send_morning_greeting`...") + try: + logging.info("Preparing to send morning greeting to user: %s", user_id) + tconfig = read_yaml(TELEGRAM_CONFIG_PATH) + user_id = tconfig.get(user_id, {}).get("chat_id") + logging.debug("Resolved user ID: %s", user_id) + + msg = get_morning_greeting() + logging.debug("Retrieved morning greeting message: %s", msg) + + sticker = get_morning_sticker() + logging.debug("Retrieved morning sticker: %s", sticker) + + media = get_morning_media() + logging.debug("Retrieved morning media: %s", media) + + await client.send_message(user_id, msg) + logging.debug("Sent morning greeting message to user: %s", user_id) + + await client.send_message( + user_id, + file=InputDocument( + id=sticker["id"], + access_hash=sticker["access_hash"], + file_reference=sticker["file_reference"], + ), + ) + logging.debug("Sent morning sticker to user: %s", user_id) + + await client.send_file(user_id, MEDIA_PATH / media["path"]) + logging.debug("Sent morning media to user: %s", user_id) + logging.info("Morning greeting sent to user") + + if set_as_used: + set_morning_sticker_as_used(sticker["uid"]) + set_morning_media_as_used(media["uid"]) + logging.debug("Marked sticker and media as used.") + + except FileNotFoundError as e: + logging.error("Configuration or media file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML configuration: %s", e) + raise e + + except Exception as e: + logging.error("Error sending messages through Telegram client: %s", e) + raise e - if set_as_used: - set_morning_sticker_as_used(sticker["uid"]) - set_morning_media_as_used(media["uid"]) + logging.debug("Method `send_morning_greeting` finished.") def start_sending_morning_greeting( - scheduler: AsyncIOScheduler, client: TelegramClient, try_today: bool = False + scheduler: AsyncIOScheduler, + client: TelegramClient, + user_id: str = "nathy", + try_today: bool = False, ): """Schedules the sending of morning greeting messages via Telegram. @@ -392,45 +626,75 @@ def start_sending_morning_greeting( Args: scheduler (AsyncIOScheduler): The scheduler instance used to manage scheduled jobs. client (TelegramClient): The Telegram client used to send messages. + user_id (str, optional): The identifier for the user to whom the greeting is sent. Defaults + to "nathy". try_today (bool, optional): If True, sends the greeting today if the calculated time has not passed. Defaults to False. """ - tconfig = read_yaml(TELEGRAM_CONFIG_PATH) - start_time = text_to_timedelta( - tconfig.get("nathy", {}).get("morning_greeting").get("start_time") - ) - end_time = text_to_timedelta(tconfig.get("nathy", {}).get("morning_greeting").get("end_time")) - tm = random_time(start=start_time, end=end_time) - dt = get_next_time(tm, timedelta(days=1), try_today) + logging.debug("Running method `start_sending_morning_greeting`...") + try: + logging.info("Starting the scheduling of morning greetings for user: %s", user_id) + tconfig = read_yaml(TELEGRAM_CONFIG_PATH) + morning_greeting_time = tconfig.get(user_id, {}).get("morning_greeting", {}) + start_time = text_to_timedelta(morning_greeting_time.get("start_time")) + end_time = text_to_timedelta(morning_greeting_time.get("end_time")) + logging.info("Morning greeting time range: %s - %s", start_time, end_time) - async def wrap(scheduler, client): - """Wraps the process of sending a morning greeting and rescheduling it. + tm = random_time(start=start_time, end=end_time) + logging.debug("Random time selected for morning greeting: %s", tm) - This asynchronous function sends a morning greeting message using the provided Telegram - client and then schedules the next morning greeting using the specified scheduler. It - ensures that the greeting process is repeated at the appropriate time. + dt = get_next_time(tm, timedelta(days=1), try_today) + logging.info("Next greeting scheduled for: %s", dt) - Args: - scheduler: The scheduler instance used to manage the scheduling of jobs. - client: The Telegram client used to send the greeting message. - """ - await send_morning_greeting(client) - start_sending_morning_greeting(scheduler, client) + async def wrap(scheduler: AsyncIOScheduler, client: TelegramClient): + """Wraps the process of sending a morning greeting and rescheduling it. + + This asynchronous function sends a morning greeting message using the provided Telegram + client and then schedules the next morning greeting using the specified scheduler. It + ensures that the greeting process is repeated at the appropriate time. + + Args: + scheduler (AsyncIOScheduler): The scheduler instance used to manage the scheduling + of jobs. + client (TelegramClient): The Telegram client used to send the greeting message. + """ + logging.info("Sending morning greeting...") + await send_morning_greeting(client) + logging.info("Morning greeting sent. Rescheduling next greeting.") + start_sending_morning_greeting(scheduler, client) + + global next_greeting_time + next_greeting_time = dt + logging.debug("Next greeting time set globally: %s", next_greeting_time) + + scheduler.add_job(wrap, "date", run_date=dt, args=[scheduler, client]) + logging.debug("Job added to scheduler for morning greeting at: %s", dt) - global next_greeting_time - next_greeting_time = dt - print(f"Next greeting at {dt}") - scheduler.add_job(wrap, "date", run_date=dt, args=[scheduler, client]) + except FileNotFoundError as e: + logging.error("Configuration file not found: %s", e) + raise e + except yaml.YAMLError as e: + logging.error("Error parsing YAML configuration: %s", e) + raise e -async def send_afternoon_media(client, user_id="nathy", set_as_used=True): + except Exception as e: + logging.error("Error scheduling morning greeting: %s", e) + raise e + + logging.debug("Method `start_sending_morning_greeting` finished.") + + +async def send_afternoon_media( + client: TelegramClient, user_id: str = "nathy", set_as_used: bool = True +): """Sends an afternoon media item to a specified user. This asynchronous function retrieves an afternoon media item and sends it to the specified user via a Telegram client. It can also mark the media item as used if specified. Args: - client: The Telegram client used to send the media. + client (TelegramClient): The Telegram client used to send the media. user_id (str, optional): The identifier for the user to whom the media is sent. Defaults to "nathy". set_as_used (bool, optional): Indicates whether to mark the media item as used after @@ -441,15 +705,44 @@ async def send_afternoon_media(client, user_id="nathy", set_as_used=True): yaml.YAMLError: If there is an error parsing the YAML configuration. Exception: If there is an error sending the media through the Telegram client. """ - tconfig = read_yaml(TELEGRAM_CONFIG_PATH) - user_id = tconfig.get(user_id, {}).get("chat_id") - media = get_afternoon_media() - await client.send_file(user_id, MEDIA_PATH / media["path"]) - if set_as_used: - set_afternoon_media_as_used(media["uid"]) + logging.debug("Running method `send_afternoon_media`...") + try: + logging.info("Preparing to send afternoon media to user: %s", user_id) + tconfig = read_yaml(TELEGRAM_CONFIG_PATH) + user_id = tconfig.get(user_id, {}).get("chat_id") + logging.debug("Resolved user ID: %s", user_id) + + media = get_afternoon_media() + logging.debug("Retrieved afternoon media item: %s", media) + await client.send_file(user_id, MEDIA_PATH / media["path"]) + logging.info("Afternoon media sent to user") -def start_sending_afternoon_media(scheduler, client, try_today=False): + if set_as_used: + set_afternoon_media_as_used(media["uid"]) + logging.debug("Marked afternoon media as used for UID: %s", media["uid"]) + + except FileNotFoundError as e: + logging.error("Configuration or media file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML configuration: %s", e) + raise e + + except Exception as e: + logging.error("Error sending media through Telegram client: %s", e) + raise e + + logging.debug("Method `send_afternoon_media` finished.") + + +def start_sending_afternoon_media( + scheduler: AsyncIOScheduler, + client: TelegramClient, + user_id: str = "nathy", + try_today: bool = False, +): """Schedules the sending of afternoon media messages via Telegram. This function retrieves the configured start and end times for afternoon media, calculates a @@ -457,8 +750,10 @@ def start_sending_afternoon_media(scheduler, client, try_today=False): It can also attempt to send the media for today if specified. Args: - scheduler: The scheduler instance used to manage scheduled jobs. - client: The Telegram client used to send messages. + scheduler (AsyncIOScheduler): The scheduler instance used to manage scheduled jobs. + client (TelegramClient): The Telegram client used to send messages. + user_id (str, optional): The identifier for the user to whom the greeting is sent. Defaults + to "nathy". try_today (bool, optional): If True, sends the media today if the calculated time has not passed. Defaults to False. @@ -466,35 +761,61 @@ def start_sending_afternoon_media(scheduler, client, try_today=False): FileNotFoundError: If the configuration files cannot be found. yaml.YAMLError: If there is an error parsing the YAML configuration. """ - tconfig = read_yaml(TELEGRAM_CONFIG_PATH) - start_time = text_to_timedelta( - tconfig.get("nathy", {}).get("afternoon_media").get("start_time") - ) - end_time = text_to_timedelta(tconfig.get("nathy", {}).get("afternoon_media").get("end_time")) - tm = random_time(start=start_time, end=end_time) - dt = get_next_time(tm, timedelta(days=1), try_today) + logging.debug("Running method `start_sending_afternoon_media`...") + try: + logging.info("Starting the scheduling of afternoon media for user: %s", user_id) + tconfig = read_yaml(TELEGRAM_CONFIG_PATH) + afternoon_media_time = tconfig.get(user_id, {}).get("afternoon_media", {}) + start_time = text_to_timedelta(afternoon_media_time.get("start_time")) + end_time = text_to_timedelta(afternoon_media_time.get("end_time")) + logging.debug("Afternoon media time range: %s - %s", start_time, end_time) - async def wrap(scheduler, client): - """Wraps the process of sending afternoon media and rescheduling it. + tm = random_time(start=start_time, end=end_time) + logging.debug("Random time selected for afternoon media: %s", tm) - This asynchronous function sends an afternoon media item using the provided Telegram client - and then schedules the next afternoon media sending using the specified scheduler. It - ensures that the media sending process is repeated at the appropriate time. + dt = get_next_time(tm, timedelta(days=1), try_today) + logging.info("Next afternoon media scheduled for: %s", dt) - Args: - scheduler: The scheduler instance used to manage the scheduling of jobs. - client: The Telegram client used to send the media item. - """ - await send_afternoon_media(client) - start_sending_afternoon_media(scheduler, client) + async def wrap(scheduler: AsyncIOScheduler, client: TelegramClient): + """Wraps the process of sending afternoon media and rescheduling it. + + This asynchronous function sends an afternoon media item using the provided Telegram client + and then schedules the next afternoon media sending using the specified scheduler. It + ensures that the media sending process is repeated at the appropriate time. + + Args: + scheduler (AsyncIOScheduler): The scheduler instance used to manage the scheduling of + jobs. + client (TelegramClient): The Telegram client used to send the media item. + """ + logging.info("Sending afternoon media...") + await send_afternoon_media(client) + logging.info("Afternoon media sent. Rescheduling next media sending.") + start_sending_afternoon_media(scheduler, client) - global next_afternoon_media_time - next_afternoon_media_time = dt - print(f"Next afternoon media at {dt}") - scheduler.add_job(wrap, "date", run_date=dt, args=[scheduler, client]) + global next_afternoon_media_time + next_afternoon_media_time = dt + logging.debug("Next afternoon media time set globally: %s", next_afternoon_media_time) + scheduler.add_job(wrap, "date", run_date=dt, args=[scheduler, client]) + logging.debug("Job added to scheduler for afternoon media at: %s", dt) -async def send_stats(client, user_id): + except FileNotFoundError as e: + logging.error("Configuration file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML configuration: %s", e) + raise e + + except Exception as e: + logging.error("Error scheduling afternoon media: %s", e) + raise e + + logging.debug("Method `start_sending_afternoon_media` finished.") + + +async def send_stats(client: TelegramClient, user_id: str): """Sends a message containing the remaining media items to a specified user. This asynchronous function retrieves the current state of media items and the register, @@ -502,55 +823,173 @@ async def send_stats(client, user_id): specified user via the Telegram client. Args: - client: The Telegram client used to send the message. - user_id: The identifier for the user to whom the message is sent. + client (TelegramClient): The Telegram client used to send the message. + user_id (str): The identifier for the user to whom the message is sent. Raises: FileNotFoundError: If the specified YAML files cannot be found. yaml.YAMLError: If there is an error parsing the YAML files. Exception: If there is an error sending the message through the Telegram client. """ - register = read_yaml(REGISTER_YAML_PATH) - media_data = read_yaml(MEDIA_YAML_PATH) + logging.debug("Running method `send_stats`...") + try: + logging.info("Preparing to send stats to user: %s", user_id) + register = read_yaml(REGISTER_YAML_PATH) + logging.debug("Loaded register data: %s", register) + + media_data = read_yaml(MEDIA_YAML_PATH) + logging.debug("Loaded media data: %s", media_data) + + msg = "Remaining:" + for data in (media_data,): + for field in data: + remaining_count = len(data[field]) - len(register[field]) + msg += f"\n - {field}: {remaining_count}" + logging.debug("Calculated remaining %s: %d", field, remaining_count) + + await client.send_message(user_id, msg) + logging.info("Sent stats message to user %s: %s", user_id, msg.replace("\n", " | ")) - msg = "Remaining:" - for data in (media_data,): - for field in data: - msg += f"\n - {field}: {len(data[field]) - len(register[field])}" + except FileNotFoundError as e: + logging.error("YAML file not found: %s", e) + raise e - await client.send_message(user_id, msg) + except yaml.YAMLError as e: + logging.error("Error parsing YAML file: %s", e) + raise e + except Exception as e: + logging.error("Error sending stats message through Telegram client: %s", e) + raise e -def start_sending_pills_reminder(scheduler, client): - """Schedules a daily reminder to take pills for a specified user. + logging.debug("Method `send_stats` finished.") - This function retrieves the user's chat ID and the reminder time from the configuration, then - sets up a scheduled job to send a reminder message via the Telegram client. The reminder message - is sent daily at the specified time. + +def start_sending_pills_reminder( + scheduler: AsyncIOScheduler, client: TelegramClient, user_id: str = "nathy" +): + """Starts a scheduled reminder for taking pills via Telegram. + + This function configures a job in the provided scheduler to send a reminder message to a + specified user at a designated time. The reminder message is sent repeatedly, adjusting the + frequency based on the number of messages sent. Args: - scheduler: The scheduler instance used to manage scheduled jobs. - client: The Telegram client used to send the reminder message. + scheduler (AsyncIOScheduler): The scheduler to manage the reminder job. + client (TelegramClient): The Telegram client used to send messages. + user_id (str, optional): The ID of the user to send reminders to. Defaults to "nathy". Raises: FileNotFoundError: If the configuration file cannot be found. yaml.YAMLError: If there is an error parsing the YAML configuration. Exception: If there is an error scheduling the job or sending the message. """ + logging.debug("Running method `start_sending_pills_reminder`...") + try: + logging.info("Starting scheduled pill reminders for user: %s", user_id) + tconfig = read_yaml(TELEGRAM_CONFIG_PATH) + user = tconfig.get(user_id, {}) + user_id = user.get("chat_id") + reminder_time = user.get("pills_reminder").get("time").split(":") + logging.debug("Resolved chat ID: %s", user_id) + logging.info("Pill reminder time set to: %s", reminder_time) + + async def wrap(client: TelegramClient): + """Sends periodic pill reminder messages to a user via Telegram. + + This asynchronous function sends a reminder message to the specified user at regular + intervals. The frequency of the messages adjusts dynamically based on the number of + messages sent, ensuring that reminders are sent consistently without overwhelming the + user. + + Args: + client (TelegramClient): The Telegram client used to send messages. + """ + global keep_sending_pill_reminder + keep_sending_pill_reminder = True + waiting_time, max_messages, cur_messages = 60, 5, 0 + logging.info( + "Starting pill reminder loop for user %s with waiting time %d seconds and %d max " + "messages", + user_id, + waiting_time, + max_messages, + ) + + while keep_sending_pill_reminder: + if cur_messages == max_messages: + waiting_time //= 2 + max_messages *= 2 + cur_messages = 0 + logging.info( + "Adjusting waiting time to: %d seconds and max messages to: %d", + waiting_time, + max_messages, + ) + + cur_messages += 1 + await client.send_message( + user_id, "💊 Amorcito, recuerda tomarte la píldora a las 10. Te amo ❤️" + ) + logging.info("Sent pill reminder message to user: %s", user_id) + await asyncio.sleep(waiting_time) + + scheduler.add_job( + wrap, + "cron", + hour=reminder_time[0], + minute=reminder_time[1], + second=reminder_time[2], + args=[client], + ) + logging.info("Pill reminder job scheduled for user: %s at %s", user_id, reminder_time) + + except FileNotFoundError as e: + logging.error("Configuration file not found: %s", e) + raise e + + except yaml.YAMLError as e: + logging.error("Error parsing YAML configuration: %s", e) + raise e + + except Exception as e: + logging.error("Error scheduling the pill reminder job: %s", e) + raise e + + logging.debug("Method `start_sending_pills_reminder` finished.") + + +def handle_stop_sending_pill_reminder_for_today(client: TelegramClient): + """Sets up an event handler to stop sending pill reminders for the day. + + This function configures an event handler that listens for incoming messages from a specific + user and stops the pill reminder process when triggered. It retrieves the user's chat ID from + the configuration and uses it to identify the relevant messages. + + Args: + client (TelegramClient): The Telegram client used to add the event handler. + """ + logging.debug("Running method `handle_stop_sending_pill_reminder_for_today`...") + logging.info("Setting up event handler to stop sending pill reminders for today.") tconfig = read_yaml(TELEGRAM_CONFIG_PATH) - user_id = tconfig.get("nathy", {}).get("chat_id") - reminder_time = tconfig.get("nathy", {}).get("pills_reminder").get("time").split(":") + nathy_id = tconfig.get("nathy", {}).get("chat_id") + logging.debug("Resolved chat ID for user 'nathy': %s", nathy_id) - async def wrap(scheduler, client): - await client.send_message( - user_id, "💊 Amorcito, recuerda tomarte la píldora a las 10. Te amo ❤️" - ) + async def handler(event: Message | events.NewMessage): + """Handles the event to stop sending pill reminders. - scheduler.add_job( - wrap, - "cron", - hour=reminder_time[0], - minute=reminder_time[1], - second=reminder_time[2], - args=[scheduler, client], - ) + This asynchronous function is triggered by an incoming message event and sets a global flag + to stop the pill reminder process. It ensures that no further reminders are sent once this + event is processed. + + Args: + event (Message | events.NewMessage): The event triggered by a new message. + """ + logging.info("Received message to stop sending pill reminders.") + global keep_sending_pill_reminder + keep_sending_pill_reminder = False + logging.info("Pill reminder sending has been stopped.") + + client.add_event_handler(handler, events.NewMessage(nathy_id, incoming=True)) + logging.debug("Event handler added for user 'nathy' to stop pill reminders.") + logging.debug("Method `handle_stop_sending_pill_reminder_for_today` finished.")