2020-05-12 16:08:03 +00:00
|
|
|
|
import re
|
|
|
|
|
import datetime
|
|
|
|
|
import parsedatetime
|
2020-06-18 18:43:20 +00:00
|
|
|
|
import ast
|
|
|
|
|
import copy
|
2020-09-10 17:51:38 +00:00
|
|
|
|
import random
|
2020-05-12 16:08:03 +00:00
|
|
|
|
from dateutil.relativedelta import relativedelta
|
2020-10-11 13:40:39 +00:00
|
|
|
|
import json
|
|
|
|
|
import discord
|
|
|
|
|
import toml
|
2020-11-01 17:07:36 +00:00
|
|
|
|
import os.path
|
|
|
|
|
from discord.ext import commands
|
2020-11-01 17:32:02 +00:00
|
|
|
|
import hashlib
|
2020-10-11 13:40:39 +00:00
|
|
|
|
|
2020-11-25 19:04:10 +00:00
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
# update in place for runtime config reload
|
|
|
|
|
def load_config():
|
|
|
|
|
for k, v in toml.load(open("config.toml", "r")).items(): config[k] = v
|
|
|
|
|
|
|
|
|
|
load_config()
|
2020-10-11 13:40:39 +00:00
|
|
|
|
|
|
|
|
|
def timestamp(): return int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
|
2020-05-12 16:08:03 +00:00
|
|
|
|
|
2020-10-30 12:26:35 +00:00
|
|
|
|
prefixes = {
|
|
|
|
|
# big SI prefixes
|
|
|
|
|
"Y": 24, "Z": 21, "E": 18, "P": 15, "T": 12, "G": 9, "M": 6, "k": 3, "h": 2, "da": 1,
|
|
|
|
|
# small SI prefixes
|
|
|
|
|
"d": -1, "c": -2, "m": -3, "µ": -6, "μ": -6, "u": -6, "n": -9, "p": -12, "f": -15, "a": -18, "z": -21, "y": -24,
|
|
|
|
|
# highly dubiously useful unofficial prefixes
|
|
|
|
|
"R": 27, "r": -27, "Q": 30, "q": -30, "X": 27, "x": -27, "W": 30, "w": -30
|
|
|
|
|
}
|
|
|
|
|
number = "(-?[0-9]+(?:\.[0-9]+)?)(" + "|".join(prefixes.keys()) + ")?"
|
|
|
|
|
|
2020-10-30 14:59:37 +00:00
|
|
|
|
time_units = (
|
|
|
|
|
("galacticyears", "cosmicyears", "gy", "[Cc]y"),
|
|
|
|
|
("years", "y"),
|
|
|
|
|
("beelifespans", "🐝", "bees?"),
|
|
|
|
|
("months", "mo"),
|
|
|
|
|
("semesters",),
|
|
|
|
|
("fortnights", "ft?n?"),
|
|
|
|
|
("weeks", "w"),
|
|
|
|
|
("days", "d"),
|
|
|
|
|
("hours", "h"),
|
|
|
|
|
# Wikipedia tells me this is a traditional Chinese timekeeping unit
|
|
|
|
|
("ke",),
|
|
|
|
|
("minutes", "m"),
|
|
|
|
|
("seconds", "s")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tu_mappings = {
|
|
|
|
|
# dateutil dislikes fractional years, but this is 250My
|
|
|
|
|
"galacticyears": (7.8892315e15, "seconds"),
|
|
|
|
|
# apparently the average lifespan of a Western honey bee - I'm not very sure whether this is workers/drones/queens or what so TODO
|
|
|
|
|
"beelifespans": lambda: (random.randint(122, 152), "days"),
|
|
|
|
|
"semesters": (18, "weeks"),
|
|
|
|
|
"fortnights": (2, "weeks"),
|
|
|
|
|
"ke": (864, "seconds")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def rpartfor(u):
|
|
|
|
|
if u[0][-1] == "s":
|
|
|
|
|
l = [u[0] + "?"]
|
|
|
|
|
l.extend(u[1:])
|
|
|
|
|
else: l = u
|
|
|
|
|
return f"(?:(?P<{u[0]}>{number})(?:{'|'.join(l)}))?"
|
|
|
|
|
|
|
|
|
|
short_timedelta_regex = re.compile("\n".join(map(rpartfor, time_units)), re.VERBOSE)
|
2020-10-30 12:26:35 +00:00
|
|
|
|
|
|
|
|
|
def parse_prefixed(s):
|
|
|
|
|
match = re.match(number, s)
|
|
|
|
|
if not match: raise ValueError("does not match metric-prefixed integer format - ensure prefix is valid")
|
|
|
|
|
num = float(match.group(1))
|
|
|
|
|
prefix = match.group(2)
|
|
|
|
|
if prefix: num *= (10 ** prefixes[prefix])
|
|
|
|
|
return num
|
2020-05-12 16:08:03 +00:00
|
|
|
|
|
|
|
|
|
def parse_short_timedelta(text):
|
|
|
|
|
match = short_timedelta_regex.fullmatch(text)
|
|
|
|
|
if match is None or not match.group(0): raise ValueError("parse failed")
|
2020-10-30 12:26:35 +00:00
|
|
|
|
data = { k: parse_prefixed(v) if v else 0 for k, v in match.groupdict().items() }
|
2020-10-30 14:59:37 +00:00
|
|
|
|
for tu, mapping in tu_mappings.items():
|
|
|
|
|
if callable(mapping): mapping = mapping()
|
|
|
|
|
qty, resunit = mapping
|
|
|
|
|
data[resunit] += qty * data[tu]
|
|
|
|
|
del data[tu]
|
2020-10-11 13:40:39 +00:00
|
|
|
|
return datetime.datetime.now(tz=datetime.timezone.utc) + relativedelta(**data)
|
2020-05-12 16:08:03 +00:00
|
|
|
|
|
|
|
|
|
cal = parsedatetime.Calendar()
|
|
|
|
|
def parse_humantime(text):
|
2020-10-30 12:26:35 +00:00
|
|
|
|
dt_tuple = cal.nlp(text)
|
2020-10-30 14:59:37 +00:00
|
|
|
|
if dt_tuple: return dt_tuple[0][0].replace(tzinfo=datetime.timezone.utc)
|
2020-05-12 16:08:03 +00:00
|
|
|
|
else: raise ValueError("parse failed")
|
|
|
|
|
|
|
|
|
|
def parse_time(text):
|
2020-10-30 14:59:37 +00:00
|
|
|
|
try: return datetime.datetime.strptime(text, "%d/%m/%Y").replace(tzinfo=datetime.timezone.utc)
|
2020-05-12 16:08:03 +00:00
|
|
|
|
except: pass
|
|
|
|
|
try: return parse_short_timedelta(text)
|
|
|
|
|
except: pass
|
|
|
|
|
try: return parse_humantime(text)
|
|
|
|
|
except: pass
|
2020-10-30 12:26:35 +00:00
|
|
|
|
raise ValueError("time matches no available format")
|
2020-05-12 16:08:03 +00:00
|
|
|
|
|
|
|
|
|
def format_time(dt):
|
2020-06-18 18:43:20 +00:00
|
|
|
|
return dt.strftime("%H:%M:%S %d/%m/%Y")
|
|
|
|
|
|
2020-10-11 13:40:39 +00:00
|
|
|
|
timeparts = (
|
|
|
|
|
("y", "years"),
|
|
|
|
|
("mo", "months"),
|
|
|
|
|
("d", "days"),
|
2020-10-30 12:49:17 +00:00
|
|
|
|
("h", "hours"),
|
2020-10-11 13:40:39 +00:00
|
|
|
|
("m", "minutes"),
|
|
|
|
|
("s", "seconds")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def format_timedelta(from_, to):
|
|
|
|
|
d = relativedelta(to, from_)
|
2020-10-30 12:49:17 +00:00
|
|
|
|
out = ""
|
2020-10-11 13:40:39 +00:00
|
|
|
|
for short, attr in timeparts:
|
|
|
|
|
x = getattr(d, attr)
|
|
|
|
|
if x != 0: out += str(x) + short
|
2020-10-30 12:49:17 +00:00
|
|
|
|
return "0s" if out == "" else out
|
2020-10-11 13:40:39 +00:00
|
|
|
|
|
2020-06-18 18:43:20 +00:00
|
|
|
|
CODEBLOCK_REGEX = "^[^`]*```[a-zA-Z0-9_\-+]*\n(.+)```$"
|
|
|
|
|
CODELINE_REGEX = "^[^`]*`(.*)`$"
|
|
|
|
|
def extract_codeblock(s):
|
|
|
|
|
match1 = re.match(CODEBLOCK_REGEX, s, flags=re.DOTALL)
|
|
|
|
|
match2 = re.match(CODELINE_REGEX, s, flags=re.DOTALL)
|
|
|
|
|
if match1: return match1.group(1)
|
|
|
|
|
elif match2: return match2.group(1)
|
2020-10-10 15:32:42 +00:00
|
|
|
|
else: return s.strip()
|
2020-06-18 18:43:20 +00:00
|
|
|
|
|
|
|
|
|
# from https://github.com/Gorialis/jishaku/blob/master/jishaku/repl/compilation.py
|
|
|
|
|
CORO_CODE = """
|
|
|
|
|
async def repl_coroutine():
|
|
|
|
|
import asyncio
|
|
|
|
|
import aiohttp
|
|
|
|
|
import discord
|
|
|
|
|
from discord.ext import commands
|
|
|
|
|
"""
|
|
|
|
|
async def async_exec(code, loc, glob):
|
|
|
|
|
user_code = ast.parse(code, mode='exec')
|
|
|
|
|
wrapper = ast.parse(CORO_CODE, mode='exec')
|
|
|
|
|
funcdef = wrapper.body[-1]
|
|
|
|
|
funcdef.body.extend(user_code.body)
|
|
|
|
|
last_expr = funcdef.body[-1]
|
|
|
|
|
|
|
|
|
|
if isinstance(last_expr, ast.Expr):
|
|
|
|
|
funcdef.body.pop()
|
|
|
|
|
funcdef.body.append(ast.Return(last_expr.value))
|
|
|
|
|
ast.fix_missing_locations(wrapper)
|
|
|
|
|
|
|
|
|
|
exec(compile(wrapper, "<repl>", "exec"), loc, glob)
|
2020-09-10 17:51:38 +00:00
|
|
|
|
return await (loc.get("repl_coroutine") or glob.get("repl_coroutine"))()
|
|
|
|
|
|
2020-10-11 13:40:39 +00:00
|
|
|
|
def make_embed(*, fields=(), footer_text=None, **kwargs):
|
|
|
|
|
embed = discord.Embed(**kwargs)
|
|
|
|
|
for field in fields:
|
|
|
|
|
if len(field) > 2:
|
|
|
|
|
embed.add_field(name=field[0], value=field[1], inline=field[2])
|
|
|
|
|
else:
|
|
|
|
|
embed.add_field(name=field[0], value=field[1], inline=False)
|
|
|
|
|
if footer_text:
|
|
|
|
|
embed.set_footer(text=footer_text)
|
|
|
|
|
return embed
|
|
|
|
|
|
|
|
|
|
def error_embed(msg, title="Error"): return make_embed(color=config["colors"]["error"], description=msg, title=title)
|
|
|
|
|
def info_embed(title, msg, fields=()): return make_embed(color=config["colors"]["info"], description=msg, title=title, fields=fields)
|
|
|
|
|
|
2020-09-10 17:51:38 +00:00
|
|
|
|
# https://github.com/LyricLy/Esobot/blob/bcc9e548c84ea9b23fc832d0b0aaa8288de64886/cogs/general.py
|
|
|
|
|
lyrictable_raw = {
|
|
|
|
|
"a": "а",
|
|
|
|
|
"c": "с",
|
|
|
|
|
"e": "е",
|
|
|
|
|
"s": "ѕ",
|
|
|
|
|
"i": "і",
|
|
|
|
|
"j": "ј",
|
|
|
|
|
"o": "о",
|
|
|
|
|
"p": "р",
|
|
|
|
|
"y": "у",
|
|
|
|
|
"x": "х"
|
|
|
|
|
}
|
|
|
|
|
lyrictable = str.maketrans({v: k for k, v in lyrictable_raw.items()})
|
|
|
|
|
|
2020-10-10 15:32:42 +00:00
|
|
|
|
apioprefixes = ["cryo", "meta", "chrono", "contra", "ortho", "macro", "micro", "apeiro", "Ægypto", "equi", "anglo", "atto", "auro", "Australo"
|
|
|
|
|
"dys", "eu", "femto", "giga", "infra", "Inver", "kilo", "meso", "mono", "nano", "neo", "omni", "pico", "proto", "pseudo", "semi", "quasi",
|
|
|
|
|
"Scando", "silico", "sub", "hyper", "super", "tauto", "topo", "trans", "ultra", "uni", "ur-", "yocto", "zepto", "zetta"]
|
|
|
|
|
apioinfixes = ["cryo", "pyro", "chrono", "meta", "anarcho", "arachno", "aqua", "accelero", "hydro", "radio", "xeno", "morto", "thanato", "memeto",
|
|
|
|
|
"contra", "umbra", "macrono", "acantho", "acousto", "aceto", "acro", "aeolo", "hexa", "aero", "aesthio", "agro", "ferro", "alumino",
|
|
|
|
|
"ammonio", "anti", "ankylo", "aniso", "annulo", "apo", "abio", "archeo", "argento", "arseno", "arithmo", "astro", "atlo", "auto", "axo",
|
|
|
|
|
"azido", "bacillo", "bario", "balneo", "baryo", "basi", "benzo", "bismuto", "boreo", "biblio", "spatio", "boro", "bromo", "brachio",
|
|
|
|
|
"bryo", "bronto", "calci", "caco", "carbo", "cardio", "cata", "iso", "centi", "ceno", "centro", "cero", "chalco", "chemo", "chloro",
|
|
|
|
|
"chiono", "choano", "choro", "chromato", "chromo", "chryso", "chylo", "cine", "circum", "cirro", "climo", "cobalti", "coeno", "conico",
|
|
|
|
|
"cono", "cortico", "cosmo", "crypto", "crano", "crystallo", "cyano", "cyber", "cyclo", "deca", "dendro", "cyno", "dactylo", "poly", "deutero",
|
|
|
|
|
"dia", "digi", "diplo", "docosa", "disto", "dromo", "duo", "dynamo", "econo", "ecclesio", "echino", "eco", "ecto", "electro", "eigen", "eka",
|
|
|
|
|
"elasto", "eicosa", "enviro", "enantio", "endo", "exo", "oeno", "femto", "ergato", "ergo", "etho", "euryo", "extro", "fluoro", "fructo",
|
|
|
|
|
"galacto", "galvano", "glacio", "gibi", "glosso", "gluco", "glyco", "grammatico", "grapho", "gravi", "gyro", "hadro", "halo", "hapto", "hecto",
|
|
|
|
|
"heli", "helio", "helico", "historio", "holo", "hella", "hemi", "hepta", "herpeto", "hiero", "hippo", "homo", "hoplo", "horo", "hyalo", "hyeto",
|
|
|
|
|
"hygro", "hylo", "hypho", "hypno", "hypso", "iatro", "icthyo", "ichno", "icosa", "ideo", "idio", "imido", "info", "infra", "insta", "inter",
|
|
|
|
|
"intro", "iodo", "iono", "irid", "iri", "iridio", "kilo", "diago", "juxta", "juridico", "bureaucrato", "entropo", "karyo", "kineto", "klepto",
|
|
|
|
|
"konio", "kymo", "lamino", "leipdo", "lepto", "levo", "dextro", "lexico", "cognito", "ligno", "limno", "lipo", "litho", "logo", "magneto",
|
|
|
|
|
"magnesio", "mega", "mento", "mercurio", "metallo", "mechano", "meco", "medio", "melo", "mero", "meso", "meteoro", "metro", "micto",
|
|
|
|
|
"mono", "miso", "mnemo", "morpho", "myco", "myo", "myria", "mytho", "nano", "necro", "neo", "neutro", "neuro", "nitro", "nycto", "nucleo",
|
|
|
|
|
"narco", "noto", "octo", "ochlo", "odonto", "oculo", "oligo", "opto", "organo", "ornitho", "osmio", "oneiro", "onto", "oxalo", "pachy",
|
|
|
|
|
"paleo", "pali", "pallado", "pano", "para", "penta", "per", "patho", "pebi", "peloro", "pene", "petro", "pharma", "pheno", "philo", "pico",
|
|
|
|
|
"piezo", "phono", "photo", "phospho", "physio", "physico", "phyto", "pico", "post", "pisci", "placo", "platy", "pleo", "plumbo", "pluto",
|
|
|
|
|
"pneumato", "politico", "proto", "potassio", "proteo", "pseudo", "psycho", "ptero", "pykno", "quasi", "quadri", "recti", "retino", "retro",
|
|
|
|
|
"rheo", "rhino", "rhizo", "rhodo", "roto", "rutheno", "saccharo", "sapo", "sauro", "seismo", "seleno", "septa", "silico", "scoto", "semanto",
|
|
|
|
|
"sialo", "socio", "sodio", "skeleto", "somato", "somno", "sono", "spectro", "speleo", "sphero", "spino", "spiro", "sporo", "stanno", "stato",
|
|
|
|
|
"steno", "stereo", "stegano", "strato", "hyper", "sulpho", "telluro", "stygo", "tachy", "tauto", "taxo", "techno", "tecto", "tele", "teleo",
|
|
|
|
|
"temporo", "tera", "tetra", "thalasso", "thaumato", "thermo", "tephro", "tessera", "thio", "titano", "tomo", "topo", "tono", "tungsto",
|
|
|
|
|
"turbo", "tyranno", "ultra", "undeca", "tribo", "trito", "tropho", "tropo", "uni", "urano", "video", "viro", "visuo", "xantho", "xenna",
|
|
|
|
|
"xeri", "xipho", "xylo", "xyro", "yocto", "yttro", "zepto", "zetta", "zinco", "zirco", "zoo", "zono", "zygo", "templateo", "rustaceo", "mnesto",
|
|
|
|
|
"amnesto", "cetaceo", "anthropo", "ioctlo"]
|
2020-09-10 17:51:38 +00:00
|
|
|
|
apiosuffixes = ["hazard", "form"]
|
|
|
|
|
|
|
|
|
|
def apioform():
|
|
|
|
|
out = ""
|
|
|
|
|
if random.randint(0, 4) == 0:
|
|
|
|
|
out += random.choice(apioprefixes)
|
|
|
|
|
out += "apio"
|
2020-10-10 15:32:42 +00:00
|
|
|
|
i = 1
|
2020-09-10 17:51:38 +00:00
|
|
|
|
while True:
|
|
|
|
|
out += random.choice(apioinfixes)
|
2020-10-10 15:32:42 +00:00
|
|
|
|
if random.randint(0, i) > 0: break
|
|
|
|
|
i *= 2
|
2020-09-10 17:51:38 +00:00
|
|
|
|
out += random.choice(apiosuffixes)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
def unlyric(text):
|
2020-10-10 15:32:42 +00:00
|
|
|
|
return text.translate(lyrictable).replace("\u200b", "")
|
2020-10-11 13:40:39 +00:00
|
|
|
|
|
|
|
|
|
def gen_codeblock(content):
|
|
|
|
|
return "```\n" + content.replace("```", "\\`\\`\\`")[:1900] + "\n```"
|
|
|
|
|
|
2020-11-01 12:05:01 +00:00
|
|
|
|
def json_encode(x): return json.dumps(x, separators=(',', ':'))
|
|
|
|
|
|
2021-01-14 09:38:32 +00:00
|
|
|
|
async def server_mod_check(ctx):
|
|
|
|
|
return ctx.author.permissions_in(ctx.channel).manage_channels or (await ctx.bot.is_owner(ctx.author))
|
|
|
|
|
|
|
|
|
|
async def admin_check(ctx):
|
|
|
|
|
return await ctx.bot.is_owner(ctx.author)
|
2020-11-01 17:07:36 +00:00
|
|
|
|
|
|
|
|
|
async def get_asset(bot: commands.Bot, identifier):
|
|
|
|
|
safe_ident = re.sub("[^A-Za-z0-9_.-]", "_", identifier)
|
|
|
|
|
x = await bot.database.execute_fetchone("SELECT * FROM assets WHERE identifier = ?", (safe_ident,))
|
|
|
|
|
if x:
|
|
|
|
|
return x["url"]
|
|
|
|
|
file = discord.File(os.path.join("./assets", identifier), filename=safe_ident)
|
|
|
|
|
message = await (bot.get_channel(config["image_upload_channel"])).send(identifier, file=file)
|
|
|
|
|
url = message.attachments[0].proxy_url
|
|
|
|
|
await bot.database.execute("INSERT INTO assets VALUES (?, ?)", (safe_ident, url))
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def hashbow(thing):
|
2021-01-14 09:38:32 +00:00
|
|
|
|
return int.from_bytes(hashlib.blake2b(thing.encode("utf-8")).digest()[:3], "little")
|
|
|
|
|
|
|
|
|
|
extensions = (
|
|
|
|
|
"reminders",
|
|
|
|
|
"debug",
|
|
|
|
|
"telephone",
|
|
|
|
|
"achievement",
|
|
|
|
|
"heavserver",
|
2021-02-09 17:17:19 +00:00
|
|
|
|
"voice",
|
|
|
|
|
"commands",
|
|
|
|
|
"userdata"
|
2021-01-14 09:38:32 +00:00
|
|
|
|
)
|