Enable commands globally if not sandboxing, add support for test guilds in env, monitor slash command & context menu usage, create error handler for slash commands, log slash commands in terminal

pull/90/head
Stijn De Clercq 2021-09-03 20:40:03 +02:00
parent ef547a7090
commit a28bd116f0
12 changed files with 117 additions and 62 deletions

View File

@ -1,9 +1,11 @@
from dislash import SlashInteraction
from data import constants from data import constants
from data.snipe import Snipe, Action, should_snipe from data.snipe import Snipe, Action, should_snipe
import datetime import datetime
import discord import discord
from discord.ext import commands from discord.ext import commands
from functions import checks, easterEggResponses from functions import checks, easterEggResponses, stringFormatters
from functions.database import stats, muttn, custom_commands, commands as command_stats from functions.database import stats, muttn, custom_commands, commands as command_stats
import pytz import pytz
from settings import READY_MESSAGE, SANDBOX, STATUS_MESSAGE from settings import READY_MESSAGE, SANDBOX, STATUS_MESSAGE
@ -87,12 +89,9 @@ class Events(commands.Cog):
Logs commands in your terminal. Logs commands in your terminal.
:param ctx: Discord Context :param ctx: Discord Context
""" """
DM = ctx.guild is None print(stringFormatters.format_command_usage(ctx))
print("{} in {}: {}".format(ctx.author.display_name,
"DM" if DM else "{} ({})".format(ctx.channel.name, ctx.guild.name),
ctx.message.content))
command_stats.invoked() command_stats.invoked(command_stats.InvocationType.TextCommand)
@commands.Cog.listener() @commands.Cog.listener()
async def on_command_error(self, ctx, err): async def on_command_error(self, ctx, err):
@ -123,14 +122,22 @@ class Events(commands.Cog):
elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)): elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)):
await ctx.send("Controleer je argumenten.") await ctx.send("Controleer je argumenten.")
else: else:
# Remove the InvokeCommandError because it's useless information usage = stringFormatters.format_command_usage(ctx)
x = traceback.format_exception(type(err), err, err.__traceback__) await self.sendErrorEmbed(err, "Command", usage)
errorString = ""
for line in x: @commands.Cog.listener()
if "direct cause of the following" in line: async def on_slash_command(self, interaction: SlashInteraction):
break """
errorString += line.replace("*", "") + "\n" if line.strip() != "" else "" Function called whenever someone uses a slash command
await self.sendErrorEmbed(ctx, err, errorString) """
print(stringFormatters.format_slash_command_usage(interaction))
command_stats.invoked(command_stats.InvocationType.SlashCommand)
@commands.Cog.listener()
async def on_slash_command_error(self, interaction, error):
usage = stringFormatters.format_slash_command_usage(interaction)
await self.sendErrorEmbed(error, "Slash Command", usage)
@commands.Cog.listener() @commands.Cog.listener()
async def on_raw_reaction_add(self, react): async def on_raw_reaction_add(self, react):
@ -286,19 +293,15 @@ class Events(commands.Cog):
self.client.snipe[message.channel.id] = Snipe(message.author.id, message.channel.id, message.guild.id, self.client.snipe[message.channel.id] = Snipe(message.author.id, message.channel.id, message.guild.id,
Action.Remove, message.content) Action.Remove, message.content)
async def sendErrorEmbed(self, ctx, error: Exception, trace): async def sendErrorEmbed(self, error: Exception, error_type: str, usage: str):
""" """
Function that sends an error embed in #ErrorLogs. Function that sends an error embed in #ErrorLogs.
:param ctx: Discord Context
:param error: the error thrown
:param trace: the stacktrace of the error
""" """
trace = stringFormatters.format_error_tb(error)
embed = discord.Embed(colour=discord.Colour.red()) embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Error") embed.set_author(name=f"{error_type} Error")
embed.add_field(name="Command:", value="{} in {}: {}".format(ctx.author.display_name, embed.add_field(name="Command:", value=usage, inline=False)
ctx.channel.name if str(
ctx.channel.type) != "private" else "DM",
ctx.message.content), inline=False)
embed.add_field(name="Error:", value=str(error)[:1024], inline=False) embed.add_field(name="Error:", value=str(error)[:1024], inline=False)
embed.add_field(name="Message:", value=str(trace)[:1024], inline=False) embed.add_field(name="Message:", value=str(trace)[:1024], inline=False)

View File

@ -1,4 +1,3 @@
import discord
from discord.ext import commands from discord.ext import commands
from decorators import help from decorators import help
from enums.help_categories import Category from enums.help_categories import Category

View File

@ -42,7 +42,7 @@ class School(commands.Cog):
embed.set_footer(text="Omwille van de coronamaatregelen is er een beperkter aanbod, en kan je enkel nog eten afhalen. Ter plaatse eten is niet meer mogelijk.") embed.set_footer(text="Omwille van de coronamaatregelen is er een beperkter aanbod, en kan je enkel nog eten afhalen. Ter plaatse eten is niet meer mogelijk.")
await ctx.send(embed=embed) await ctx.send(embed=embed)
# @commands.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"], usage="[Jaargang]* [Dag]*") # @commands.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"], usage="[Dag]*")
# @commands.check(checks.allowedChannels) # @commands.check(checks.allowedChannels)
# @help.Category(category=Category.School) # @help.Category(category=Category.School)
async def les(self, ctx, day=None): async def les(self, ctx, day=None):

View File

@ -13,8 +13,7 @@ class DefineSlash(commands.Cog):
description="Urban Dictionary", description="Urban Dictionary",
options=[ options=[
Option("query", "Search query", OptionType.STRING, required=True) Option("query", "Search query", OptionType.STRING, required=True)
], ]
guild_ids=[728361030404538488, 880175869841277008]
) )
async def _define_slash(self, interaction: SlashInteraction, query): async def _define_slash(self, interaction: SlashInteraction, query):
embed = Definition(query).to_embed() embed = Definition(query).to_embed()

View File

@ -12,8 +12,7 @@ class GoogleSlash(commands.Cog):
description="Google search", description="Google search",
options=[ options=[
Option("query", "Search query", OptionType.STRING, required=True) Option("query", "Search query", OptionType.STRING, required=True)
], ]
guild_ids=[728361030404538488, 880175869841277008]
) )
async def _google_slash(self, interaction: SlashInteraction, query: str): async def _google_slash(self, interaction: SlashInteraction, query: str):
result = google_search(query) result = google_search(query)

View File

@ -14,11 +14,12 @@ class TranslateSlash(commands.Cog):
description="Google Translate", description="Google Translate",
options=[ options=[
Option("text", "Tekst om te vertalen", OptionType.STRING, required=True), Option("text", "Tekst om te vertalen", OptionType.STRING, required=True),
Option("to", "Taal om naar te vertalen (default NL)", OptionType.STRING) Option("from_lang", "Taal om van te vertalen (default auto-detect)", OptionType.STRING),
Option("to_lang", "Taal om naar te vertalen (default NL)", OptionType.STRING)
] ]
) )
async def _translate_slash(self, interaction: SlashInteraction, text: str, to: str = "nl"): async def _translate_slash(self, interaction: SlashInteraction, text: str, from_lang: str = "auto", to_lang: str = "nl"):
translation = Translation(text=text, to=to.lower()) translation = Translation(text=text, fr=from_lang.lower(), to=to_lang.lower())
await interaction.reply(embed=translation.to_embed()) await interaction.reply(embed=translation.to_embed())

View File

@ -5,26 +5,32 @@ from typing import Optional
class Translation: class Translation:
def __init__(self, text: str, to: str): def __init__(self, text: str, fr: str, to: str):
self.text = text self.text = text
self.fr = fr
self.to = to self.to = to
self.embed: Optional[discord.Embed] = None self.embed: Optional[discord.Embed] = None
self.translation = None self.translation = None
self.translate(text, to) self.translate(text, fr, to)
def translate(self, query: str, to: str): def translate(self, query: str, fr: str, to: str):
""" """
Translate [query] into [to] Translate [query] into [to]
""" """
try: try:
translator = Translator() translator = Translator()
self.translation = translator.translate(query, to, "auto") self.translation = translator.translate(query, to, fr)
except ValueError as e: except ValueError as e:
message = str(e) message = str(e)
if "destination" in message: if "destination" in message:
self._create_error_embed(f"{title_case(to)} is geen geldige taal.") self._create_error_embed(f"{title_case(to)} is geen geldige taal.")
return
if "source" in message:
self._create_error_embed(f"{title_case(fr)} is geen geldige taal.")
return
raise e raise e
@ -42,8 +48,9 @@ class Translation:
embed = discord.Embed(colour=discord.Colour.blue()) embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Didier Translate") embed.set_author(name="Didier Translate")
language = self.translation.src if self.fr == "auto":
embed.add_field(name="Gedetecteerde taal", value=title_case(LANGUAGES[language])) language = self.translation.src
embed.add_field(name="Gedetecteerde taal", value=title_case(LANGUAGES[language]))
if self.translation.extra_data["confidence"] is not None: if self.translation.extra_data["confidence"] is not None:
embed.add_field(name="Zekerheid", value="{}%".format(self.translation.extra_data["confidence"] * 100)) embed.add_field(name="Zekerheid", value="{}%".format(self.translation.extra_data["confidence"] * 100))

View File

@ -58,7 +58,7 @@
"jpl table": "De huidige stand van het klassement.", "jpl table": "De huidige stand van het klassement.",
"jpl update": "Haalt de nieuwe code voor de competitie van dit jaar op.", "jpl update": "Haalt de nieuwe code voor de competitie van dit jaar op.",
"leaderboard": "Bekijk de Top 10 van [Categorie].\nIndien je geen categorie opgeeft krijg je een lijst van categorieën.", "leaderboard": "Bekijk de Top 10 van [Categorie].\nIndien je geen categorie opgeeft krijg je een lijst van categorieën.",
"les": "Bekijk het lessenrooster voor [Dag] in het [Jaargang]-de jaar.\nIndien je geen dag opgeeft, is dit standaard vandaag. De jaargang is standaard 2.\nLes Morgen/Overmorgen werkt ook.", "les": "Bekijk het lessenrooster voor [Dag].\nIndien je geen dag opgeeft, is dit standaard vandaag.\nLes Morgen/Overmorgen werkt ook.",
"lmgtfy": "Stuur iemand een LMGTFY link wanneer ze je een domme vraag stellen in plaats van het zelf op te zoeken.\nQueries met spaties moeten **niet** tussen aanhalingstekens staan.", "lmgtfy": "Stuur iemand een LMGTFY link wanneer ze je een domme vraag stellen in plaats van het zelf op te zoeken.\nQueries met spaties moeten **niet** tussen aanhalingstekens staan.",
"load": "Laadt [Cog] in.", "load": "Laadt [Cog] in.",
"load all": "Laadt alle cogs in.", "load all": "Laadt alle cogs in.",

View File

@ -1,23 +1,20 @@
from enum import IntEnum
from functions.database import utils from functions.database import utils
from functions.stringFormatters import leading_zero as lz
import time import time
def invoked(): class InvocationType(IntEnum):
TextCommand = 0
SlashCommand = 1
ContextMenu = 2
def invoked(inv: InvocationType):
t = time.localtime() t = time.localtime()
day_string: str = f"{t.tm_year}-{_lz(t.tm_mon)}-{_lz(t.tm_mday)}" day_string: str = f"{t.tm_year}-{lz(t.tm_mon)}-{lz(t.tm_mday)}"
_update(day_string) _update(day_string, inv)
def _lz(arg: int) -> str:
"""
Add leading zeroes if necessary (YYYY-MM-DD)
"""
arg = str(arg)
if len(arg) == 1:
return f"0{arg}"
return arg
def _is_present(date: str) -> bool: def _is_present(date: str) -> bool:
@ -43,25 +40,27 @@ def _add_date(date: str):
connection = utils.connect() connection = utils.connect()
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("INSERT INTO command_stats(day, amount) VALUES (%s, 1)", (date,)) cursor.execute("INSERT INTO command_stats(day, commands, slash_commands, context_menus) VALUES (%s, 0, 0, 0)", (date,))
connection.commit() connection.commit()
def _update(date: str): def _update(date: str, inv: InvocationType):
""" """
Increase the counter for a given day Increase the counter for a given day
""" """
# Date wasn't present yet, add it with a value of 1 # Date wasn't present yet, add it
if not _is_present(date): if not _is_present(date):
_add_date(date) _add_date(date)
return
connection = utils.connect() connection = utils.connect()
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute(""" column_name = ["commands", "slash_commands", "context_menus"][inv.value]
# String formatting is safe here because the input comes from above ^
cursor.execute(f"""
UPDATE command_stats UPDATE command_stats
SET amount = amount + 1 SET {column_name} = {column_name} + 1
WHERE day = %s WHERE day = %s
""", (date,)) """, (date,))
connection.commit() connection.commit()

View File

@ -1,3 +1,9 @@
import traceback
from discord.ext.commands import Context
from dislash import SlashInteraction
def title_case(string): def title_case(string):
return " ".join(capitalize(word) for word in string.split(" ")) return " ".join(capitalize(word) for word in string.split(" "))
@ -13,3 +19,35 @@ def leading_zero(string, size=2):
while len(string) < size: while len(string) < size:
string = "0" + string string = "0" + string
return string return string
def format_error_tb(err: Exception) -> str:
# Remove the InvokeCommandError because it's useless information
x = traceback.format_exception(type(err), err, err.__traceback__)
error_string = ""
for line in x:
if "direct cause of the following" in line:
break
error_string += line.replace("*", "") + "\n" if line.strip() != "" else ""
return error_string
def _format_error_location(src) -> str:
DM = src.guild is None
return "DM" if DM else f"{src.channel.name} ({src.guild.name})"
def format_command_usage(ctx: Context) -> str:
return f"{ctx.author.display_name} in {_format_error_location(ctx)}: {ctx.message.content}"
def format_slash_command_usage(interaction: SlashInteraction) -> str:
# Create a string with the options used
options = " ".join(list(map(
lambda option: f"{option.name}: \"{option.value}\"",
interaction.data.options.values()
)))
command = f"{interaction.slash_command.name} {options or ''}"
return f"{interaction.author.display_name} in {_format_error_location(interaction)}: /{command}"

View File

@ -1,3 +1,5 @@
from typing import List
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
@ -31,3 +33,11 @@ TOKEN = os.getenv("TOKEN", "")
HOST_IPC = _to_bool(os.getenv("HOSTIPC", "false")) HOST_IPC = _to_bool(os.getenv("HOSTIPC", "false"))
READY_MESSAGE = os.getenv("READYMESSAGE", "I'M READY I'M READY I'M READY I'M READY") # Yes, this is a Spongebob reference READY_MESSAGE = os.getenv("READYMESSAGE", "I'M READY I'M READY I'M READY I'M READY") # Yes, this is a Spongebob reference
STATUS_MESSAGE = os.getenv("STATUSMESSAGE", "with your Didier Dinks.") STATUS_MESSAGE = os.getenv("STATUSMESSAGE", "with your Didier Dinks.")
# Guilds to test slash commands in
# Ex: 123,456,789
SLASH_TEST_GUILDS: List[int] = list(
map(lambda x: int(x),
os.getenv("SLASHTESTGUILDS", "").replace(" ", "").split(",")
)
)

View File

@ -2,7 +2,7 @@ from data.snipe import Snipe
from discord.ext import commands, ipc from discord.ext import commands, ipc
from dislash import InteractionClient from dislash import InteractionClient
import os import os
from settings import HOST_IPC from settings import HOST_IPC, SLASH_TEST_GUILDS
from startup.init_files import check_all from startup.init_files import check_all
from typing import Dict from typing import Dict
@ -33,7 +33,7 @@ class Didier(commands.Bot):
self.remove_command("help") self.remove_command("help")
# Create interactions client # Create interactions client
self.interactions = InteractionClient(self, test_guilds=[728361030404538488, 880175869841277008]) self.interactions = InteractionClient(self, test_guilds=SLASH_TEST_GUILDS)
# Load all extensions # Load all extensions
self.init_extensions() self.init_extensions()