From 81a0d90a12615efadf2654a296b49d1e3589c518 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sun, 6 Feb 2022 01:11:40 +0100 Subject: [PATCH] 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)