Fixed timer plugin, now checks every hour (with aiocron) for timers in the next hour and sets them

This commit is contained in:
mrhanky 2017-08-22 13:20:44 +02:00
parent 4fdcad950c
commit 9d8405092e
No known key found for this signature in database
GPG Key ID: 67D772C481CB41B8
3 changed files with 63 additions and 105 deletions

View File

@ -1,15 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime
import irc3 import irc3
from aiocron import crontab
from docopt import Dict as DocOptDict from docopt import Dict as DocOptDict
from irc3.plugins.command import command from irc3.plugins.command import command
from irc3.utils import IrcString from irc3.utils import IrcString
from psycopg2 import Error from psycopg2 import Error
from psycopg2.extras import DictRow
from . import DatabasePlugin from . import DatabasePlugin
from ..utils import time_delta
@irc3.plugin @irc3.plugin
@ -19,23 +21,25 @@ class Timer(DatabasePlugin):
def __init__(self, bot: irc3.IrcBot): def __init__(self, bot: irc3.IrcBot):
super().__init__(bot) super().__init__(bot)
self.timers = set()
self.set_timers()
crontab('0 * * * *', func=self.set_timers)
# Fetch timers from database def set_timers(self):
"""Function which queries all timers in the next hour and schedules them."""
self.log.debug('Fetching timers')
self.cur.execute(''' self.cur.execute('''
select SELECT
id, mask, target, message, delay, ends_at *
from FROM
timers timers
WHERE
ends_at >= now()
AND ends_at < now() + INTERVAL '1h'
''') ''')
# Recreate timers for timer in self.cur.fetchall():
for res in self.cur.fetchall(): asyncio.ensure_future(self.exec_timer(timer))
self.start_timer(IrcString(res['mask']),
res['target'],
res['message'],
res['delay'],
res['ends_at'] - datetime.now(),
res['id'])
@command @command
def timer(self, mask: IrcString, target: IrcString, args: DocOptDict): def timer(self, mask: IrcString, target: IrcString, args: DocOptDict):
@ -44,63 +48,51 @@ class Timer(DatabasePlugin):
%%timer <delay> <message>... %%timer <delay> <message>...
""" """
delay = args['<delay>'] delay = args['<delay>']
delta = time_delta(delay) message = ' '.join(args['<message>'])
if not delta: if not re.match(r'\d+[smhdwy]|mon', delay):
self.bot.privmsg(target, 'Invalid timer delay') return 'Invalid timer delay: {}'.format(delay)
else:
message = ' '.join(args['<message>'])
values = [mask, target, message, delay]
try: values = [mask, target, message, delay]
# Insert into database (add now + delta)
self.cur.execute(''' try:
insert into self.cur.execute('''
INSERT INTO
timers (mask, target, message, delay, ends_at) timers (mask, target, message, delay, ends_at)
values VALUES
(%s, %s, %s, %s, %s) (%s, %s, %s, %s, now() + INTERVAL %s)
returning id RETURNING *
''', values + [datetime.now() + delta]) ''', values + [delay])
self.con.commit() self.con.commit()
# Add delta and id from inserted and start timer asyncio.ensure_future(self.exec_timer(self.cur.fetchone()))
values.extend([delta, self.cur.fetchone()['id']])
self.start_timer(*values)
# Send notice to user that timer has been set self.bot.notice(mask.nick, 'Timer in {delay} set: {message}'.format(delay=delay, message=message))
self.bot.notice(mask.nick, 'Timer in {delay} set: {message}' except Error as ex:
.format(delay=delay, message=message)) self.log.error(ex)
except Error: self.con.rollback()
# Rollback transaction on error
self.con.rollback()
def start_timer(self, mask: IrcString, target: IrcString, message: str, async def exec_timer(self, timer: DictRow):
delay: str, delta: timedelta, row_id: int): """Sets the actual timer (sleeps until it fires), sends the reminder and deletes the timer from database."""
"""Async function, sleeps for `delay` seconds and sends notification""" if timer['id'] in self.timers:
return
async def callback(): self.timers.add(timer['id'])
# Sleep if necessary until timed seconds = (timer['ends_at'] - datetime.now()).total_seconds()
seconds = delta.total_seconds() if seconds > 0.0:
if seconds > 0: await asyncio.sleep(seconds)
await asyncio.sleep(seconds)
# Send reminder self.bot.privmsg(timer['target'], '\x02[Timer]\x02 {nick}: {message} ({delay})'.format(
self.bot.privmsg(target, '\x02[Timer]\x0F {nick}: {message} ' message=timer['message'],
'({delay})'.format(message=message, nick=IrcString(timer['mask']).nick,
nick=mask.nick, delay=timer['delay'],
delay=delay)) ))
try: self.timers.remove(timer['id'])
# Delete timer from database self.cur.execute('''
self.cur.execute(''' DELETE FROM
delete from timers
timers WHERE
where id = %s
id = %s ''', [timer['id']])
''', [row_id]) self.con.commit()
self.con.commit()
except Error:
# Rollback transaction on error
self.con.rollback()
asyncio.ensure_future(callback())

View File

@ -1,51 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re
import random import random
from typing import Tuple from typing import Tuple
from pprint import pprint from pprint import pprint
from datetime import datetime, timedelta from datetime import datetime
TIME_UNITS = {
's': 'seconds',
'm': 'minutes',
'h': 'hours',
'd': 'days',
'w': 'weeks',
'mon': 'months',
'y': 'years',
}
pp = pprint pp = pprint
def re_generator(low: int = 5, high: int = 20) -> str:
return 'R{}'.format('E' * random.randint(low, high))
def date_from_iso(date: str) -> datetime: def date_from_iso(date: str) -> datetime:
return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ') return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ')
def time_delta(text: str) -> timedelta:
match = re.match(r'(\d+)(s|m|h|d|mon|w|y)', text)
if match:
num, unit = match.groups()
num = int(num)
if unit == 's':
unit = 'seconds'
elif unit == 'm':
unit = 'minutes'
elif unit == 'h':
unit = 'hours'
elif unit == 'd':
unit = 'days'
elif unit == 'w':
unit = 'weeks'
elif unit == 'mon':
unit = 'weeks'
num *= 4
elif unit == 'y':
unit = 'weeks'
num *= 52
return timedelta(**{unit: num})
def parse_int(val: str, select: bool = True) -> Tuple[int, str]: def parse_int(val: str, select: bool = True) -> Tuple[int, str]:
try: try:
val = int(val) val = int(val)
@ -60,7 +29,3 @@ def parse_int(val: str, select: bool = True) -> Tuple[int, str]:
return val, order return val, order
except ValueError: except ValueError:
pass pass
def re_generator(low: int = 5, high: int = 20) -> str:
return 'R{}'.format('E' * random.randint(low, high))

View File

@ -1,6 +1,7 @@
#git+https://github.com/gawel/irc3.git#egg=irc3 #git+https://github.com/gawel/irc3.git#egg=irc3
git+https://github.com/mrhanky17/irc3.git#egg=irc3 git+https://github.com/mrhanky17/irc3.git#egg=irc3
git+https://github.com/mrhanky17/docopt.git#egg=docopt git+https://github.com/mrhanky17/docopt.git#egg=docopt
aiocron==0.6
psycopg2==2.7.1 psycopg2==2.7.1
requests==2.14.2 requests==2.14.2
feedparser==5.2.1 feedparser==5.2.1