Files
Lamont/modules/reminders/commands.py
Chelsea Lee fbdf33e894
Some checks failed
CI / test (push) Has been cancelled
CI / compose-smoke (push) Has been cancelled
Build reusable bot framework
2026-07-19 21:53:24 -05:00

110 lines
4.0 KiB
Python

"""Discord-facing reminder command handler and parser validation."""
from datetime import datetime, timezone
from core import users
from modules.reminders import service
VALID_ACTIONS = {"create", "list", "cancel", "set_timezone"}
def validateReminder(data):
if not isinstance(data, dict):
return ["Response must be a JSON object"]
if data.get("needs_clarification"):
return []
errors = []
action = data.get("action")
if action not in VALID_ACTIONS:
errors.append("action must be create, list, cancel, or set_timezone")
return errors
if action == "create":
if not isinstance(data.get("message"), str) or not data["message"].strip():
errors.append("create requires a reminder message")
try:
runAt = datetime.fromisoformat(
str(data.get("run_at", "")).replace("Z", "+00:00")
)
if runAt.tzinfo is None:
errors.append("run_at must include a timezone offset")
elif runAt.astimezone(timezone.utc) <= datetime.now(timezone.utc):
errors.append("run_at must be in the future")
except ValueError:
errors.append("create requires an ISO-8601 run_at")
try:
service.normalizeRecurrence(data.get("recurrence"))
except ValueError as error:
errors.append(str(error))
elif action == "cancel" and not data.get("reminder_id"):
errors.append("cancel requires reminder_id from the reminder list")
elif action == "set_timezone" and not users.isValidTimezone(data.get("timezone")):
errors.append("set_timezone requires a valid IANA timezone")
return errors
async def handleReminder(context, parsed):
action = parsed["action"]
if action == "create":
result, status = await context.api.request(
"post",
"/api/reminders",
{
"message": parsed["message"],
"run_at": parsed["run_at"],
"recurrence": parsed.get("recurrence"),
},
)
if status == 201:
recurrence = " (recurring)" if result.get("recurrence") else ""
await context.reply(
f"Reminder set for **{result['next_run_at']}**{recurrence}: "
f"{result['message']}"
)
else:
await context.reply(_errorMessage(result, "I couldn't create that reminder."))
return
if action == "list":
result, status = await context.api.request("get", "/api/reminders")
if status != 200:
await context.reply(_errorMessage(result, "I couldn't list reminders."))
return
reminders = result.get("reminders", [])
if not reminders:
await context.reply("You have no active reminders.")
return
lines = [
f"- `{item['id']}` — {item['next_run_at']}: {item['message']}"
for item in reminders
]
await context.reply("**Active reminders:**\n" + "\n".join(lines))
return
if action == "cancel":
result, status = await context.api.request(
"delete", f"/api/reminders/{parsed['reminder_id']}"
)
if status == 200:
await context.reply(f"Cancelled reminder: {result['message']}")
else:
await context.reply(_errorMessage(result, "I couldn't cancel that reminder."))
return
result, status = await context.api.request(
"put", "/api/user/me/timezone", {"timezone": parsed["timezone"]}
)
if status == 200:
context.api.timezone = result["timezone"]
context.timezone = result["timezone"]
await context.reply(f"Your timezone is now **{result['timezone']}**.")
else:
await context.reply(_errorMessage(result, "I couldn't update your timezone."))
def _errorMessage(result, fallback):
detail = result.get("error") if isinstance(result, dict) else None
return f"{fallback} {detail}" if detail else fallback