Reminder support!

This commit is contained in:
osmarks 2020-05-12 17:08:03 +01:00
parent d12a7833d2
commit 6db8a42318
5 changed files with 100 additions and 298 deletions

View File

@ -11,6 +11,16 @@ CREATE TABLE deleted_items (
""", """,
""" """
CREATE INDEX deleted_items_timestamps ON deleted_items(timestamp); CREATE INDEX deleted_items_timestamps ON deleted_items(timestamp);
""",
"""
CREATE TABLE reminders (
id INTEGER PRIMARY KEY,
remind_timestamp INTEGER NOT NULL,
created_timestamp INTEGER NOT NULL,
reminder TEXT NOT NULL,
expired INTEGER NOT NULL,
extra TEXT NOT NULL
);
""" """
] ]

View File

@ -3,6 +3,7 @@ import toml
import logging import logging
import subprocess import subprocess
import discord.ext.commands as commands import discord.ext.commands as commands
import discord.ext.tasks as tasks
import re import re
import asyncio import asyncio
import json import json
@ -11,6 +12,7 @@ from datetime import timezone, datetime
import tio import tio
import db import db
import util
def timestamp(): return int(datetime.now(tz=timezone.utc).timestamp()) def timestamp(): return int(datetime.now(tz=timezone.utc).timestamp())
@ -42,10 +44,6 @@ cleaner = discord.ext.commands.clean_content()
def clean(ctx, text): def clean(ctx, text):
return cleaner.convert(ctx, text) return cleaner.convert(ctx, text)
@bot.event
async def on_ready():
await bot.change_presence(status=discord.Status.online, activity=discord.Activity(type=discord.ActivityType.listening, name="commands beginning with ++"))
@bot.command(help="Gives you a random fortune as generated by `fortune`.") @bot.command(help="Gives you a random fortune as generated by `fortune`.")
async def fortune(ctx): async def fortune(ctx):
await ctx.send(subprocess.run(["fortune"], stdout=subprocess.PIPE, encoding="UTF-8").stdout) await ctx.send(subprocess.run(["fortune"], stdout=subprocess.PIPE, encoding="UTF-8").stdout)
@ -138,6 +136,52 @@ async def supported_langs(ctx, search=None):
if acc == "": acc = "No results." if acc == "": acc = "No results."
await ctx.send(acc) await ctx.send(acc)
@bot.command(help="Set a reminder. All times are UTC. Reminders are only checked once per minute.", rest_is_raw=True)
async def remind(ctx, time, *, reminder):
reminder = reminder.strip()
if len(reminder) > 512:
await ctx.send(embed=error_embed("Maximum reminder length is 512 characters"))
return
extra_data = {
"author_id": ctx.author.id,
"channel_id": ctx.message.channel.id,
"original_time_spec": time
}
time = util.parse_time(time)
await database.execute("INSERT INTO reminders (remind_timestamp, created_timestamp, reminder, expired, extra) VALUES (?, ?, ?, ?, ?)",
(time.timestamp(), timestamp(), reminder, 0, json.dumps(extra_data)))
await database.commit()
await ctx.send(f"Reminder scheduled for {util.format_time(time)}.")
@tasks.loop(seconds=60)
async def remind_worker():
csr = database.execute("SELECT * FROM reminders WHERE expired = 0 AND remind_timestamp < ?", (timestamp(),))
to_expire = []
async with csr as cursor:
async for row in cursor:
rid, remind_timestamp, created_timestamp, reminder_text, _, extra = row
try:
remind_timestamp = datetime.utcfromtimestamp(remind_timestamp)
created_timestamp = datetime.utcfromtimestamp(created_timestamp)
extra = json.loads(extra)
uid = extra["author_id"]
text = f"<@{uid}> Reminder queued at {util.format_time(created_timestamp)}: {reminder_text}"
channel = bot.get_channel(extra["channel_id"])
await channel.send(text)
to_expire.append(rid)
except Exception as e:
logging.warning("Could not send reminder %d", rid, exc_info=e)
for expiry_id in to_expire:
logging.info("Expiring reminder %d", expiry_id)
await database.execute("UPDATE reminders SET expired = 1 WHERE id = ?", (expiry_id,))
await database.commit()
@bot.event
async def on_ready():
logging.info("Connected as " + bot.user.name)
await bot.change_presence(status=discord.Status.online, activity=discord.Activity(type=discord.ActivityType.listening, name="commands beginning with ++"))
remind_worker.start()
async def run_bot(): async def run_bot():
global database global database
database = await db.init(config["database"]) database = await db.init(config["database"])
@ -148,6 +192,7 @@ if __name__ == '__main__':
try: try:
loop.run_until_complete(run_bot()) loop.run_until_complete(run_bot())
except KeyboardInterrupt: except KeyboardInterrupt:
remind_worker.cancel()
loop.run_until_complete(bot.logout()) loop.run_until_complete(bot.logout())
finally: finally:
loop.close() loop.close()

View File

@ -1,292 +0,0 @@
import random
def generate(length):
# Generated from here (https://unicode-table.com/en/blocks/) with this vaguely horrible snippet:
# Array.from(document.querySelectorAll("span.range")).map(node => node.textContent.split("—").map(x => x.trim())).map(([x, y]) => ` (0x${x}, 0x${y}),`).join("\n")
include_ranges = [
(0x0000, 0x001F),
(0x0020, 0x007F),
(0x0080, 0x00FF),
(0x0100, 0x017F),
(0x0180, 0x024F),
(0x0250, 0x02AF),
(0x02B0, 0x02FF),
(0x0300, 0x036F),
(0x0370, 0x03FF),
(0x0400, 0x04FF),
(0x0500, 0x052F),
(0x0530, 0x058F),
(0x0590, 0x05FF),
(0x0600, 0x06FF),
(0x0700, 0x074F),
(0x0750, 0x077F),
(0x0780, 0x07BF),
(0x07C0, 0x07FF),
(0x0800, 0x083F),
(0x0840, 0x085F),
(0x0860, 0x086F),
(0x08A0, 0x08FF),
(0x0900, 0x097F),
(0x0980, 0x09FF),
(0x0A00, 0x0A7F),
(0x0A80, 0x0AFF),
(0x0B00, 0x0B7F),
(0x0B80, 0x0BFF),
(0x0C00, 0x0C7F),
(0x0C80, 0x0CFF),
(0x0D00, 0x0D7F),
(0x0D80, 0x0DFF),
(0x0E00, 0x0E7F),
(0x0E80, 0x0EFF),
(0x0F00, 0x0FFF),
(0x1000, 0x109F),
(0x10A0, 0x10FF),
(0x1100, 0x11FF),
(0x1200, 0x137F),
(0x1380, 0x139F),
(0x13A0, 0x13FF),
(0x1400, 0x167F),
(0x1680, 0x169F),
(0x16A0, 0x16FF),
(0x1700, 0x171F),
(0x1720, 0x173F),
(0x1740, 0x175F),
(0x1760, 0x177F),
(0x1780, 0x17FF),
(0x1800, 0x18AF),
(0x18B0, 0x18FF),
(0x1900, 0x194F),
(0x1950, 0x197F),
(0x1980, 0x19DF),
(0x19E0, 0x19FF),
(0x1A00, 0x1A1F),
(0x1A20, 0x1AAF),
(0x1AB0, 0x1AFF),
(0x1B00, 0x1B7F),
(0x1B80, 0x1BBF),
(0x1BC0, 0x1BFF),
(0x1C00, 0x1C4F),
(0x1C50, 0x1C7F),
(0x1C80, 0x1C8F),
(0x1CC0, 0x1CCF),
(0x1CD0, 0x1CFF),
(0x1D00, 0x1D7F),
(0x1D80, 0x1DBF),
(0x1DC0, 0x1DFF),
(0x1E00, 0x1EFF),
(0x1F00, 0x1FFF),
(0x2000, 0x206F),
(0x2070, 0x209F),
(0x20A0, 0x20CF),
(0x20D0, 0x20FF),
(0x2100, 0x214F),
(0x2150, 0x218F),
(0x2190, 0x21FF),
(0x2200, 0x22FF),
(0x2300, 0x23FF),
(0x2400, 0x243F),
(0x2440, 0x245F),
(0x2460, 0x24FF),
(0x2500, 0x257F),
(0x2580, 0x259F),
(0x25A0, 0x25FF),
(0x2600, 0x26FF),
(0x2700, 0x27BF),
(0x27C0, 0x27EF),
(0x27F0, 0x27FF),
(0x2800, 0x28FF),
(0x2900, 0x297F),
(0x2980, 0x29FF),
(0x2A00, 0x2AFF),
(0x2B00, 0x2BFF),
(0x2C00, 0x2C5F),
(0x2C60, 0x2C7F),
(0x2C80, 0x2CFF),
(0x2D00, 0x2D2F),
(0x2D30, 0x2D7F),
(0x2D80, 0x2DDF),
(0x2DE0, 0x2DFF),
(0x2E00, 0x2E7F),
(0x2E80, 0x2EFF),
(0x2F00, 0x2FDF),
(0x2FF0, 0x2FFF),
(0x3000, 0x303F),
(0x3040, 0x309F),
(0x30A0, 0x30FF),
(0x3100, 0x312F),
(0x3130, 0x318F),
(0x3190, 0x319F),
(0x31A0, 0x31BF),
(0x31C0, 0x31EF),
(0x31F0, 0x31FF),
(0x3200, 0x32FF),
(0x3300, 0x33FF),
(0x3400, 0x4DBF),
(0x4DC0, 0x4DFF),
(0x4E00, 0x9FFF),
(0xA000, 0xA48F),
(0xA490, 0xA4CF),
(0xA4D0, 0xA4FF),
(0xA500, 0xA63F),
(0xA640, 0xA69F),
(0xA6A0, 0xA6FF),
(0xA700, 0xA71F),
(0xA720, 0xA7FF),
(0xA800, 0xA82F),
(0xA830, 0xA83F),
(0xA840, 0xA87F),
(0xA880, 0xA8DF),
(0xA8E0, 0xA8FF),
(0xA900, 0xA92F),
(0xA930, 0xA95F),
(0xA960, 0xA97F),
(0xA980, 0xA9DF),
(0xA9E0, 0xA9FF),
(0xAA00, 0xAA5F),
(0xAA60, 0xAA7F),
(0xAA80, 0xAADF),
(0xAAE0, 0xAAFF),
(0xAB00, 0xAB2F),
(0xAB30, 0xAB6F),
(0xAB70, 0xABBF),
(0xABC0, 0xABFF),
(0xAC00, 0xD7AF),
(0xD7B0, 0xD7FF),
(0xD800, 0xDB7F),
(0xDB80, 0xDBFF),
(0xDC00, 0xDFFF),
(0xE000, 0xF8FF),
(0xF900, 0xFAFF),
(0xFB00, 0xFB4F),
(0xFB50, 0xFDFF),
(0xFE00, 0xFE0F),
(0xFE10, 0xFE1F),
(0xFE20, 0xFE2F),
(0xFE30, 0xFE4F),
(0xFE50, 0xFE6F),
(0xFE70, 0xFEFF),
(0xFF00, 0xFFEF),
(0xFFF0, 0xFFFF),
(0x10000, 0x1007F),
(0x10080, 0x100FF),
(0x10100, 0x1013F),
(0x10140, 0x1018F),
(0x10190, 0x101CF),
(0x101D0, 0x101FF),
(0x10280, 0x1029F),
(0x102A0, 0x102DF),
(0x102E0, 0x102FF),
(0x10300, 0x1032F),
(0x10330, 0x1034F),
(0x10350, 0x1037F),
(0x10380, 0x1039F),
(0x103A0, 0x103DF),
(0x10400, 0x1044F),
(0x10450, 0x1047F),
(0x10480, 0x104AF),
(0x104B0, 0x104FF),
(0x10500, 0x1052F),
(0x10530, 0x1056F),
(0x10600, 0x1077F),
(0x10800, 0x1083F),
(0x10840, 0x1085F),
(0x10860, 0x1087F),
(0x10880, 0x108AF),
(0x108E0, 0x108FF),
(0x10900, 0x1091F),
(0x10920, 0x1093F),
(0x10980, 0x1099F),
(0x109A0, 0x109FF),
(0x10A00, 0x10A5F),
(0x10A60, 0x10A7F),
(0x10A80, 0x10A9F),
(0x10AC0, 0x10AFF),
(0x10B00, 0x10B3F),
(0x10B40, 0x10B5F),
(0x10B60, 0x10B7F),
(0x10B80, 0x10BAF),
(0x10C00, 0x10C4F),
(0x10C80, 0x10CFF),
(0x10E60, 0x10E7F),
(0x11000, 0x1107F),
(0x11080, 0x110CF),
(0x110D0, 0x110FF),
(0x11100, 0x1114F),
(0x11150, 0x1117F),
(0x11180, 0x111DF),
(0x111E0, 0x111FF),
(0x11200, 0x1124F),
(0x11280, 0x112AF),
(0x112B0, 0x112FF),
(0x11300, 0x1137F),
(0x11400, 0x1147F),
(0x11480, 0x114DF),
(0x11580, 0x115FF),
(0x11600, 0x1165F),
(0x11660, 0x1167F),
(0x11680, 0x116CF),
(0x11700, 0x1173F),
(0x118A0, 0x118FF),
(0x11A00, 0x11A4F),
(0x11A50, 0x11AAF),
(0x11AC0, 0x11AFF),
(0x11C00, 0x11C6F),
(0x11C70, 0x11CBF),
(0x11D00, 0x11D5F),
(0x12000, 0x123FF),
(0x12400, 0x1247F),
(0x12480, 0x1254F),
(0x13000, 0x1342F),
(0x14400, 0x1467F),
(0x16800, 0x16A3F),
(0x16A40, 0x16A6F),
(0x16AD0, 0x16AFF),
(0x16B00, 0x16B8F),
(0x16F00, 0x16F9F),
(0x16FE0, 0x16FFF),
(0x17000, 0x187FF),
(0x18800, 0x18AFF),
(0x1B000, 0x1B0FF),
(0x1B100, 0x1B12F),
(0x1B170, 0x1B2FF),
(0x1BC00, 0x1BC9F),
(0x1BCA0, 0x1BCAF),
(0x1D000, 0x1D0FF),
(0x1D100, 0x1D1FF),
(0x1D200, 0x1D24F),
(0x1D300, 0x1D35F),
(0x1D360, 0x1D37F),
(0x1D400, 0x1D7FF),
(0x1D800, 0x1DAAF),
(0x1E000, 0x1E02F),
(0x1E800, 0x1E8DF),
(0x1E900, 0x1E95F),
(0x1EE00, 0x1EEFF),
(0x1F000, 0x1F02F),
(0x1F030, 0x1F09F),
(0x1F0A0, 0x1F0FF),
(0x1F100, 0x1F1FF),
(0x1F200, 0x1F2FF),
(0x1F300, 0x1F5FF),
(0x1F600, 0x1F64F),
(0x1F650, 0x1F67F),
(0x1F680, 0x1F6FF),
(0x1F700, 0x1F77F),
(0x1F780, 0x1F7FF),
(0x1F800, 0x1F8FF),
(0x1F900, 0x1F9FF),
(0x20000, 0x2A6DF),
(0x2A700, 0x2B73F),
(0x2B740, 0x2B81F),
(0x2B820, 0x2CEAF),
(0x2CEB0, 0x2EBEF),
(0x2F800, 0x2FA1F),
(0xE0000, 0xE007F),
(0xE0100, 0xE01EF),
]
alphabet = [
chr(code_point) for current_range in include_ranges
for code_point in range(current_range[0], current_range[1] + 1)
]
return ''.join(random.choice(alphabet) for i in range(length))

View File

@ -9,7 +9,8 @@ def languages():
return tio.query_languages() return tio.query_languages()
aliases = { aliases = {
"python": "python3" "python": "python3",
"javascript": "javascript-node"
} }
client = http3.AsyncClient() client = http3.AsyncClient()
@ -23,4 +24,4 @@ async def run(lang, code):
if len(split) == 1: if len(split) == 1:
return False, real_lang, split[0], None return False, real_lang, split[0], None
else: else:
return True, real_lang, split[0], split[1] return True, real_lang, split[0], split[1]

38
src/util.py Normal file
View File

@ -0,0 +1,38 @@
import re
import datetime
import parsedatetime
from dateutil.relativedelta import relativedelta
# from here: https://github.com/Rapptz/RoboDanny/blob/18b92ae2f53927aedebc25fb5eca02c8f6d7a874/cogs/utils/time.py
short_timedelta_regex = re.compile("""
(?:(?P<years>[0-9])(?:years?|y))? # e.g. 2y
(?:(?P<months>[0-9]{1,2})(?:months?|mo))? # e.g. 2months
(?:(?P<weeks>[0-9]{1,4})(?:weeks?|w))? # e.g. 10w
(?:(?P<days>[0-9]{1,5})(?:days?|d))? # e.g. 14d
(?:(?P<hours>[0-9]{1,5})(?:hours?|h))? # e.g. 12h
(?:(?P<minutes>[0-9]{1,6})(?:minutes?|m))? # e.g. 10m
(?:(?P<seconds>[0-9]{1,7})(?:seconds?|s))? # e.g. 15s """, re.VERBOSE)
def parse_short_timedelta(text):
match = short_timedelta_regex.fullmatch(text)
if match is None or not match.group(0): raise ValueError("parse failed")
data = { k: int(v) for k, v in match.groupdict(default=0).items() }
return datetime.datetime.utcnow() + relativedelta(**data)
cal = parsedatetime.Calendar()
def parse_humantime(text):
time_struct, parse_status = cal.parse(text)
if parse_status == 1: return datetime.datetime(*time_struct[:6])
else: raise ValueError("parse failed")
def parse_time(text):
try: return datetime.datetime.strptime(text, "%d/%m/%Y")
except: pass
try: return parse_short_timedelta(text)
except: pass
try: return parse_humantime(text)
except: pass
raise ValueError("could not parse time")
def format_time(dt):
return dt.strftime("%H:%M:%S %d/%m/%Y")