diff --git a/cogs/events.py b/cogs/events.py index 9069c3b..76dbae2 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -1,9 +1,11 @@ +from dislash import SlashInteraction + from data import constants from data.snipe import Snipe, Action, should_snipe import datetime import discord 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 import pytz from settings import READY_MESSAGE, SANDBOX, STATUS_MESSAGE @@ -87,12 +89,9 @@ class Events(commands.Cog): Logs commands in your terminal. :param ctx: Discord Context """ - DM = ctx.guild is None - print("{} in {}: {}".format(ctx.author.display_name, - "DM" if DM else "{} ({})".format(ctx.channel.name, ctx.guild.name), - ctx.message.content)) + print(stringFormatters.format_command_usage(ctx)) - command_stats.invoked() + command_stats.invoked(command_stats.InvocationType.TextCommand) @commands.Cog.listener() async def on_command_error(self, ctx, err): @@ -101,8 +100,8 @@ class Events(commands.Cog): :param ctx: Discord Context :param err: the error thrown """ - # Zandbak Didier shouldn't spam the error logs - if self.client.user.id == int(constants.coolerDidierId): + # Debugging Didier shouldn't spam the error logs + if self.client.user.id != int(constants.didierId): raise err # Don't handle commands that have their own custom error handler @@ -123,14 +122,26 @@ class Events(commands.Cog): elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)): await ctx.send("Controleer je argumenten.") else: - # Remove the InvokeCommandError because it's useless information - x = traceback.format_exception(type(err), err, err.__traceback__) - errorString = "" - for line in x: - if "direct cause of the following" in line: - break - errorString += line.replace("*", "") + "\n" if line.strip() != "" else "" - await self.sendErrorEmbed(ctx, err, errorString) + usage = stringFormatters.format_command_usage(ctx) + await self.sendErrorEmbed(err, "Command", usage) + + @commands.Cog.listener() + async def on_slash_command(self, interaction: SlashInteraction): + """ + Function called whenever someone uses a slash command + """ + 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, err): + # Debugging Didier shouldn't spam the error logs + if self.client.user.id != int(constants.didierId): + raise err + + usage = stringFormatters.format_slash_command_usage(interaction) + await self.sendErrorEmbed(err, "Slash Command", usage) @commands.Cog.listener() async def on_raw_reaction_add(self, react): @@ -286,19 +297,15 @@ class Events(commands.Cog): self.client.snipe[message.channel.id] = Snipe(message.author.id, message.channel.id, message.guild.id, 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. - :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.set_author(name="Error") - embed.add_field(name="Command:", value="{} in {}: {}".format(ctx.author.display_name, - ctx.channel.name if str( - ctx.channel.type) != "private" else "DM", - ctx.message.content), inline=False) + embed.add_field(name=f"{error_type}:", value=usage, inline=False) embed.add_field(name="Error:", value=str(error)[:1024], inline=False) embed.add_field(name="Message:", value=str(trace)[:1024], inline=False) diff --git a/cogs/football.py b/cogs/football.py index 1f77c85..d02ec15 100644 --- a/cogs/football.py +++ b/cogs/football.py @@ -2,7 +2,7 @@ from decorators import help from discord.ext import commands from enums.help_categories import Category from functions import checks, config -from functions.football import getMatches, getTable, get_jpl_code +from functions.football import get_matches, get_table, get_jpl_code class Football(commands.Cog): @@ -20,21 +20,16 @@ class Football(commands.Cog): pass @jpl.command(name="Matches", aliases=["M"], usage="[Week]*") - async def matches(self, ctx, *args): - args = list(args) - + async def matches(self, ctx, day: int = None): # Default is current day - if not args: - args = [str(config.get("jpl_day"))] + if day is None: + day = int(config.get("jpl_day")) - if all(letter.isdigit() for letter in args[0]): - await ctx.send(getMatches(int(args[0]))) - else: - return await ctx.send("Dit is geen geldige speeldag.") + await ctx.send(get_matches(day)) @jpl.command(name="Table", aliases=["Ranking", "Rankings", "Ranks", "T"]) - async def table(self, ctx, *args): - await ctx.send(getTable()) + async def table(self, ctx): + await ctx.send(get_table()) @commands.check(checks.isMe) @jpl.command(name="Update") diff --git a/cogs/google.py b/cogs/google.py index 429b4f4..204075e 100644 --- a/cogs/google.py +++ b/cogs/google.py @@ -1,4 +1,3 @@ -import discord from discord.ext import commands from decorators import help from enums.help_categories import Category diff --git a/cogs/school.py b/cogs/school.py index 94b3fb7..a206e64 100644 --- a/cogs/school.py +++ b/cogs/school.py @@ -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.") 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) # @help.Category(category=Category.School) async def les(self, ctx, day=None): diff --git a/cogs/slash/define_slash.py b/cogs/slash/define_slash.py index 55899d8..b291bb9 100644 --- a/cogs/slash/define_slash.py +++ b/cogs/slash/define_slash.py @@ -13,8 +13,7 @@ class DefineSlash(commands.Cog): description="Urban Dictionary", options=[ Option("query", "Search query", OptionType.STRING, required=True) - ], - guild_ids=[728361030404538488, 880175869841277008] + ] ) async def _define_slash(self, interaction: SlashInteraction, query): embed = Definition(query).to_embed() diff --git a/cogs/slash/football_slash.py b/cogs/slash/football_slash.py new file mode 100644 index 0000000..d5a7283 --- /dev/null +++ b/cogs/slash/football_slash.py @@ -0,0 +1,44 @@ +from discord.ext import commands +from dislash import SlashInteraction, slash_command, Option, OptionType +from functions import config, checks +from functions.football import get_matches, get_table, get_jpl_code +from startup.didier import Didier + + +class FootballSlash(commands.Cog): + def __init__(self, client: Didier): + self.client: Didier = client + + @slash_command(name="jpl", description="Jupiler Pro League commands") + async def _jpl_group(self, interaction: SlashInteraction): + pass + + @_jpl_group.sub_command(name="matches", + description="Schema voor een bepaalde speeldag", + options=[ + Option("day", "Speeldag (default huidige)", OptionType.INTEGER) + ] + ) + async def _jpl_matches_slash(self, interaction: SlashInteraction, day: int = None): + # Default is current day + if day is None: + day = int(config.get("jpl_day")) + + await interaction.reply(get_matches(day)) + + @_jpl_group.sub_command(name="table", description="Huidige rangschikking") + async def _jpl_table_slash(self, interaction: SlashInteraction): + await interaction.reply(get_table()) + + @_jpl_group.sub_command(name="update", description="Update de code voor deze competitie (owner-only)") + async def _jpl_update_slash(self, interaction: SlashInteraction): + if not await checks.isMe(interaction): + return await interaction.reply(f"Je hebt geen toegang tot dit commando.") + + code = get_jpl_code() + config.config("jpl", code) + await interaction.reply(f"Done (code: {code})") + + +def setup(client: Didier): + client.add_cog(FootballSlash(client)) diff --git a/cogs/slash/google_slash.py b/cogs/slash/google_slash.py index fe9ba61..6978492 100644 --- a/cogs/slash/google_slash.py +++ b/cogs/slash/google_slash.py @@ -12,8 +12,7 @@ class GoogleSlash(commands.Cog): description="Google search", options=[ Option("query", "Search query", OptionType.STRING, required=True) - ], - guild_ids=[728361030404538488, 880175869841277008] + ] ) async def _google_slash(self, interaction: SlashInteraction, query: str): result = google_search(query) diff --git a/cogs/slash/translate_slash.py b/cogs/slash/translate_slash.py index 226aa0d..6ee293d 100644 --- a/cogs/slash/translate_slash.py +++ b/cogs/slash/translate_slash.py @@ -14,11 +14,12 @@ class TranslateSlash(commands.Cog): description="Google Translate", options=[ 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"): - translation = Translation(text=text, to=to.lower()) + async def _translate_slash(self, interaction: SlashInteraction, text: str, from_lang: str = "auto", to_lang: str = "nl"): + translation = Translation(text=text, fr=from_lang.lower(), to=to_lang.lower()) await interaction.reply(embed=translation.to_embed()) diff --git a/data/embeds/translate.py b/data/embeds/translate.py index 86ade0e..32f4853 100644 --- a/data/embeds/translate.py +++ b/data/embeds/translate.py @@ -5,26 +5,32 @@ from typing import Optional class Translation: - def __init__(self, text: str, to: str): + def __init__(self, text: str, fr: str, to: str): self.text = text + self.fr = fr self.to = to self.embed: Optional[discord.Embed] = 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] """ try: translator = Translator() - self.translation = translator.translate(query, to, "auto") + self.translation = translator.translate(query, to, fr) except ValueError as e: message = str(e) if "destination" in message: 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 @@ -42,8 +48,9 @@ class Translation: embed = discord.Embed(colour=discord.Colour.blue()) embed.set_author(name="Didier Translate") - language = self.translation.src - embed.add_field(name="Gedetecteerde taal", value=title_case(LANGUAGES[language])) + if self.fr == "auto": + language = self.translation.src + embed.add_field(name="Gedetecteerde taal", value=title_case(LANGUAGES[language])) if self.translation.extra_data["confidence"] is not None: embed.add_field(name="Zekerheid", value="{}%".format(self.translation.extra_data["confidence"] * 100)) diff --git a/files/help.json b/files/help.json index d6112ac..5738649 100644 --- a/files/help.json +++ b/files/help.json @@ -58,7 +58,7 @@ "jpl table": "De huidige stand van het klassement.", "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.", - "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.", "load": "Laadt [Cog] in.", "load all": "Laadt alle cogs in.", diff --git a/functions/checks.py b/functions/checks.py index 0cbb042..6d18dfe 100644 --- a/functions/checks.py +++ b/functions/checks.py @@ -1,8 +1,4 @@ -import math - import discord -from discord import utils, Member, User -from discord.ext import commands from data import constants import requests from functions.database import currency diff --git a/functions/database/commands.py b/functions/database/commands.py index fda1d11..f1bf35c 100644 --- a/functions/database/commands.py +++ b/functions/database/commands.py @@ -1,23 +1,20 @@ +from enum import IntEnum + from functions.database import utils +from functions.stringFormatters import leading_zero as lz import time -def invoked(): +class InvocationType(IntEnum): + TextCommand = 0 + SlashCommand = 1 + ContextMenu = 2 + + +def invoked(inv: InvocationType): t = time.localtime() - day_string: str = f"{t.tm_year}-{_lz(t.tm_mon)}-{_lz(t.tm_mday)}" - _update(day_string) - - -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 + day_string: str = f"{t.tm_year}-{lz(t.tm_mon)}-{lz(t.tm_mday)}" + _update(day_string, inv) def _is_present(date: str) -> bool: @@ -43,25 +40,27 @@ def _add_date(date: str): connection = utils.connect() 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() -def _update(date: str): +def _update(date: str, inv: InvocationType): """ 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): _add_date(date) - return connection = utils.connect() 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 - SET amount = amount + 1 + SET {column_name} = {column_name} + 1 WHERE day = %s """, (date,)) connection.commit() diff --git a/functions/football.py b/functions/football.py index 59edf45..ffe2a20 100644 --- a/functions/football.py +++ b/functions/football.py @@ -39,11 +39,11 @@ class Match: Parse class attributes out of a dictionary returned from an API request """ # The API isn't public, so every single game state is differently formatted - self.status = self._getStatus(self.matchDict[Navigation.Status.value]) + self.status = self._get_status(self.matchDict[Navigation.Status.value]) self.home = self.matchDict[Navigation.HomeTeam.value][Navigation.Name.value] self.away = self.matchDict[Navigation.AwayTeam.value][Navigation.Name.value] - if self._hasStarted(): + if self._has_started(): self.homeScore = self.matchDict[Navigation.HomeScore.value] self.awayScore = self.matchDict[Navigation.AwayScore.value] @@ -53,9 +53,9 @@ class Match: self.start = None self.date = self.start.strftime("%d/%m") if self.start is not None else "Uitgesteld" - self.weekDay = self._getWeekday() if self.start is not None else "??" + self.weekDay = self._get_weekday() if self.start is not None else "??" - def _getStatus(self, status: str): + def _get_status(self, status: str): """ Gets the string representation for the status of this match """ @@ -80,7 +80,7 @@ class Match: return statusses[status.lower()] - def _getWeekday(self): + def _get_weekday(self): """ Gets the day of the week this match is played on """ @@ -88,13 +88,13 @@ class Match: days = ["Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"] return days[day] - def getInfo(self): + def get_info(self): """ Returns a list of all the info of this class in order to create a table """ - return [self.weekDay, self.date, self.home, self._getScore(), self.away, self.status] + return [self.weekDay, self.date, self.home, self._get_score(), self.away, self.status] - def _getScore(self): + def _get_score(self): """ Returns a string representing the scoreboard """ @@ -102,12 +102,12 @@ class Match: return "??" # No score to show yet, show time when the match starts - if not self._hasStarted(): + if not self._has_started(): return "{}:{}".format(leading_zero(str(self.start.hour)), leading_zero(str(self.start.minute))) return "{} - {}".format(self.homeScore, self.awayScore) - def _hasStarted(self): + def _has_started(self): return self.status not in [Status.AfterToday.value, Status.NotStarted.value, Status.Postponed.value] @@ -128,7 +128,7 @@ class Navigation(Enum): Name = "name" -def getMatches(matchweek: int): +def get_matches(matchweek: int): """ Function that constructs the list of matches for a given matchweek """ @@ -139,7 +139,7 @@ def getMatches(matchweek: int): return "Er ging iets fout. Probeer het later opnieuw." matches = list(map(Match, current_day)) - matches = list(map(lambda x: x.getInfo(), matches)) + matches = list(map(lambda x: x.get_info(), matches)) header = "Jupiler Pro League - Speeldag {}".format(matchweek) table = tabulate.tabulate(matches, headers=["Dag", "Datum", "Thuis", "Stand", "Uit", "Tijd"]) @@ -147,7 +147,7 @@ def getMatches(matchweek: int): return "```{}\n\n{}```".format(header, table) -def getTable(): +def get_table(): """ Function that constructs the current table of the JPL """ @@ -157,7 +157,7 @@ def getTable(): return "Er ging iets fout. Probeer het later opnieuw." # Format every row to work for Tabulate - formatted = [_formatRow(row) for row in rows] + formatted = [_format_row(row) for row in rows] header = "Jupiler Pro League Klassement" table = tabulate.tabulate(formatted, headers=["#", "Ploeg", "Punten", "M", "M+", "M-", "M=", "D+", "D-", "D+/-"]) @@ -165,7 +165,7 @@ def getTable(): return "```{}\n\n{}```".format(header, table) -def _formatRow(row): +def _format_row(row): """ Function that formats a row into a list for Tabulate to use """ diff --git a/functions/stringFormatters.py b/functions/stringFormatters.py index b1646cf..f8eb20f 100644 --- a/functions/stringFormatters.py +++ b/functions/stringFormatters.py @@ -1,3 +1,9 @@ +import traceback + +from discord.ext.commands import Context +from dislash import SlashInteraction + + def title_case(string): return " ".join(capitalize(word) for word in string.split(" ")) @@ -13,3 +19,35 @@ def leading_zero(string, size=2): while len(string) < size: string = "0" + 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}" diff --git a/settings.py b/settings.py index b64e27c..12dd5e6 100644 --- a/settings.py +++ b/settings.py @@ -1,3 +1,5 @@ +from typing import List + from dotenv import load_dotenv import os @@ -31,3 +33,11 @@ TOKEN = os.getenv("TOKEN", "") 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 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(",") + ) +) diff --git a/startup/didier.py b/startup/didier.py index 850080c..365df73 100644 --- a/startup/didier.py +++ b/startup/didier.py @@ -2,7 +2,7 @@ from data.snipe import Snipe from discord.ext import commands, ipc from dislash import InteractionClient import os -from settings import HOST_IPC +from settings import HOST_IPC, SLASH_TEST_GUILDS from startup.init_files import check_all from typing import Dict @@ -33,7 +33,7 @@ class Didier(commands.Bot): self.remove_command("help") # Create interactions client - self.interactions = InteractionClient(self, test_guilds=[728361030404538488, 880175869841277008]) + self.interactions = InteractionClient(self, test_guilds=SLASH_TEST_GUILDS) # Load all extensions self.init_extensions()