|
|
@@ -21,13 +21,15 @@ |
|
|
|
# SOFTWARE. |
|
|
|
|
|
|
|
import ast |
|
|
|
from contextlib import contextmanager |
|
|
|
from itertools import chain |
|
|
|
import operator |
|
|
|
import random |
|
|
|
from threading import Thread |
|
|
|
from threading import RLock, Thread |
|
|
|
import time |
|
|
|
|
|
|
|
from earwigbot.commands import Command |
|
|
|
from earwigbot.irc import Data |
|
|
|
|
|
|
|
DISPLAY = ["display", "show", "list", "info", "details"] |
|
|
|
CANCEL = ["cancel", "stop", "delete", "del", "stop", "unremind", "forget", |
|
|
@@ -60,6 +62,7 @@ class Remind(Command): |
|
|
|
ast.Pow: operator.pow |
|
|
|
} |
|
|
|
time_units = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800} |
|
|
|
|
|
|
|
def _evaluate(node): |
|
|
|
"""Convert an AST node into a real number or raise an exception.""" |
|
|
|
if isinstance(node, ast.Num): |
|
|
@@ -76,6 +79,7 @@ class Remind(Command): |
|
|
|
factor, arg = time_units[arg[-1]], arg[:-1] |
|
|
|
else: |
|
|
|
factor = 1 |
|
|
|
|
|
|
|
try: |
|
|
|
parsed = int(_evaluate(ast.parse(arg, mode="eval").body) * factor) |
|
|
|
except (SyntaxError, KeyError): |
|
|
@@ -84,6 +88,12 @@ class Remind(Command): |
|
|
|
raise ValueError(parsed) |
|
|
|
return parsed |
|
|
|
|
|
|
|
@contextmanager |
|
|
|
def _db(self): |
|
|
|
"""Return a threadsafe context manager for the permissions database.""" |
|
|
|
with self._db_lock: |
|
|
|
yield self.config.irc["permissions"] |
|
|
|
|
|
|
|
def _really_get_reminder_by_id(self, user, rid): |
|
|
|
"""Return the _Reminder object that corresponds to a particular ID. |
|
|
|
|
|
|
@@ -111,6 +121,14 @@ class Remind(Command): |
|
|
|
num = random.choice(list(set(range(4096)) - taken)) |
|
|
|
return "R{0:03X}".format(num) |
|
|
|
|
|
|
|
def _start_reminder(self, reminder, user): |
|
|
|
"""Start the given reminder object for the given user.""" |
|
|
|
reminder.start() |
|
|
|
if user in self.reminders: |
|
|
|
self.reminders[user].append(reminder) |
|
|
|
else: |
|
|
|
self.reminders[user] = [reminder] |
|
|
|
|
|
|
|
def _create_reminder(self, data, user): |
|
|
|
"""Create a new reminder for the given user.""" |
|
|
|
try: |
|
|
@@ -118,22 +136,22 @@ class Remind(Command): |
|
|
|
except ValueError: |
|
|
|
msg = "Invalid time \x02{0}\x0F. Time must be a positive integer, in seconds." |
|
|
|
return self.reply(data, msg.format(data.args[0])) |
|
|
|
|
|
|
|
if wait > 1000 * 365 * 24 * 60 * 60: |
|
|
|
# Hard to think of a good upper limit, but 1000 years works. |
|
|
|
msg = "Given time \x02{0}\x0F is too large. Keep it reasonable." |
|
|
|
return self.reply(data, msg.format(data.args[0])) |
|
|
|
|
|
|
|
end = time.time() + wait |
|
|
|
message = " ".join(data.args[1:]) |
|
|
|
try: |
|
|
|
rid = self._get_new_id() |
|
|
|
except IndexError: |
|
|
|
msg = "Couldn't set a new reminder: no free IDs available." |
|
|
|
return self.reply(data, msg) |
|
|
|
reminder = _Reminder(rid, user, wait, message, data, self) |
|
|
|
reminder.start() |
|
|
|
if user in self.reminders: |
|
|
|
self.reminders[user].append(reminder) |
|
|
|
else: |
|
|
|
self.reminders[user] = [reminder] |
|
|
|
|
|
|
|
reminder = _Reminder(rid, user, wait, end, message, data, self) |
|
|
|
self._start_reminder(reminder, user) |
|
|
|
msg = "Set reminder \x0303{0}\x0F ({1})." |
|
|
|
self.reply(data, msg.format(rid, reminder.end_time)) |
|
|
|
|
|
|
@@ -162,11 +180,30 @@ class Remind(Command): |
|
|
|
reminder.wait = duration |
|
|
|
except (IndexError, ValueError): |
|
|
|
pass |
|
|
|
|
|
|
|
reminder.end = time.time() + reminder.wait |
|
|
|
reminder.start() |
|
|
|
end = time.strftime("%b %d %H:%M:%S %Z", time.localtime(reminder.end)) |
|
|
|
msg = "Reminder \x0303{0}\x0F {1} until {2}." |
|
|
|
self.reply(data, msg.format(reminder.id, verb, end)) |
|
|
|
|
|
|
|
def _load_reminders(self): |
|
|
|
"""Load previously made reminders from the database.""" |
|
|
|
with self._db() as permdb: |
|
|
|
try: |
|
|
|
database = permdb.get_attr("command:remind", "data") |
|
|
|
except KeyError: |
|
|
|
return |
|
|
|
permdb.set_attr("command:remind", "data", "[]") |
|
|
|
|
|
|
|
for item in ast.literal_eval(database): |
|
|
|
rid, user, wait, end, message, data = item |
|
|
|
if end < time.time(): |
|
|
|
continue |
|
|
|
data = Data.unserialize(data) |
|
|
|
reminder = _Reminder(rid, user, wait, end, message, data, self) |
|
|
|
self._start_reminder(reminder, user) |
|
|
|
|
|
|
|
def _handle_command(self, command, data, user, reminder, arg=None): |
|
|
|
"""Handle a reminder-processing subcommand.""" |
|
|
|
if command in DISPLAY: |
|
|
@@ -190,7 +227,8 @@ class Remind(Command): |
|
|
|
rlist = ", ".join(fmt(robj) for robj in self.reminders[user]) |
|
|
|
msg = "Your reminders: {0}.".format(rlist) |
|
|
|
else: |
|
|
|
msg = "You have no reminders. Set one with \x0306!remind [time] [message]\x0F. See also: \x0306!remind help\x0F." |
|
|
|
msg = ("You have no reminders. Set one with \x0306!remind [time] " |
|
|
|
"[message]\x0F. See also: \x0306!remind help\x0F.") |
|
|
|
self.reply(data, msg) |
|
|
|
|
|
|
|
def _process_snooze_command(self, data, user): |
|
|
@@ -239,6 +277,8 @@ class Remind(Command): |
|
|
|
|
|
|
|
def setup(self): |
|
|
|
self.reminders = {} |
|
|
|
self._db_lock = RLock() |
|
|
|
self._load_reminders() |
|
|
|
|
|
|
|
def process(self, data): |
|
|
|
if data.command == "snooze": |
|
|
@@ -284,15 +324,42 @@ class Remind(Command): |
|
|
|
|
|
|
|
self._handle_command(data.args[1], data, user, reminder, 2) |
|
|
|
|
|
|
|
def unload(self): |
|
|
|
for reminder in chain(*self.reminders.values()): |
|
|
|
reminder.stop(delete=False) |
|
|
|
|
|
|
|
def store_reminder(self, reminder): |
|
|
|
"""Store a serialized reminder into the database.""" |
|
|
|
with self._db() as permdb: |
|
|
|
try: |
|
|
|
dump = permdb.get_attr("command:remind", "data") |
|
|
|
except KeyError: |
|
|
|
dump = "[]" |
|
|
|
|
|
|
|
database = ast.literal_eval(dump) |
|
|
|
database.append(reminder) |
|
|
|
permdb.set_attr("command:remind", "data", str(database)) |
|
|
|
|
|
|
|
def unstore_reminder(self, rid): |
|
|
|
"""Remove a reminder from the database by ID.""" |
|
|
|
with self._db() as permdb: |
|
|
|
try: |
|
|
|
dump = permdb.get_attr("command:remind", "data") |
|
|
|
except KeyError: |
|
|
|
dump = "[]" |
|
|
|
|
|
|
|
database = ast.literal_eval(dump) |
|
|
|
database = [item for item in database if item[0] != rid] |
|
|
|
permdb.set_attr("command:remind", "data", str(database)) |
|
|
|
|
|
|
|
class _Reminder(object): |
|
|
|
"""Represents a single reminder.""" |
|
|
|
|
|
|
|
def __init__(self, rid, user, wait, message, data, cmdobj): |
|
|
|
def __init__(self, rid, user, wait, end, message, data, cmdobj): |
|
|
|
self.id = rid |
|
|
|
self.wait = wait |
|
|
|
self.end = end |
|
|
|
self.message = message |
|
|
|
self.end = None |
|
|
|
|
|
|
|
self._user = user |
|
|
|
self._data = data |
|
|
@@ -307,6 +374,7 @@ class _Reminder(object): |
|
|
|
if thread.abort: |
|
|
|
return |
|
|
|
self._cmdobj.reply(self._data, self.message) |
|
|
|
self._delete() |
|
|
|
for i in xrange(60): |
|
|
|
time.sleep(1) |
|
|
|
if thread.abort: |
|
|
@@ -318,6 +386,16 @@ class _Reminder(object): |
|
|
|
except (KeyError, ValueError): # Already canceled by the user |
|
|
|
pass |
|
|
|
|
|
|
|
def _save(self): |
|
|
|
"""Save this reminder to the database.""" |
|
|
|
data = self._data.serialize() |
|
|
|
item = (self.id, self._user, self.wait, self.end, self.message, data) |
|
|
|
self._cmdobj.store_reminder(item) |
|
|
|
|
|
|
|
def _delete(self): |
|
|
|
"""Remove this reminder from the database.""" |
|
|
|
self._cmdobj.unstore_reminder(self.id) |
|
|
|
|
|
|
|
@property |
|
|
|
def end_time(self): |
|
|
|
"""Return a string representing the end time of a reminder.""" |
|
|
@@ -330,14 +408,17 @@ class _Reminder(object): |
|
|
|
"""Start the reminder timer thread. Stops it if already running.""" |
|
|
|
self.stop() |
|
|
|
self._thread = Thread(target=self._callback, name="remind-" + self.id) |
|
|
|
self._thread.end = self.end = time.time() + self.wait |
|
|
|
self._thread.end = self.end |
|
|
|
self._thread.daemon = True |
|
|
|
self._thread.abort = False |
|
|
|
self._thread.start() |
|
|
|
self._save() |
|
|
|
|
|
|
|
def stop(self): |
|
|
|
def stop(self, delete=True): |
|
|
|
"""Stop a currently running reminder.""" |
|
|
|
if not self._thread: |
|
|
|
return |
|
|
|
if delete: |
|
|
|
self._delete() |
|
|
|
self._thread.abort = True |
|
|
|
self._thread = None |