autobotrobot/src/util.py

249 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
import datetime
import parsedatetime
import ast
import copy
import random
from dateutil.relativedelta import relativedelta
import json
import discord
import toml
import os.path
from discord.ext import commands
import hashlib
config = toml.load(open("config.toml", "r"))
def timestamp(): return int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
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()) + ")?"
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)
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
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: parse_prefixed(v) if v else 0 for k, v in match.groupdict().items() }
for tu, mapping in tu_mappings.items():
if callable(mapping): mapping = mapping()
qty, resunit = mapping
data[resunit] += qty * data[tu]
del data[tu]
return datetime.datetime.now(tz=datetime.timezone.utc) + relativedelta(**data)
cal = parsedatetime.Calendar()
def parse_humantime(text):
dt_tuple = cal.nlp(text)
if dt_tuple: return dt_tuple[0][0].replace(tzinfo=datetime.timezone.utc)
else: raise ValueError("parse failed")
def parse_time(text):
try: return datetime.datetime.strptime(text, "%d/%m/%Y").replace(tzinfo=datetime.timezone.utc)
except: pass
try: return parse_short_timedelta(text)
except: pass
try: return parse_humantime(text)
except: pass
raise ValueError("time matches no available format")
def format_time(dt):
return dt.strftime("%H:%M:%S %d/%m/%Y")
timeparts = (
("y", "years"),
("mo", "months"),
("d", "days"),
("h", "hours"),
("m", "minutes"),
("s", "seconds")
)
def format_timedelta(from_, to):
d = relativedelta(to, from_)
out = ""
for short, attr in timeparts:
x = getattr(d, attr)
if x != 0: out += str(x) + short
return "0s" if out == "" else out
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)
else: return s.strip()
# 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)
return await (loc.get("repl_coroutine") or glob.get("repl_coroutine"))()
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)
# 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()})
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"]
apiosuffixes = ["hazard", "form"]
def apioform():
out = ""
if random.randint(0, 4) == 0:
out += random.choice(apioprefixes)
out += "apio"
i = 1
while True:
out += random.choice(apioinfixes)
if random.randint(0, i) > 0: break
i *= 2
out += random.choice(apiosuffixes)
return out
def unlyric(text):
return text.translate(lyrictable).replace("\u200b", "")
def gen_codeblock(content):
return "```\n" + content.replace("```", "\\`\\`\\`")[:1900] + "\n```"
def json_encode(x): return json.dumps(x, separators=(',', ':'))
def server_mod_check(bot):
async def check(ctx):
return ctx.author.permissions_in(ctx.channel).manage_channels or (await bot.is_owner(ctx.author))
return check
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):
return int.from_bytes(hashlib.blake2b(thing.encode("utf-8")).digest()[:3], "little")