Compare commits

..

2 Commits

Author SHA1 Message Date
ba683bcdf6 rewrite remind module 2020-01-13 13:12:36 -05:00
7a17e50069 added scheduler 2020-01-13 12:54:11 -05:00
3 changed files with 94 additions and 161 deletions

4
bot.py
View File

@ -16,6 +16,7 @@ import db
import tools import tools
import config import config
import loader import loader
import scheduler
from trigger import Trigger from trigger import Trigger
class Fulvia(irc.IRCClient): class Fulvia(irc.IRCClient):
@ -97,6 +98,9 @@ class Fulvia(irc.IRCClient):
self._disabled_modules = config.disabled_modules self._disabled_modules = config.disabled_modules
"""These modules will NOT be loaded when load_modules() is called.""" """These modules will NOT be loaded when load_modules() is called."""
self.scheduler = scheduler.Scheduler(self)
"""The bot's task scheduler."""
self.load_modules() self.load_modules()

View File

@ -4,193 +4,79 @@ Reminds of things.
""" """
import os import os
import re import re
import time from datetime import datetime, timedelta
import sqlite3
import threading
from datetime import datetime
import config import config
from module import commands, example import module
class MonitorThread(threading.Thread):
"""
A custom custom thread class for monitoring the time to announce
reminders. It allows itself to be stopped when there are no reminders
to look for.
"""
def __init__(self, bot):
threading.Thread.__init__(self)
self._bot = bot
self.stop = threading.Event()
def run(self):
while not self._bot.stillConnected():
time.sleep(1)
# don't try to say anything if we're not fully connected yet
while not self.stop.is_set():
now = int(time.time())
unixtimes = [int(key) for key in self._bot.memory["remind"].keys()]
oldtimes = [t for t in unixtimes if t <= now]
if oldtimes:
for oldtime in oldtimes:
for reminder in self._bot.memory["remind"][oldtime]:
channel, nick, message = reminder
if message:
self._bot.msg(channel, nick + ': ' + message)
else:
self._bot.msg(channel, nick + '!')
del self._bot.memory["remind"][oldtime]
delete_reminder(self._bot, oldtime)
if not self._bot.memory["remind"] or not self._bot.stillConnected():
self.stop.set()
self.stop.wait(2.5)
del self._bot.memory["remind_monitor"]
def start_monitor(bot):
"""
Starts the monitor thread. Does nothing if one is already running.
"""
if bot.memory.get("remind_monitor"):
return
t = MonitorThread(bot)
t.start()
bot.memory["remind_monitor"] = t
def load_database(bot):
"""
Loads all entries from the 'remind' table in the bot's database and
stores them in memory
"""
data = {}
reminds = bot.db.execute("SELECT * FROM remind").fetchall()
for remind in reminds:
unixtime, channel, nick, message = remind
reminder = (channel, nick, message)
try:
data[unixtime].append(reminder)
except KeyError:
data[unixtime] = [reminder]
return data
def insert_reminder(bot, unixtime, reminder):
"""
Inserts a new reminder into the 'remind' table in the bot's database.
reminder - a tuple containing (channel, nick, message)
"""
bot.db.execute("INSERT INTO remind (unixtime, channel, nick, message) "
"VALUES(?,?,?,?)", (unixtime,) + reminder)
def delete_reminder(bot, unixtime):
"""
Deletes a reminder from the 'remind' table in the bot's database, using
unixtime as the key.
"""
bot.db.execute("DELETE FROM remind WHERE unixtime = ?", (unixtime,))
def setup(bot):
con = bot.db.connect()
cur = con.cursor()
try:
cur.execute("SELECT * FROM remind").fetchone()
except sqlite3.OperationalError:
cur.execute("CREATE TABLE remind("
"unixtime INTEGER DEFAULT (STRFTIME('%s', 'now')),"
"channel TEXT,"
"nick TEXT,"
"message TEXT)")
con.commit()
con.close()
bot.memory["remind"] = load_database(bot)
start_monitor(bot)
regex = ( regex = (
"(?=\d+[ywdhms])" "(?=\d+[ywdhms])"
"(?:(\d+)y)?" "(\d+y)?"
"(?:(\d+)w)?" "(\d+w)?"
"(?:(\d+)d)?" "(\d+d)?"
"(?:(\d+)h)?" "(\d+h)?"
"(?:(\d+)m)?" "(\d+m)?"
"(?:(\d+)s)?" "(\d+s)?"
) )
multiplier = [ shorthand = {
60*60*24*365, 'y': 'years',
60*60*24*7, 'w': 'weeks',
60*60*24, 'd': 'days',
60*60, 'h': 'hours',
60, 'm': 'minutes',
1 's': 'seconds',
] }
@commands('remind') @module.commands('remind')
@example('.remind 3h45m Go to class') @module.example('.remind 3h45m Go to class')
def remind(bot, trigger): def remind(bot, trigger):
"""Gives you a reminder in the given amount of time.""" """Gives you a reminder in the given amount of time."""
if len(trigger.args) == 1: if len(trigger.args) == 1:
return bot.msg("Missing arguments for reminder command.") return bot.msg("Missing arguments for reminder command.")
if len(trigger.args) == 2:
reminder = '' reg = re.search(regex, trigger.args[1])
else: if not reg:
return bot.reply("I didn't understand that.")
args = {shorthand[g[-1]]: int(g[:-1]) for g in reg.groups() if g}
delta = timedelta(**args)
dt = datetime.now() + delta
reminder = ' '.join(trigger.args[2:]) reminder = ' '.join(trigger.args[2:])
args = (trigger.channel, trigger.nick, reminder)
duration = 0 bot.scheduler.add_task(announce_reminder, dt, args)
for n, group in enumerate(re.search(regex, trigger.args[1]).groups()): bot.reply("Okay, will remind at", dt.strftime('[%Y-%m-%d %H:%M:%S]'))
if not group:
continue
duration += int(group) * multiplier[n]
create_reminder(bot, trigger, duration, reminder)
@commands('at') @module.commands('at')
@example('.at 2012-12-21 18:00:00 End the world.') @module.example('.at 2012-12-21 18:00:00 End the world.')
def at(bot, trigger): def at(bot, trigger):
""" """
Gives you a reminder at the given time and date. Datetime must be in Gives you a reminder at the given time and date. Datetime must be in
YYYY-MM-DD HH:MM:SS format. Only the bot's timezone is used. YYYY-MM-DD HH:MM:SS format. Only the bot's timezone is used. If
YYYY-MM-DD is excluded, it is assumed to be today's date.
""" """
if len(trigger.args) < 3: if len(trigger.args) < 2:
return bot.msg("Missing arguments for reminder command.") return bot.msg("Missing arguments for reminder command.")
if len(trigger.args) < 4: if ':' in trigger.args[1]:
reminder = '' at_time = datetime.now().strftime('%Y-%m-%d') + ' ' + trigger.args[1]
reminder = ' '.join(trigger.args[2:])
else: else:
at_time = ' '.join(trigger.args[1:3])
reminder = ' '.join(trigger.args[3:]) reminder = ' '.join(trigger.args[3:])
try: try:
at_time = datetime.strptime(' '.join(trigger.args[1:2]), dt = datetime.strptime(at_time, '%Y-%m-%d %H:%M:%S')
'%Y-%m-%d %H:%M:%S')
except ValueError: except ValueError:
return bot.msg("Datetime improperly formatted.") return bot.msg("Datetime improperly formatted.")
diff = at_time - datetime.now()
duration = diff.seconds
create_reminder(bot, trigger, duration, reminder) args = (trigger.channel, trigger.nick, reminder)
bot.scheduler.add_task(announce_reminder, dt, args)
bot.reply("Okay, will remind at", dt.strftime('[%Y-%m-%d %H:%M:%S]'))
def create_reminder(bot, trigger, duration, message): def announce_reminder(bot, channel, remindee, reminder):
""" """Announce the reminder."""
Inserts the reminder into the bot's memory and database so it can if reminder:
eventually announce for it. msg = f"{remindee}: {reminder}"
"""
t = int(time.time()) + duration
reminder = (trigger.channel, trigger.nick, message)
try:
bot.memory["remind"][t].append(reminder)
except KeyError:
bot.memory["remind"][t] = [reminder]
start_monitor(bot)
insert_reminder(bot, t, reminder)
if duration >= 60:
remind_at = datetime.fromtimestamp(t)
t_format = config.default_time_format
timef = datetime.strftime(remind_at, t_format)
bot.reply('Okay, will remind at %s' % timef)
else: else:
bot.reply('Okay, will remind in %s secs' % duration) msg = f"{remindee}!"
bot.msg(channel, msg)

43
scheduler.py Normal file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""
Allows the bot to scheduler tasks to be performed later.
"""
import time
import pickle
import threading
from datetime import datetime
class Scheduler:
def __init__(self, bot):
self.bot = bot
self.tasks = []
self.lock = threading.Lock()
self._t = threading.Thread(target=self.loop)
self._t.start()
def loop(self):
while True:
self.lock.acquire()
tasks_due = [t for t in self.tasks if t[1] <= datetime.now()]
for task in tasks_due:
args = (self.bot,) + task[2]
t = threading.Thread(target=task[0], args=args)
t.start()
self.tasks.remove(task)
self.lock.release()
time.sleep(5)
def add_task(self, func, dt, args):
"""
`func` - The function to call. Must accept `bot` as the first
argument. Must be picklable.
`dt` - A datetime object representing when to call the function.
`args` - Arguments to call the function with, not including `bot`.
"""
self.lock.acquire()
self.tasks.append((func, dt, args))
self.lock.release()
# db
def add_periodic_task(self):
pass