mirror of
https://github.com/osmarks/autobotrobot
synced 2024-06-16 10:09:59 +00:00
refactoring, userdata module
This commit is contained in:
parent
8f25254204
commit
7b11e95542
170
src/commands.py
Normal file
170
src/commands.py
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import subprocess
|
||||||
|
import asyncio
|
||||||
|
import argparse
|
||||||
|
import random
|
||||||
|
from numpy.random import default_rng
|
||||||
|
import re
|
||||||
|
import discord.ext.commands
|
||||||
|
|
||||||
|
import tio
|
||||||
|
import util
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
cleaner = discord.ext.commands.clean_content()
|
||||||
|
def clean(ctx, text):
|
||||||
|
return cleaner.convert(ctx, text)
|
||||||
|
|
||||||
|
@bot.command(help="Gives you a random fortune as generated by `fortune`.")
|
||||||
|
async def fortune(ctx):
|
||||||
|
await ctx.send(subprocess.run(["fortune"], stdout=subprocess.PIPE, encoding="UTF-8").stdout)
|
||||||
|
|
||||||
|
@bot.command(help="Generates an apioform type.")
|
||||||
|
async def apioform(ctx):
|
||||||
|
await ctx.send(util.apioform())
|
||||||
|
|
||||||
|
@bot.command(help="Says Pong.")
|
||||||
|
async def ping(ctx):
|
||||||
|
await ctx.send("Pong.")
|
||||||
|
|
||||||
|
@bot.command(help="Deletes the specified target.", rest_is_raw=True)
|
||||||
|
async def delete(ctx, *, raw_target):
|
||||||
|
target = await clean(ctx, raw_target.strip().replace("\n", " "))
|
||||||
|
if len(target) > 256:
|
||||||
|
await ctx.send(embed=util.error_embed("Deletion target must be max 256 chars"))
|
||||||
|
return
|
||||||
|
async with ctx.typing():
|
||||||
|
await ctx.send(f"Deleting {target}...")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await bot.database.execute("INSERT INTO deleted_items (timestamp, item) VALUES (?, ?)", (util.timestamp(), target))
|
||||||
|
await bot.database.commit()
|
||||||
|
await ctx.send(f"Deleted {target} successfully.")
|
||||||
|
|
||||||
|
@bot.command(help="View recently deleted things, optionally matching a filter.")
|
||||||
|
async def list_deleted(ctx, search=None):
|
||||||
|
acc = "Recently deleted:\n"
|
||||||
|
if search: acc = f"Recently deleted (matching {search}):\n"
|
||||||
|
csr = None
|
||||||
|
if search:
|
||||||
|
csr = bot.database.execute("SELECT * FROM deleted_items WHERE item LIKE ? ORDER BY timestamp DESC LIMIT 100", (f"%{search}%",))
|
||||||
|
else:
|
||||||
|
csr = bot.database.execute("SELECT * FROM deleted_items ORDER BY timestamp DESC LIMIT 100")
|
||||||
|
async with csr as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
to_add = "- " + row[2].replace("```", "[REDACTED]") + "\n"
|
||||||
|
if len(acc + to_add) > 2000:
|
||||||
|
break
|
||||||
|
acc += to_add
|
||||||
|
await ctx.send(acc)
|
||||||
|
|
||||||
|
# Python, for some *very intelligent reason*, makes the default ArgumentParser exit the program on error.
|
||||||
|
# This is obviously undesirable behavior in a Discord bot, so we override this.
|
||||||
|
class NonExitingArgumentParser(argparse.ArgumentParser):
|
||||||
|
def exit(self, status=0, message=None):
|
||||||
|
if status:
|
||||||
|
raise Exception(f'Flag parse error: {message}')
|
||||||
|
exit(status)
|
||||||
|
|
||||||
|
EXEC_REGEX = "^(.*)```([a-zA-Z0-9_\\-+]+)?\n(.*)```$"
|
||||||
|
|
||||||
|
exec_flag_parser = NonExitingArgumentParser(add_help=False)
|
||||||
|
exec_flag_parser.add_argument("--verbose", "-v", action="store_true")
|
||||||
|
exec_flag_parser.add_argument("--language", "-L")
|
||||||
|
|
||||||
|
@bot.command(rest_is_raw=True, help="Execute provided code (in a codeblock) using TIO.run.")
|
||||||
|
async def exec(ctx, *, arg):
|
||||||
|
match = re.match(EXEC_REGEX, arg, flags=re.DOTALL)
|
||||||
|
if match == None:
|
||||||
|
await ctx.send(embed=util.error_embed("Invalid format. Expected a codeblock."))
|
||||||
|
return
|
||||||
|
flags_raw = match.group(1)
|
||||||
|
flags = exec_flag_parser.parse_args(flags_raw.split())
|
||||||
|
lang = flags.language or match.group(2)
|
||||||
|
if not lang:
|
||||||
|
await ctx.send(embed=util.error_embed("No language specified. Use the -L flag or add a language to your codeblock."))
|
||||||
|
return
|
||||||
|
lang = lang.strip()
|
||||||
|
code = match.group(3)
|
||||||
|
|
||||||
|
async with ctx.typing():
|
||||||
|
ok, real_lang, result, debug = await tio.run(lang, code)
|
||||||
|
if not ok:
|
||||||
|
await ctx.send(embed=util.error_embed(util.gen_codeblock(result), "Execution failed"))
|
||||||
|
else:
|
||||||
|
out = result
|
||||||
|
if flags.verbose:
|
||||||
|
debug_block = "\n" + util.gen_codeblock(f"""{debug}\nLanguage: {real_lang}""")
|
||||||
|
out = out[:2000 - len(debug_block)] + debug_block
|
||||||
|
else:
|
||||||
|
out = out[:2000]
|
||||||
|
await ctx.send(out)
|
||||||
|
|
||||||
|
@bot.command(help="List supported languages, optionally matching a filter.")
|
||||||
|
async def supported_langs(ctx, search=None):
|
||||||
|
langs = sorted(tio.languages())
|
||||||
|
acc = ""
|
||||||
|
for lang in langs:
|
||||||
|
if len(acc + lang) > 2000:
|
||||||
|
await ctx.send(acc)
|
||||||
|
acc = ""
|
||||||
|
if search == None or search in lang: acc += lang + " "
|
||||||
|
if acc == "": acc = "No results."
|
||||||
|
await ctx.send(acc)
|
||||||
|
|
||||||
|
@bot.command(help="Get some information about the bot.", aliases=["invite"])
|
||||||
|
async def about(ctx):
|
||||||
|
await ctx.send("""**AutoBotRobot: The least useful Discord bot ever designed.**
|
||||||
|
AutoBotRobot has many features, but not necessarily any practical ones.
|
||||||
|
It can execute code via TIO.run, do reminders, print fortunes, and not any more!
|
||||||
|
AutoBotRobot is open source - the code is available at <https://github.com/osmarks/autobotrobot> - and you could run your own instance if you wanted to and could get around the complete lack of user guide or documentation.
|
||||||
|
You can also invite it to your server: <https://discordapp.com/oauth2/authorize?&client_id=509849474647064576&scope=bot&permissions=68608>
|
||||||
|
""")
|
||||||
|
|
||||||
|
@bot.command(help="Roll simulated dice (basic NdX syntax only, no + etc., N <= 50, X <= 1000000).")
|
||||||
|
async def roll(ctx, dice):
|
||||||
|
match = re.match("([-0-9]*)d([0-9]+)", dice)
|
||||||
|
if not match: raise ValueError("Invalid dice notation")
|
||||||
|
n, x = match.groups()
|
||||||
|
if n == "": n = 1
|
||||||
|
n, x = int(n), int(x)
|
||||||
|
if n > 50 or x > 1e6: raise ValueError("N or X exceeds limit")
|
||||||
|
rolls = [ random.randint(1, x) for _ in range(n) ]
|
||||||
|
await ctx.send(f"{sum(rolls)} ({' '.join(map(str, sorted(rolls)))})")
|
||||||
|
|
||||||
|
def weight(thing):
|
||||||
|
lthing = thing.lower()
|
||||||
|
weight = 1.0
|
||||||
|
if lthing == "c": weight *= 0.3
|
||||||
|
for bad_thing in util.config["autobias"]["bad_things"]:
|
||||||
|
if bad_thing in lthing: weight *= 0.5
|
||||||
|
for good_thing in util.config["autobias"]["good_things"]:
|
||||||
|
if good_thing in lthing: weight *= 2.0
|
||||||
|
for negation in util.config["autobias"]["negations"]:
|
||||||
|
for _ in range(lthing.count(negation)): weight = 1 / weight
|
||||||
|
return weight
|
||||||
|
|
||||||
|
rng = default_rng()
|
||||||
|
|
||||||
|
@bot.command(help="'Randomly' choose between the specified options.", name="choice", aliases=["choose"])
|
||||||
|
async def random_choice(ctx, *choices):
|
||||||
|
choices = list(choices)
|
||||||
|
samples = 1
|
||||||
|
# apparently doing typing.Optional[int] doesn't work properly with this, so just bodge around it
|
||||||
|
try:
|
||||||
|
samples = int(choices[0])
|
||||||
|
choices.pop(0)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if samples > 9223372036854775807 or samples < 1 or len(choices) < 1:
|
||||||
|
await ctx.send("No.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# because of python weirdness, using sum() on the bare map iterator consumes it, which means we have to actually make a list
|
||||||
|
weights = list(map(weight, choices))
|
||||||
|
|
||||||
|
if samples == 1: return await ctx.send(random.choices(choices, weights=weights, k=1)[0])
|
||||||
|
|
||||||
|
total = sum(weights)
|
||||||
|
probabilities = list(map(lambda x: x / total, weights))
|
||||||
|
results = map(lambda t: (choices[t[0]], t[1]), enumerate(rng.multinomial(samples, list(probabilities))))
|
||||||
|
|
||||||
|
await ctx.send("\n".join(map(lambda x: f"{x[0]} x{x[1]}", results)))
|
|
@ -67,6 +67,15 @@ CREATE TABLE assets (
|
||||||
identifier TEXT PRIMARY KEY,
|
identifier TEXT PRIMARY KEY,
|
||||||
url TEXT NOT NULL
|
url TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE TABLE user_data (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
guild_id INTEGER,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
UNIQUE (user_id, guild_id, key)
|
||||||
|
);
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
19
src/debug.py
19
src/debug.py
|
@ -6,14 +6,15 @@ from discord.ext import commands
|
||||||
import util
|
import util
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
@bot.group()
|
@bot.group(help="Debug/random messing around utilities. Owner-only.")
|
||||||
@commands.check(util.admin_check)
|
@commands.check(util.admin_check)
|
||||||
async def magic(ctx):
|
async def magic(ctx):
|
||||||
if ctx.invoked_subcommand == None:
|
if ctx.invoked_subcommand == None:
|
||||||
return await ctx.send("Invalid magic command.")
|
return await ctx.send("Invalid magic command.")
|
||||||
|
|
||||||
@magic.command(rest_is_raw=True)
|
@magic.command(rest_is_raw=True, brief="Execute Python.")
|
||||||
async def py(ctx, *, code):
|
async def py(ctx, *, code):
|
||||||
|
"Executes Python. You may supply a codeblock. Comments in the form #timeout:([0-9]+) will be used as a timeout specifier. React with :x: to stop, probably."
|
||||||
timeout = 5.0
|
timeout = 5.0
|
||||||
timeout_match = re.search("#timeout:([0-9]+)", code, re.IGNORECASE)
|
timeout_match = re.search("#timeout:([0-9]+)", code, re.IGNORECASE)
|
||||||
if timeout_match:
|
if timeout_match:
|
||||||
|
@ -49,8 +50,9 @@ def setup(bot):
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
await ctx.send(embed=util.error_embed(util.gen_codeblock(traceback.format_exc())))
|
await ctx.send(embed=util.error_embed(util.gen_codeblock(traceback.format_exc())))
|
||||||
|
|
||||||
@magic.command(rest_is_raw=True)
|
@magic.command(rest_is_raw=True, help="Execute SQL code against the database.")
|
||||||
async def sql(ctx, *, code):
|
async def sql(ctx, *, code):
|
||||||
|
"Executes SQL (and commits). You may use a codeblock, similarly to with py."
|
||||||
code = util.extract_codeblock(code)
|
code = util.extract_codeblock(code)
|
||||||
try:
|
try:
|
||||||
csr = bot.database.execute(code)
|
csr = bot.database.execute(code)
|
||||||
|
@ -63,11 +65,14 @@ def setup(bot):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(embed=util.error_embed(util.gen_codeblock(traceback.format_exc())))
|
await ctx.send(embed=util.error_embed(util.gen_codeblock(traceback.format_exc())))
|
||||||
|
|
||||||
@magic.command()
|
@magic.command(help="Reload configuration file.")
|
||||||
async def reload_config(ctx):
|
async def reload_config(ctx):
|
||||||
util.load_config()
|
util.load_config()
|
||||||
ctx.send("Done!")
|
ctx.send("Done!")
|
||||||
|
|
||||||
@magic.command()
|
@magic.command(help="Reload extensions (all or the specified one).")
|
||||||
async def reload_ext(ctx):
|
async def reload_ext(ctx, ext="all"):
|
||||||
for ext in util.extensions: bot.reload_extension(ext)
|
if ext == "all":
|
||||||
|
for ext in util.extensions: bot.reload_extension(ext)
|
||||||
|
else: bot.reload_extension(ext)
|
||||||
|
await ctx.send("Done!")
|
159
src/main.py
159
src/main.py
|
@ -1,21 +1,17 @@
|
||||||
import discord
|
import discord
|
||||||
import toml
|
import toml
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
import discord.ext.tasks as tasks
|
import discord.ext.tasks as tasks
|
||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import argparse
|
|
||||||
import traceback
|
import traceback
|
||||||
import random
|
import random
|
||||||
import rolldice
|
|
||||||
import collections
|
import collections
|
||||||
import prometheus_client
|
import prometheus_client
|
||||||
import prometheus_async.aio
|
import prometheus_async.aio
|
||||||
import typing
|
import typing
|
||||||
from numpy.random import default_rng
|
|
||||||
|
|
||||||
import tio
|
import tio
|
||||||
import db
|
import db
|
||||||
|
@ -33,10 +29,6 @@ bot = commands.Bot(command_prefix=config["prefix"], description="AutoBotRobot, t
|
||||||
case_insensitive=True, allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=True))
|
case_insensitive=True, allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=True))
|
||||||
bot._skip_check = lambda x, y: False
|
bot._skip_check = lambda x, y: False
|
||||||
|
|
||||||
cleaner = discord.ext.commands.clean_content()
|
|
||||||
def clean(ctx, text):
|
|
||||||
return cleaner.convert(ctx, text)
|
|
||||||
|
|
||||||
messages = prometheus_client.Counter("abr_messages", "Messages seen/handled by bot")
|
messages = prometheus_client.Counter("abr_messages", "Messages seen/handled by bot")
|
||||||
command_invocations = prometheus_client.Counter("abr_command_invocations", "Total commands invoked (includes failed)")
|
command_invocations = prometheus_client.Counter("abr_command_invocations", "Total commands invoked (includes failed)")
|
||||||
@bot.event
|
@bot.event
|
||||||
|
@ -57,7 +49,7 @@ command_errors = prometheus_client.Counter("abr_errors", "Count of errors encoun
|
||||||
async def on_command_error(ctx, err):
|
async def on_command_error(ctx, err):
|
||||||
#print(ctx, err)
|
#print(ctx, err)
|
||||||
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return
|
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return
|
||||||
if isinstance(err, commands.MissingRequiredArgument): return await ctx.send(embed=util.error_embed(str(err)))
|
if isinstance(err, (commands.MissingRequiredArgument, ValueError)): return await ctx.send(embed=util.error_embed(str(err)))
|
||||||
try:
|
try:
|
||||||
command_errors.inc()
|
command_errors.inc()
|
||||||
trace = re.sub("\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__)))
|
trace = re.sub("\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__)))
|
||||||
|
@ -66,155 +58,6 @@ async def on_command_error(ctx, err):
|
||||||
await achievement.achieve(ctx.bot, ctx.message, "error")
|
await achievement.achieve(ctx.bot, ctx.message, "error")
|
||||||
except Exception as e: print("meta-error:", e)
|
except Exception as e: print("meta-error:", e)
|
||||||
|
|
||||||
@bot.command(help="Gives you a random fortune as generated by `fortune`.")
|
|
||||||
async def fortune(ctx):
|
|
||||||
await ctx.send(subprocess.run(["fortune"], stdout=subprocess.PIPE, encoding="UTF-8").stdout)
|
|
||||||
|
|
||||||
@bot.command(help="Generates an apioform type.")
|
|
||||||
async def apioform(ctx):
|
|
||||||
await ctx.send(util.apioform())
|
|
||||||
|
|
||||||
@bot.command(help="Says Pong.")
|
|
||||||
async def ping(ctx):
|
|
||||||
await ctx.send("Pong.")
|
|
||||||
|
|
||||||
@bot.command(help="Deletes the specified target.", rest_is_raw=True)
|
|
||||||
async def delete(ctx, *, raw_target):
|
|
||||||
target = await clean(ctx, raw_target.strip().replace("\n", " "))
|
|
||||||
if len(target) > 256:
|
|
||||||
await ctx.send(embed=util.error_embed("Deletion target must be max 256 chars"))
|
|
||||||
return
|
|
||||||
async with ctx.typing():
|
|
||||||
await ctx.send(f"Deleting {target}...")
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await bot.database.execute("INSERT INTO deleted_items (timestamp, item) VALUES (?, ?)", (util.timestamp(), target))
|
|
||||||
await bot.database.commit()
|
|
||||||
await ctx.send(f"Deleted {target} successfully.")
|
|
||||||
|
|
||||||
@bot.command(help="View recently deleted things, optionally matching a filter.")
|
|
||||||
async def list_deleted(ctx, search=None):
|
|
||||||
acc = "Recently deleted:\n"
|
|
||||||
if search: acc = f"Recently deleted (matching {search}):\n"
|
|
||||||
csr = None
|
|
||||||
if search:
|
|
||||||
csr = bot.database.execute("SELECT * FROM deleted_items WHERE item LIKE ? ORDER BY timestamp DESC LIMIT 100", (f"%{search}%",))
|
|
||||||
else:
|
|
||||||
csr = bot.database.execute("SELECT * FROM deleted_items ORDER BY timestamp DESC LIMIT 100")
|
|
||||||
async with csr as cursor:
|
|
||||||
async for row in cursor:
|
|
||||||
to_add = "- " + row[2].replace("```", "[REDACTED]") + "\n"
|
|
||||||
if len(acc + to_add) > 2000:
|
|
||||||
break
|
|
||||||
acc += to_add
|
|
||||||
await ctx.send(acc)
|
|
||||||
|
|
||||||
# Python, for some *very intelligent reason*, makes the default ArgumentParser exit the program on error.
|
|
||||||
# This is obviously undesirable behavior in a Discord bot, so we override this.
|
|
||||||
class NonExitingArgumentParser(argparse.ArgumentParser):
|
|
||||||
def exit(self, status=0, message=None):
|
|
||||||
if status:
|
|
||||||
raise Exception(f'Flag parse error: {message}')
|
|
||||||
exit(status)
|
|
||||||
|
|
||||||
EXEC_REGEX = "^(.*)```([a-zA-Z0-9_\\-+]+)?\n(.*)```$"
|
|
||||||
|
|
||||||
exec_flag_parser = NonExitingArgumentParser(add_help=False)
|
|
||||||
exec_flag_parser.add_argument("--verbose", "-v", action="store_true")
|
|
||||||
exec_flag_parser.add_argument("--language", "-L")
|
|
||||||
|
|
||||||
@bot.command(rest_is_raw=True, help="Execute provided code (in a codeblock) using TIO.run.")
|
|
||||||
async def exec(ctx, *, arg):
|
|
||||||
match = re.match(EXEC_REGEX, arg, flags=re.DOTALL)
|
|
||||||
if match == None:
|
|
||||||
await ctx.send(embed=util.error_embed("Invalid format. Expected a codeblock."))
|
|
||||||
return
|
|
||||||
flags_raw = match.group(1)
|
|
||||||
flags = exec_flag_parser.parse_args(flags_raw.split())
|
|
||||||
lang = flags.language or match.group(2)
|
|
||||||
if not lang:
|
|
||||||
await ctx.send(embed=util.error_embed("No language specified. Use the -L flag or add a language to your codeblock."))
|
|
||||||
return
|
|
||||||
lang = lang.strip()
|
|
||||||
code = match.group(3)
|
|
||||||
|
|
||||||
async with ctx.typing():
|
|
||||||
ok, real_lang, result, debug = await tio.run(lang, code)
|
|
||||||
if not ok:
|
|
||||||
await ctx.send(embed=util.error_embed(util.gen_codeblock(result), "Execution failed"))
|
|
||||||
else:
|
|
||||||
out = result
|
|
||||||
if flags.verbose:
|
|
||||||
debug_block = "\n" + util.gen_codeblock(f"""{debug}\nLanguage: {real_lang}""")
|
|
||||||
out = out[:2000 - len(debug_block)] + debug_block
|
|
||||||
else:
|
|
||||||
out = out[:2000]
|
|
||||||
await ctx.send(out)
|
|
||||||
|
|
||||||
@bot.command(help="List supported languages, optionally matching a filter.")
|
|
||||||
async def supported_langs(ctx, search=None):
|
|
||||||
langs = sorted(tio.languages())
|
|
||||||
acc = ""
|
|
||||||
for lang in langs:
|
|
||||||
if len(acc + lang) > 2000:
|
|
||||||
await ctx.send(acc)
|
|
||||||
acc = ""
|
|
||||||
if search == None or search in lang: acc += lang + " "
|
|
||||||
if acc == "": acc = "No results."
|
|
||||||
await ctx.send(acc)
|
|
||||||
|
|
||||||
@bot.command(help="Get some information about the bot.", aliases=["invite"])
|
|
||||||
async def about(ctx):
|
|
||||||
await ctx.send("""**AutoBotRobot: The least useful Discord bot ever designed.**
|
|
||||||
AutoBotRobot has many features, but not necessarily any practical ones.
|
|
||||||
It can execute code via TIO.run, do reminders, print fortunes, and not any more!
|
|
||||||
AutoBotRobot is open source - the code is available at <https://github.com/osmarks/autobotrobot> - and you could run your own instance if you wanted to and could get around the complete lack of user guide or documentation.
|
|
||||||
You can also invite it to your server: <https://discordapp.com/oauth2/authorize?&client_id=509849474647064576&scope=bot&permissions=68608>
|
|
||||||
""")
|
|
||||||
|
|
||||||
@bot.command(help="Randomly generate an integer using dice syntax.", name="random", rest_is_raw=True)
|
|
||||||
async def random_int(ctx, *, dice):
|
|
||||||
await ctx.send("Disabled until CPU use restrictions can be done.")
|
|
||||||
#await ctx.send(rolldice.roll_dice(dice)[0])
|
|
||||||
|
|
||||||
def weight(thing):
|
|
||||||
lthing = thing.lower()
|
|
||||||
weight = 1.0
|
|
||||||
if lthing == "c": weight *= 0.3
|
|
||||||
for bad_thing in util.config["autobias"]["bad_things"]:
|
|
||||||
if bad_thing in lthing: weight *= 0.5
|
|
||||||
for good_thing in util.config["autobias"]["good_things"]:
|
|
||||||
if good_thing in lthing: weight *= 2.0
|
|
||||||
for negation in util.config["autobias"]["negations"]:
|
|
||||||
for _ in range(lthing.count(negation)): weight = 1 / weight
|
|
||||||
return weight
|
|
||||||
|
|
||||||
rng = default_rng()
|
|
||||||
|
|
||||||
@bot.command(help="'Randomly' choose between the specified options.", name="choice", aliases=["choose"])
|
|
||||||
async def random_choice(ctx, *choices):
|
|
||||||
choices = list(choices)
|
|
||||||
samples = 1
|
|
||||||
# apparently doing typing.Optional[int] doesn't work properly with this, so just bodge around it
|
|
||||||
try:
|
|
||||||
samples = int(choices[0])
|
|
||||||
choices.pop(0)
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
if samples > 9223372036854775807 or samples < 1 or len(choices) < 1:
|
|
||||||
await ctx.send("No.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# because of python weirdness, using sum() on the bare map iterator consumes it, which means we have to actually make a list
|
|
||||||
weights = list(map(weight, choices))
|
|
||||||
|
|
||||||
if samples == 1: return await ctx.send(random.choices(choices, weights=weights, k=1)[0])
|
|
||||||
|
|
||||||
total = sum(weights)
|
|
||||||
probabilities = list(map(lambda x: x / total, weights))
|
|
||||||
results = map(lambda t: (choices[t[0]], t[1]), enumerate(rng.multinomial(samples, list(probabilities))))
|
|
||||||
|
|
||||||
await ctx.send("\n".join(map(lambda x: f"{x[0]} x{x[1]}", results)))
|
|
||||||
|
|
||||||
@bot.check
|
@bot.check
|
||||||
async def andrew_bad(ctx):
|
async def andrew_bad(ctx):
|
||||||
return ctx.message.author.id != 543131534685765673
|
return ctx.message.author.id != 543131534685765673
|
||||||
|
|
87
src/userdata.py
Normal file
87
src/userdata.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import util
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
@bot.group(name="userdata", aliases=["data"], brief="Store guild/user-localized data AND retrieve it later!")
|
||||||
|
async def userdata(ctx): pass
|
||||||
|
|
||||||
|
async def get_userdata(db, user, guild, key):
|
||||||
|
return (await db.execute_fetchone("SELECT * FROM user_data WHERE user_id = ? AND guild_id = ? AND key = ?", (user, guild, key))
|
||||||
|
or await db.execute_fetchone("SELECT * FROM user_data WHERE user_id = ? AND guild_id IS NULL AND key = ?", (user, key)))
|
||||||
|
async def set_userdata(db, user, guild, key, value):
|
||||||
|
await db.execute("INSERT OR REPLACE INTO user_data VALUES (?, ?, ?, ?)", (user, guild, key, value))
|
||||||
|
await bot.database.commit()
|
||||||
|
|
||||||
|
@userdata.command(help="Get a userdata key. Checks guild first, then global.")
|
||||||
|
async def get(ctx, *, key):
|
||||||
|
no_header = False
|
||||||
|
if key.startswith("noheader "):
|
||||||
|
key = key[9:]
|
||||||
|
no_header = True
|
||||||
|
row = await get_userdata(bot.database, ctx.author.id, ctx.guild.id, key)
|
||||||
|
if not row:
|
||||||
|
raise ValueError("No such key")
|
||||||
|
if no_header:
|
||||||
|
await ctx.send(row["value"])
|
||||||
|
else:
|
||||||
|
await ctx.send(f"**{key}**: {row['value']}")
|
||||||
|
|
||||||
|
@userdata.command(name="list", help="List userdata keys in a given scope matching a query. Can also show associated values.")
|
||||||
|
async def list_cmd(ctx, query="%", scope="guild", show_values: bool = False):
|
||||||
|
if scope == "global":
|
||||||
|
rows = await bot.database.execute_fetchall("SELECT * FROM user_data WHERE user_id = ? AND guild_id IS NULL AND key LIKE ?", (ctx.author.id, query))
|
||||||
|
else:
|
||||||
|
rows = await bot.database.execute_fetchall("SELECT * FROM user_data WHERE user_id = ? AND guild_id = ? AND key LIKE ?", (ctx.author.id, ctx.guild.id, query))
|
||||||
|
out = []
|
||||||
|
for row in rows:
|
||||||
|
if show_values:
|
||||||
|
out.append(f"**{row['key']}**: {row['value']}")
|
||||||
|
else:
|
||||||
|
out.append(row["key"])
|
||||||
|
if len(out) == 0: return await ctx.send("No data")
|
||||||
|
await ctx.send(("\n" if show_values else " ").join(out)[:2000]) # TODO: split better
|
||||||
|
|
||||||
|
def check_key(key):
|
||||||
|
if len(key) > 128: raise ValueError("Key too long")
|
||||||
|
|
||||||
|
def preprocess_value(value):
|
||||||
|
value = value.replace("\n", "").strip()
|
||||||
|
if len(value) > 256: raise ValueError("Value too long")
|
||||||
|
return value
|
||||||
|
|
||||||
|
@userdata.command(name="set", help="Set a userdata key in the guild scope.")
|
||||||
|
async def set_cmd(ctx, key, *, value):
|
||||||
|
check_key(key)
|
||||||
|
value = preprocess_value(value)
|
||||||
|
await set_userdata(bot.database, ctx.author.id, ctx.guild.id, key, value)
|
||||||
|
await ctx.send(f"**{key}** set (scope guild)")
|
||||||
|
|
||||||
|
@userdata.command(help="Set a userdata key in the global scope.")
|
||||||
|
async def set_global(ctx, key, *, value):
|
||||||
|
check_key(key)
|
||||||
|
value = preprocess_value(value)
|
||||||
|
await set_userdata(bot.database, ctx.author.id, None, key, value)
|
||||||
|
await ctx.send(f"**{key}** set (scope global)")
|
||||||
|
|
||||||
|
@userdata.command()
|
||||||
|
async def inc(ctx, key, by: int = 1):
|
||||||
|
check_key(key)
|
||||||
|
row = await get_userdata(bot.database, ctx.author.id, ctx.guild.id, key)
|
||||||
|
if not row:
|
||||||
|
value = 0
|
||||||
|
guild = ctx.guild.id
|
||||||
|
else:
|
||||||
|
value = int(row["value"])
|
||||||
|
guild = row["guild_id"]
|
||||||
|
new_value = value + by
|
||||||
|
await set_userdata(bot.database, ctx.author.id, guild, key, str(new_value))
|
||||||
|
await ctx.send(f"**{key}** set to {new_value}")
|
||||||
|
|
||||||
|
@userdata.command()
|
||||||
|
async def delete(ctx, *keys):
|
||||||
|
for key in keys:
|
||||||
|
row = await get_userdata(bot.database, ctx.author.id, ctx.guild.id, key)
|
||||||
|
if not row:
|
||||||
|
return await ctx.send(embed=util.error_embed(f"No such key {key}"))
|
||||||
|
await bot.database.execute("DELETE FROM user_data WHERE user_id = ? AND guild_id = ? AND key = ?", (ctx.author.id, row["guild_id"], key))
|
||||||
|
await bot.database.commit()
|
||||||
|
await ctx.send(f"**{key}** deleted")
|
|
@ -261,5 +261,7 @@ extensions = (
|
||||||
"telephone",
|
"telephone",
|
||||||
"achievement",
|
"achievement",
|
||||||
"heavserver",
|
"heavserver",
|
||||||
"voice"
|
"voice",
|
||||||
|
"commands",
|
||||||
|
"userdata"
|
||||||
)
|
)
|
Loading…
Reference in New Issue
Block a user