Merge branch 'countdown-reminders' into 'master'
Store and report on reminders via the countdown module See merge request !15
This commit is contained in:
commit
081648c074
|
@ -0,0 +1 @@
|
||||||
|
"""Countdown supports denoting points in time, and sending reminders about them to IRC."""
|
|
@ -1,43 +1,133 @@
|
||||||
"""Access to countdown items through bot commands."""
|
"""Access to countdown items through bot commands."""
|
||||||
|
|
||||||
import logging
|
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 dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from ircbot.lib import Plugin
|
|
||||||
from countdown.models import CountdownItem
|
from countdown.models import CountdownItem
|
||||||
|
from ircbot.lib import Plugin, reply_destination_for_event
|
||||||
|
|
||||||
log = logging.getLogger('countdown.ircplugin')
|
log = logging.getLogger('countdown.ircplugin')
|
||||||
|
|
||||||
|
|
||||||
class Countdown(Plugin):
|
class Countdown(Plugin):
|
||||||
|
|
||||||
"""Report on countdown items."""
|
"""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):
|
def start(self):
|
||||||
"""Set up handlers."""
|
"""Set up handlers."""
|
||||||
|
self.running_reminders = []
|
||||||
|
self.send_reminders = True
|
||||||
|
|
||||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$',
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$',
|
||||||
self.handle_item_list, -20)
|
self.handle_item_list, -20)
|
||||||
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(\S+)$',
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(\S+)$',
|
||||||
self.handle_item_detail, -20)
|
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()
|
super(Countdown, self).start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Tear down handlers."""
|
"""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_list)
|
||||||
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_item_detail)
|
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()
|
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):
|
def handle_item_detail(self, connection, event, match):
|
||||||
"""Provide the details of one countdown item."""
|
"""Provide the details of one countdown item."""
|
||||||
|
|
||||||
name = match.group(1)
|
name = match.group(1)
|
||||||
|
|
||||||
if name != 'list':
|
if name != 'list':
|
||||||
|
@ -65,7 +155,6 @@ class Countdown(Plugin):
|
||||||
|
|
||||||
def handle_item_list(self, connection, event, match):
|
def handle_item_list(self, connection, event, match):
|
||||||
"""List all countdown items."""
|
"""List all countdown items."""
|
||||||
|
|
||||||
items = CountdownItem.objects.all()
|
items = CountdownItem.objects.all()
|
||||||
if len(items) > 0:
|
if len(items) > 0:
|
||||||
reply = "countdown items: {0:s}".format(", ".join([x.name for x in items]))
|
reply = "countdown items: {0:s}".format(", ".join([x.name for x in items]))
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -10,15 +10,19 @@ log = logging.getLogger('countdown.models')
|
||||||
|
|
||||||
|
|
||||||
class CountdownItem(models.Model):
|
class CountdownItem(models.Model):
|
||||||
|
|
||||||
"""Track points in time."""
|
"""Track points in time."""
|
||||||
|
|
||||||
name = models.CharField(max_length=64, default='')
|
name = models.CharField(max_length=64, default='')
|
||||||
at_time = models.DateTimeField()
|
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)
|
created_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""String representation."""
|
"""String representation."""
|
||||||
|
|
||||||
return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z'))
|
return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z'))
|
||||||
|
|
|
@ -144,51 +144,56 @@ class DrReactor(irc.client.Reactor):
|
||||||
Also supports regex handlers.
|
Also supports regex handlers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.debug("EVENT: e[%s] s[%s] t[%s] a[%s]", event.type, event.source,
|
try:
|
||||||
event.target, event.arguments)
|
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
|
# only do aliasing for pubmsg/privmsg
|
||||||
if event.type in ['pubmsg', 'privmsg']:
|
if event.type in ['pubmsg', 'privmsg']:
|
||||||
what = event.arguments[0]
|
what = event.arguments[0]
|
||||||
log.debug("checking for alias for %s", what)
|
log.debug("checking for alias for %s", what)
|
||||||
|
|
||||||
for alias in Alias.objects.all():
|
for alias in Alias.objects.all():
|
||||||
repl = alias.replace(what)
|
repl = alias.replace(what)
|
||||||
if repl:
|
if repl:
|
||||||
# we found an alias for our given string, doing a replace
|
# we found an alias for our given string, doing a replace
|
||||||
event.arguments[0] = repl
|
event.arguments[0] = repl
|
||||||
# recurse, in case there's another alias in this one
|
# recurse, in case there's another alias in this one
|
||||||
return self._handle_event(connection, event)
|
return self._handle_event(connection, event)
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
# doing regex version first as it has the potential to be more specific
|
# doing regex version first as it has the potential to be more specific
|
||||||
log.debug("checking regex handlers for %s", event.type)
|
log.debug("checking regex handlers for %s", event.type)
|
||||||
matching_handlers = sorted(
|
matching_handlers = sorted(
|
||||||
self.regex_handlers.get("all_events", []) +
|
self.regex_handlers.get("all_events", []) +
|
||||||
self.regex_handlers.get(event.type, [])
|
self.regex_handlers.get(event.type, [])
|
||||||
)
|
)
|
||||||
log.debug("got %d", len(matching_handlers))
|
log.debug("got %d", len(matching_handlers))
|
||||||
for handler in matching_handlers:
|
for handler in matching_handlers:
|
||||||
log.debug("checking %s vs. %s", handler, event.arguments)
|
log.debug("checking %s vs. %s", handler, event.arguments)
|
||||||
for line in event.arguments:
|
for line in event.arguments:
|
||||||
match = re.search(handler.regex, line)
|
match = re.search(handler.regex, line)
|
||||||
if match:
|
if match:
|
||||||
log.debug("match!")
|
log.debug("match!")
|
||||||
result = handler.callback(connection, event, match)
|
result = handler.callback(connection, event, match)
|
||||||
if result == "NO MORE":
|
if result == "NO MORE":
|
||||||
return
|
return
|
||||||
|
|
||||||
matching_handlers = sorted(
|
matching_handlers = sorted(
|
||||||
self.handlers.get("all_events", []) +
|
self.handlers.get("all_events", []) +
|
||||||
self.handlers.get(event.type, [])
|
self.handlers.get(event.type, [])
|
||||||
)
|
)
|
||||||
for handler in matching_handlers:
|
for handler in matching_handlers:
|
||||||
log.debug("not-match")
|
log.debug("not-match")
|
||||||
result = handler.callback(connection, event)
|
result = handler.callback(connection, event)
|
||||||
if result == "NO MORE":
|
if result == "NO MORE":
|
||||||
return
|
return
|
||||||
|
except Exception as ex:
|
||||||
|
log.error("caught exception!")
|
||||||
|
log.exception(ex)
|
||||||
|
connection.privmsg(event.target, str(ex))
|
||||||
|
|
||||||
def try_recursion(self, connection, event):
|
def try_recursion(self, connection, event):
|
||||||
"""Scan message for subcommands to execute and use as part of this command.
|
"""Scan message for subcommands to execute and use as part of this command.
|
||||||
|
|
|
@ -11,10 +11,11 @@ django-adminplus==0.5
|
||||||
django-bootstrap3==8.1.0
|
django-bootstrap3==8.1.0
|
||||||
django-extensions==1.7.6
|
django-extensions==1.7.6
|
||||||
django-registration-redux==1.4
|
django-registration-redux==1.4
|
||||||
Django==1.10.5
|
django==1.10.5
|
||||||
djangorestframework==3.5.3
|
djangorestframework==3.5.3
|
||||||
dodgy==0.1.9 # via prospector
|
dodgy==0.1.9 # via prospector
|
||||||
first==2.0.1 # via pip-tools
|
first==2.0.1 # via pip-tools
|
||||||
|
future==0.16.0 # via parsedatetime
|
||||||
inflect==0.2.5 # via jaraco.itertools
|
inflect==0.2.5 # via jaraco.itertools
|
||||||
irc==15.0.6
|
irc==15.0.6
|
||||||
isort==4.2.5 # via pylint
|
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
|
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
|
||||||
oauthlib==2.0.1 # via requests-oauthlib
|
oauthlib==2.0.1 # via requests-oauthlib
|
||||||
packaging==16.8 # via setuptools
|
packaging==16.8 # via setuptools
|
||||||
|
parsedatetime==2.2
|
||||||
pep8-naming==0.4.1 # via prospector
|
pep8-naming==0.4.1 # via prospector
|
||||||
pip-tools==1.8.0
|
pip-tools==1.8.0
|
||||||
ply==3.10
|
ply==3.10
|
||||||
|
|
|
@ -8,8 +8,9 @@ django-adminplus==0.5
|
||||||
django-bootstrap3==8.1.0
|
django-bootstrap3==8.1.0
|
||||||
django-extensions==1.7.6
|
django-extensions==1.7.6
|
||||||
django-registration-redux==1.4
|
django-registration-redux==1.4
|
||||||
Django==1.10.5
|
django==1.10.5
|
||||||
djangorestframework==3.5.3
|
djangorestframework==3.5.3
|
||||||
|
future==0.16.0 # via parsedatetime
|
||||||
inflect==0.2.5 # via jaraco.itertools
|
inflect==0.2.5 # via jaraco.itertools
|
||||||
irc==15.0.6
|
irc==15.0.6
|
||||||
jaraco.classes==1.4 # via jaraco.collections
|
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
|
jaraco.text==1.9 # via irc, jaraco.collections
|
||||||
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
|
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
|
||||||
oauthlib==2.0.1 # via requests-oauthlib
|
oauthlib==2.0.1 # via requests-oauthlib
|
||||||
|
parsedatetime==2.2
|
||||||
ply==3.10
|
ply==3.10
|
||||||
psycopg2==2.6.2
|
psycopg2==2.6.2
|
||||||
python-dateutil==2.6.0
|
python-dateutil==2.6.0
|
||||||
|
|
|
@ -5,6 +5,7 @@ django-extensions # more commands
|
||||||
django-registration-redux # registration views/forms
|
django-registration-redux # registration views/forms
|
||||||
djangorestframework # dispatch WS API
|
djangorestframework # dispatch WS API
|
||||||
irc # core
|
irc # core
|
||||||
|
parsedatetime # relative date stuff in countdown
|
||||||
ply # dice lex/yacc compiler
|
ply # dice lex/yacc compiler
|
||||||
python-dateutil # countdown relative math
|
python-dateutil # countdown relative math
|
||||||
python-gitlab # client for the gitlab bot
|
python-gitlab # client for the gitlab bot
|
||||||
|
|
|
@ -8,8 +8,9 @@ django-adminplus==0.5
|
||||||
django-bootstrap3==8.1.0
|
django-bootstrap3==8.1.0
|
||||||
django-extensions==1.7.6
|
django-extensions==1.7.6
|
||||||
django-registration-redux==1.4
|
django-registration-redux==1.4
|
||||||
Django==1.10.5
|
django==1.10.5
|
||||||
djangorestframework==3.5.3
|
djangorestframework==3.5.3
|
||||||
|
future==0.16.0 # via parsedatetime
|
||||||
inflect==0.2.5 # via jaraco.itertools
|
inflect==0.2.5 # via jaraco.itertools
|
||||||
irc==15.0.6
|
irc==15.0.6
|
||||||
jaraco.classes==1.4 # via jaraco.collections
|
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
|
jaraco.text==1.9 # via irc, jaraco.collections
|
||||||
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
|
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
|
||||||
oauthlib==2.0.1 # via requests-oauthlib
|
oauthlib==2.0.1 # via requests-oauthlib
|
||||||
|
parsedatetime==2.2
|
||||||
ply==3.10
|
ply==3.10
|
||||||
python-dateutil==2.6.0
|
python-dateutil==2.6.0
|
||||||
python-gitlab==0.18
|
python-gitlab==0.18
|
||||||
|
|
Loading…
Reference in New Issue