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

View File

@ -4,193 +4,79 @@ Reminds of things.
"""
import os
import re
import time
import sqlite3
import threading
from datetime import datetime
from datetime import datetime, timedelta
import config
from module import commands, example
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)
import module
regex = (
"(?=\d+[ywdhms])"
"(?:(\d+)y)?"
"(?:(\d+)w)?"
"(?:(\d+)d)?"
"(?:(\d+)h)?"
"(?:(\d+)m)?"
"(?:(\d+)s)?"
"(\d+y)?"
"(\d+w)?"
"(\d+d)?"
"(\d+h)?"
"(\d+m)?"
"(\d+s)?"
)
multiplier = [
60*60*24*365,
60*60*24*7,
60*60*24,
60*60,
60,
1
]
shorthand = {
'y': 'years',
'w': 'weeks',
'd': 'days',
'h': 'hours',
'm': 'minutes',
's': 'seconds',
}
@commands('remind')
@example('.remind 3h45m Go to class')
@module.commands('remind')
@module.example('.remind 3h45m Go to class')
def remind(bot, trigger):
"""Gives you a reminder in the given amount of time."""
if len(trigger.args) == 1:
return bot.msg("Missing arguments for reminder command.")
if len(trigger.args) == 2:
reminder = ''
else:
reg = re.search(regex, trigger.args[1])
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:])
duration = 0
for n, group in enumerate(re.search(regex, trigger.args[1]).groups()):
if not group:
continue
duration += int(group) * multiplier[n]
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]'))
@commands('at')
@example('.at 2012-12-21 18:00:00 End the world.')
@module.commands('at')
@module.example('.at 2012-12-21 18:00:00 End the world.')
def at(bot, trigger):
"""
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.")
if len(trigger.args) < 4:
reminder = ''
if ':' in trigger.args[1]:
at_time = datetime.now().strftime('%Y-%m-%d') + ' ' + trigger.args[1]
reminder = ' '.join(trigger.args[2:])
else:
at_time = ' '.join(trigger.args[1:3])
reminder = ' '.join(trigger.args[3:])
try:
at_time = datetime.strptime(' '.join(trigger.args[1:2]),
'%Y-%m-%d %H:%M:%S')
dt = datetime.strptime(at_time, '%Y-%m-%d %H:%M:%S')
except ValueError:
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):
"""
Inserts the reminder into the bot's memory and database so it can
eventually announce for it.
"""
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)
def announce_reminder(bot, channel, remindee, reminder):
"""Announce the reminder."""
if reminder:
msg = f"{remindee}: {reminder}"
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