Build reusable bot framework
This commit is contained in:
109
modules/reminders/commands.py
Normal file
109
modules/reminders/commands.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user