diff --git a/.gitignore b/.gitignore index 02e269f..df503ea 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ files/stats.json files/lost.json files/locked.json files/ufora_notifications.json +files/compbio_benchmarks_2.json .idea/ __pycache__ .env diff --git a/cogs/slash/school_slash.py b/cogs/slash/school_slash.py index b2bf76f..5464dee 100644 --- a/cogs/slash/school_slash.py +++ b/cogs/slash/school_slash.py @@ -1,3 +1,6 @@ +import json + +from discord import SlashCommandGroup from discord.ext import commands from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext @@ -5,6 +8,7 @@ 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 data.menus import leaderboards from functions import les, config from functions.stringFormatters import capitalize from functions.timeFormatters import skip_weekends @@ -83,6 +87,30 @@ class SchoolSlash(commands.Cog): year = 2018 + int(config.get("year")) return await ctx.respond(f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}") + _compbio_group = SlashCommandGroup("compbio", "Commands voor compbio opdrachten") + + @_compbio_group.command(name="leaderboard", description="Gesorteerd en ingevuld leaderboard") + async def _compbio_lb_slash(self, ctx: ApplicationContext): + lb = leaderboards.CompbioLeaderboard(ctx) + await lb.respond() + + @_compbio_group.command(name="submit", description="Link een Dodona-submission aan jouw username") + async def _compbio_submit_slash(self, ctx: ApplicationContext, + submission: Option(int, description="Id van je Dodona indiening.", required=True)): + with open("files/compbio_benchmarks_2.json", "r") as fp: + file = json.load(fp) + + submission = str(submission) + + if submission in file: + return await ctx.respond("❌ Deze submission is al aan iemand gelinkt.", ephemeral=True) + + with open("files/compbio_benchmarks_2.json", "w") as fp: + file[submission] = ctx.user.id + json.dump(file, fp) + + return await ctx.respond(f"✅ Submission **{submission}** is aan jouw naam gelinkt.", ephemeral=True) + def setup(client: Didier): client.add_cog(SchoolSlash(client)) diff --git a/data/menus/leaderboards.py b/data/menus/leaderboards.py index 2b91b48..befd8d7 100644 --- a/data/menus/leaderboards.py +++ b/data/menus/leaderboards.py @@ -1,3 +1,4 @@ +import json import math from abc import ABC, abstractmethod from dataclasses import dataclass, field @@ -8,11 +9,12 @@ import requests from discord import ApplicationContext from discord.ext.commands import Context +import settings 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 +from functions.utils import get_display_name, get_mention @dataclass @@ -21,6 +23,7 @@ class Leaderboard(Paginated, ABC): colour: discord.Colour = discord.Colour.blue() fetch_names: bool = True ignore_non_pos: bool = True + reverse: bool = True def __post_init__(self): self.data = self.process_data(self.get_data()) @@ -31,7 +34,7 @@ class Leaderboard(Paginated, ABC): def process_data(self, entries: list[tuple]) -> Optional[list[tuple]]: data = [] - for i, v in enumerate(sorted(entries, key=self.get_value, reverse=True)): + for i, v in enumerate(sorted(entries, key=self.get_value, reverse=self.reverse)): entry_data = self.get_value(v) # Leaderboard is empty @@ -90,13 +93,13 @@ class Leaderboard(Paginated, ABC): return await ctx.reply(embed=embed, **kwargs) async def respond(self, **kwargs) -> discord.Message: - if self.data is None: + if self.data is None or not self.data: 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: + if self.data is None or not self.data: return await self.empty_leaderboard(self.ctx, **kwargs) return await super().send(**kwargs) @@ -117,6 +120,56 @@ class BitcoinLeaderboard(Leaderboard): return "Er zijn nog geen personen met Bitcoins." +@dataclass +class CompbioLeaderboard(Leaderboard): + colour: discord.Colour = field(default=discord.Colour.green()) + title: str = field(default="Leaderboard Computationele Biologie #2") + reverse: bool = False + + def get_submission_user(self, submission_id: str) -> str: + with open("files/compbio_benchmarks_2.json", "r") as fp: + file = json.load(fp) + + if submission_id in file: + user_id = file[submission_id] + return get_mention(self.ctx, user_id) + + return f"[# {submission_id}]" + + def get_data(self) -> list[tuple]: + headers = {"Authorization": f"token {settings.UGENT_GH_TOKEN}"} + result = requests.get(f"https://github.ugent.be/raw/computationele-biologie/benchmarks-2022/main/reconstruction/J02459.1.50mers.md", headers=headers).text + # Remove table headers + result = result.split("\n")[2:] + data = [] + + for line in result: + try: + cells = line.split("|") + submission_id = cells[1].strip() + mean = float(cells[2].strip().split(" ")[0]) + except IndexError: + # Other lines because of markdown formatting + continue + + data.append((submission_id, mean, )) + + return data + + def _should_highlight(self, data) -> bool: + # TODO maybe find a fix for this? + return False + + def format_entry(self, index: int, data: tuple) -> str: + return f"{index + 1}: {self.get_submission_user(data[0])} ({self.format_entry_data(data)})" + + def format_entry_data(self, data: tuple) -> str: + return f"{str(data[1])} ms" + + def get_value(self, data: tuple): + return data[1] + + @dataclass class CoronaLeaderboard(Leaderboard): colour: discord.Colour = field(default=discord.Colour.red()) diff --git a/functions/stringFormatters.py b/functions/stringFormatters.py index 37d67a0..edb9080 100644 --- a/functions/stringFormatters.py +++ b/functions/stringFormatters.py @@ -45,7 +45,7 @@ def format_command_usage(ctx: Context) -> str: def format_slash_command_usage(interaction: Interaction) -> str: # Create a string with the options used options = " ".join(list(map( - lambda o: f"{o['name']}: \"{o['value']}\"", + lambda o: f"{o['name']}: \"{o['value']}\"" if "value" in o else o["name"], interaction.data.get("options", []) ))) diff --git a/functions/utils.py b/functions/utils.py index a56865c..9aaaa64 100644 --- a/functions/utils.py +++ b/functions/utils.py @@ -4,34 +4,55 @@ import discord from discord import ApplicationContext from discord.ext.commands import Context -from data import constants +import settings -def get_display_name(ctx: Union[ApplicationContext, Context], user_id: int) -> str: +def get_member_or_user(ctx: Union[ApplicationContext, Context], user_id: int) -> Optional[Union[discord.Member, discord.User]]: + """Get a COC Member instance of a user if they are in the server, + otherwise the regular User instance + """ 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 + return author # Get member instance from CoC - COC = ctx.bot.get_guild(int(constants.DeZandbak)) + COC = ctx.bot.get_guild(settings.COC_ID) member = COC.get_member(user_id) if member is not None: - return member.display_name + return member # Try to fetch the user - user = ctx.bot.get_user(user_id) - if user is not None: - return user.name + return ctx.bot.get_user(user_id) - # User couldn't be found + # Guild exists, use that instead + return ctx.guild.get_member(user_id) + + +def get_display_name(ctx: Union[ApplicationContext, Context], user_id: int) -> str: + member = get_member_or_user(ctx, user_id) + + # Nothing found + if member is None: return f"[? | {user_id}]" - mem = ctx.guild.get_member(user_id) - return mem.display_name + if isinstance(member, discord.Member): + return member.display_name + + return member.name + + +def get_mention(ctx: Union[ApplicationContext, Context], user_id: int) -> str: + member = get_member_or_user(ctx, user_id) + + # Nothing found + if member is None: + return f"[? | {user_id}]" + + return member.mention async def reply_to_reference(ctx: Context, content: Optional[str] = None, embed: Optional[discord.Embed] = None, always_mention=False): diff --git a/requirements.txt b/requirements.txt index b19b0c7..ec29cf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Beta version of Discord.py fork -py-cord==2.0.0b1 +py-cord==2.0.0b5 python-dotenv==0.14.0 beautifulsoup4==4.9.1 diff --git a/settings.py b/settings.py index dcd8067..6939351 100644 --- a/settings.py +++ b/settings.py @@ -41,3 +41,6 @@ COC_ID: int = int(os.getenv("COC_ID", "626699611192688641")) # Ex: 123,456,789 _guilds = os.getenv("SLASHTESTGUILDS", "").replace(" ", "") SLASH_TEST_GUILDS: List[int] = list(map(lambda x: int(x), _guilds.split(","))) if _guilds else None + +# GitHub token (UGent) +UGENT_GH_TOKEN = os.getenv("UGENTGHTOKEN", "")