瀏覽代碼

Make reminders persistent.

tags/v0.2
Ben Kurtovic 9 年之前
父節點
當前提交
d7e57910c0
共有 1 個文件被更改,包括 93 次插入12 次删除
  1. +93
    -12
      earwigbot/commands/remind.py

+ 93
- 12
earwigbot/commands/remind.py 查看文件

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

Loading…
取消
儲存