Merge branch 'countdown-reminders' into 'master'

Store and report on reminders via the countdown module

See merge request !15
This commit is contained in:
Brian S. Stephan 2017-02-22 22:11:06 -06:00
commit 081648c074
9 changed files with 192 additions and 51 deletions

View File

@ -0,0 +1 @@
"""Countdown supports denoting points in time, and sending reminders about them to IRC."""

View File

@ -1,43 +1,133 @@
"""Access to countdown items through bot commands."""
import logging
import re
import threading
import time
import irc.client
import parsedatetime as pdt
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from django.utils import timezone
from ircbot.lib import Plugin
from countdown.models import CountdownItem
from ircbot.lib import Plugin, reply_destination_for_event
log = logging.getLogger('countdown.ircplugin')
class Countdown(Plugin):
"""Report on countdown items."""
new_reminder_regex = r'remind\s+([^\s]+)\s+(at|in|on)\s+(.*)\s+(to|that)\s+(.*)'
def __init__(self, bot, connection, event):
"""Initialize some stuff."""
self.running_reminders = []
self.send_reminders = True
t = threading.Thread(target=self.reminder_thread)
t.daemon = True
t.start()
super(Countdown, self).__init__(bot, connection, event)
def start(self):
"""Set up handlers."""
self.running_reminders = []
self.send_reminders = True
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$',
self.handle_item_list, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(\S+)$',
self.handle_item_detail, -20)
# let this interrupt markov
self.connection.add_global_handler('pubmsg', self.handle_new_reminder, -50)
self.connection.add_global_handler('privmsg', self.handle_new_reminder, -50)
super(Countdown, self).start()
def stop(self):
"""Tear down handlers."""
self.running_reminders = []
self.send_reminders = False
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_item_list)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_item_detail)
self.connection.remove_global_handler('pubmsg', self.handle_new_reminder)
self.connection.remove_global_handler('privmsg', self.handle_new_reminder)
super(Countdown, self).stop()
def reminder_thread(self):
"""After IRC is started up, begin sending reminders."""
time.sleep(30)
while self.send_reminders:
reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False)
for reminder in reminders:
log.debug("%s @ %s", reminder.reminder_message, reminder.at_time)
if reminder.at_time <= timezone.now():
log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target)
self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target)
reminder.sent_reminder = True
reminder.save()
time.sleep(1)
def handle_new_reminder(self, connection, event):
"""Watch IRC for requests to remind new things, create countdown items."""
what = event.arguments[0]
my_nick = connection.get_nickname()
addressed_my_nick = r'^{0:s}[:,]\s+'.format(my_nick)
sender_nick = irc.client.NickMask(event.source).nick
sent_location = reply_destination_for_event(event)
if re.search(addressed_my_nick, what, re.IGNORECASE) is not None:
# we were addressed, were we told to add a reminder?
trimmed_what = re.sub(addressed_my_nick, '', what)
log.debug(trimmed_what)
match = re.match(self.new_reminder_regex, trimmed_what, re.IGNORECASE)
if match:
log.debug("%s is a new reminder request", trimmed_what)
who = match.group(1)
when_type = match.group(2)
when = match.group(3)
text = match.group(5)
log.debug("%s / %s / %s", who, when, text)
item_name = '{0:s}-{1:s}'.format(sender_nick, timezone.now().strftime('%s'))
# parse when to send the notification
if when_type == 'in':
# relative time
calendar = pdt.Calendar()
when_t = calendar.parseDT(when, timezone.localtime(timezone.now()),
tzinfo=timezone.get_current_timezone())[0]
else:
# absolute time
when_t = timezone.make_aware(parse(when))
# parse the person to address, if anyone, when sending the notification
if who == 'us' or who == sent_location:
message = text
elif who == 'me':
message = '{0:s}: {1:s}'.format(sender_nick, text)
else:
message = '{0:s}: {1:s}'.format(who, text)
log.debug("%s / %s / %s", item_name, when_t, message)
countdown_item = CountdownItem.objects.create(name=item_name, at_time=when_t, is_reminder=True,
reminder_message=message, reminder_target=sent_location)
log.info("created countdown item %s", str(countdown_item))
return 'NO MORE'
def handle_item_detail(self, connection, event, match):
"""Provide the details of one countdown item."""
name = match.group(1)
if name != 'list':
@ -65,7 +155,6 @@ class Countdown(Plugin):
def handle_item_list(self, connection, event, match):
"""List all countdown items."""
items = CountdownItem.objects.all()
if len(items) > 0:
reply = "countdown items: {0:s}".format(", ".join([x.name for x in items]))

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-23 02:25
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('countdown', '0002_remove_countdownitem_source'),
]
operations = [
migrations.AddField(
model_name='countdownitem',
name='is_reminder',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='countdownitem',
name='reminder_message',
field=models.TextField(default=''),
),
migrations.AddField(
model_name='countdownitem',
name='reminder_target',
field=models.CharField(default='', max_length=64),
),
migrations.AddField(
model_name='countdownitem',
name='sent_reminder',
field=models.BooleanField(default=False),
),
]

View File

@ -10,15 +10,19 @@ log = logging.getLogger('countdown.models')
class CountdownItem(models.Model):
"""Track points in time."""
name = models.CharField(max_length=64, default='')
at_time = models.DateTimeField()
is_reminder = models.BooleanField(default=False)
sent_reminder = models.BooleanField(default=False)
reminder_message = models.TextField(default="")
reminder_target = models.CharField(max_length=64, default='')
created_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String representation."""
return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z'))

View File

@ -144,51 +144,56 @@ class DrReactor(irc.client.Reactor):
Also supports regex handlers.
"""
log.debug("EVENT: e[%s] s[%s] t[%s] a[%s]", event.type, event.source,
event.target, event.arguments)
try:
log.debug("EVENT: e[%s] s[%s] t[%s] a[%s]", event.type, event.source,
event.target, event.arguments)
self.try_recursion(connection, event)
self.try_recursion(connection, event)
# only do aliasing for pubmsg/privmsg
if event.type in ['pubmsg', 'privmsg']:
what = event.arguments[0]
log.debug("checking for alias for %s", what)
# only do aliasing for pubmsg/privmsg
if event.type in ['pubmsg', 'privmsg']:
what = event.arguments[0]
log.debug("checking for alias for %s", what)
for alias in Alias.objects.all():
repl = alias.replace(what)
if repl:
# we found an alias for our given string, doing a replace
event.arguments[0] = repl
# recurse, in case there's another alias in this one
return self._handle_event(connection, event)
for alias in Alias.objects.all():
repl = alias.replace(what)
if repl:
# we found an alias for our given string, doing a replace
event.arguments[0] = repl
# recurse, in case there's another alias in this one
return self._handle_event(connection, event)
with self.mutex:
# doing regex version first as it has the potential to be more specific
log.debug("checking regex handlers for %s", event.type)
matching_handlers = sorted(
self.regex_handlers.get("all_events", []) +
self.regex_handlers.get(event.type, [])
)
log.debug("got %d", len(matching_handlers))
for handler in matching_handlers:
log.debug("checking %s vs. %s", handler, event.arguments)
for line in event.arguments:
match = re.search(handler.regex, line)
if match:
log.debug("match!")
result = handler.callback(connection, event, match)
if result == "NO MORE":
return
with self.mutex:
# doing regex version first as it has the potential to be more specific
log.debug("checking regex handlers for %s", event.type)
matching_handlers = sorted(
self.regex_handlers.get("all_events", []) +
self.regex_handlers.get(event.type, [])
)
log.debug("got %d", len(matching_handlers))
for handler in matching_handlers:
log.debug("checking %s vs. %s", handler, event.arguments)
for line in event.arguments:
match = re.search(handler.regex, line)
if match:
log.debug("match!")
result = handler.callback(connection, event, match)
if result == "NO MORE":
return
matching_handlers = sorted(
self.handlers.get("all_events", []) +
self.handlers.get(event.type, [])
)
for handler in matching_handlers:
log.debug("not-match")
result = handler.callback(connection, event)
if result == "NO MORE":
return
matching_handlers = sorted(
self.handlers.get("all_events", []) +
self.handlers.get(event.type, [])
)
for handler in matching_handlers:
log.debug("not-match")
result = handler.callback(connection, event)
if result == "NO MORE":
return
except Exception as ex:
log.error("caught exception!")
log.exception(ex)
connection.privmsg(event.target, str(ex))
def try_recursion(self, connection, event):
"""Scan message for subcommands to execute and use as part of this command.

View File

@ -11,10 +11,11 @@ django-adminplus==0.5
django-bootstrap3==8.1.0
django-extensions==1.7.6
django-registration-redux==1.4
Django==1.10.5
django==1.10.5
djangorestframework==3.5.3
dodgy==0.1.9 # via prospector
first==2.0.1 # via pip-tools
future==0.16.0 # via parsedatetime
inflect==0.2.5 # via jaraco.itertools
irc==15.0.6
isort==4.2.5 # via pylint
@ -31,6 +32,7 @@ mccabe==0.6.1 # via prospector, pylint
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==2.0.1 # via requests-oauthlib
packaging==16.8 # via setuptools
parsedatetime==2.2
pep8-naming==0.4.1 # via prospector
pip-tools==1.8.0
ply==3.10

View File

@ -8,8 +8,9 @@ django-adminplus==0.5
django-bootstrap3==8.1.0
django-extensions==1.7.6
django-registration-redux==1.4
Django==1.10.5
django==1.10.5
djangorestframework==3.5.3
future==0.16.0 # via parsedatetime
inflect==0.2.5 # via jaraco.itertools
irc==15.0.6
jaraco.classes==1.4 # via jaraco.collections
@ -21,6 +22,7 @@ jaraco.stream==1.1.1 # via irc
jaraco.text==1.9 # via irc, jaraco.collections
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==2.0.1 # via requests-oauthlib
parsedatetime==2.2
ply==3.10
psycopg2==2.6.2
python-dateutil==2.6.0

View File

@ -5,6 +5,7 @@ django-extensions # more commands
django-registration-redux # registration views/forms
djangorestframework # dispatch WS API
irc # core
parsedatetime # relative date stuff in countdown
ply # dice lex/yacc compiler
python-dateutil # countdown relative math
python-gitlab # client for the gitlab bot

View File

@ -8,8 +8,9 @@ django-adminplus==0.5
django-bootstrap3==8.1.0
django-extensions==1.7.6
django-registration-redux==1.4
Django==1.10.5
django==1.10.5
djangorestframework==3.5.3
future==0.16.0 # via parsedatetime
inflect==0.2.5 # via jaraco.itertools
irc==15.0.6
jaraco.classes==1.4 # via jaraco.collections
@ -21,6 +22,7 @@ jaraco.stream==1.1.1 # via irc
jaraco.text==1.9 # via irc, jaraco.collections
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==2.0.1 # via requests-oauthlib
parsedatetime==2.2
ply==3.10
python-dateutil==2.6.0
python-gitlab==0.18