Bot & Automation

Activity Reminder Bot

/root/hermes-projects/Activity Reminder Bot

apps/activity_reminder_bot/services/reminders.py text
import asyncio
import secrets
from datetime import date, datetime, timedelta
from typing import Any

from activity_reminder_bot.core.config import get_settings
from activity_reminder_bot.models import Reminder, ReminderDraft, ReminderStatus
from activity_reminder_bot.services.repeat import next_repeat_due_at
from activity_reminder_bot.services.sheets import GoogleSheetsReminderRepository, get_reminder_repository


class ReminderService:
    def __init__(self, repository: GoogleSheetsReminderRepository | None = None) -> None:
        self.repository = repository or get_reminder_repository()
        self.settings = get_settings()

    async def create(self, draft: ReminderDraft) -> Reminder:
        now = self.now()
        public_id = self._new_public_id()
        return await asyncio.to_thread(self.repository.create, draft, public_id, now)

    async def list_today(self, user_id: int) -> list[Reminder]:
        today = self.now().date()
        reminders = await asyncio.to_thread(self.repository.list_for_user, user_id, False)
        return sorted(
            [reminder for reminder in reminders if reminder.reminder_date == today],
            key=lambda reminder: reminder.reminder_time,
        )

    async def list_week(self, user_id: int) -> list[Reminder]:
        today = self.now().date()
        end = today + timedelta(days=7)
        reminders = await asyncio.to_thread(self.repository.list_for_user, user_id, False)
        return sorted(
            [reminder for reminder in reminders if today <= reminder.reminder_date <= end],
            key=lambda reminder: (reminder.reminder_date, reminder.reminder_time),
        )

    async def history(self, user_id: int, limit: int = 10) -> list[Reminder]:
        reminders = await asyncio.to_thread(self.repository.list_for_user, user_id, True)
        return list(reversed(reminders))[:limit]

    async def search(self, user_id: int, query: str) -> list[Reminder]:
        return await asyncio.to_thread(self.repository.search, user_id, query, 10)

    async def cleanup_user_history(self, user_id: int) -> dict[str, int]:
        return await asyncio.to_thread(self.repository.cleanup_user_history, user_id)

    async def cleanup_invalid_rows(self) -> dict[str, int]:
        return await asyncio.to_thread(self.repository.cleanup_invalid_rows)

    async def get(self, public_id: str) -> Reminder | None:
        return await asyncio.to_thread(self.repository.get_by_id, public_id)

    async def update(self, public_id: str, updates: dict[str, Any]) -> Reminder:
        return await asyncio.to_thread(self.repository.update, public_id, updates, self.now())

    async def delete(self, public_id: str) -> Reminder:
        return await self.update(public_id, {"status": ReminderStatus.deleted})

    async def mark_notified(self, reminder: Reminder, alarm_message_id: int | None = None) -> Reminder:
        return await self.update(
            reminder.public_id,
            {
                "status": ReminderStatus.notified,
                "notified_at": self.now(),
                "alarm_message_id": alarm_message_id,
                "alarm_count": reminder.alarm_count + 1,
            },
        )

    async def complete(self, reminder: Reminder) -> Reminder:
        next_due_at = next_repeat_due_at(reminder.due_at, reminder.repeat)
        if next_due_at:
            return await self.update(
                reminder.public_id,
                {
                    "reminder_date": next_due_at.date(),
                    "reminder_time": next_due_at.time().replace(second=0, microsecond=0),
                    "status": ReminderStatus.active,
                    "notified_at": None,
                    "alarm_message_id": None,
                    "alarm_count": 0,
                },
            )
        return await self.update(
            reminder.public_id,
            {"status": ReminderStatus.done, "alarm_message_id": None},
        )

    async def snooze(self, reminder: Reminder, delta: timedelta) -> Reminder:
        due_at = self.now() + delta
        return await self.update(
            reminder.public_id,
            {
                "reminder_date": due_at.date(),
                "reminder_time": due_at.time().replace(second=0, microsecond=0),
                "status": ReminderStatus.snoozed,
                "notified_at": None,
                "alarm_message_id": None,
                "alarm_count": 0,
            },
        )

    async def due_reminders(self) -> list[Reminder]:
        now = self.now()
        reminders = await asyncio.to_thread(self.repository.list_all, False)
        due: list[Reminder] = []
        for reminder in reminders:
            if reminder.status in {ReminderStatus.active, ReminderStatus.snoozed} and reminder.due_at <= now:
                due.append(reminder)
                continue
            if self.should_repeat_alarm(reminder, now):
                due.append(reminder)
        return due

    def should_repeat_alarm(self, reminder: Reminder, now: datetime | None = None) -> bool:
        if not self.settings.alarm_repeat_enabled:
            return False
        if reminder.status != ReminderStatus.notified:
            return False
        if not reminder.notified_at:
            return False
        if reminder.alarm_count >= self.settings.alarm_repeat_max_count:
            return False
        current = now or self.now()
        elapsed = (current - reminder.notified_at).total_seconds()
        return elapsed >= self.settings.alarm_repeat_interval_seconds

    def now(self) -> datetime:
        return datetime.now(self.settings.timezone).replace(tzinfo=None)

    def is_configured(self) -> bool:
        return self.repository.is_configured()

    def reminders_on_date(self, reminders: list[Reminder], target: date) -> list[Reminder]:
        return [reminder for reminder in reminders if reminder.reminder_date == target]

    def _new_public_id(self) -> str:
        return f"REM-{secrets.token_hex(4).upper()}"