271 lines
7.8 KiB
Python
271 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Reminder Manager for OpenClaw Discord
|
|
Handles one-shot and recurring reminders via OpenClaw cron + SQLite
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
import sys
|
|
import json
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
DB_PATH = os.path.expanduser("~/.openclaw/workspace/data/reminders.db")
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
|
|
|
|
def init_db():
|
|
"""Create the reminders database."""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
c = conn.cursor()
|
|
c.execute('''CREATE TABLE IF NOT EXISTS reminders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
channel_id TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
remind_at TEXT NOT NULL,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
is_recurring INTEGER DEFAULT 0,
|
|
recurrence_rule TEXT,
|
|
cron_job_id TEXT,
|
|
active INTEGER DEFAULT 1
|
|
)''')
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def parse_time(time_str: str) -> datetime:
|
|
"""Parse various time formats into datetime."""
|
|
now = datetime.now()
|
|
time_str = time_str.lower().strip()
|
|
|
|
# Relative times: 20m, 2h, 1h30m
|
|
match = re.match(r'^(\d+)m$', time_str)
|
|
if match:
|
|
return now + timedelta(minutes=int(match.group(1)))
|
|
|
|
match = re.match(r'^(\d+)h$', time_str)
|
|
if match:
|
|
return now + timedelta(hours=int(match.group(1)))
|
|
|
|
match = re.match(r'^(\d+)h(\d+)m$', time_str)
|
|
if match:
|
|
return now + timedelta(hours=int(match.group(1)), minutes=int(match.group(2)))
|
|
|
|
# Tomorrow
|
|
if time_str == 'tomorrow':
|
|
return now + timedelta(days=1)
|
|
|
|
# Tomorrow at time: tomorrow 9am, tomorrow 14:00
|
|
match = re.match(r'^tomorrow\s+([\d:]+)(am|pm)?$', time_str)
|
|
if match:
|
|
time_part = match.group(1)
|
|
ampm = match.group(2)
|
|
tomorrow = now + timedelta(days=1)
|
|
|
|
if ':' in time_part:
|
|
hour, minute = map(int, time_part.split(':'))
|
|
else:
|
|
hour, minute = int(time_part), 0
|
|
|
|
if ampm == 'pm' and hour != 12:
|
|
hour += 12
|
|
if ampm == 'am' and hour == 12:
|
|
hour = 0
|
|
|
|
return tomorrow.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
|
|
# Today at time: 9am, 14:00, 2:30pm
|
|
match = re.match(r'^(\d{1,2}):(\d{2})(am|pm)?$', time_str)
|
|
if match:
|
|
hour = int(match.group(1))
|
|
minute = int(match.group(2))
|
|
ampm = match.group(3)
|
|
|
|
if ampm == 'pm' and hour != 12:
|
|
hour += 12
|
|
if ampm == 'am' and hour == 12:
|
|
hour = 0
|
|
|
|
result = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
if result < now:
|
|
result += timedelta(days=1)
|
|
return result
|
|
|
|
match = re.match(r'^(\d{1,2})(am|pm)$', time_str)
|
|
if match:
|
|
hour = int(match.group(1))
|
|
ampm = match.group(2)
|
|
|
|
if ampm == 'pm' and hour != 12:
|
|
hour += 12
|
|
if ampm == 'am' and hour == 12:
|
|
hour = 0
|
|
|
|
result = now.replace(hour=hour, minute=0, second=0, microsecond=0)
|
|
if result < now:
|
|
result += timedelta(days=1)
|
|
return result
|
|
|
|
raise ValueError(f"Can't parse time: {time_str}")
|
|
|
|
|
|
def add_reminder(user_id: str, channel_id: str, message: str, time_str: str) -> dict:
|
|
"""Add a new reminder and schedule it via OpenClaw cron."""
|
|
init_db()
|
|
|
|
remind_at = parse_time(time_str)
|
|
|
|
if remind_at < datetime.now():
|
|
return {"error": "Reminder time is in the past"}
|
|
|
|
# Insert into DB
|
|
conn = sqlite3.connect(DB_PATH)
|
|
c = conn.cursor()
|
|
c.execute('''INSERT INTO reminders (user_id, channel_id, message, remind_at, active)
|
|
VALUES (?, ?, ?, ?, 1)''',
|
|
(user_id, channel_id, message, remind_at.isoformat()))
|
|
reminder_id = c.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Schedule via OpenClaw cron (will be handled by caller)
|
|
return {
|
|
"id": reminder_id,
|
|
"message": message,
|
|
"remind_at": remind_at.isoformat(),
|
|
"user_id": user_id,
|
|
"channel_id": channel_id
|
|
}
|
|
|
|
|
|
def list_reminders(user_id: str = None) -> list:
|
|
"""List active reminders for a user or all users."""
|
|
init_db()
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
c = conn.cursor()
|
|
|
|
if user_id:
|
|
c.execute('''SELECT id, message, remind_at, channel_id
|
|
FROM reminders
|
|
WHERE user_id = ? AND active = 1 AND remind_at > datetime('now')
|
|
ORDER BY remind_at''', (user_id,))
|
|
else:
|
|
c.execute('''SELECT id, message, remind_at, channel_id, user_id
|
|
FROM reminders
|
|
WHERE active = 1 AND remind_at > datetime('now')
|
|
ORDER BY remind_at''')
|
|
|
|
results = c.fetchall()
|
|
conn.close()
|
|
|
|
return results
|
|
|
|
|
|
def delete_reminder(reminder_id: int, user_id: str = None) -> bool:
|
|
"""Delete/cancel a reminder."""
|
|
init_db()
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
c = conn.cursor()
|
|
|
|
if user_id:
|
|
c.execute('DELETE FROM reminders WHERE id = ? AND user_id = ?',
|
|
(reminder_id, user_id))
|
|
else:
|
|
c.execute('DELETE FROM reminders WHERE id = ?', (reminder_id,))
|
|
|
|
deleted = c.rowcount > 0
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return deleted
|
|
|
|
|
|
def delete_past_reminders():
|
|
"""Clean up old reminder entries."""
|
|
init_db()
|
|
conn = sqlite3.connect(DB_PATH)
|
|
c = conn.cursor()
|
|
c.execute('DELETE FROM reminders WHERE remind_at < datetime("now", "-1 day")')
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def cron_callback(reminder_id: int):
|
|
"""Called when a cron job fires - returns the reminder details."""
|
|
init_db()
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
c = conn.cursor()
|
|
c.execute('SELECT message, user_id, channel_id FROM reminders WHERE id = ?',
|
|
(reminder_id,))
|
|
result = c.fetchone()
|
|
|
|
if result:
|
|
# Mark as inactive after firing
|
|
c.execute('UPDATE reminders SET active = 0 WHERE id = ?', (reminder_id,))
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
|
|
if result:
|
|
return {
|
|
"message": result[0],
|
|
"user_id": result[1],
|
|
"channel_id": result[2]
|
|
}
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
command = sys.argv[1] if len(sys.argv) > 1 else "help"
|
|
|
|
if command == "add":
|
|
# Usage: reminder-manager.py add "user_id" "channel_id" "message" "time"
|
|
user_id = sys.argv[2]
|
|
channel_id = sys.argv[3]
|
|
message = sys.argv[4]
|
|
time_str = sys.argv[5]
|
|
|
|
result = add_reminder(user_id, channel_id, message, time_str)
|
|
print(json.dumps(result))
|
|
|
|
elif command == "list":
|
|
user_id = sys.argv[2] if len(sys.argv) > 2 else None
|
|
reminders = list_reminders(user_id)
|
|
print(json.dumps([{
|
|
"id": r[0],
|
|
"message": r[1],
|
|
"remind_at": r[2],
|
|
"channel_id": r[3],
|
|
"user_id": r[4] if len(r) > 4 else None
|
|
} for r in reminders]))
|
|
|
|
elif command == "delete":
|
|
reminder_id = int(sys.argv[2])
|
|
user_id = sys.argv[3] if len(sys.argv) > 3 else None
|
|
deleted = delete_reminder(reminder_id, user_id)
|
|
print(json.dumps({"deleted": deleted}))
|
|
|
|
elif command == "callback":
|
|
reminder_id = int(sys.argv[2])
|
|
result = cron_callback(reminder_id)
|
|
print(json.dumps(result) if result else "null")
|
|
|
|
elif command == "cleanup":
|
|
delete_past_reminders()
|
|
print("Cleanup complete")
|
|
|
|
else:
|
|
print("""Usage:
|
|
reminder-manager.py add "user_id" "channel_id" "message" "time"
|
|
reminder-manager.py list [user_id]
|
|
reminder-manager.py delete <reminder_id> [user_id]
|
|
reminder-manager.py callback <reminder_id>
|
|
reminder-manager.py cleanup
|
|
""")
|