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

View File

@ -1,51 +1,20 @@
# -*- coding: utf-8 -*-
import re
import random
from typing import Tuple
from pprint import pprint
from datetime import datetime, timedelta
TIME_UNITS = {
's': 'seconds',
'm': 'minutes',
'h': 'hours',
'd': 'days',
'w': 'weeks',
'mon': 'months',
'y': 'years',
}
from datetime import datetime
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:
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]:
try:
val = int(val)
@ -60,7 +29,3 @@ def parse_int(val: str, select: bool = True) -> Tuple[int, str]:
return val, order
except ValueError:
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/mrhanky17/irc3.git#egg=irc3
git+https://github.com/mrhanky17/docopt.git#egg=docopt
aiocron==0.6
psycopg2==2.7.1
requests==2.14.2
feedparser==5.2.1