From 3444414638a98acf4d04f633030addf22f8a2de9 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 3 Feb 2022 01:43:54 +0100 Subject: [PATCH 01/21] Port slash commands to pc --- cogs/slash/db_slash.py | 3 +- cogs/slash/define_slash.py | 13 +++------ cogs/slash/football_slash.py | 37 ++++++++++-------------- cogs/slash/fun_slash.py | 21 ++++---------- cogs/slash/google_slash.py | 15 ++++------ cogs/slash/school_slash.py | 53 ++++++++++++----------------------- cogs/slash/translate_slash.py | 20 ++++++------- requirements.txt | 6 ++-- startup/didier.py | 5 +--- 9 files changed, 62 insertions(+), 111 deletions(-) diff --git a/cogs/slash/db_slash.py b/cogs/slash/db_slash.py index 1109b35..bb2ee89 100644 --- a/cogs/slash/db_slash.py +++ b/cogs/slash/db_slash.py @@ -84,4 +84,5 @@ class DBSlash(commands.Cog): def setup(client: Didier): - client.add_cog(DBSlash(client)) + # client.add_cog(DBSlash(client)) + pass diff --git a/cogs/slash/define_slash.py b/cogs/slash/define_slash.py index b291bb9..9fb8ce1 100644 --- a/cogs/slash/define_slash.py +++ b/cogs/slash/define_slash.py @@ -1,5 +1,5 @@ from discord.ext import commands -from dislash import SlashInteraction, slash_command, Option, OptionType +from discord.commands import slash_command, ApplicationContext, Option from data.embeds.urban_dictionary import Definition from startup.didier import Didier @@ -9,15 +9,10 @@ class DefineSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client - @slash_command(name="define", - description="Urban Dictionary", - options=[ - Option("query", "Search query", OptionType.STRING, required=True) - ] - ) - async def _define_slash(self, interaction: SlashInteraction, query): + @slash_command(name="define", description="Urban Dictionary") + async def _define_slash(self, ctx: ApplicationContext, query: Option(str, "Search query", required=True)): embed = Definition(query).to_embed() - await interaction.reply(embed=embed) + await ctx.respond(embed=embed) def setup(client: Didier): diff --git a/cogs/slash/football_slash.py b/cogs/slash/football_slash.py index d5a7283..d10ba31 100644 --- a/cogs/slash/football_slash.py +++ b/cogs/slash/football_slash.py @@ -1,6 +1,6 @@ from discord.ext import commands -from dislash import SlashInteraction, slash_command, Option, OptionType -from functions import config, checks +from discord.commands import Option, SlashCommandGroup, ApplicationContext, permissions +from functions import config from functions.football import get_matches, get_table, get_jpl_code from startup.didier import Didier @@ -9,35 +9,28 @@ 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 = SlashCommandGroup("jpl", "Jupiler Pro League commands") - @_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): + @_jpl_group.command(name="matches", description="Schema voor een bepaalde speeldag") + async def _jpl_matches_slash(self, ctx: ApplicationContext, + day: Option(int, name="day", description="Speeldag (default huidige)", required=False, default=None) + ): # Default is current day if day is None: day = int(config.get("jpl_day")) - await interaction.reply(get_matches(day)) + await ctx.respond(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.") + @_jpl_group.command(name="table", description="Huidige rangschikking") + async def _jpl_table_slash(self, ctx: ApplicationContext): + await ctx.respond(get_table()) + @_jpl_group.command(name="update", description="Update de code voor deze competitie (owner-only)", default_permission=False) + @permissions.is_owner() + async def _jpl_update_slash(self, ctx: ApplicationContext): code = get_jpl_code() config.config("jpl", code) - await interaction.reply(f"Done (code: {code})") + await ctx.respond(f"Done (code: {code})") def setup(client: Didier): diff --git a/cogs/slash/fun_slash.py b/cogs/slash/fun_slash.py index bd4191a..00c3c24 100644 --- a/cogs/slash/fun_slash.py +++ b/cogs/slash/fun_slash.py @@ -1,5 +1,5 @@ from discord.ext import commands -from dislash import SlashInteraction, slash_command, Option, OptionType +from discord.commands import slash_command, ApplicationContext, Option from data.embeds.xkcd import XKCDEmbed from startup.didier import Didier @@ -9,20 +9,11 @@ class FunSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client - @slash_command( - name="xkcd", - description="Zoek xkcd comics", - options=[ - Option( - "num", - description="Nummer van de comic (default de comic van vandaag).", - type=OptionType.INTEGER, - required=False - ) - ] - ) - async def _xkcd_slash(self, interaction: SlashInteraction, num: int = None): - return await interaction.reply(embed=XKCDEmbed(num).create()) + @slash_command(name="xkcd", description="Zoek xkcd comics") + async def _xkcd_slash(self, ctx: ApplicationContext, + num: Option(int, description="Nummer van de comic (default de comic van vandaag).", required=False, default=None) + ): + return await ctx.respond(embed=XKCDEmbed(num).create()) def setup(client: Didier): diff --git a/cogs/slash/google_slash.py b/cogs/slash/google_slash.py index 6978492..b115165 100644 --- a/cogs/slash/google_slash.py +++ b/cogs/slash/google_slash.py @@ -1,5 +1,5 @@ from discord.ext import commands -from dislash import slash_command, SlashInteraction, Option, OptionType +from discord.commands import slash_command, ApplicationContext, Option from functions.scrapers.google import google_search, create_google_embed from startup.didier import Didier @@ -8,20 +8,15 @@ class GoogleSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client - @slash_command(name="google", - description="Google search", - options=[ - Option("query", "Search query", OptionType.STRING, required=True) - ] - ) - async def _google_slash(self, interaction: SlashInteraction, query: str): + @slash_command(name="google", description="Google search") + async def _google_slash(self, ctx: ApplicationContext, query: Option(str, "Search query")): result = google_search(query) if not result.results: - return await interaction.reply("Er ging iets fout (Response {})".format(result.status_code)) + return await ctx.respond("Er ging iets fout (Response {})".format(result.status_code)) embed = create_google_embed(result) - await interaction.reply(embed=embed) + await ctx.respond(embed=embed) def setup(client: Didier): diff --git a/cogs/slash/school_slash.py b/cogs/slash/school_slash.py index 21b72d3..525e43d 100644 --- a/cogs/slash/school_slash.py +++ b/cogs/slash/school_slash.py @@ -1,5 +1,5 @@ from discord.ext import commands -from dislash import SlashInteraction, slash_command, Option, OptionType +from discord.commands import slash_command, ApplicationContext, Option from data import schedule from data.embeds.food import Menu @@ -14,39 +14,22 @@ class SchoolSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client - @slash_command( - name="eten", - description="Menu in de UGENT resto's op een bepaalde dag", - options=[ - Option( - "dag", - description="Dag", - type=OptionType.STRING - ) - ] - ) - async def _food_slash(self, interaction: SlashInteraction, dag: str = None): + @slash_command(name="eten", description="Menu in de UGent resto's op een bepaalde dag") + async def _food_slash(self, ctx: ApplicationContext, + dag: Option(str, description="Dag", required=False, default=None) + ): embed = Menu(dag).to_embed() - await interaction.reply(embed=embed) + await ctx.respond(embed=embed) @slash_command(name="deadlines", description="Aanstaande deadlines") - async def _deadlines_slash(self, interaction: SlashInteraction): + async def _deadlines_slash(self, ctx: ApplicationContext): embed = Deadlines().to_embed() - await interaction.reply(embed=embed) + await ctx.respond(embed=embed) - @slash_command( - name="les", - description="Lessenrooster voor [Dag] (default vandaag)", - options=[ - Option( - "dag", - description="dag", - type=OptionType.STRING, - required=False - ) - ] - ) - async def _schedule_slash(self, interaction: SlashInteraction, day: str = None): + @slash_command(name="les", description="Lessenrooster voor [Dag] (default vandaag)",) + async def _schedule_slash(self, ctx: ApplicationContext, + day: Option(str, description="Dag", required=False, default=None) + ): """It's late and I really don't want to refactor the original right now""" if day is not None: day = day.lower() @@ -55,21 +38,21 @@ class SchoolSlash(commands.Cog): # Person explicitly requested a weekend-day if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4: - return await interaction.reply(f"{capitalize(day)} is het weekend.", ephemeral=True) + return await ctx.respond(f"{capitalize(day)} is het weekend.", ephemeral=True) date = skip_weekends(date) s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None) if s.semester_over: - return await interaction.reply("Het semester is afgelopen.", ephemeral=True) + return await ctx.respond("Het semester is afgelopen.", ephemeral=True) # DM only shows user's own minor - if interaction.guild is None: - minor_roles = [*schedule.find_minor(self.client, interaction.author.id)] - return await interaction.reply(embed=s.create_schedule(minor_roles=minor_roles).to_embed()) + if ctx.guild is None: + minor_roles = [*schedule.find_minor(self.client, ctx.interaction.user.id)] + return await ctx.respond(embed=s.create_schedule(minor_roles=minor_roles).to_embed()) - return await interaction.reply(embed=s.create_schedule().to_embed()) + return await ctx.respond(embed=s.create_schedule().to_embed()) def setup(client: Didier): diff --git a/cogs/slash/translate_slash.py b/cogs/slash/translate_slash.py index 6ee293d..0cc20ac 100644 --- a/cogs/slash/translate_slash.py +++ b/cogs/slash/translate_slash.py @@ -1,5 +1,5 @@ from discord.ext import commands -from dislash import SlashInteraction, slash_command, Option, OptionType +from discord.commands import slash_command, ApplicationContext, Option from data.embeds.translate import Translation from startup.didier import Didier @@ -9,18 +9,14 @@ class TranslateSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client - @slash_command( - name="translate", - description="Google Translate", - options=[ - Option("text", "Tekst om te vertalen", OptionType.STRING, required=True), - 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, from_lang: str = "auto", to_lang: str = "nl"): + @slash_command(name="translate", description="Google Translate") + async def _translate_slash(self, ctx: ApplicationContext, + text: Option(str, description="Tekst om te vertalen"), + from_lang: Option(str, description="Taal om van te vertalen (default auto-detect)", default="auto"), + to_lang: Option(str, description="Taal om naar te vertalen (default NL)", default="nl") + ): translation = Translation(text=text, fr=from_lang.lower(), to=to_lang.lower()) - await interaction.reply(embed=translation.to_embed()) + await ctx.respond(embed=translation.to_embed()) def setup(client: Didier): diff --git a/requirements.txt b/requirements.txt index 3f96792..82af5ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ python-dotenv==0.14.0 beautifulsoup4==4.9.1 -discord.py==1.7.3 +# discord.py==1.7.3 git+https://github.com/Rapptz/discord-ext-menus@master discord-ext-ipc==2.0.0 psycopg2==2.8.5 @@ -20,5 +20,5 @@ dacite~=1.6.0 pytest==6.2.4 markdownify==0.9.2 -# Experimental package for slash commands & menus -dislash.py==1.4.9 \ No newline at end of file +# Beta version of Discord.py fork +py-cord==2.0.0b1 \ No newline at end of file diff --git a/startup/didier.py b/startup/didier.py index 365df73..d442e98 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, SLASH_TEST_GUILDS +from settings import HOST_IPC from startup.init_files import check_all from typing import Dict @@ -32,9 +32,6 @@ class Didier(commands.Bot): # Remove default help command self.remove_command("help") - # Create interactions client - self.interactions = InteractionClient(self, test_guilds=SLASH_TEST_GUILDS) - # Load all extensions self.init_extensions() From 853b708eceee39e8807a176c7f5ef480629d5be2 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 3 Feb 2022 01:49:00 +0100 Subject: [PATCH 02/21] Fix broken test import paths --- tests/{data => test_data}/__init__.py | 0 tests/{data/embeds => test_data/test_embeds}/__init__.py | 0 .../embeds => test_data/test_embeds}/test_urban_dictionary.py | 0 tests/{data => test_data}/test_regexes.py | 0 tests/{data => test_data}/test_schedule.py | 0 tests/{data => test_data}/test_snipe.py | 0 tests/{functions => test_functions}/__init__.py | 0 .../scrapers => test_functions/test_scrapers}/__init__.py | 0 .../scrapers => test_functions/test_scrapers}/test_sporza.py | 0 tests/{functions => test_functions}/test_timeFormatters.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename tests/{data => test_data}/__init__.py (100%) rename tests/{data/embeds => test_data/test_embeds}/__init__.py (100%) rename tests/{data/embeds => test_data/test_embeds}/test_urban_dictionary.py (100%) rename tests/{data => test_data}/test_regexes.py (100%) rename tests/{data => test_data}/test_schedule.py (100%) rename tests/{data => test_data}/test_snipe.py (100%) rename tests/{functions => test_functions}/__init__.py (100%) rename tests/{functions/scrapers => test_functions/test_scrapers}/__init__.py (100%) rename tests/{functions/scrapers => test_functions/test_scrapers}/test_sporza.py (100%) rename tests/{functions => test_functions}/test_timeFormatters.py (100%) diff --git a/tests/data/__init__.py b/tests/test_data/__init__.py similarity index 100% rename from tests/data/__init__.py rename to tests/test_data/__init__.py diff --git a/tests/data/embeds/__init__.py b/tests/test_data/test_embeds/__init__.py similarity index 100% rename from tests/data/embeds/__init__.py rename to tests/test_data/test_embeds/__init__.py diff --git a/tests/data/embeds/test_urban_dictionary.py b/tests/test_data/test_embeds/test_urban_dictionary.py similarity index 100% rename from tests/data/embeds/test_urban_dictionary.py rename to tests/test_data/test_embeds/test_urban_dictionary.py diff --git a/tests/data/test_regexes.py b/tests/test_data/test_regexes.py similarity index 100% rename from tests/data/test_regexes.py rename to tests/test_data/test_regexes.py diff --git a/tests/data/test_schedule.py b/tests/test_data/test_schedule.py similarity index 100% rename from tests/data/test_schedule.py rename to tests/test_data/test_schedule.py diff --git a/tests/data/test_snipe.py b/tests/test_data/test_snipe.py similarity index 100% rename from tests/data/test_snipe.py rename to tests/test_data/test_snipe.py diff --git a/tests/functions/__init__.py b/tests/test_functions/__init__.py similarity index 100% rename from tests/functions/__init__.py rename to tests/test_functions/__init__.py diff --git a/tests/functions/scrapers/__init__.py b/tests/test_functions/test_scrapers/__init__.py similarity index 100% rename from tests/functions/scrapers/__init__.py rename to tests/test_functions/test_scrapers/__init__.py diff --git a/tests/functions/scrapers/test_sporza.py b/tests/test_functions/test_scrapers/test_sporza.py similarity index 100% rename from tests/functions/scrapers/test_sporza.py rename to tests/test_functions/test_scrapers/test_sporza.py diff --git a/tests/functions/test_timeFormatters.py b/tests/test_functions/test_timeFormatters.py similarity index 100% rename from tests/functions/test_timeFormatters.py rename to tests/test_functions/test_timeFormatters.py From 17964a23fb5e7d417629a8acc4736106337b54d4 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 3 Feb 2022 01:52:26 +0100 Subject: [PATCH 03/21] Fix bug in help pages because of slash commands --- cogs/help.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/help.py b/cogs/help.py index c4b944b..598a8de 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -1,5 +1,6 @@ from data import constants import discord +from discord.commands import SlashCommand from discord.ext import commands from enums.help_categories import categories, getCategory, Category import json @@ -49,7 +50,7 @@ class HelpCommand(commands.MinimalHelpCommand): return await self.send_bot_help(self.get_bot_mapping()) # Turn dic to lowercase to allow proper name searching - all_commands = dict((k.lower(), v) for k, v in bot.all_commands.items()) + all_commands = dict((k.lower(), v) for k, v in bot.all_commands.items() if not isinstance(v, SlashCommand)) if spl[0].lower() not in all_commands: return await self.send_error_message(await self.command_not_found(spl[0])) From bca4fbf616c497f1ec3f4f2511453a077a17a083 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 3 Feb 2022 02:01:09 +0100 Subject: [PATCH 04/21] Add X Is X meme, small cleanups & style fixes --- functions/memes.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/functions/memes.py b/functions/memes.py index 561327f..4ffa2e4 100644 --- a/functions/memes.py +++ b/functions/memes.py @@ -14,7 +14,7 @@ def generate(meme: Meme, fields): if meme.fields == 1: fields = [" ".join(fields)] - fields = _applyMeme(meme, fields) + fields = _apply_meme(meme, fields) # List of fields to send to the API boxes = [{"text": ""}, {"text": ""}, {"text": ""}, {"text": ""}] @@ -33,7 +33,7 @@ def generate(meme: Meme, fields): return {"success": False, "message": "Er is een fout opgetreden."} # Post meme - reply = _postMeme(meme, boxes) + reply = _post_meme(meme, boxes) # Adding a message parameter makes the code in the cog a lot cleaner if not reply["success"]: @@ -45,7 +45,7 @@ def generate(meme: Meme, fields): return reply -def _postMeme(meme: Meme, boxes): +def _post_meme(meme: Meme, boxes): """ Performs API request to generate the meme """ @@ -65,7 +65,7 @@ def _postMeme(meme: Meme, boxes): return memeReply -def _applyMeme(meme: Meme, fields): +def _apply_meme(meme: Meme, fields): """ Some memes are in a special format that only requires a few words to be added, or needs the input to be changed. @@ -74,9 +74,10 @@ def _applyMeme(meme: Meme, fields): Links certain meme id's to functions that need to be applied first. """ memeDict = { - 102156234: _mockingSpongebob, - 91538330: _xXEverywhere, - 252600902: _alwaysHasBeen + 102156234: mocking_spongebob, + 91538330: _x_x_everywhere, + 252600902: _always_has_been, + 167754325: _math_is_math } # Meme needs no special treatment @@ -86,17 +87,23 @@ def _applyMeme(meme: Meme, fields): return memeDict[meme.meme_id](fields) -def _mockingSpongebob(fields): +def mocking_spongebob(fields): return list(map(mock, fields)) -def _xXEverywhere(fields): +def _x_x_everywhere(fields): word = fields[0] return ["{}".format(word), "{} everywhere".format(word)] -def _alwaysHasBeen(fields): +def _always_has_been(fields): word = fields[0] - return ["Wait, it's all {}?".format(word), "Always has been"] + return ["Wait, {}?".format(word), "Always has been"] + + +def _math_is_math(fields): + word = fields[0] + + return [f"{word.upper()} IS {word.upper()}!"] From 9eb0eb5a61f0bf3a5003f2ee9053cd1f0ba0625a Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 3 Feb 2022 02:02:58 +0100 Subject: [PATCH 05/21] Change order of requirements in a desperate attempt that it would matter at all --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 82af5ae..e1c003b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ +# Beta version of Discord.py fork +py-cord==2.0.0b1 + python-dotenv==0.14.0 beautifulsoup4==4.9.1 # discord.py==1.7.3 @@ -18,7 +21,4 @@ Quart-CORS==0.5.0 attrs~=21.2.0 dacite~=1.6.0 pytest==6.2.4 -markdownify==0.9.2 - -# Beta version of Discord.py fork -py-cord==2.0.0b1 \ No newline at end of file +markdownify==0.9.2 \ No newline at end of file From eaed08168c13cf7286672ac72e973f27bf838eda Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 5 Feb 2022 14:33:11 +0100 Subject: [PATCH 06/21] Inspire slash command, defer jpl table --- .gitignore | 1 + cogs/oneliners.py | 2 +- cogs/slash/football_slash.py | 1 + cogs/slash/other_slash.py | 23 +++++++++++++++++++++++ files/help.json | 2 +- 5 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 cogs/slash/other_slash.py diff --git a/.gitignore b/.gitignore index 9b3c80f..02e269f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ files/ufora_notifications.json __pycache__ .env /venv/ +.pytest_cache \ No newline at end of file diff --git a/cogs/oneliners.py b/cogs/oneliners.py index 20bc822..358a822 100644 --- a/cogs/oneliners.py +++ b/cogs/oneliners.py @@ -151,7 +151,7 @@ class Oneliners(commands.Cog): @commands.command(name="Inspire") @help.Category(Category.Other) async def inspire(self, ctx): - image = get("http://inspirobot.me/api?generate=true") + image = get("https://inspirobot.me/api?generate=true") if image.status_code == 200: await ctx.send(image.text) diff --git a/cogs/slash/football_slash.py b/cogs/slash/football_slash.py index d10ba31..0f75eee 100644 --- a/cogs/slash/football_slash.py +++ b/cogs/slash/football_slash.py @@ -23,6 +23,7 @@ class FootballSlash(commands.Cog): @_jpl_group.command(name="table", description="Huidige rangschikking") async def _jpl_table_slash(self, ctx: ApplicationContext): + await ctx.response.defer() await ctx.respond(get_table()) @_jpl_group.command(name="update", description="Update de code voor deze competitie (owner-only)", default_permission=False) diff --git a/cogs/slash/other_slash.py b/cogs/slash/other_slash.py new file mode 100644 index 0000000..39350c7 --- /dev/null +++ b/cogs/slash/other_slash.py @@ -0,0 +1,23 @@ +from discord.ext import commands +from discord.commands import slash_command, ApplicationContext +from requests import get + +from startup.didier import Didier + + +class OtherSlash(commands.Cog): + def __init__(self, client: Didier): + self.client: Didier = client + + @slash_command(name="inspire", description="Genereer quotes via Inspirobot.") + async def _inspire_slash(self, ctx: ApplicationContext): + image = get("https://inspirobot.me/api?generate=true") + + if image.status_code == 200: + await ctx.respond(image.text) + else: + await ctx.respond("Uh oh API down.") + + +def setup(client: Didier): + client.add_cog(OtherSlash(client)) diff --git a/files/help.json b/files/help.json index a522c9b..f14e6f1 100644 --- a/files/help.json +++ b/files/help.json @@ -52,7 +52,7 @@ "hangman start": "Start een nieuwe Hangman game indien er nog geen bezig is. Indien je geen woord opgeeft, wordt er een willekeurig woord gekozen.\n**Indien je wel een woord opgeeft, werkt dit enkel in DM.**", "hangman guess": "Probeer het woord te raden.", "claim": "Claim [Aantal] Didier Dinks uit je profit.\nIndien je geen aantal opgeeft (of \"all\"), claim je alles, inclusief je investering.", - "inspire": "Generate quotes via [InspiroBot](https://inspirobot.me/).", + "inspire": "Genereer quotes via [InspiroBot](https://inspirobot.me/).", "inventory": "Bekijk de items in jouw inventory.", "invest": "Investeer [Aantal] Didier Dinks in jouw Didier Bank om rente te vergaren.", "jpl": "Informatie over de Jupiler Pro League.", From 829729c8db75f7abdb532968ba4d1be33fcb4f55 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 5 Feb 2022 14:43:19 +0100 Subject: [PATCH 07/21] Disable IPC --- cogs/ipc.py | 3 ++- didier.py | 5 ----- requirements.txt | 2 +- startup/didier.py | 15 +-------------- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/cogs/ipc.py b/cogs/ipc.py index 1e3c394..d24864d 100644 --- a/cogs/ipc.py +++ b/cogs/ipc.py @@ -23,4 +23,5 @@ class IPC(commands.Cog): def setup(client): - client.add_cog(IPC(client)) + # client.add_cog(IPC(client)) + pass diff --git a/didier.py b/didier.py index 4530c68..70790bd 100644 --- a/didier.py +++ b/didier.py @@ -16,9 +16,4 @@ if __name__ == "__main__": intents.members = True client = Didier(command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status) - - # Run IPC server if necessary - if client.ipc is not None: - client.ipc.start() - client.run(TOKEN) diff --git a/requirements.txt b/requirements.txt index e1c003b..e02e23c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ python-dotenv==0.14.0 beautifulsoup4==4.9.1 # discord.py==1.7.3 git+https://github.com/Rapptz/discord-ext-menus@master -discord-ext-ipc==2.0.0 +# discord-ext-ipc==2.0.0 psycopg2==2.8.5 psycopg2-binary==2.8.5 python-dateutil==2.6.1 diff --git a/startup/didier.py b/startup/didier.py index d442e98..dbdfc9f 100644 --- a/startup/didier.py +++ b/startup/didier.py @@ -1,8 +1,7 @@ from data.snipe import Snipe -from discord.ext import commands, ipc +from discord.ext import commands from dislash import InteractionClient import os -from settings import HOST_IPC from startup.init_files import check_all from typing import Dict @@ -20,12 +19,6 @@ class Didier(commands.Bot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._host_ipc = HOST_IPC - - # IPC Server - # TODO secret key - self.ipc = ipc.Server(self, secret_key="SOME_SECRET_KEY") if self._host_ipc else None - # Cogs that should be loaded before the others self._preload = ("ipc", "utils", "failedchecks", "events",) @@ -62,9 +55,3 @@ class Didier(commands.Bot): # Subdirectory # Also walrus operator hype self._init_directory(new_path) - - async def on_ipc_ready(self): - print("IPC server is ready.") - - async def on_ipc_error(self, endpoint, error): - print(endpoint, "raised", error) From 06dc3d3fb9ba0573716f0b6790eabb2b9e59ad60 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 5 Feb 2022 19:36:25 +0100 Subject: [PATCH 08/21] Create updated leaderboards --- cogs/fun.py | 7 +- cogs/leaderboards.py | 58 +++++++------ cogs/store.py | 10 +-- cogs/train.py | 10 +-- data/menus/paginatedLeaderboard.py | 31 ------- data/menus/paginated_leaderboard.py | 127 ++++++++++++++++++++++++++++ data/menus/storePages.py | 28 ------ functions/utils.py | 33 ++++++++ requirements.txt | 3 - 9 files changed, 205 insertions(+), 102 deletions(-) delete mode 100644 data/menus/paginatedLeaderboard.py create mode 100644 data/menus/paginated_leaderboard.py delete mode 100644 data/menus/storePages.py create mode 100644 functions/utils.py diff --git a/cogs/fun.py b/cogs/fun.py index f797803..affd2c9 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -1,5 +1,5 @@ from data.embeds.xkcd import XKCDEmbed -from data.menus import paginatedLeaderboard +from data.menus import paginated_leaderboard from decorators import help import discord from discord.ext import commands @@ -124,8 +124,9 @@ class Fun(commands.Cog): memeList = [": ".join([stringFormatters.title_case(meme[1]), str(meme[2])]) for meme in sorted(memeList, key=lambda x: x[1])] - pages = paginatedLeaderboard.Pages(source=paginatedLeaderboard.Source(memeList, "Memes", discord.Colour.blue()), - clear_reactions_after=True) + pages = paginated_leaderboard.Pages( + source=paginated_leaderboard.Source(memeList, "Memes", discord.Colour.blue()), + clear_reactions_after=True) await pages.start(ctx) @commands.command(name="Pjoke") diff --git a/cogs/leaderboards.py b/cogs/leaderboards.py index 724bf54..6eb9edf 100644 --- a/cogs/leaderboards.py +++ b/cogs/leaderboards.py @@ -1,4 +1,4 @@ -from data.menus import paginatedLeaderboard +from data.menus import paginated_leaderboard from decorators import help import discord from discord.ext import commands @@ -45,35 +45,36 @@ class Leaderboards(commands.Cog): user[1] += platDinks[str(user[0])] * Numbers.q.value entries[i] = user - boardTop = [] + data = [] for i, user in enumerate(sorted(entries, key=lambda x: (float(x[1]) + float(x[3])), reverse=True)): if i == 0 and float(user[1]) + float(user[3]) == 0.0: - return await self.emptyLeaderboard(ctx, "Dinks Leaderboard", "Er zijn nog geen personen met Didier Dinks.") + return await self.empty_leaderboard(ctx, "Dinks Leaderboard", + "Er zijn nog geen personen met Didier Dinks.") elif float(user[1]) + float(user[3]) > 0.0: + total_dinks = math.floor(float(user[1]) + float(user[3])) + data.append((user[0], total_dinks,)) - # Get the username in this guild - name = self.utilsCog.getDisplayName(ctx, user[0]) + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Dinks Leaderboard", data=data, fetch_names=True + ) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} ({:,})**".format(name, math.floor(float(user[1]) + float(user[3])))) - else: - boardTop.append("{} ({:,})".format(name, math.floor(float(user[1]) + float(user[3])))) - - await self.startPaginated(ctx, boardTop, "Dinks Leaderboard") + await lb.send(ctx) @leaderboard.command(name="Corona", hidden=True) async def corona(self, ctx): - result = requests.get("http://corona.lmao.ninja/v2/countries").json() + result = requests.get("https://disease.sh/v3/covid-19/countries").json() result.sort(key=lambda x: int(x["cases"]), reverse=True) - board = [] - for land in result: - if land["country"] == "Belgium": - board.append("**{} ({:,})**".format(land["country"], land["cases"])) - else: - board.append("{} ({:,})".format(land["country"], land["cases"])) + data = [] + for country in result: + data.append((country["country"], f"{country['cases']:,}",)) - await self.startPaginated(ctx, board, "Corona Leaderboard", discord.Colour.red()) + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Corona Leaderboard", data=data, highlight="Belgium", + colour=discord.Colour.red() + ) + + await lb.send(ctx) @leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True) async def bitcoin(self, ctx): @@ -82,7 +83,8 @@ class Leaderboards(commands.Cog): for i, user in enumerate(sorted(users, key=lambda x: x[8], reverse=True)): # Don't create an empty leaderboard if i == 0 and float(user[8]) == 0.0: - return await self.emptyLeaderboard(ctx, "Bitcoin Leaderboard", "Er zijn nog geen personen met Bitcoins.") + return await self.empty_leaderboard(ctx, "Bitcoin Leaderboard", + "Er zijn nog geen personen met Bitcoins.") elif float(user[8]) > 0.0: # Only add people with more than 0 # Get the username in this guild @@ -101,7 +103,8 @@ class Leaderboards(commands.Cog): for i, user in enumerate(sorted(users, key=lambda x: x[4], reverse=True)): # Don't create an empty leaderboard if i == 0 and float(user[4]) == 0.0: - return await self.emptyLeaderboard(ctx, "Rob Leaderboard", "Er heeft nog niemand Didier Dinks gestolen.") + return await self.empty_leaderboard(ctx, "Rob Leaderboard", + "Er heeft nog niemand Didier Dinks gestolen.") elif float(user[4]) > 0.0: # Only add people with more than 0 # Get the username in this guild @@ -119,7 +122,7 @@ class Leaderboards(commands.Cog): boardTop = [] for i, user in enumerate(sorted(s, key=lambda x: x[1], reverse=True)): if i == 0 and int(user[1]) == 0: - return await self.emptyLeaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.") + return await self.empty_leaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.") elif int(user[1]) == 0: break @@ -177,7 +180,7 @@ class Leaderboards(commands.Cog): boardTop = [] for i, user in enumerate(sorted(users, key=lambda x: x[1], reverse=True)): if i == 0 and int(user[1]) == 0: - return await self.emptyLeaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.") + return await self.empty_leaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.") if float(user[1]) == 0: break @@ -190,14 +193,15 @@ class Leaderboards(commands.Cog): await self.startPaginated(ctx, boardTop, "Muttn Leaderboard") async def callLeaderboard(self, name, ctx): - await [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0](ctx) + command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0] + await command(ctx) async def startPaginated(self, ctx, source, name, colour=discord.Colour.blue()): - pages = paginatedLeaderboard.Pages(source=paginatedLeaderboard.Source(source, name, colour), - clear_reactions_after=True) + pages = paginated_leaderboard.Pages(source=paginated_leaderboard.Source(source, name, colour), + clear_reactions_after=True) await pages.start(ctx) - async def emptyLeaderboard(self, ctx, name, message, colour=discord.Colour.blue()): + async def empty_leaderboard(self, ctx, name, message, colour=discord.Colour.blue()): embed = discord.Embed(colour=colour) embed.set_author(name=name) embed.description = message diff --git a/cogs/store.py b/cogs/store.py index 25ee5c1..d24a500 100644 --- a/cogs/store.py +++ b/cogs/store.py @@ -1,5 +1,4 @@ from converters.numbers import Abbreviated -from data.menus import storePages from decorators import help import discord from discord.ext import commands @@ -22,11 +21,12 @@ class Store(commands.Cog): @commands.check(checks.allowedChannels) @help.Category(Category.Currency) async def store(self, ctx): - entries = store.getAllItems() - await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx) + pass + # entries = store.getAllItems() + # await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx) @store.command(name="Buy", aliases=["Get"], hidden=True) - async def storeBuy(self, ctx, item, amount: Abbreviated = 1): + async def store_buy(self, ctx, item, amount: Abbreviated = 1): if amount is None: return @@ -56,7 +56,7 @@ class Store(commands.Cog): )) @store.command(name="Sell", hidden=True) - async def storeSell(self, ctx, itemid, amount: Abbreviated = 1): + async def store_sell(self, ctx, itemid, amount: Abbreviated = 1): if amount is None: return await self.sell(ctx, itemid, amount) diff --git a/cogs/train.py b/cogs/train.py index 10dcbb0..5865bf8 100644 --- a/cogs/train.py +++ b/cogs/train.py @@ -1,4 +1,4 @@ -from data.menus import paginatedLeaderboard +from data.menus import paginated_leaderboard from decorators import help import discord from discord.ext import commands, menus @@ -36,10 +36,10 @@ class Train(commands.Cog): await self.sendEmbed(ctx, embed) return - pages = paginatedLeaderboard.Pages(source=TrainPagination(self.formatConnections(req["connection"]), - self.formatCity(departure), - self.formatCity(destination)), - clear_reactions_after=True) + pages = paginated_leaderboard.Pages(source=TrainPagination(self.formatConnections(req["connection"]), + self.formatCity(departure), + self.formatCity(destination)), + clear_reactions_after=True) await pages.start(ctx) def formatConnections(self, connections): diff --git a/data/menus/paginatedLeaderboard.py b/data/menus/paginatedLeaderboard.py deleted file mode 100644 index 45b5e35..0000000 --- a/data/menus/paginatedLeaderboard.py +++ /dev/null @@ -1,31 +0,0 @@ -import discord -from discord.ext import menus - - -# https://github.com/Rapptz/discord-ext-menus -class Source(menus.ListPageSource): - def __init__(self, data, name, colour=discord.Colour.blue()): - super().__init__(data, per_page=10) - self.name = name - self.colour = colour - - async def format_page(self, menu: menus.MenuPages, entries): - offset = menu.current_page * self.per_page - - description = "" - for i, v in enumerate(entries, start=offset): - # Check if the person's name has to be highlighted - if v.startswith("**") and v.endswith("**"): - description += "**" - v = v[2:] - description += "{}: {}\n".format(i + 1, v) - embed = discord.Embed(colour=self.colour) - embed.set_author(name=self.name) - embed.description = description - embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) - return embed - - -class Pages(menus.MenuPages): - def __init__(self, source, clear_reactions_after, timeout=30.0): - super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) diff --git a/data/menus/paginated_leaderboard.py b/data/menus/paginated_leaderboard.py new file mode 100644 index 0000000..dd9bec7 --- /dev/null +++ b/data/menus/paginated_leaderboard.py @@ -0,0 +1,127 @@ +from typing import Callable + +import discord +from discord import ApplicationContext +from discord.ext import menus, pages +from dataclasses import dataclass + +from discord.ext.commands import Context + +from functions.utils import get_display_name + + +@dataclass +class Leaderboard: + ctx: Context + title: str + data: list + highlight: str = None + format_f: Callable = None + per_page: int = 10 + colour: discord.Colour = discord.Colour.blue() + fetch_names: bool = False + + def __post_init__(self): + if self.format_f is None: + self.format_f = self._format + + def _should_highlight(self, data) -> bool: + """Check if an entry should be highlighted""" + if self.fetch_names: + return data == self.ctx.author.id + + return data == self.highlight + + def _format(self, index: int, data: tuple) -> str: + name = data[0] + + if self.fetch_names: + name = get_display_name(self.ctx, int(data[0])) + + s = f"{index + 1}: {name} ({data[1]})" + + return s + + def _get_page_count(self) -> int: + """Get the amount of pages required to represent this data""" + count = len(self.data) // self.per_page + if len(self.data) % self.per_page != 0: + count += 1 + + return count + + def _create_embed(self, description: str) -> discord.Embed: + embed = discord.Embed(colour=self.colour) + embed.set_author(name=self.title) + embed.description = description + + return embed + + def create_pages(self) -> list[discord.Embed]: + # Amount of entries added to this page + added = 0 + page_list = [] + + description = "" + for i, v in enumerate(self.data): + s = self.format_f(i, v) + + if self._should_highlight(v[0]): + s = f"**{s}**" + + description += s + "\n" + added += 1 + + # Page full, create an embed & change counters + if added == self.per_page: + embed = self._create_embed(description) + + description = "" + added = 0 + page_list.append(embed) + + # Add final embed + if added != 0: + embed = self._create_embed(description) + page_list.append(embed) + + return page_list + + def create_paginator(self) -> pages.Paginator: + return pages.Paginator(pages=self.create_pages(), show_disabled=False, disable_on_timeout=True, timeout=30) + + async def respond(self, ctx: ApplicationContext, **kwargs) -> discord.Message: + paginator = self.create_paginator() + return await paginator.respond(ctx.interaction, **kwargs) + + async def send(self, ctx: Context, **kwargs) -> discord.Message: + paginator = self.create_paginator() + return await paginator.send(ctx, **kwargs) + + +class Source(menus.ListPageSource): + def __init__(self, data, name, colour=discord.Colour.blue()): + super().__init__(data, per_page=10) + self.name = name + self.colour = colour + + async def format_page(self, menu: menus.MenuPages, entries): + offset = menu.current_page * self.per_page + + description = "" + for i, v in enumerate(entries, start=offset): + # Check if the person's name has to be highlighted + if v.startswith("**") and v.endswith("**"): + description += "**" + v = v[2:] + description += "{}: {}\n".format(i + 1, v) + embed = discord.Embed(colour=self.colour) + embed.set_author(name=self.name) + embed.description = description + embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) + return embed + + +class Pages(menus.MenuPages): + def __init__(self, source, clear_reactions_after, timeout=30.0): + super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) diff --git a/data/menus/storePages.py b/data/menus/storePages.py deleted file mode 100644 index f794554..0000000 --- a/data/menus/storePages.py +++ /dev/null @@ -1,28 +0,0 @@ -import discord -from discord.ext import menus - - -# https://github.com/Rapptz/discord-ext-menus -class Source(menus.ListPageSource): - def __init__(self, data): - super().__init__(data, per_page=10) - self.name = "Didier Store" - self.colour = discord.Colour.blue() - - async def format_page(self, menu: menus.MenuPages, entries): - offset = menu.current_page * self.per_page - - embed = discord.Embed(colour=self.colour) - embed.set_author(name=self.name) - embed.description = "Heb je een idee voor een item? DM DJ STIJN met je idee!" - embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) - - for i, v in enumerate(entries, start=offset): - embed.add_field(name="#{} - {}".format(v[0], v[1]), value="{:,} Didier Dinks".format(v[2])) - - return embed - - -class Pages(menus.MenuPages): - def __init__(self, source, clear_reactions_after, timeout=30.0): - super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) diff --git a/functions/utils.py b/functions/utils.py new file mode 100644 index 0000000..b1a692a --- /dev/null +++ b/functions/utils.py @@ -0,0 +1,33 @@ +from typing import Union + +from discord import ApplicationContext +from discord.ext.commands import Context + +from data import constants + + +def get_display_name(ctx: Union[ApplicationContext, Context], user_id: int) -> str: + author = ctx.author if isinstance(ctx, Context) else ctx.user + + # Check if this is a DM, or the user is not in the guild + if ctx.guild is None or ctx.guild.get_member(user_id) is None: + # User is the author, no need to fetch their name + if user_id == author.id: + return author.display_name + + # Get member instance from CoC + COC = ctx.bot.get_guild(int(constants.DeZandbak)) + member = COC.get_member(user_id) + if member is not None: + return member.display_name + + # Try to fetch the user + user = ctx.bot.get_user(user_id) + if user is not None: + return user.name + + # User couldn't be found + return f"[? | {user_id}]" + + mem = ctx.guild.get_member(user_id) + return mem.display_name diff --git a/requirements.txt b/requirements.txt index e02e23c..a3d6c9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,6 @@ py-cord==2.0.0b1 python-dotenv==0.14.0 beautifulsoup4==4.9.1 -# discord.py==1.7.3 -git+https://github.com/Rapptz/discord-ext-menus@master -# discord-ext-ipc==2.0.0 psycopg2==2.8.5 psycopg2-binary==2.8.5 python-dateutil==2.6.1 From 7ad2bf351e5c38e211e381e84c2f0c579d48b3c7 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 5 Feb 2022 21:24:08 +0100 Subject: [PATCH 09/21] Remove broken libraries & functionality, format slash command usage --- backend/server.py | 82 -------------------------------- cogs/events.py | 17 ++++--- cogs/ipc.py | 27 ----------- cogs/slash/db_slash.py | 88 ----------------------------------- didier.py | 3 -- functions/stringFormatters.py | 13 +++--- requirements.txt | 2 - settings.py | 3 +- startup/didier.py | 6 +-- 9 files changed, 18 insertions(+), 223 deletions(-) delete mode 100644 backend/server.py delete mode 100644 cogs/ipc.py delete mode 100644 cogs/slash/db_slash.py diff --git a/backend/server.py b/backend/server.py deleted file mode 100644 index 3450233..0000000 --- a/backend/server.py +++ /dev/null @@ -1,82 +0,0 @@ -from discord.ext import ipc -from functions.database import custom_commands -import json -from quart import Quart, jsonify, request -from quart_cors import cors -from time import time - - -app = Quart(__name__) -# TODO allow_origin=re.compile(r"http://localhost:.*") -# needs higher Python & Quart version -app = cors(app, allow_origin="*") -app.config.from_object(__name__) - - -ipc_client = ipc.Client(secret_key="SOME_SECRET_KEY") - - -@app.route("/ping", methods=["GET"]) -async def ping(): - """ - Send a ping request, monitors bot latency and endpoint time - """ - latency = await ipc_client.request("get_bot_latency") - - return jsonify({"bot_latency": latency, "response_sent": time()}) - - -@app.route("/dm", methods=["POST"]) -async def send_dm(): - """ - Send a DM to the given user - """ - data = json.loads((await request.body).decode('UTF-8')) - - dm = await ipc_client.request( - "send_dm", - user=int(data["userid"]), - message=data.get("message") - ) - - return jsonify({"response": dm}) - - -@app.route("/custom", methods=["GET"]) -async def get_all_custom_commands(): - """ - Return a list of all custom commands in the bot - """ - commands = custom_commands.get_all() - - return jsonify(commands) - - -@app.route("/custom/") -async def get_custom_command(command_id): - try: - command_id = int(command_id) - except ValueError: - # Id is not an int - return unprocessable_entity("Parameter id was not a valid integer.") - - command = custom_commands.get_by_id(command_id) - - if command is None: - return page_not_found("") - - return jsonify(command) - - -@app.errorhandler(404) -def page_not_found(e): - return jsonify({"error": "No resource could be found matching the given URL."}), 404 - - -@app.errorhandler(422) -def unprocessable_entity(e): - return jsonify({"error": e}), 422 - - -if __name__ == "__main__": - app.run() diff --git a/cogs/events.py b/cogs/events.py index 70809b1..6696d4c 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -1,11 +1,10 @@ -from dislash import SlashInteraction +from discord import Interaction from data import constants from data.snipe import Snipe, Action, should_snipe import datetime import discord from discord.ext import commands -from dislash.application_commands.errors import InteractionCheckFailure from functions import checks, easterEggResponses, stringFormatters from functions.database import stats, muttn, custom_commands, commands as command_stats import pytz @@ -86,6 +85,7 @@ class Events(commands.Cog): Logs commands in your terminal. :param ctx: Discord Context """ + print("a") print(stringFormatters.format_command_usage(ctx)) command_stats.invoked(command_stats.InvocationType.TextCommand) @@ -123,24 +123,27 @@ class Events(commands.Cog): await self.sendErrorEmbed(err, "Command", usage) @commands.Cog.listener() - async def on_slash_command(self, interaction: SlashInteraction): + async def on_interaction(self, interaction: Interaction): """ Function called whenever someone uses a slash command """ + if not interaction.is_command(): + return + 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): + async def on_application_command_error(self, ctx: discord.ApplicationContext, err): # Debugging Didier shouldn't spam the error logs if self.client.user.id != int(constants.didierId): raise err - if isinstance(err, InteractionCheckFailure): - return await interaction.reply("Je hebt geen toegang tot dit commando.", ephemeral=True) + if isinstance(err, commands.CheckFailure): + return await ctx.respond("Je hebt geen toegang tot dit commando.", ephemeral=True) - usage = stringFormatters.format_slash_command_usage(interaction) + usage = stringFormatters.format_slash_command_usage(ctx.interaction) await self.sendErrorEmbed(err, "Slash Command", usage) @commands.Cog.listener() diff --git a/cogs/ipc.py b/cogs/ipc.py deleted file mode 100644 index d24864d..0000000 --- a/cogs/ipc.py +++ /dev/null @@ -1,27 +0,0 @@ -from discord.ext import commands, ipc - - -class IPC(commands.Cog): - def __init__(self, client): - self.client = client - - @ipc.server.route() - async def send_dm(self, data): - print("got here") - user = self.client.get_user(data.user) - await user.send(data.message) - print("sent") - return True - - @ipc.server.route() - async def get_bot_latency(self, data): - """ - Get Didier's latency - """ - - return self.client.latency * 1000 - - -def setup(client): - # client.add_cog(IPC(client)) - pass diff --git a/cogs/slash/db_slash.py b/cogs/slash/db_slash.py deleted file mode 100644 index bb2ee89..0000000 --- a/cogs/slash/db_slash.py +++ /dev/null @@ -1,88 +0,0 @@ -import datetime -import json - -from discord.ext import commands -from dislash import SlashInteraction, slash_command, Option, OptionType, check -from functions.checks import isMe -from functions.timeFormatters import fromString -from startup.didier import Didier - - -class DBSlash(commands.Cog): - def __init__(self, client: Didier): - self.client: Didier = client - - @slash_command(name="db") - @check(isMe) - async def _db_slash(self, interaction: SlashInteraction): - pass - - @_db_slash.sub_command_group(name="add") - async def _add_slash(self, interaction: SlashInteraction): - pass - - @_add_slash.sub_command( - name="deadline", - options=[ - Option( - "year", - description="Year (1-based)", - type=OptionType.INTEGER, - required=True - ), - Option( - "course", - description="Course (abbreviated)", - type=OptionType.STRING, - required=True - ), - Option( - "name", - description="Name of the deadline/project", - type=OptionType.STRING, - required=True - ), - Option( - "date", - description="Date (DD/MM)", - type=OptionType.STRING, - required=True - ), - Option( - "time", - description="Timestamp (HH:MM or HH:MM:SS)", - type=OptionType.STRING, - required=False - ) - ] - ) - async def _add_deadline_slash(self, interaction: SlashInteraction, year: int, course: str, name: str, date: str, time: str = "00:00:00"): - with open("files/deadlines.json", "r") as f: - deadlines = json.load(f) - - date += "/" + str(datetime.datetime.now().year) - - # Fix format - if time.count(":") == 1: - time += ":00" - - dt = fromString(f"{date} {time}", formatString="%d/%m/%Y %H:%M:%S", tzinfo=None) - - # Add year & course if necessary - if str(year) not in deadlines: - deadlines[str(year)] = {} - - if course not in deadlines[str(year)]: - deadlines[str(year)][course] = {} - - deadlines[str(year)][course][name] = round(dt.timestamp()) - - with open("files/deadlines.json", "w") as f: - json.dump(deadlines, f) - - await interaction.reply("Addition successful", ephemeral=True) - - -def setup(client: Didier): - # client.add_cog(DBSlash(client)) - pass diff --git a/didier.py b/didier.py index 70790bd..461e9a2 100644 --- a/didier.py +++ b/didier.py @@ -1,12 +1,9 @@ import discord -from dotenv import load_dotenv from functions.prefixes import get_prefix from settings import STATUS_MESSAGE, TOKEN from startup.didier import Didier if __name__ == "__main__": - load_dotenv(verbose=True) - # Activities activity = discord.Activity(type=discord.ActivityType.playing, name=STATUS_MESSAGE) status = discord.Status.online diff --git a/functions/stringFormatters.py b/functions/stringFormatters.py index f34e242..37d67a0 100644 --- a/functions/stringFormatters.py +++ b/functions/stringFormatters.py @@ -1,7 +1,7 @@ import traceback +from discord import Interaction from discord.ext.commands import Context -from dislash import SlashInteraction def title_case(string): @@ -42,16 +42,15 @@ 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: +def format_slash_command_usage(interaction: Interaction) -> str: # Create a string with the options used - # TODO look into the format used by the lib because it's terrible options = " ".join(list(map( - lambda option: f"{option.name}: \"{option.value}\"", - interaction.data.options.values() + lambda o: f"{o['name']}: \"{o['value']}\"", + interaction.data.get("options", []) ))) - command = f"{interaction.slash_command.name} {options or ''}" - return f"{interaction.author.display_name} in {_format_error_location(interaction)}: /{command}" + command = f"{interaction.data['name']} {options or ''}" + return f"{interaction.user.display_name} in {_format_error_location(interaction)}: /{command}" def get_edu_year(index: int) -> str: diff --git a/requirements.txt b/requirements.txt index a3d6c9c..b19b0c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,6 @@ tabulate==0.8.7 yarl==1.4.2 feedparser==6.0.2 googletrans==4.0.0rc1 -quart==0.15.1 -Quart-CORS==0.5.0 attrs~=21.2.0 dacite~=1.6.0 pytest==6.2.4 diff --git a/settings.py b/settings.py index b2fed8e..dcd8067 100644 --- a/settings.py +++ b/settings.py @@ -4,7 +4,7 @@ from dotenv import load_dotenv import os -load_dotenv() +load_dotenv(verbose=True) def _to_bool(value: str) -> bool: @@ -31,7 +31,6 @@ DB_NAME = os.getenv("DBNAME", "") # Discord-related 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.") diff --git a/startup/didier.py b/startup/didier.py index dbdfc9f..4dc2615 100644 --- a/startup/didier.py +++ b/startup/didier.py @@ -1,6 +1,5 @@ from data.snipe import Snipe from discord.ext import commands -from dislash import InteractionClient import os from startup.init_files import check_all from typing import Dict @@ -10,9 +9,6 @@ class Didier(commands.Bot): """ Main Bot class for Didier """ - # Reference to interactions client - interactions: InteractionClient - # Dict to store the most recent Snipe info per channel snipe: Dict[int, Snipe] = {} @@ -20,7 +16,7 @@ class Didier(commands.Bot): super().__init__(*args, **kwargs) # Cogs that should be loaded before the others - self._preload = ("ipc", "utils", "failedchecks", "events",) + self._preload = ("utils", "failedchecks", "events",) # Remove default help command self.remove_command("help") From 062d54722b73206ba9c2cd45f4c4b164e36f2523 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 5 Feb 2022 22:55:09 +0100 Subject: [PATCH 10/21] Rework bc, rob & poke leaderboards --- cogs/events.py | 1 - cogs/leaderboards.py | 115 +++++++++++++++------------- data/menus/custom_commands.py | 1 + data/menus/paginated_leaderboard.py | 6 +- 4 files changed, 65 insertions(+), 58 deletions(-) diff --git a/cogs/events.py b/cogs/events.py index 6696d4c..67abe79 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -85,7 +85,6 @@ class Events(commands.Cog): Logs commands in your terminal. :param ctx: Discord Context """ - print("a") print(stringFormatters.format_command_usage(ctx)) command_stats.invoked(command_stats.InvocationType.TextCommand) diff --git a/cogs/leaderboards.py b/cogs/leaderboards.py index 6eb9edf..b974a0c 100644 --- a/cogs/leaderboards.py +++ b/cogs/leaderboards.py @@ -1,3 +1,5 @@ +from typing import Callable, Optional + from data.menus import paginated_leaderboard from decorators import help import discord @@ -10,7 +12,7 @@ import math import requests -# TODO some sort of general leaderboard because all of them are the same +# TODO some sort of general leaderboard generation because all of them are the same class Leaderboards(commands.Cog): def __init__(self, client): @@ -21,6 +23,26 @@ class Leaderboards(commands.Cog): def cog_check(self, ctx): return not self.client.locked + def _generate_embed_data(self, entries: list, + key_f: Callable = lambda x: x[0], + data_f: Callable = lambda x: x[1], + ignore_non_pos: bool = True) -> Optional[list[tuple]]: + data = [] + for i, v in enumerate(sorted(entries, key=data_f, reverse=True)): + entry_data = data_f(v) + + # Leaderboard is empty + if i == 0 and entry_data == 0 and ignore_non_pos: + return None + + # Ignore entries with no data + if ignore_non_pos and entry_data <= 0: + continue + + data.append((key_f(v), f"{entry_data:,}",)) + + return data + @commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*", invoke_without_command=True) @commands.check(checks.allowedChannels) @@ -45,14 +67,11 @@ class Leaderboards(commands.Cog): user[1] += platDinks[str(user[0])] * Numbers.q.value entries[i] = user - data = [] - for i, user in enumerate(sorted(entries, key=lambda x: (float(x[1]) + float(x[3])), reverse=True)): - if i == 0 and float(user[1]) + float(user[3]) == 0.0: - return await self.empty_leaderboard(ctx, "Dinks Leaderboard", - "Er zijn nog geen personen met Didier Dinks.") - elif float(user[1]) + float(user[3]) > 0.0: - total_dinks = math.floor(float(user[1]) + float(user[3])) - data.append((user[0], total_dinks,)) + data = self._generate_embed_data(entries, key_f=lambda x: x[0], data_f=lambda x: (float(x[1]) + float(x[3]))) + + if data is None: + return await self.empty_leaderboard(ctx, "Dinks Leaderboard", + "Er zijn nog geen personen met Didier Dinks.") lb = paginated_leaderboard.Leaderboard( ctx=ctx, title="Dinks Leaderboard", data=data, fetch_names=True @@ -79,61 +98,49 @@ class Leaderboards(commands.Cog): @leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True) async def bitcoin(self, ctx): users = currency.getAllRows() - boardTop = [] - for i, user in enumerate(sorted(users, key=lambda x: x[8], reverse=True)): - # Don't create an empty leaderboard - if i == 0 and float(user[8]) == 0.0: - return await self.empty_leaderboard(ctx, "Bitcoin Leaderboard", - "Er zijn nog geen personen met Bitcoins.") - elif float(user[8]) > 0.0: - # Only add people with more than 0 - # Get the username in this guild - name = self.utilsCog.getDisplayName(ctx, user[0]) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} ({:,})**".format(name, round(user[8], 8))) - else: - boardTop.append("{} ({:,})".format(name, round(user[8], 8))) + data = self._generate_embed_data(users, data_f=lambda x: round(float(x[8]), 8)) - await self.startPaginated(ctx, boardTop, "Bitcoin Leaderboard") + if data is None: + return await self.empty_leaderboard(ctx, "Bitcoin Leaderboard", + "Er zijn nog geen personen met Bitcoins.") + + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Bitcoin Leaderboard", data=data, fetch_names=True + ) + + await lb.send(ctx) @leaderboard.command(name="Rob", hidden=True) async def rob(self, ctx): users = list(stats.getAllRows()) - boardTop = [] - for i, user in enumerate(sorted(users, key=lambda x: x[4], reverse=True)): - # Don't create an empty leaderboard - if i == 0 and float(user[4]) == 0.0: - return await self.empty_leaderboard(ctx, "Rob Leaderboard", - "Er heeft nog niemand Didier Dinks gestolen.") - elif float(user[4]) > 0.0: - # Only add people with more than 0 - # Get the username in this guild - name = self.utilsCog.getDisplayName(ctx, user[0]) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} ({:,})**".format(name, math.floor(float(user[4])))) - else: - boardTop.append("{} ({:,})".format(name, math.floor(float(user[4])))) - await self.startPaginated(ctx, boardTop, "Rob Leaderboard") + data = self._generate_embed_data(users, data_f=lambda x: math.floor(float(x[4]))) + + if data is None: + return await self.empty_leaderboard(ctx, "Rob Leaderboard", + "Er heeft nog niemand Didier Dinks gestolen.") + + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Rob Leaderboard", data=data, fetch_names=True + ) + + await lb.send(ctx) @leaderboard.command(name="Poke", hidden=True) async def poke(self, ctx): - s = stats.getAllRows() + entries = stats.getAllRows() blacklist = poke.getAllBlacklistedUsers() - boardTop = [] - for i, user in enumerate(sorted(s, key=lambda x: x[1], reverse=True)): - if i == 0 and int(user[1]) == 0: - return await self.empty_leaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.") + # Remove blacklisted users + entries = list(filter(lambda x: x[0] not in blacklist, entries)) - elif int(user[1]) == 0: - break - # Don't include blacklisted users - elif str(user[0]) not in blacklist: - name = self.utilsCog.getDisplayName(ctx, user[0]) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} ({:,})**".format(name, round(int(user[1])))) - else: - boardTop.append("{} ({:,})".format(name, round(int(user[1])))) - await self.startPaginated(ctx, boardTop, "Poke Leaderboard") + data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[1]))) + if data is None: + return await self.empty_leaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.") + + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Poke Leaderboard", data=data, fetch_names=True + ) + + await lb.send(ctx) @leaderboard.command(name="Xp", aliases=["Level"], hidden=True) async def xp(self, ctx): diff --git a/data/menus/custom_commands.py b/data/menus/custom_commands.py index 68c79c9..6e7c215 100644 --- a/data/menus/custom_commands.py +++ b/data/menus/custom_commands.py @@ -2,6 +2,7 @@ import discord from discord.ext import menus +# TODO rework pagination class CommandsList(menus.ListPageSource): def __init__(self, data, colour=discord.Colour.blue()): super().__init__(data, per_page=15) diff --git a/data/menus/paginated_leaderboard.py b/data/menus/paginated_leaderboard.py index dd9bec7..a4b94eb 100644 --- a/data/menus/paginated_leaderboard.py +++ b/data/menus/paginated_leaderboard.py @@ -23,7 +23,7 @@ class Leaderboard: def __post_init__(self): if self.format_f is None: - self.format_f = self._format + self .format_f = lambda x: x def _should_highlight(self, data) -> bool: """Check if an entry should be highlighted""" @@ -38,7 +38,7 @@ class Leaderboard: if self.fetch_names: name = get_display_name(self.ctx, int(data[0])) - s = f"{index + 1}: {name} ({data[1]})" + s = f"{index + 1}: {name} ({self.format_f(data[1])})" return s @@ -64,7 +64,7 @@ class Leaderboard: description = "" for i, v in enumerate(self.data): - s = self.format_f(i, v) + s = self._format(i, v) if self._should_highlight(v[0]): s = f"**{s}**" From ca687956f6c77f5e4e1c66665f09d948ce24f613 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 5 Feb 2022 23:21:47 +0100 Subject: [PATCH 11/21] Rework last leaderboards --- cogs/leaderboards.py | 74 +++++++++++++---------------- data/menus/paginated_leaderboard.py | 8 ++-- 2 files changed, 35 insertions(+), 47 deletions(-) diff --git a/cogs/leaderboards.py b/cogs/leaderboards.py index b974a0c..f830307 100644 --- a/cogs/leaderboards.py +++ b/cogs/leaderboards.py @@ -39,7 +39,7 @@ class Leaderboards(commands.Cog): if ignore_non_pos and entry_data <= 0: continue - data.append((key_f(v), f"{entry_data:,}",)) + data.append((key_f(v), f"{entry_data:,}", entry_data,)) return data @@ -144,60 +144,50 @@ class Leaderboards(commands.Cog): @leaderboard.command(name="Xp", aliases=["Level"], hidden=True) async def xp(self, ctx): - s = stats.getAllRows() - boardTop = [] - for i, user in enumerate(sorted(s, key=lambda x: x[12], reverse=True)): - if int(user[12]) == 0: - break + entries = stats.getAllRows() + data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[12]))) - name = self.utilsCog.getDisplayName(ctx, user[0]) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} (Level {:,} | {:,} XP)**".format(name, - xp.calculate_level(round(int(user[12]))), - round(int(user[12])))) - else: - boardTop.append("{} (Level {:,} | {:,} XP)".format(name, - xp.calculate_level(round(int(user[12]))), - round(int(user[12])))) - await self.startPaginated(ctx, boardTop, "XP Leaderboard") + def _format_entry(entry: int) -> str: + return f"Level {xp.calculate_level(entry):,} | {entry:,} XP" + + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="XP Leaderboard", data=data, fetch_names=True, format_f=_format_entry + ) + + await lb.send(ctx) @leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True) async def messages(self, ctx): - s = stats.getAllRows() - boardTop = [] - + entries = stats.getAllRows() message_count = stats.getTotalMessageCount() - for i, user in enumerate(sorted(s, key=lambda x: x[11], reverse=True)): - if int(user[11]) == 0: - break + data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[11]))) - perc = round(int(user[11]) * 100 / message_count, 2) + def _format_entry(entry: int) -> str: + perc = round(entry * 100 / message_count, 2) + return f"{entry:,} | {perc}%" - name = self.utilsCog.getDisplayName(ctx, user[0]) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} ({:,} | {}%)**".format(name, round(int(user[11])), perc)) - else: - boardTop.append("{} ({:,} | {}%)".format(name, round(int(user[11])), perc)) - await self.startPaginated(ctx, boardTop, "Messages Leaderboard") + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Messages Leaderboard", data=data, fetch_names=True, format_f=_format_entry + ) + + await lb.send(ctx) @leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True) async def muttn(self, ctx): - users = muttn.getAllRows() - boardTop = [] - for i, user in enumerate(sorted(users, key=lambda x: x[1], reverse=True)): - if i == 0 and int(user[1]) == 0: - return await self.empty_leaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.") + entries = muttn.getAllRows() + data = self._generate_embed_data(entries, data_f=lambda x: round(float(x[1]), 2)) + if data is None: + return await self.empty_leaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.") - if float(user[1]) == 0: - break + def _format_entry(entry: float) -> str: + return f"{entry}%" - name = self.utilsCog.getDisplayName(ctx, user[0]) - if int(user[0]) == int(ctx.author.id): - boardTop.append("**{} ({})%**".format(name, round(float(user[1]), 2))) - else: - boardTop.append("{} ({}%)".format(name, round(float(user[1]), 2))) - await self.startPaginated(ctx, boardTop, "Muttn Leaderboard") + lb = paginated_leaderboard.Leaderboard( + ctx=ctx, title="Muttn Leaderboard", data=data, fetch_names=True, format_f=_format_entry + ) + + await lb.send(ctx) async def callLeaderboard(self, name, ctx): command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0] diff --git a/data/menus/paginated_leaderboard.py b/data/menus/paginated_leaderboard.py index a4b94eb..5017e4f 100644 --- a/data/menus/paginated_leaderboard.py +++ b/data/menus/paginated_leaderboard.py @@ -21,10 +21,6 @@ class Leaderboard: colour: discord.Colour = discord.Colour.blue() fetch_names: bool = False - def __post_init__(self): - if self.format_f is None: - self .format_f = lambda x: x - def _should_highlight(self, data) -> bool: """Check if an entry should be highlighted""" if self.fetch_names: @@ -38,7 +34,9 @@ class Leaderboard: if self.fetch_names: name = get_display_name(self.ctx, int(data[0])) - s = f"{index + 1}: {name} ({self.format_f(data[1])})" + formatted_data = self.format_f(data[2]) if self.format_f is not None else data[1] + + s = f"{index + 1}: {name} ({formatted_data})" return s From 81a0d90a12615efadf2654a296b49d1e3589c518 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sun, 6 Feb 2022 01:11:40 +0100 Subject: [PATCH 12/21] Leaderboard cleanup --- cogs/fun.py | 2 +- cogs/leaderboards.py | 174 +++--------------- cogs/train.py | 2 +- data/menus/leaderboards.py | 275 ++++++++++++++++++++++++++++ data/menus/paginated.py | 67 +++++++ data/menus/paginated_leaderboard.py | 125 ------------- 6 files changed, 365 insertions(+), 280 deletions(-) create mode 100644 data/menus/leaderboards.py create mode 100644 data/menus/paginated.py delete mode 100644 data/menus/paginated_leaderboard.py diff --git a/cogs/fun.py b/cogs/fun.py index affd2c9..cb938bb 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -1,5 +1,5 @@ from data.embeds.xkcd import XKCDEmbed -from data.menus import paginated_leaderboard +from data.menus import leaderboards from decorators import help import discord from discord.ext import commands diff --git a/cogs/leaderboards.py b/cogs/leaderboards.py index f830307..9f41c5b 100644 --- a/cogs/leaderboards.py +++ b/cogs/leaderboards.py @@ -1,18 +1,12 @@ -from typing import Callable, Optional - -from data.menus import paginated_leaderboard -from decorators import help import discord from discord.ext import commands + +from data.menus import leaderboards +from decorators import help from enums.help_categories import Category -from enums.numbers import Numbers -from functions import checks, xp -from functions.database import currency, stats, poke, muttn -import math -import requests +from functions import checks -# TODO some sort of general leaderboard generation because all of them are the same class Leaderboards(commands.Cog): def __init__(self, client): @@ -20,29 +14,9 @@ class Leaderboards(commands.Cog): self.utilsCog = self.client.get_cog("Utils") # Don't allow any commands to work when locked - def cog_check(self, ctx): + def cog_check(self, _): return not self.client.locked - def _generate_embed_data(self, entries: list, - key_f: Callable = lambda x: x[0], - data_f: Callable = lambda x: x[1], - ignore_non_pos: bool = True) -> Optional[list[tuple]]: - data = [] - for i, v in enumerate(sorted(entries, key=data_f, reverse=True)): - entry_data = data_f(v) - - # Leaderboard is empty - if i == 0 and entry_data == 0 and ignore_non_pos: - return None - - # Ignore entries with no data - if ignore_non_pos and entry_data <= 0: - continue - - data.append((key_f(v), f"{entry_data:,}", entry_data,)) - - return data - @commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*", invoke_without_command=True) @commands.check(checks.allowedChannels) @@ -56,154 +30,48 @@ class Leaderboards(commands.Cog): @leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True) async def dinks(self, ctx): - entries = currency.getAllRows() - platDinks = currency.getAllPlatDinks() - - # Take platinum dinks into account - for i, user in enumerate(entries): - if str(user[0]) in platDinks: - # Tuples don't support assignment, cast to list - user = list(user) - user[1] += platDinks[str(user[0])] * Numbers.q.value - entries[i] = user - - data = self._generate_embed_data(entries, key_f=lambda x: x[0], data_f=lambda x: (float(x[1]) + float(x[3]))) - - if data is None: - return await self.empty_leaderboard(ctx, "Dinks Leaderboard", - "Er zijn nog geen personen met Didier Dinks.") - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Dinks Leaderboard", data=data, fetch_names=True - ) - - await lb.send(ctx) + lb = leaderboards.DinksLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Corona", hidden=True) async def corona(self, ctx): - result = requests.get("https://disease.sh/v3/covid-19/countries").json() - result.sort(key=lambda x: int(x["cases"]), reverse=True) - - data = [] - for country in result: - data.append((country["country"], f"{country['cases']:,}",)) - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Corona Leaderboard", data=data, highlight="Belgium", - colour=discord.Colour.red() - ) - - await lb.send(ctx) + lb = leaderboards.CoronaLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True) async def bitcoin(self, ctx): - users = currency.getAllRows() - data = self._generate_embed_data(users, data_f=lambda x: round(float(x[8]), 8)) - - if data is None: - return await self.empty_leaderboard(ctx, "Bitcoin Leaderboard", - "Er zijn nog geen personen met Bitcoins.") - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Bitcoin Leaderboard", data=data, fetch_names=True - ) - - await lb.send(ctx) + lb = leaderboards.BitcoinLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Rob", hidden=True) async def rob(self, ctx): - users = list(stats.getAllRows()) - data = self._generate_embed_data(users, data_f=lambda x: math.floor(float(x[4]))) - - if data is None: - return await self.empty_leaderboard(ctx, "Rob Leaderboard", - "Er heeft nog niemand Didier Dinks gestolen.") - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Rob Leaderboard", data=data, fetch_names=True - ) - - await lb.send(ctx) + lb = leaderboards.RobLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Poke", hidden=True) async def poke(self, ctx): - entries = stats.getAllRows() - blacklist = poke.getAllBlacklistedUsers() - # Remove blacklisted users - entries = list(filter(lambda x: x[0] not in blacklist, entries)) - - data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[1]))) - if data is None: - return await self.empty_leaderboard(ctx, "Poke Leaderboard", "Er is nog niemand getikt.") - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Poke Leaderboard", data=data, fetch_names=True - ) - - await lb.send(ctx) + lb = leaderboards.PokeLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Xp", aliases=["Level"], hidden=True) async def xp(self, ctx): - entries = stats.getAllRows() - data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[12]))) - - def _format_entry(entry: int) -> str: - return f"Level {xp.calculate_level(entry):,} | {entry:,} XP" - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="XP Leaderboard", data=data, fetch_names=True, format_f=_format_entry - ) - - await lb.send(ctx) + lb = leaderboards.XPLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True) async def messages(self, ctx): - entries = stats.getAllRows() - message_count = stats.getTotalMessageCount() - - data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[11]))) - - def _format_entry(entry: int) -> str: - perc = round(entry * 100 / message_count, 2) - return f"{entry:,} | {perc}%" - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Messages Leaderboard", data=data, fetch_names=True, format_f=_format_entry - ) - - await lb.send(ctx) + lb = leaderboards.MessageLeaderboard(ctx=ctx) + await lb.send() @leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True) async def muttn(self, ctx): - entries = muttn.getAllRows() - data = self._generate_embed_data(entries, data_f=lambda x: round(float(x[1]), 2)) - if data is None: - return await self.empty_leaderboard(ctx, "Muttn Leaderboard", "Der zittn nog geen muttns in de server.") - - def _format_entry(entry: float) -> str: - return f"{entry}%" - - lb = paginated_leaderboard.Leaderboard( - ctx=ctx, title="Muttn Leaderboard", data=data, fetch_names=True, format_f=_format_entry - ) - - await lb.send(ctx) + lb = leaderboards.MuttnLeaderboard(ctx=ctx) + await lb.send() async def callLeaderboard(self, name, ctx): command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0] await command(ctx) - async def startPaginated(self, ctx, source, name, colour=discord.Colour.blue()): - pages = paginated_leaderboard.Pages(source=paginated_leaderboard.Source(source, name, colour), - clear_reactions_after=True) - await pages.start(ctx) - - async def empty_leaderboard(self, ctx, name, message, colour=discord.Colour.blue()): - embed = discord.Embed(colour=colour) - embed.set_author(name=name) - embed.description = message - await ctx.send(embed=embed) - def setup(client): client.add_cog(Leaderboards(client)) diff --git a/cogs/train.py b/cogs/train.py index 5865bf8..4f643ff 100644 --- a/cogs/train.py +++ b/cogs/train.py @@ -1,4 +1,4 @@ -from data.menus import paginated_leaderboard +from data.menus import leaderboards from decorators import help import discord from discord.ext import commands, menus diff --git a/data/menus/leaderboards.py b/data/menus/leaderboards.py new file mode 100644 index 0000000..bd6e342 --- /dev/null +++ b/data/menus/leaderboards.py @@ -0,0 +1,275 @@ +import math +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Union, Optional + +import discord +import requests +from discord import ApplicationContext +from discord.ext import menus +from discord.ext.commands import Context + +from data.menus.paginated import Paginated +from enums.numbers import Numbers +from functions import xp +from functions.database import currency, stats, poke, muttn +from functions.utils import get_display_name + + +@dataclass +class Leaderboard(Paginated, ABC): + highlight: str = None + colour: discord.Colour = discord.Colour.blue() + fetch_names: bool = True + ignore_non_pos: bool = True + + def __post_init__(self): + self.data = self.process_data(self.get_data()) + + @abstractmethod + def get_data(self) -> list[tuple]: + pass + + def process_data(self, entries: list[tuple]) -> Optional[list[tuple]]: + data = [] + for i, v in enumerate(sorted(entries, key=self.get_value, reverse=True)): + entry_data = self.get_value(v) + + # Leaderboard is empty + if i == 0 and entry_data == 0 and self.ignore_non_pos: + return None + + # Ignore entries with no data + if self.ignore_non_pos and entry_data <= 0: + continue + + data.append((self.get_key(v), f"{entry_data:,}", entry_data,)) + + return data + + def get_key(self, data: tuple): + return data[0] + + def get_value(self, data: tuple): + return data[1] + + def _should_highlight(self, data) -> bool: + """Check if an entry should be highlighted""" + if self.fetch_names: + return data == self.ctx.author.id + + return data == self.highlight + + def format_entry_data(self, data: tuple) -> str: + return str(data[1]) + + def format_entry(self, index: int, data: tuple) -> str: + name = data[0] + + if self.fetch_names: + name = get_display_name(self.ctx, int(data[0])) + + s = f"{index + 1}: {name} ({self.format_entry_data(data)})" + + if self._should_highlight(data[0]): + return f"**{s}**" + + return s + + @property + def empty_description(self) -> str: + return "" + + async def empty_leaderboard(self, ctx: Union[ApplicationContext, Context]): + embed = discord.Embed(colour=self.colour) + embed.set_author(name=self.title) + embed.description = self.empty_description + + if isinstance(ctx, ApplicationContext): + return await ctx.respond(embed=embed) + + return await ctx.reply(embed=embed, mention_author=False) + + async def respond(self, **kwargs) -> discord.Message: + if self.data is None: + return await self.empty_leaderboard(self.ctx) + + return await super().respond(**kwargs) + + async def send(self, **kwargs) -> discord.Message: + if self.data is None: + return await self.empty_leaderboard(self.ctx) + + return await super().send(**kwargs) + + +@dataclass +class BitcoinLeaderboard(Leaderboard): + title: str = field(default="Bitcoin Leaderboard") + + def get_data(self) -> list[tuple]: + return currency.getAllRows() + + def get_value(self, data: tuple): + return round(float(data[8]), 8) + + @property + def empty_description(self) -> str: + return "Er zijn nog geen personen met Bitcoins." + + +@dataclass +class CoronaLeaderboard(Leaderboard): + colour: discord.Colour = field(default=discord.Colour.red()) + fetch_names: bool = field(default=False) + highlight: str = field(default="Belgium") + title: str = field(default="Corona Leaderboard") + + def get_data(self) -> list[tuple]: + result = requests.get("https://disease.sh/v3/covid-19/countries").json() + result.sort(key=lambda x: int(x["cases"]), reverse=True) + + data = [] + for country in result: + data.append((country["country"], f"{country['cases']:,}", country["cases"])) + + return data + + def get_value(self, data: tuple): + return data[2] + + +@dataclass +class DinksLeaderboard(Leaderboard): + title: str = field(default="Dinks Leaderboard") + + def get_data(self) -> list[tuple]: + entries = currency.getAllRows() + platDinks = currency.getAllPlatDinks() + + # Take platinum dinks into account + for i, user in enumerate(entries): + if str(user[0]) in platDinks: + # Tuples don't support assignment, cast to list + user = list(user) + user[1] += platDinks[str(user[0])] * Numbers.q.value + entries[i] = user + + return entries + + def get_value(self, data: tuple): + return float(data[1]) + float(data[3]) + + @property + def empty_description(self) -> str: + return "Er zijn nog geen personen met Didier Dinks." + + +@dataclass +class MessageLeaderboard(Leaderboard): + title: str = field(default="Message Leaderboard") + message_count: int = field(init=False) + + def get_data(self) -> list[tuple]: + entries = stats.getAllRows() + self.message_count = stats.getTotalMessageCount() + return entries + + def get_value(self, data: tuple): + return round(int(data[11])) + + def format_entry_data(self, data: tuple) -> str: + perc = round(data[2] * 100 / self.message_count, 2) + return f"{data[2]:,} | {perc}%" + + +@dataclass +class MuttnLeaderboard(Leaderboard): + title: str = field(default="Muttn Leaderboard") + + def get_data(self) -> list[tuple]: + return muttn.getAllRows() + + def get_value(self, data: tuple): + return round(float(data[1]), 2) + + def format_entry_data(self, data: tuple) -> str: + return f"{data[2]}%" + + def empty_description(self) -> str: + return "Der zittn nog geen muttns in de server." + + +@dataclass +class PokeLeaderboard(Leaderboard): + title: str = field(default="Poke Leaderboard") + + def get_data(self) -> list[tuple]: + data = stats.getAllRows() + blacklist = poke.getAllBlacklistedUsers() + return list(filter(lambda x: x[0] not in blacklist, data)) + + def get_value(self, data: tuple): + return round(int(data[1])) + + @property + def empty_description(self) -> str: + return "Er is nog niemand getikt." + + +@dataclass +class RobLeaderboard(Leaderboard): + title: str = field(default="Rob Leaderboard") + + def get_data(self) -> list[tuple]: + return list(stats.getAllRows()) + + def get_value(self, data: tuple): + return math.floor(float(data[4])) + + @property + def empty_description(self) -> str: + return "Er heeft nog niemand Didier Dinks gestolen." + + +@dataclass +class XPLeaderboard(Leaderboard): + title: str = field(default="XP Leaderboard") + + def get_data(self) -> list[tuple]: + return stats.getAllRows() + + def get_value(self, data: tuple): + return round(int(data[12])) + + def format_entry_data(self, data: tuple) -> str: + entry = data[2] + return f"Level {xp.calculate_level(entry):,} | {entry:,} XP" + + +class Source(menus.ListPageSource): + def __init__(self, data, name, colour=discord.Colour.blue()): + super().__init__(data, per_page=10) + self.name = name + self.colour = colour + + async def format_page(self, menu: menus.MenuPages, entries): + offset = menu.current_page * self.per_page + + description = "" + for i, v in enumerate(entries, start=offset): + # Check if the person's name has to be highlighted + if v.startswith("**") and v.endswith("**"): + description += "**" + v = v[2:] + description += "{}: {}\n".format(i + 1, v) + embed = discord.Embed(colour=self.colour) + embed.set_author(name=self.name) + embed.description = description + embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) + return embed + + +class Pages(menus.MenuPages): + def __init__(self, source, clear_reactions_after, timeout=30.0): + super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) diff --git a/data/menus/paginated.py b/data/menus/paginated.py new file mode 100644 index 0000000..7a06cc3 --- /dev/null +++ b/data/menus/paginated.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Union + +import discord +from discord import ApplicationContext +from discord.ext import pages +from discord.ext.commands import Context + + +@dataclass +class Paginated(ABC): + """Abstract class to support paginated menus easily""" + ctx: Union[ApplicationContext, Context] + title: str + data: list[tuple] = None + per_page: int = 10 + colour: discord.Colour = discord.Colour.blue() + + def create_embed(self, description: str) -> discord.Embed: + embed = discord.Embed(colour=self.colour) + embed.set_author(name=self.title) + embed.description = description + + return embed + + @abstractmethod + def format_entry(self, index: int, value: tuple) -> str: + pass + + def create_pages(self, data: list[tuple]) -> list[discord.Embed]: + # Amount of entries added to this page + added = 0 + page_list = [] + + description = "" + for i, v in enumerate(data): + s = self.format_entry(i, v) + + description += s + "\n" + added += 1 + + # Page full, create an embed & change counters + if added == self.per_page: + embed = self.create_embed(description) + + description = "" + added = 0 + page_list.append(embed) + + # Add final embed if necessary + if added != 0: + embed = self.create_embed(description) + page_list.append(embed) + + return page_list + + def create_paginator(self) -> pages.Paginator: + return pages.Paginator(pages=self.create_pages(self.data), show_disabled=False, disable_on_timeout=True, timeout=30) + + async def respond(self, **kwargs) -> discord.Message: + paginator = self.create_paginator() + return await paginator.respond(self.ctx.interaction, **kwargs) + + async def send(self, **kwargs) -> discord.Message: + paginator = self.create_paginator() + return await paginator.send(self.ctx, **kwargs) diff --git a/data/menus/paginated_leaderboard.py b/data/menus/paginated_leaderboard.py deleted file mode 100644 index 5017e4f..0000000 --- a/data/menus/paginated_leaderboard.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import Callable - -import discord -from discord import ApplicationContext -from discord.ext import menus, pages -from dataclasses import dataclass - -from discord.ext.commands import Context - -from functions.utils import get_display_name - - -@dataclass -class Leaderboard: - ctx: Context - title: str - data: list - highlight: str = None - format_f: Callable = None - per_page: int = 10 - colour: discord.Colour = discord.Colour.blue() - fetch_names: bool = False - - def _should_highlight(self, data) -> bool: - """Check if an entry should be highlighted""" - if self.fetch_names: - return data == self.ctx.author.id - - return data == self.highlight - - def _format(self, index: int, data: tuple) -> str: - name = data[0] - - if self.fetch_names: - name = get_display_name(self.ctx, int(data[0])) - - formatted_data = self.format_f(data[2]) if self.format_f is not None else data[1] - - s = f"{index + 1}: {name} ({formatted_data})" - - return s - - def _get_page_count(self) -> int: - """Get the amount of pages required to represent this data""" - count = len(self.data) // self.per_page - if len(self.data) % self.per_page != 0: - count += 1 - - return count - - def _create_embed(self, description: str) -> discord.Embed: - embed = discord.Embed(colour=self.colour) - embed.set_author(name=self.title) - embed.description = description - - return embed - - def create_pages(self) -> list[discord.Embed]: - # Amount of entries added to this page - added = 0 - page_list = [] - - description = "" - for i, v in enumerate(self.data): - s = self._format(i, v) - - if self._should_highlight(v[0]): - s = f"**{s}**" - - description += s + "\n" - added += 1 - - # Page full, create an embed & change counters - if added == self.per_page: - embed = self._create_embed(description) - - description = "" - added = 0 - page_list.append(embed) - - # Add final embed - if added != 0: - embed = self._create_embed(description) - page_list.append(embed) - - return page_list - - def create_paginator(self) -> pages.Paginator: - return pages.Paginator(pages=self.create_pages(), show_disabled=False, disable_on_timeout=True, timeout=30) - - async def respond(self, ctx: ApplicationContext, **kwargs) -> discord.Message: - paginator = self.create_paginator() - return await paginator.respond(ctx.interaction, **kwargs) - - async def send(self, ctx: Context, **kwargs) -> discord.Message: - paginator = self.create_paginator() - return await paginator.send(ctx, **kwargs) - - -class Source(menus.ListPageSource): - def __init__(self, data, name, colour=discord.Colour.blue()): - super().__init__(data, per_page=10) - self.name = name - self.colour = colour - - async def format_page(self, menu: menus.MenuPages, entries): - offset = menu.current_page * self.per_page - - description = "" - for i, v in enumerate(entries, start=offset): - # Check if the person's name has to be highlighted - if v.startswith("**") and v.endswith("**"): - description += "**" - v = v[2:] - description += "{}: {}\n".format(i + 1, v) - embed = discord.Embed(colour=self.colour) - embed.set_author(name=self.name) - embed.description = description - embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) - return embed - - -class Pages(menus.MenuPages): - def __init__(self, source, clear_reactions_after, timeout=30.0): - super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) From a71232e292c39d72b89ba81b1e87b77e9e76d1a9 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sun, 6 Feb 2022 01:34:00 +0100 Subject: [PATCH 13/21] Custom commands cleanup, remove train for my own sanity --- cogs/other.py | 14 ++-- cogs/train.py | 127 ---------------------------------- data/menus/custom_commands.py | 34 ++++----- data/menus/leaderboards.py | 39 ++--------- 4 files changed, 24 insertions(+), 190 deletions(-) delete mode 100644 cogs/train.py diff --git a/cogs/other.py b/cogs/other.py index 0c62b8d..859c55d 100644 --- a/cogs/other.py +++ b/cogs/other.py @@ -5,8 +5,6 @@ from data.menus import custom_commands from data.snipe import Action, Snipe from decorators import help from enums.help_categories import Category -from functions.database.custom_commands import get_all -from functions.stringFormatters import capitalize from startup.didier import Didier @@ -14,10 +12,9 @@ class Other(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client - # TODO add locked field to Didier instead of client - # # Don't allow any commands to work when locked - # def cog_check(self, ctx): - # return not self.client.locked + # Don't allow any commands to work when locked + def cog_check(self, _): + return not self.client.locked @commands.command(name="Custom") @help.Category(category=Category.Didier) @@ -25,10 +22,7 @@ class Other(commands.Cog): """ Get a list of all custom commands """ - all_commands = get_all() - formatted = list(sorted(map(lambda x: capitalize(x["name"]), all_commands))) - src = custom_commands.CommandsList(formatted) - await custom_commands.Pages(source=src, clear_reactions_after=True).start(ctx) + await custom_commands.CommandsList(ctx).send() @commands.command(name="Snipe") @help.Category(category=Category.Other) diff --git a/cogs/train.py b/cogs/train.py deleted file mode 100644 index 4f643ff..0000000 --- a/cogs/train.py +++ /dev/null @@ -1,127 +0,0 @@ -from data.menus import leaderboards -from decorators import help -import discord -from discord.ext import commands, menus -from enums.help_categories import Category -from functions import checks, timeFormatters -import requests - - -class Train(commands.Cog): - - def __init__(self, client): - self.client = client - - # Don't allow any commands to work when locked - def cog_check(self, ctx): - return not self.client.locked - - @commands.command(name="Train", aliases=["Trein"], usage="[Vertrek]* [Bestemming]") - @help.Category(category=Category.School) - async def train(self, ctx, *args): - if not args or len(args) > 2: - await ctx.send("Controleer je argumenten.") - return - destination = args[-1] - departure = args[0] if len(args) > 1 else "Gent Sint-Pieters" - - req = requests.get( - "http://api.irail.be/connections/?from={}&to={}&alerts=true&lang=nl&format=json".format(departure, - destination)).json() - if "error" in req: - embed = discord.Embed(colour=discord.Colour.red()) - embed.set_author(name="Treinen van {} naar {}".format( - self.formatCity(departure), self.formatCity(destination))) - embed.add_field(name="Error", value="Er ging iets fout, probeer het later opnieuw.", inline=False) - await self.sendEmbed(ctx, embed) - return - - pages = paginated_leaderboard.Pages(source=TrainPagination(self.formatConnections(req["connection"]), - self.formatCity(departure), - self.formatCity(destination)), - clear_reactions_after=True) - await pages.start(ctx) - - def formatConnections(self, connections): - response = [] - for connection in sorted(connections, key=lambda con: con["departure"]["time"]): - conn = {} - if connection["departure"]["canceled"] != "0" or connection["arrival"]["canceled"] != "0": - conn = {"Canceled": "Afgeschaft"} - dep = connection["departure"] - arr = connection["arrival"] - conn["depStation"] = self.formatCity(dep["station"]) - conn["depTime"] = self.formatTime(dep["time"]) - conn["delay"] = self.formatDelay(dep["delay"]) - conn["track"] = dep["platform"] - conn["arrStation"] = self.formatCity(arr["station"]) - conn["direction"] = self.formatCity(dep["direction"]["name"]) - conn["arrTime"] = self.formatTime(arr["time"]) - conn["duration"] = self.formatTime(connection["duration"]) - response.append(conn) - return response - - def formatTime(self, timestamp): - if int(timestamp) <= 86400: - minutes = int(timestamp) // 60 - if minutes < 60: - return str(minutes) + "m" - return "{}h{:02}m".format(minutes // 60, minutes % 60) - else: - return timeFormatters.epochToDate(int(timestamp), "%H:%M")["date"] - - def formatDelay(self, seconds): - seconds = int(seconds) - return self.sign(seconds) + self.formatTime(abs(seconds)) if seconds != 0 else "" - - def sign(self, number): - return "-" if int(number) < 0 else "+" - - def formatCity(self, city): - city = city[0].upper() + city[1:] - arr = [] - for i, letter in enumerate(city): - if (i > 0 and (city[i - 1] == " " or city[i - 1] == "-")) or i == 0: - arr.append(letter.upper()) - else: - arr.append(letter.lower()) - return "".join(arr) - - async def sendEmbed(self, ctx, embed): - if await checks.allowedChannels(ctx): - await ctx.send(embed=embed) - else: - await ctx.author.send(embed=embed) - - -class TrainPagination(menus.ListPageSource): - def __init__(self, data, departure, destination): - super().__init__(data, per_page=3) - self.departure = departure - self.destination = destination - - async def format_page(self, menu: menus.MenuPages, entries): - offset = menu.current_page * self.per_page - embed = discord.Embed(colour=discord.Colour.blue()) - embed.set_author(name="Treinen van {} naar {}".format(self.departure, self.destination)) - embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) - - for i, connection in enumerate(entries, start=offset): - afgeschaft = "Canceled" in connection - embed.add_field(name="Van", value=str(connection["depStation"]), inline=True) - embed.add_field(name="Om", value=str(connection["depTime"]), inline=True) - embed.add_field(name="Spoor", value=str(connection["track"]), inline=True) - embed.add_field(name="Richting", value=str(connection["direction"]), inline=True) - embed.add_field(name="Aankomst", value=(str(connection["arrTime"]) - if not afgeschaft else "**AFGESCHAFT**"), inline=True) - embed.add_field(name="Vertraging", value=str(connection["delay"]) if connection["delay"] != "" else "0", - inline=True) - - # White space - if i - offset < 2: - embed.add_field(name="\u200b", value="\u200b", inline=False) - return embed - - -def setup(client): - client.add_cog(Train(client)) diff --git a/data/menus/custom_commands.py b/data/menus/custom_commands.py index 6e7c215..cb5261b 100644 --- a/data/menus/custom_commands.py +++ b/data/menus/custom_commands.py @@ -1,22 +1,18 @@ -import discord -from discord.ext import menus +from typing import Union + +from discord import ApplicationContext +from discord.ext.commands import Context + +from data.menus.paginated import Paginated +from functions.database.custom_commands import get_all +from functions.stringFormatters import capitalize -# TODO rework pagination -class CommandsList(menus.ListPageSource): - def __init__(self, data, colour=discord.Colour.blue()): - super().__init__(data, per_page=15) - self.colour = colour +class CommandsList(Paginated): + def __init__(self, ctx: Union[ApplicationContext, Context]): + all_commands = get_all() + commands_sorted = list(sorted(map(lambda x: (capitalize(x["name"]),), all_commands))) + super().__init__(ctx=ctx, title="Custom Commands", data=commands_sorted, per_page=15) - async def format_page(self, menu: menus.MenuPages, entries): - embed = discord.Embed(colour=self.colour) - embed.set_author(name="Custom Commands") - embed.description = "\n".join(entries) - embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) - - return embed - - -class Pages(menus.MenuPages): - def __init__(self, source, clear_reactions_after, timeout=30.0): - super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) + def format_entry(self, index: int, value: tuple) -> str: + return value[0] diff --git a/data/menus/leaderboards.py b/data/menus/leaderboards.py index bd6e342..f35652f 100644 --- a/data/menus/leaderboards.py +++ b/data/menus/leaderboards.py @@ -6,7 +6,6 @@ from typing import Union, Optional import discord import requests from discord import ApplicationContext -from discord.ext import menus from discord.ext.commands import Context from data.menus.paginated import Paginated @@ -80,7 +79,7 @@ class Leaderboard(Paginated, ABC): def empty_description(self) -> str: return "" - async def empty_leaderboard(self, ctx: Union[ApplicationContext, Context]): + async def empty_leaderboard(self, ctx: Union[ApplicationContext, Context], **kwargs): embed = discord.Embed(colour=self.colour) embed.set_author(name=self.title) embed.description = self.empty_description @@ -88,19 +87,19 @@ class Leaderboard(Paginated, ABC): if isinstance(ctx, ApplicationContext): return await ctx.respond(embed=embed) - return await ctx.reply(embed=embed, mention_author=False) + return await ctx.reply(embed=embed, **kwargs) async def respond(self, **kwargs) -> discord.Message: if self.data is None: - return await self.empty_leaderboard(self.ctx) + return await self.empty_leaderboard(self.ctx, **kwargs) return await super().respond(**kwargs) async def send(self, **kwargs) -> discord.Message: if self.data is None: - return await self.empty_leaderboard(self.ctx) + return await self.empty_leaderboard(self.ctx, mention_author=False, **kwargs) - return await super().send(**kwargs) + return await super().send(mention_author=False, **kwargs) @dataclass @@ -245,31 +244,3 @@ class XPLeaderboard(Leaderboard): def format_entry_data(self, data: tuple) -> str: entry = data[2] return f"Level {xp.calculate_level(entry):,} | {entry:,} XP" - - -class Source(menus.ListPageSource): - def __init__(self, data, name, colour=discord.Colour.blue()): - super().__init__(data, per_page=10) - self.name = name - self.colour = colour - - async def format_page(self, menu: menus.MenuPages, entries): - offset = menu.current_page * self.per_page - - description = "" - for i, v in enumerate(entries, start=offset): - # Check if the person's name has to be highlighted - if v.startswith("**") and v.endswith("**"): - description += "**" - v = v[2:] - description += "{}: {}\n".format(i + 1, v) - embed = discord.Embed(colour=self.colour) - embed.set_author(name=self.name) - embed.description = description - embed.set_footer(text="{}/{}".format(menu.current_page + 1, self.get_max_pages())) - return embed - - -class Pages(menus.MenuPages): - def __init__(self, source, clear_reactions_after, timeout=30.0): - super().__init__(source, timeout=timeout, delete_message_after=True, clear_reactions_after=clear_reactions_after) From 93ede132a216f115afcf2d895df44442a4f060df Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sun, 6 Feb 2022 17:58:57 +0100 Subject: [PATCH 14/21] Memegen slash commands + autocompletion --- cogs/fun.py | 39 +++++++++------------------ cogs/slash/football_slash.py | 2 +- cogs/slash/fun_slash.py | 52 +++++++++++++++++++++++++++++++++++- data/menus/memes.py | 26 ++++++++++++++++++ functions/memes.py | 16 +++++++++-- 5 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 data/menus/memes.py diff --git a/cogs/fun.py b/cogs/fun.py index cb938bb..5eaac9f 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -1,15 +1,16 @@ -from data.embeds.xkcd import XKCDEmbed -from data.menus import leaderboards -from decorators import help -import discord -from discord.ext import commands -from enums.help_categories import Category -from functions import checks, stringFormatters -from functions.database import memes, trump, dadjoke -from functions.memes import generate import json import random + import requests +from discord.ext import commands + +from data.embeds.xkcd import XKCDEmbed +from data.menus.memes import MemesList +from decorators import help +from enums.help_categories import Category +from functions import checks +from functions.database import memes, trump, dadjoke +from functions.memes import generate class Fun(commands.Cog): @@ -98,17 +99,10 @@ class Fun(commands.Cog): if result is None: return await ctx.send("Deze meme staat niet in de database.") - # Convert to list to support item assignment - fields = list(fields) - generated = generate(result, fields) - # If the request was successful, remove the message calling it - if generated["success"]: - await self.utilsCog.removeMessage(ctx.message) - # Send the meme's url or the error message - await ctx.send(generated["message"]) + await ctx.reply(generated["message"], mention_author=False) @commands.command(name="Memes") @commands.check(checks.allowedChannels) @@ -118,16 +112,7 @@ class Fun(commands.Cog): Command that shows a list of memes in the database. :param ctx: Discord Context """ - memeList = memes.getAllMemes() - - # Turn the list into a list of [Name: fields] - memeList = [": ".join([stringFormatters.title_case(meme[1]), - str(meme[2])]) for meme in sorted(memeList, key=lambda x: x[1])] - - pages = paginated_leaderboard.Pages( - source=paginated_leaderboard.Source(memeList, "Memes", discord.Colour.blue()), - clear_reactions_after=True) - await pages.start(ctx) + return await MemesList(ctx=ctx).send() @commands.command(name="Pjoke") @help.Category(category=Category.Fun) diff --git a/cogs/slash/football_slash.py b/cogs/slash/football_slash.py index 0f75eee..b930b97 100644 --- a/cogs/slash/football_slash.py +++ b/cogs/slash/football_slash.py @@ -24,7 +24,7 @@ class FootballSlash(commands.Cog): @_jpl_group.command(name="table", description="Huidige rangschikking") async def _jpl_table_slash(self, ctx: ApplicationContext): await ctx.response.defer() - await ctx.respond(get_table()) + await ctx.send_followup(get_table()) @_jpl_group.command(name="update", description="Update de code voor deze competitie (owner-only)", default_permission=False) @permissions.is_owner() diff --git a/cogs/slash/fun_slash.py b/cogs/slash/fun_slash.py index 00c3c24..3e69a8c 100644 --- a/cogs/slash/fun_slash.py +++ b/cogs/slash/fun_slash.py @@ -1,10 +1,34 @@ from discord.ext import commands -from discord.commands import slash_command, ApplicationContext, Option +from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext +from functions.database import memes +from functions.database.memes import getAllMemes from data.embeds.xkcd import XKCDEmbed +from data.menus.memes import MemesList +from functions.memes import generate +from functions.stringFormatters import title_case from startup.didier import Didier +all_memes = getAllMemes() + + +def autocomplete_memes(ctx: AutocompleteContext) -> list[str]: + starting = [] + containing = [] + + val = ctx.value.lower() + + # First show matches that start with this word, then matches that contain it + for meme in all_memes: + if meme[1].startswith(val): + starting.append(title_case(meme[1])) + elif val in meme[1]: + containing.append(title_case(meme[1])) + + return [*starting, *containing] + + class FunSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client @@ -15,6 +39,32 @@ class FunSlash(commands.Cog): ): return await ctx.respond(embed=XKCDEmbed(num).create()) + @slash_command(name="memes", description="Lijst van memegen-memes") + async def _memes_slash(self, ctx: ApplicationContext): + return await MemesList(ctx=ctx).respond() + + @slash_command(name="memegen", description="Genereer memes") + async def _memegen_slash(self, ctx: ApplicationContext, + meme: Option(str, description="Naam van de template", required=True, autocomplete=autocomplete_memes), + field1: Option(str, required=True), + field2: Option(str, required=False, default=""), + field3: Option(str, required=False, default=""), + field4: Option(str, required=False, default="")): + # Get the meme info that corresponds to this name + result: memes.Meme = memes.getMeme(meme) + + # No meme found + if result is None: + return await ctx.respond("Deze meme staat niet in de database.", ephemeral=True) + + await ctx.response.defer() + + fields = (field1, field2, field3, field4) + generated = generate(result, fields) + + # Send generated meme or error message + await ctx.send_followup(generated["message"]) + def setup(client: Didier): client.add_cog(FunSlash(client)) diff --git a/data/menus/memes.py b/data/menus/memes.py new file mode 100644 index 0000000..0c3fe66 --- /dev/null +++ b/data/menus/memes.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass, field + +from data.menus.paginated import Paginated +from functions import stringFormatters +from functions.database import memes + + +@dataclass +class MemesList(Paginated): + title: str = field(default="Memes") + + def __post_init__(self): + self.data = self.get_data() + + def get_data(self) -> list[tuple]: + data = [] + meme_list = memes.getAllMemes() + for meme in sorted(meme_list, key=lambda x: x[1]): + name = stringFormatters.title_case(meme[1]) + fields = meme[2] + data.append((name, fields,)) + + return data + + def format_entry(self, index: int, value: tuple) -> str: + return f"{value[0]} ({value[1]})" diff --git a/functions/memes.py b/functions/memes.py index 4ffa2e4..a38d03c 100644 --- a/functions/memes.py +++ b/functions/memes.py @@ -10,6 +10,8 @@ def generate(meme: Meme, fields): """ Main function that takes a Meme as input & generates an image. """ + fields = list(fields) + # If there's only one field, the user isn't required to use quotes if meme.fields == 1: fields = [" ".join(fields)] @@ -37,11 +39,14 @@ def generate(meme: Meme, fields): # Adding a message parameter makes the code in the cog a lot cleaner if not reply["success"]: + reply["success"] = False reply["message"] = "Error! Controleer of je de juiste syntax hebt gebruikt. Gebruik het commando " \ "\"memes\" voor een lijst aan geaccepteerde meme-namen." else: reply["message"] = reply["data"]["url"] + reply["success"] = False + return reply @@ -77,7 +82,8 @@ def _apply_meme(meme: Meme, fields): 102156234: mocking_spongebob, 91538330: _x_x_everywhere, 252600902: _always_has_been, - 167754325: _math_is_math + 167754325: _math_is_math, + 206493414: _i_used_the_x_to_destroy_the_x } # Meme needs no special treatment @@ -104,6 +110,12 @@ def _always_has_been(fields): def _math_is_math(fields): + word = fields[0].upper() + + return ["", f"{word} IS {word}!"] + + +def _i_used_the_x_to_destroy_the_x(fields): word = fields[0] - return [f"{word.upper()} IS {word.upper()}!"] + return ["", f"I used the {word} to destroy the {word}"] From 9a999fb34b581c91072433fd8d62cb2406d65187 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 10 Feb 2022 19:34:40 +0100 Subject: [PATCH 15/21] Automatically join threads, add placeholder code for pin context menu --- cogs/context_menus/school_cm.py | 32 ++++++++++++++++++++++++++++++++ cogs/events.py | 6 ++++++ cogs/school.py | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 cogs/context_menus/school_cm.py diff --git a/cogs/context_menus/school_cm.py b/cogs/context_menus/school_cm.py new file mode 100644 index 0000000..f65e609 --- /dev/null +++ b/cogs/context_menus/school_cm.py @@ -0,0 +1,32 @@ +import discord +from discord import ApplicationContext +from discord.ext import commands +from discord.commands import message_command + +from startup.didier import Didier + + +class SchoolCM(commands.Cog): + def __init__(self, client: Didier): + self.client: Didier = client + + @message_command(name="Pin") + async def _pin_cm(self, ctx: ApplicationContext, message: discord.Message): + # In case people abuse, check if they're blacklisted + blacklist = [] + + if ctx.user.id in blacklist: + return await ctx.respond(":angry:", ephemeral=True) + + if message.is_system(): + return await ctx.respond("Dus jij wil system messages pinnen?\nMag niet.", ephemeral=True) + + await message.pin(reason=f"Didier Pin door {ctx.user.display_name}") + await ctx.respond("📌", ephemeral=True) + + +def setup(client: Didier): + # client.add_cog(SchoolCM(client)) + # TODO wait for bug to be fixed in lib then uncomment this + # when used in dm, tries to create a DM with the bot? + pass diff --git a/cogs/events.py b/cogs/events.py index 67abe79..7668d40 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -78,6 +78,12 @@ class Events(commands.Cog): # Earn XP & Message count stats.sentMessage(message) + @commands.Cog.listener() + async def on_thread_join(self, thread: discord.Thread): + # Join threads automatically + if thread.me is None: + await thread.join() + @commands.Cog.listener() async def on_command(self, ctx): """ diff --git a/cogs/school.py b/cogs/school.py index 520c87f..51066f2 100644 --- a/cogs/school.py +++ b/cogs/school.py @@ -82,7 +82,7 @@ class School(commands.Cog): if message.is_system(): return await ctx.send("Dus jij wil system messages pinnen?\nMag niet.") - await message.pin(reason="Didier Pin door {}".format(ctx.author.display_name)) + await message.pin(reason=f"Didier Pin door {ctx.author.display_name}") await ctx.message.add_reaction("✅") @commands.command(name="Deadlines", aliases=["dl"]) From a734191973a933e2353d6006eb0f3adbe0746f8a Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Thu, 10 Feb 2022 19:55:04 +0100 Subject: [PATCH 16/21] Add command to join threads that Didier is not in yet --- cogs/events.py | 5 ++++- cogs/other.py | 11 +++++++++++ files/help.json | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cogs/events.py b/cogs/events.py index 7668d40..284acd8 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -109,6 +109,7 @@ class Events(commands.Cog): # Don't handle commands that have their own custom error handler if hasattr(ctx.command, 'on_error'): return + # Someone just mentioned Didier without calling a real command, # don't care about this error if isinstance(err, (commands.CommandNotFound, commands.CheckFailure, commands.TooManyArguments, commands.ExpectedClosingQuoteError), ): @@ -120,9 +121,11 @@ class Events(commands.Cog): await ctx.send("Geen message gevonden die overeenkomt met het opgegeven argument.") elif isinstance(err, (commands.ChannelNotFound, commands.ChannelNotReadable)): await ctx.send("Geen channel gevonden dat overeenkomt met het opgegeven argument.") + elif isinstance(err, commands.ThreadNotFound): + await ctx.reply("Thread niet gevonden.", mention_author=False) # Someone forgot an argument or passed an invalid argument elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)): - await ctx.send("Controleer je argumenten.") + await ctx.reply("Controleer je argumenten.", mention_author=False) else: usage = stringFormatters.format_command_usage(ctx) await self.sendErrorEmbed(err, "Command", usage) diff --git a/cogs/other.py b/cogs/other.py index 859c55d..efddcab 100644 --- a/cogs/other.py +++ b/cogs/other.py @@ -1,3 +1,4 @@ +import discord from discord.ext import commands from data.embeds.snipe import EditSnipe, DeleteSnipe @@ -24,6 +25,16 @@ class Other(commands.Cog): """ await custom_commands.CommandsList(ctx).send() + @commands.command(name="Join", usage="[Thread]") + @help.Category(category=Category.Didier) + async def join_thread(self, ctx, thread: discord.Thread): + """ + Join threads + """ + if thread.me is None: + await thread.join() + await ctx.message.add_reaction("✅") + @commands.command(name="Snipe") @help.Category(category=Category.Other) async def snipe(self, ctx): diff --git a/files/help.json b/files/help.json index f14e6f1..c15ecb7 100644 --- a/files/help.json +++ b/files/help.json @@ -55,6 +55,7 @@ "inspire": "Genereer quotes via [InspiroBot](https://inspirobot.me/).", "inventory": "Bekijk de items in jouw inventory.", "invest": "Investeer [Aantal] Didier Dinks in jouw Didier Bank om rente te vergaren.", + "join": "Laat Didier [Thread] joinen.", "jpl": "Informatie over de Jupiler Pro League.", "jpl matches": "Bekijk de wedstrijden die gespeeld worden op [Week]. Default naar de huidige speeldag.", "jpl table": "De huidige stand van het klassement.", From e78e13e26afadb8679ac20942b4e06d091236bdc Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Fri, 11 Feb 2022 21:00:31 +0100 Subject: [PATCH 17/21] Add schedule for 2nd semester --- files/config.json | 2 +- files/schedules/32.json | 208 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 files/schedules/32.json diff --git a/files/config.json b/files/config.json index 3d513ac..e0f5850 100644 --- a/files/config.json +++ b/files/config.json @@ -1 +1 @@ -{"semester": "1", "year": "3", "years": 3, "jpl": 161733, "jpl_day": 24} \ No newline at end of file +{"semester": "2", "year": "3", "years": 3, "jpl": 161733, "jpl_day": 24} \ No newline at end of file diff --git a/files/schedules/32.json b/files/schedules/32.json new file mode 100644 index 0000000..1d0e9e5 --- /dev/null +++ b/files/schedules/32.json @@ -0,0 +1,208 @@ +{ + "semester_start": [14, 2, 2022], + "semester_end": [20, 2, 2022], + "holidays": [ + { + "start_date": [18, 3, 2022, 0, 0, 0], + "end_date": [18, 3, 2022, 23, 59, 59] + }, + { + "start_date": [1, 4, 2022, 11, 30, 0], + "end_date": [17, 4, 2022, 23, 59, 59] + }, + { + "start_date": [18, 4, 2022, 0, 0, 0], + "end_date": [18, 4, 2022, 23, 59, 59] + } + ], + "minors": [ + { + "name": "Beveiliging en parallele systemen", + "role": 891744461405687808, + "schedule": [ + { + "course": "Informatiebeveiliging", + "online_links": { + "opencast": "https://elosp.ugent.be/opencastplanner/live/GCMobile-ocrec79" + }, + "slots": [ + { + "online": "opencast", + "location": { + "campus": "Ardoyen", + "building": "iGent 126", + "room": "Auditorium 1" + }, + "time": { + "day": "maandag", + "start": 1300, + "end": 1430 + } + }, + { + "online": "opencast", + "location": { + "campus": "Ardoyen", + "building": "iGent 126", + "room": "Auditorium 1" + }, + "time": { + "day": "dinsdag", + "start": 830, + "end": 1130 + } + } + ] + } + ] + }, + { + "name": "Elektrotechniek en Telecommunicatie", + "role": 891744390035415111, + "schedule": [ + { + "course": "Elektrotechniek", + "slots": [ + { + "location": { + "campus": "Sterre", + "building": "S9", + "room": "3.2" + }, + "time": { + "day": "dinsdag", + "start": 1300, + "end": 1430 + } + }, + { + "location": { + "campus": "Sterre", + "building": "S8", + "room": "3.1" + }, + "time": { + "day": "woensdag", + "start": 1430, + "end": 1730 + } + }, + { + "location": { + "campus": "Sterre", + "building": "S9", + "room": "3.2" + }, + "time": { + "day": "donderdag", + "start": 1130, + "end": 1300 + } + } + ] + } + ] + } + ], + "schedule": [ + { + "course": "Automaten, Berekenbaarheid en Complexiteit", + "slots": [ + { + "location": { + "campus": "Sterre", + "building": "S25", + "room": "Emmy Noether" + }, + "time": { + "day": "dinsdag", + "start": 1430, + "end": 1730 + } + }, + { + "location": { + "campus": "Sterre", + "building": "S9", + "room": "PC 3.1 Konrad Zuse" + }, + "time": { + "day": "donderdag", + "start": 1430, + "end": 1730 + } + } + ] + }, + { + "course": "Computationele Biologie", + "slots": [ + { + "location": { + "campus": "Sterre", + "building": "S9", + "room": "3.2" + }, + "time": { + "day": "woensdag", + "start": 830, + "end": 1130 + } + }, + { + "location": { + "campus": "Sterre", + "building": "S9", + "room": "PC 3.1 Konrad Zuse" + }, + "time": { + "day": "vrijdag", + "start": 830, + "end": 1130 + } + } + ] + }, + { + "course": "Logisch Programmeren", + "online_links": { + "zoom": "https://ufora.ugent.be/d2l/ext/rp/443368/lti/framedlaunch/556e197e-e87b-4c27-be5d-53adc7a41826" + }, + "slots": [ + { + "online": "zoom", + "time": { + "day": "maandag", + "start": 1430, + "end": 1730 + } + }, + { + "online": "zoom", + "time": { + "day": "donderdag", + "start": 830, + "end": 1130 + } + } + ] + }, + { + "course": "Software Engineering Lab 2", + "slots": [ + { + "location": { + "campus": "Sterre", + "building": "S8", + "room": "3.1" + }, + "time": { + "day": "vrijdag", + "start": 1430, + "end": 1730 + } + } + ] + } + ] +} \ No newline at end of file From c6958d22f3f56963ded374a227870d134ad4736a Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Fri, 11 Feb 2022 22:11:20 +0100 Subject: [PATCH 18/21] Add command to generate course guides, add autocompletion for some commands, create json file with course information & abbreviations --- cogs/slash/school_slash.py | 47 +++++++++++--- data/courses.py | 64 ++++++++++++++++++++ files/courses.json | 121 +++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 data/courses.py create mode 100644 files/courses.json diff --git a/cogs/slash/school_slash.py b/cogs/slash/school_slash.py index 525e43d..03c8305 100644 --- a/cogs/slash/school_slash.py +++ b/cogs/slash/school_slash.py @@ -1,7 +1,8 @@ from discord.ext import commands -from discord.commands import slash_command, ApplicationContext, Option +from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext from data import schedule +from data.courses import load_courses, find_course_from_name from data.embeds.food import Menu from data.embeds.deadlines import Deadlines from functions import les, config @@ -10,13 +11,26 @@ from functions.timeFormatters import skip_weekends from startup.didier import Didier +# Preload autocomplete constants to allow for smoother results +courses = load_courses() +days = ["Morgen", "Overmorgen", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag"] + + +def day_autocomplete(ctx: AutocompleteContext) -> list[str]: + return [day for day in days if day.lower().startswith(ctx.value.lower())] + + +def course_autocomplete(ctx: AutocompleteContext) -> list[str]: + return [course for course in courses if course.lower().startswith(ctx.value.lower())] + + class SchoolSlash(commands.Cog): def __init__(self, client: Didier): self.client: Didier = client @slash_command(name="eten", description="Menu in de UGent resto's op een bepaalde dag") async def _food_slash(self, ctx: ApplicationContext, - dag: Option(str, description="Dag", required=False, default=None) + dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete) ): embed = Menu(dag).to_embed() await ctx.respond(embed=embed) @@ -28,21 +42,21 @@ class SchoolSlash(commands.Cog): @slash_command(name="les", description="Lessenrooster voor [Dag] (default vandaag)",) async def _schedule_slash(self, ctx: ApplicationContext, - day: Option(str, description="Dag", required=False, default=None) + dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete) ): """It's late and I really don't want to refactor the original right now""" - if day is not None: - day = day.lower() + if dag is not None: + dag = dag.lower() - date = les.find_target_date(day) + date = les.find_target_date(dag) # Person explicitly requested a weekend-day - if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4: - return await ctx.respond(f"{capitalize(day)} is het weekend.", ephemeral=True) + if dag is not None and dag.lower() in ("morgen", "overmorgen") and date.weekday() > 4: + return await ctx.respond(f"{capitalize(dag)} is het weekend.", ephemeral=True) date = skip_weekends(date) - s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None) + s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), dag is not None) if s.semester_over: return await ctx.respond("Het semester is afgelopen.", ephemeral=True) @@ -54,6 +68,21 @@ class SchoolSlash(commands.Cog): return await ctx.respond(embed=s.create_schedule().to_embed()) + @slash_command(name="fiche", description="Zoek de studiefiche voor een vak.") + async def _study_guide_slash(self, ctx: ApplicationContext, + vak: Option(str, description="Naam van het vak. Afkortingen werken ook, maar worden niet ge-autocompletet.", + required=True, autocomplete=course_autocomplete)): + # Find code corresponding to the search query + course = find_course_from_name(vak, courses) + + # Code not found + if course is None: + return await ctx.respond(f"Onbekend vak: \"{vak}\".", ephemeral=True) + + # Get the guide for the current year + year = 2018 + int(config.get("year")) + return await ctx.respond(f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}") + def setup(client: Didier): client.add_cog(SchoolSlash(client)) diff --git a/data/courses.py b/data/courses.py new file mode 100644 index 0000000..ce49649 --- /dev/null +++ b/data/courses.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass +from typing import Optional + +import dacite +import json + + +@dataclass +class Course: + abbreviations: list[str] + code: str + name: str + year: int + alt: Optional[str] = None + + +def load_courses() -> dict[str, Course]: + """Create a list of all courses""" + with open("files/courses.json", "r") as file: + data = json.load(file) + + courses = {} + + for course_name in data: + # Add name into the dict to allow flexibility + course_data = data[course_name] + course_data["name"] = course_name + + courses[course_name] = dacite.from_dict(data_class=Course, data=course_data) + + return courses + + +def find_course_from_name(name: str, courses: Optional[dict[str, Course]] = None, case_insensitive: bool = True) -> Optional[Course]: + # Allow passing a course dict in to avoid having to create it all the time + if courses is None: + courses = load_courses() + + if case_insensitive: + name = name.lower() + + def _perhaps_lower(inp: str) -> str: + """Cast a string to lowercase if necessary""" + if case_insensitive: + return inp.lower() + + return inp + + # Iterate over all courses to look for a match + for course_name, course in courses.items(): + # Check name first + if _perhaps_lower(course_name) == name: + return course + + # Then abbreviations + for abbreviation in course.abbreviations: + if _perhaps_lower(abbreviation) == name: + return course + + # Finally alternative names + if course.alt is not None and _perhaps_lower(course.alt) == name: + return course + + return None diff --git a/files/courses.json b/files/courses.json new file mode 100644 index 0000000..005ea83 --- /dev/null +++ b/files/courses.json @@ -0,0 +1,121 @@ +{ + "Algoritmen en Datastructuren 2": { + "abbreviations": ["AD2"], + "code": "C003777", + "year": 2 + }, + "Algoritmen en Datastructuren 3": { + "abbreviations": ["AD3"], + "code": "C003782", + "year": 3 + }, + "Artificiële Intelligentie": { + "abbreviations": ["AI"], + "code": "C003756", + "year": 3 + }, + "Automaten, Berekenbaarheid en Complexiteit": { + "abbreviations": ["ABC"], + "code": "C003785", + "year": 3 + }, + "Besturingssystemen": { + "abbreviations": ["BS"], + "code": "E019010", + "year": 3 + }, + "Communicatienetwerken": { + "abbreviations": ["Comnet"], + "code": "E008620", + "year": 2 + }, + "Computationele Biologie": { + "abbreviations": ["Compbio"], + "code": "C003789", + "year": 3 + }, + "Computerarchitectuur": { + "abbreviations": ["CA", "Comparch"], + "code": "E034110", + "year": 2 + }, + "Functioneel Programmeren": { + "abbreviations": ["FP", "Funcprog"], + "code": "C003775", + "year": 2 + }, + "Informatiebeveiliging": { + "abbreviations": ["Infosec"], + "alt": "Information Security", + "code": "E019400", + "year": 3 + }, + "Inleiding tot Elektrotechniek": { + "abbreviations": [], + "alt": "Elektrotechniek", + "code": "C003806", + "year": 3 + }, + "Inleiding tot Telecommunicatie": { + "abbreviations": ["Telecom"], + "code": "C003787", + "year": 3 + }, + "Logisch Programmeren": { + "abbreviations": ["LP", "Logprog", "Prolog"], + "code": "C003783", + "year": 3 + }, + "Modelleren en Simuleren": { + "abbreviations": ["Modsim"], + "code": "C003786", + "year": 3 + }, + "Multimedia": { + "abbreviations": ["MM"], + "code": "C002126", + "year": 2 + }, + "Parallelle Computersystemen": { + "abbreviations": ["PCS"], + "alt": "Parallel Computer Systems", + "code": "E034140", + "year": 3 + }, + "Statistiek en Probabiliteit": { + "abbreviations": ["Stat","Statistiek", "Statprob"], + "code": "C003778", + "year": 2 + }, + "Software Engineering Lab 1": { + "abbreviations": ["SEL1"], + "code": "C003780", + "year": 2 + }, + "Software Engineering Lab 2": { + "abbreviations": ["SEL2"], + "code": "C003784", + "year": 3 + }, + "Systeemprogrammeren": { + "abbreviations": ["Sysprog"], + "code": "C003776", + "year": 2 + }, + "Webdevelopment": { + "abbreviations": ["Webdev"], + "code": "C003779", + "year": 2 + }, + "Wetenschappelijk Rekenen": { + "abbreviations": ["Wetrek"], + "code": "C001521", + "year": 2 + }, + "Wiskundige Modellering in de Ingenieurswetenschappen": { + "abbreviations": ["Wimo"], + "alt": "Wiskundige Modellering", + "code": "C003788", + "year": 3 + } +} \ No newline at end of file From 9819e826389c6c58a91e471e02f7c17fdc7a6709 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Fri, 11 Feb 2022 22:26:23 +0100 Subject: [PATCH 19/21] Add tests for courses, change study guide description --- cogs/slash/school_slash.py | 2 +- data/courses.py | 5 ++++- tests/test_data/test_courses.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/test_data/test_courses.py diff --git a/cogs/slash/school_slash.py b/cogs/slash/school_slash.py index 03c8305..b2bf76f 100644 --- a/cogs/slash/school_slash.py +++ b/cogs/slash/school_slash.py @@ -70,7 +70,7 @@ class SchoolSlash(commands.Cog): @slash_command(name="fiche", description="Zoek de studiefiche voor een vak.") async def _study_guide_slash(self, ctx: ApplicationContext, - vak: Option(str, description="Naam van het vak. Afkortingen werken ook, maar worden niet ge-autocompletet.", + vak: Option(str, description="Naam van het vak. Afkortingen werken ook, maar worden niet geautocompletet.", required=True, autocomplete=course_autocomplete)): # Find code corresponding to the search query course = find_course_from_name(vak, courses) diff --git a/data/courses.py b/data/courses.py index ce49649..a846c19 100644 --- a/data/courses.py +++ b/data/courses.py @@ -3,6 +3,7 @@ from typing import Optional import dacite import json +from os import path @dataclass @@ -16,7 +17,9 @@ class Course: def load_courses() -> dict[str, Course]: """Create a list of all courses""" - with open("files/courses.json", "r") as file: + # Allows testing + filepath = path.join(path.dirname(__file__), "..", "files", "courses.json") + with open(filepath, "r") as file: data = json.load(file) courses = {} diff --git a/tests/test_data/test_courses.py b/tests/test_data/test_courses.py new file mode 100644 index 0000000..69fac06 --- /dev/null +++ b/tests/test_data/test_courses.py @@ -0,0 +1,32 @@ +import unittest + +from data.courses import load_courses, find_course_from_name + + +class TestCourses(unittest.TestCase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.courses = load_courses() + + def test_find_course(self): + self.assertIsNone(find_course_from_name("garbage", self.courses)) + self.assertIsNone(find_course_from_name("garbage")) + + # Find by name + webdev = find_course_from_name("Webdevelopment", self.courses) + self.assertIsNotNone(webdev) + self.assertEqual(webdev.code, "C003779") + + # Find by abbreviation + infosec = find_course_from_name("infosec", self.courses) + self.assertIsNotNone(infosec) + self.assertEqual(infosec.code, "E019400") + + # Case sensitive + not_found = find_course_from_name("ad3", self.courses, case_insensitive=False) + self.assertIsNone(not_found) + + # Find by alt name + pcs = find_course_from_name("parallel computer systems", self.courses) + self.assertIsNotNone(pcs) + self.assertEqual(pcs.code, "E034140") From 0165700d9f122a359c2993f45fc58fd601b2961e Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Fri, 11 Feb 2022 22:54:40 +0100 Subject: [PATCH 20/21] Add slash command to send links, add normal commands for study guides & links --- cogs/other.py | 14 ++++++++++++++ cogs/school.py | 18 ++++++++++++++++++ cogs/slash/other_slash.py | 20 +++++++++++++++++++- data/links.py | 16 ++++++++++++++++ files/help.json | 6 ++++-- files/links.json | 3 +++ 6 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 data/links.py create mode 100644 files/links.json diff --git a/cogs/other.py b/cogs/other.py index efddcab..83c5ed4 100644 --- a/cogs/other.py +++ b/cogs/other.py @@ -2,6 +2,7 @@ import discord from discord.ext import commands from data.embeds.snipe import EditSnipe, DeleteSnipe +from data.links import get_link_for from data.menus import custom_commands from data.snipe import Action, Snipe from decorators import help @@ -17,6 +18,19 @@ class Other(commands.Cog): def cog_check(self, _): return not self.client.locked + @commands.command(name="Link", usage="[Naam]") + @help.Category(category=Category.Other) + async def link(self, ctx, name: str): + """ + Send commonly used links + """ + match = get_link_for(name) + + if match is None: + return await ctx.reply(f"Geen match gevonden voor \"{name}\".", mention_author=False, delete_after=15) + + return await ctx.reply(match, mention_author=False) + @commands.command(name="Custom") @help.Category(category=Category.Didier) async def list_custom(self, ctx): diff --git a/cogs/school.py b/cogs/school.py index 51066f2..e2fc347 100644 --- a/cogs/school.py +++ b/cogs/school.py @@ -1,4 +1,5 @@ from data import schedule +from data.courses import find_course_from_name from data.embeds.deadlines import Deadlines from data.embeds.food import Menu from decorators import help @@ -85,6 +86,23 @@ class School(commands.Cog): await message.pin(reason=f"Didier Pin door {ctx.author.display_name}") await ctx.message.add_reaction("✅") + @commands.command(name="Fiche", usage="[Vak]", aliases=["guide", "studiefiche"]) + @help.Category(category=Category.School) + async def study_guide(self, ctx, name: str): + """ + Send links to study guides + """ + # Find code corresponding to the search query + course = find_course_from_name(name) + + # Code not found + if course is None: + return await ctx.reply(f"Onbekend vak: \"{name}\".", mention_author=False, delete_after=15) + + # Get the guide for the current year + year = 2018 + int(config.get("year")) + return await ctx.reply(f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}", mention_author=False) + @commands.command(name="Deadlines", aliases=["dl"]) @help.Category(category=Category.School) async def deadlines(self, ctx): diff --git a/cogs/slash/other_slash.py b/cogs/slash/other_slash.py index 39350c7..1e6f12d 100644 --- a/cogs/slash/other_slash.py +++ b/cogs/slash/other_slash.py @@ -1,9 +1,16 @@ from discord.ext import commands -from discord.commands import slash_command, ApplicationContext +from discord.commands import slash_command, ApplicationContext, AutocompleteContext, Option from requests import get +from data.links import load_all_links, get_link_for from startup.didier import Didier +links = load_all_links() + + +def link_autocomplete(ctx: AutocompleteContext) -> list[str]: + return [link for link in links if link.lower().startswith(ctx.value.lower())] + class OtherSlash(commands.Cog): def __init__(self, client: Didier): @@ -18,6 +25,17 @@ class OtherSlash(commands.Cog): else: await ctx.respond("Uh oh API down.") + @slash_command(name="link", description="Shortcut voor nuttige links.") + async def _link_slash(self, ctx: ApplicationContext, + name: Option(str, description="Naam van de link.", required=True, + autocomplete=link_autocomplete)): + match = get_link_for(name) + + if match is None: + return await ctx.respond(f"Geen match gevonden voor \"{name}\".") + + return await ctx.respond(match) + def setup(client: Didier): client.add_cog(OtherSlash(client)) diff --git a/data/links.py b/data/links.py new file mode 100644 index 0000000..a994123 --- /dev/null +++ b/data/links.py @@ -0,0 +1,16 @@ +import json +from typing import Optional + + +def load_all_links() -> dict[str, str]: + with open("files/links.json", "r") as file: + return json.load(file) + + +def get_link_for(name: str) -> Optional[str]: + links = load_all_links() + for link in links: + if link.lower() == name.lower(): + return links[link] + + return None diff --git a/files/help.json b/files/help.json index c15ecb7..95f6be3 100644 --- a/files/help.json +++ b/files/help.json @@ -44,9 +44,10 @@ "faq": "Stuurt een lijst van alle FAQ's in [Categorie] naar jouw DM's.\nGeef geen categorie op om een lijst van categorieën te krijgen.\nIndien je een of meerdere personen tagt, wordt de lijst naar hun DM's gestuurd in plaats van de jouwe.", "faq add": "Voegt een vraag & antwoord toe aan FAQ [Categorie].\nIndien je geen vraag & antwoord opgeeft, maakt het een categorie aan.", "faq quote": "Stuurt een specifieke entry uit de FAQ in het huidige channel in plaats van de hele lijst naar iemand's DM's.", + "fiche": "Stuurt de URL naar de studiefiche voor [Vak]. Veelgebruikte afkortingen van vakken werken ook.\n\nVoorbeeld: Funcprog, FP, InfoSec, SEL2, ...", "github": "Stuurt de GitHub link van [Persoon] indien je iemand tagt, zoniet een lijst van GitHubs van mensen uit deze Discord.\nIndien je jouw eigen GitHub hier graag bij zou zetten, gebruik je ``GitHub Add`` of kan ke een DM sturen naar DJ STIJN.", "github add": "Voegt jouw eigen GitHub toe aan de lijst.\nZowel ``github.com/username`` als ``username`` werken.", - "give": "Geef [Persoon] [Aantal] van jouw eigen Didier Dinks.", + "give": "Geeft [Persoon] [Aantal] van jouw eigen Didier Dinks.", "google": "Geeft de eerste 10 zoekresultaten voor [Query] op Google.", "hangman": "Raad [Letter]. Indien je geen letter opgeeft, toont het de status van de huidige Hangman game indien er een bezig is.", "hangman start": "Start een nieuwe Hangman game indien er nog geen bezig is. Indien je geen woord opgeeft, wordt er een willekeurig woord gekozen.\n**Indien je wel een woord opgeeft, werkt dit enkel in DM.**", @@ -62,7 +63,8 @@ "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].\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.", + "link": "Stuurt de link die overeenkomt met [Naam].", + "lmgtfy": "Stuurt 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.", "lost": "Bekijk het aantal Didier Dinks die al verloren zijn door gambling.", diff --git a/files/links.json b/files/links.json new file mode 100644 index 0000000..520e847 --- /dev/null +++ b/files/links.json @@ -0,0 +1,3 @@ +{ + "DERP": "https://github.ugent.be/DERP" +} \ No newline at end of file From b1fdd2205867870733c39c63c9bea05b26197ebe Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Fri, 11 Feb 2022 23:08:28 +0100 Subject: [PATCH 21/21] Reply to original message if command invocation was a reply --- cogs/other.py | 5 +++-- cogs/school.py | 5 ++++- files/courses.json | 2 +- functions/utils.py | 22 +++++++++++++++++++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/cogs/other.py b/cogs/other.py index 83c5ed4..819ab72 100644 --- a/cogs/other.py +++ b/cogs/other.py @@ -7,6 +7,7 @@ from data.menus import custom_commands from data.snipe import Action, Snipe from decorators import help from enums.help_categories import Category +from functions.utils import reply_to_reference from startup.didier import Didier @@ -20,7 +21,7 @@ class Other(commands.Cog): @commands.command(name="Link", usage="[Naam]") @help.Category(category=Category.Other) - async def link(self, ctx, name: str): + async def link(self, ctx: commands.Context, name: str): """ Send commonly used links """ @@ -29,7 +30,7 @@ class Other(commands.Cog): if match is None: return await ctx.reply(f"Geen match gevonden voor \"{name}\".", mention_author=False, delete_after=15) - return await ctx.reply(match, mention_author=False) + await reply_to_reference(ctx, content=match) @commands.command(name="Custom") @help.Category(category=Category.Didier) diff --git a/cogs/school.py b/cogs/school.py index e2fc347..4e6d484 100644 --- a/cogs/school.py +++ b/cogs/school.py @@ -9,6 +9,7 @@ from enums.help_categories import Category from functions import config, les from functions.stringFormatters import capitalize from functions.timeFormatters import skip_weekends +from functions.utils import reply_to_reference class School(commands.Cog): @@ -101,7 +102,9 @@ class School(commands.Cog): # Get the guide for the current year year = 2018 + int(config.get("year")) - return await ctx.reply(f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}", mention_author=False) + link = f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}" + + return await reply_to_reference(ctx, content=link) @commands.command(name="Deadlines", aliases=["dl"]) @help.Category(category=Category.School) diff --git a/files/courses.json b/files/courses.json index 005ea83..d7ec000 100644 --- a/files/courses.json +++ b/files/courses.json @@ -77,7 +77,7 @@ "year": 2 }, "Parallelle Computersystemen": { - "abbreviations": ["PCS"], + "abbreviations": ["Paracomp", "Parallelle", "PCS"], "alt": "Parallel Computer Systems", "code": "E034140", "year": 3 diff --git a/functions/utils.py b/functions/utils.py index b1a692a..a56865c 100644 --- a/functions/utils.py +++ b/functions/utils.py @@ -1,5 +1,6 @@ -from typing import Union +from typing import Union, Optional +import discord from discord import ApplicationContext from discord.ext.commands import Context @@ -31,3 +32,22 @@ def get_display_name(ctx: Union[ApplicationContext, Context], user_id: int) -> s mem = ctx.guild.get_member(user_id) return mem.display_name + + +async def reply_to_reference(ctx: Context, content: Optional[str] = None, embed: Optional[discord.Embed] = None, always_mention=False): + """Reply to a message + In case the message is a reply to another message, try to reply to that one instead and ping the author + otherwise, reply to the message that invoked the command & only mention the author if necessary + """ + # Message is a reply + if ctx.message.reference is not None: + cached = ctx.message.reference.cached_message + + # Reference is not cached anymore: fetch it + if cached is None: + # Message is in the same channel, otherwise no way to reply to it + cached = await ctx.channel.fetch_message(ctx.message.reference.message_id) + + return await cached.reply(content, embed=embed, mention_author=cached.author != ctx.author) + + return await ctx.reply(content, embed=embed, mention_author=always_mention)