110 lines
4.0 KiB
Python
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
|