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()}"