Browse Source

Make reminders persistent.

tags/v0.2
Ben Kurtovic 9 years ago
parent
commit
d7e57910c0
1 changed files with 93 additions and 12 deletions
  1. +93
    -12
      earwigbot/commands/remind.py

+ 93
- 12
earwigbot/commands/remind.py View File

@@ -21,13 +21,15 @@
# SOFTWARE. # SOFTWARE.


import ast import ast
from contextlib import contextmanager
from itertools import chain from itertools import chain
import operator import operator
import random import random
from threading import Thread
from threading import RLock, Thread
import time import time


from earwigbot.commands import Command from earwigbot.commands import Command
from earwigbot.irc import Data


DISPLAY = ["display", "show", "list", "info", "details"] DISPLAY = ["display", "show", "list", "info", "details"]
CANCEL = ["cancel", "stop", "delete", "del", "stop", "unremind", "forget", CANCEL = ["cancel", "stop", "delete", "del", "stop", "unremind", "forget",
@@ -60,6 +62,7 @@ class Remind(Command):
ast.Pow: operator.pow ast.Pow: operator.pow
} }
time_units = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800} time_units = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}

def _evaluate(node): def _evaluate(node):
"""Convert an AST node into a real number or raise an exception.""" """Convert an AST node into a real number or raise an exception."""
if isinstance(node, ast.Num): if isinstance(node, ast.Num):
@@ -76,6 +79,7 @@ class Remind(Command):
factor, arg = time_units[arg[-1]], arg[:-1] factor, arg = time_units[arg[-1]], arg[:-1]
else: else:
factor = 1 factor = 1

try: try:
parsed = int(_evaluate(ast.parse(arg, mode="eval").body) * factor) parsed = int(_evaluate(ast.parse(arg, mode="eval").body) * factor)
except (SyntaxError, KeyError): except (SyntaxError, KeyError):
@@ -84,6 +88,12 @@ class Remind(Command):
raise ValueError(parsed) raise ValueError(parsed)
return 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): def _really_get_reminder_by_id(self, user, rid):
"""Return the _Reminder object that corresponds to a particular ID. """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)) num = random.choice(list(set(range(4096)) - taken))
return "R{0:03X}".format(num) 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): def _create_reminder(self, data, user):
"""Create a new reminder for the given user.""" """Create a new reminder for the given user."""
try: try:
@@ -118,22 +136,22 @@ class Remind(Command):
except ValueError: except ValueError:
msg = "Invalid time \x02{0}\x0F. Time must be a positive integer, in seconds." msg = "Invalid time \x02{0}\x0F. Time must be a positive integer, in seconds."
return self.reply(data, msg.format(data.args[0])) return self.reply(data, msg.format(data.args[0]))

if wait > 1000 * 365 * 24 * 60 * 60: if wait > 1000 * 365 * 24 * 60 * 60:
# Hard to think of a good upper limit, but 1000 years works. # Hard to think of a good upper limit, but 1000 years works.
msg = "Given time \x02{0}\x0F is too large. Keep it reasonable." msg = "Given time \x02{0}\x0F is too large. Keep it reasonable."
return self.reply(data, msg.format(data.args[0])) return self.reply(data, msg.format(data.args[0]))

end = time.time() + wait
message = " ".join(data.args[1:]) message = " ".join(data.args[1:])
try: try:
rid = self._get_new_id() rid = self._get_new_id()
except IndexError: except IndexError:
msg = "Couldn't set a new reminder: no free IDs available." msg = "Couldn't set a new reminder: no free IDs available."
return self.reply(data, msg) 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})." msg = "Set reminder \x0303{0}\x0F ({1})."
self.reply(data, msg.format(rid, reminder.end_time)) self.reply(data, msg.format(rid, reminder.end_time))


@@ -162,11 +180,30 @@ class Remind(Command):
reminder.wait = duration reminder.wait = duration
except (IndexError, ValueError): except (IndexError, ValueError):
pass pass

reminder.end = time.time() + reminder.wait
reminder.start() reminder.start()
end = time.strftime("%b %d %H:%M:%S %Z", time.localtime(reminder.end)) end = time.strftime("%b %d %H:%M:%S %Z", time.localtime(reminder.end))
msg = "Reminder \x0303{0}\x0F {1} until {2}." msg = "Reminder \x0303{0}\x0F {1} until {2}."
self.reply(data, msg.format(reminder.id, verb, end)) 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): def _handle_command(self, command, data, user, reminder, arg=None):
"""Handle a reminder-processing subcommand.""" """Handle a reminder-processing subcommand."""
if command in DISPLAY: if command in DISPLAY:
@@ -190,7 +227,8 @@ class Remind(Command):
rlist = ", ".join(fmt(robj) for robj in self.reminders[user]) rlist = ", ".join(fmt(robj) for robj in self.reminders[user])
msg = "Your reminders: {0}.".format(rlist) msg = "Your reminders: {0}.".format(rlist)
else: 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) self.reply(data, msg)


def _process_snooze_command(self, data, user): def _process_snooze_command(self, data, user):
@@ -239,6 +277,8 @@ class Remind(Command):


def setup(self): def setup(self):
self.reminders = {} self.reminders = {}
self._db_lock = RLock()
self._load_reminders()


def process(self, data): def process(self, data):
if data.command == "snooze": if data.command == "snooze":
@@ -284,15 +324,42 @@ class Remind(Command):


self._handle_command(data.args[1], data, user, reminder, 2) 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): class _Reminder(object):
"""Represents a single reminder.""" """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.id = rid
self.wait = wait self.wait = wait
self.end = end
self.message = message self.message = message
self.end = None


self._user = user self._user = user
self._data = data self._data = data
@@ -307,6 +374,7 @@ class _Reminder(object):
if thread.abort: if thread.abort:
return return
self._cmdobj.reply(self._data, self.message) self._cmdobj.reply(self._data, self.message)
self._delete()
for i in xrange(60): for i in xrange(60):
time.sleep(1) time.sleep(1)
if thread.abort: if thread.abort:
@@ -318,6 +386,16 @@ class _Reminder(object):
except (KeyError, ValueError): # Already canceled by the user except (KeyError, ValueError): # Already canceled by the user
pass 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 @property
def end_time(self): def end_time(self):
"""Return a string representing the end time of a reminder.""" """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.""" """Start the reminder timer thread. Stops it if already running."""
self.stop() self.stop()
self._thread = Thread(target=self._callback, name="remind-" + self.id) 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.daemon = True
self._thread.abort = False self._thread.abort = False
self._thread.start() self._thread.start()
self._save()


def stop(self):
def stop(self, delete=True):
"""Stop a currently running reminder.""" """Stop a currently running reminder."""
if not self._thread: if not self._thread:
return return
if delete:
self._delete()
self._thread.abort = True self._thread.abort = True
self._thread = None self._thread = None

Loading…
Cancel
Save