From d7ea1c9ec4c523472e6f30fd26347be9c600107e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Sun, 6 Jul 2025 06:02:05 +0200 Subject: [PATCH] Improve message sent to Discord for missed reminder --- .env.example | 9 +++++++ .gitignore | 1 + Dockerfile | 1 + discord_reminder_bot/main.py | 18 ++++++++++++-- discord_reminder_bot/settings.py | 40 ++++++++++++++++++++++++++++++++ docker-compose.yml | 1 + 6 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index b9a8a99..1bfed92 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,15 @@ TIMEZONE= # On Linux you will need to use double slashes before the path to get the absolute path. # SQLITE_LOCATION=//home/lovinator/foo.db +# Additional directory to store data in. +# Note: You will still need to set the SQLITE_LOCATION to a valid path. +# This is used to store markdown files with the reminder data. +# The directory will be created if it does not exist. +# Example: DATA_DIR=C:/Code/discord-reminder-bot/data +# Example: DATA_DIR=/home/lovinator/data +# Example: DATA_DIR=./data +DATA_DIR=./data + # Log level, CRITICAL, ERROR, WARNING, INFO, DEBUG. LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore index 81fe2b2..abb98df 100644 --- a/.gitignore +++ b/.gitignore @@ -209,3 +209,4 @@ __marimo__/ # SQLite *.sqlite *.sqlite.* +data/reminder_data/* diff --git a/Dockerfile b/Dockerfile index c17f655..68f075a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,5 +18,6 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --no-install-project +ARG DATA_DIR=${DATA_DIR:-/home/botuser/data} VOLUME ["/home/botuser/data/"] CMD ["uv", "run", "python", "-m", "discord_reminder_bot.main"] diff --git a/discord_reminder_bot/main.py b/discord_reminder_bot/main.py index 756c2a5..858cc46 100644 --- a/discord_reminder_bot/main.py +++ b/discord_reminder_bot/main.py @@ -26,7 +26,7 @@ from loguru import logger from discord_reminder_bot.helpers import calculate, generate_markdown_state, generate_state, get_human_readable_time, parse_time from discord_reminder_bot.modals import CronReminderModifyModal, DateReminderModifyModal, IntervalReminderModifyModal -from discord_reminder_bot.settings import scheduler +from discord_reminder_bot.settings import export_reminder_jobs_to_markdown, get_markdown_contents_from_markdown_file, scheduler if TYPE_CHECKING: from collections.abc import Callable @@ -44,9 +44,21 @@ def my_listener(event: JobExecutionEvent) -> None: Args: event: The event that occurred. """ + if event.code == events.EVENT_JOB_ADDED: + export_reminder_jobs_to_markdown() + if event.code == events.EVENT_JOB_MISSED: scheduled_time: str = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S") - msg: str = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}" + + # Get data from markdown file that was created by export_reminder_jobs_to_markdown() + job_data: str = get_markdown_contents_from_markdown_file(event.job_id) + if not job_data: + msg: str = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}" + logger.warning(msg) + else: + msg: str = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}\nData:\n```json\n{job_data}\n```" + logger.warning(msg) + send_webhook(message=msg) if event.exception: @@ -120,6 +132,8 @@ class RemindBotClient(discord.Client): else: logger.error("Scheduler is already running.") + export_reminder_jobs_to_markdown() + def format_job_for_ui(job: Job) -> str: """Format a single job for display in the UI. diff --git a/discord_reminder_bot/settings.py b/discord_reminder_bot/settings.py index e28bd32..bc01892 100644 --- a/discord_reminder_bot/settings.py +++ b/discord_reminder_bot/settings.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import platform +from pathlib import Path from zoneinfo import ZoneInfo, ZoneInfoNotFoundError import pytz @@ -11,6 +12,8 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv from loguru import logger +from discord_reminder_bot.helpers import generate_state + load_dotenv(verbose=True) default_sentry_dsn: str = "https://c4c61a52838be9b5042144420fba5aaa@o4505228040339456.ingest.us.sentry.io/4508707268984832" @@ -57,3 +60,40 @@ def get_scheduler() -> AsyncIOScheduler: scheduler: AsyncIOScheduler = get_scheduler() + + +def export_reminder_jobs_to_markdown() -> None: + """Loop through the APScheduler database and save each job's data to a markdown file if changed.""" + data_dir: str = os.getenv("DATA_DIR", default="./data") + logger.info(f"Exporting reminder jobs to markdown files in directory: {data_dir}") + + for job in scheduler.get_jobs(): + job_state: str = generate_state(job.__getstate__(), job) + file_path: Path = Path(data_dir) / "reminder_data" / f"{job.id}.md" + file_path.parent.mkdir(parents=True, exist_ok=True) + try: + if file_path.exists(): + existing_content = file_path.read_text(encoding="utf-8") + if existing_content == job_state: + logger.debug(f"No changes for {file_path}, skipping write.") + continue + file_path.write_text(job_state, encoding="utf-8") + logger.info(f"Data saved to {file_path}") + except OSError as e: + logger.error(f"Failed to save data to {file_path}: {e}") + + +def get_markdown_contents_from_markdown_file(job_id: str) -> str: + """Get the contents of a markdown file for a specific job ID. + + Args: + job_id (str): The ID of the job. + + Returns: + str: The contents of the markdown file, or an empty string if the file does not exist. + """ + data_dir: str = os.getenv("DATA_DIR", default="./data") + file_path: Path = Path(data_dir) / "reminder_data" / f"{job_id}.md" + if file_path.exists(): + return file_path.read_text(encoding="utf-8") + return "" diff --git a/docker-compose.yml b/docker-compose.yml index 085fecb..12f2da5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - TIMEZONE=${TIMEZONE} - LOG_LEVEL=${LOG_LEVEL} - SQLITE_LOCATION=/data/jobs.sqlite + - DATA_DIR=/data restart: unless-stopped volumes: - data_folder:/home/botuser/data/