"""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