Leaderboard cleanup

pull/104/head
Stijn De Clercq 2022-02-06 01:11:40 +01:00
parent ca687956f6
commit 81a0d90a12
6 changed files with 365 additions and 280 deletions

View File

@ -1,5 +1,5 @@
from data.embeds.xkcd import XKCDEmbed from data.embeds.xkcd import XKCDEmbed
from data.menus import paginated_leaderboard from data.menus import leaderboards
from decorators import help from decorators import help
import discord import discord
from discord.ext import commands from discord.ext import commands

View File

@ -1,18 +1,12 @@
from typing import Callable, Optional
from data.menus import paginated_leaderboard
from decorators import help
import discord import discord
from discord.ext import commands from discord.ext import commands
from data.menus import leaderboards
from decorators import help
from enums.help_categories import Category from enums.help_categories import Category
from enums.numbers import Numbers from functions import checks
from functions import checks, xp
from functions.database import currency, stats, poke, muttn
import math
import requests
# TODO some sort of general leaderboard generation because all of them are the same
class Leaderboards(commands.Cog): class Leaderboards(commands.Cog):
def __init__(self, client): def __init__(self, client):
@ -20,29 +14,9 @@ class Leaderboards(commands.Cog):
self.utilsCog = self.client.get_cog("Utils") self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked # Don't allow any commands to work when locked
def cog_check(self, ctx): def cog_check(self, _):
return not self.client.locked 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]*", @commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*",
invoke_without_command=True) invoke_without_command=True)
@commands.check(checks.allowedChannels) @commands.check(checks.allowedChannels)
@ -56,154 +30,48 @@ class Leaderboards(commands.Cog):
@leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True) @leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True)
async def dinks(self, ctx): async def dinks(self, ctx):
entries = currency.getAllRows() lb = leaderboards.DinksLeaderboard(ctx=ctx)
platDinks = currency.getAllPlatDinks() await lb.send()
# 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)
@leaderboard.command(name="Corona", hidden=True) @leaderboard.command(name="Corona", hidden=True)
async def corona(self, ctx): async def corona(self, ctx):
result = requests.get("https://disease.sh/v3/covid-19/countries").json() lb = leaderboards.CoronaLeaderboard(ctx=ctx)
result.sort(key=lambda x: int(x["cases"]), reverse=True) await lb.send()
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)
@leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True) @leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True)
async def bitcoin(self, ctx): async def bitcoin(self, ctx):
users = currency.getAllRows() lb = leaderboards.BitcoinLeaderboard(ctx=ctx)
data = self._generate_embed_data(users, data_f=lambda x: round(float(x[8]), 8)) await lb.send()
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) @leaderboard.command(name="Rob", hidden=True)
async def rob(self, ctx): async def rob(self, ctx):
users = list(stats.getAllRows()) lb = leaderboards.RobLeaderboard(ctx=ctx)
data = self._generate_embed_data(users, data_f=lambda x: math.floor(float(x[4]))) await lb.send()
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) @leaderboard.command(name="Poke", hidden=True)
async def poke(self, ctx): async def poke(self, ctx):
entries = stats.getAllRows() lb = leaderboards.PokeLeaderboard(ctx=ctx)
blacklist = poke.getAllBlacklistedUsers() await lb.send()
# 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)
@leaderboard.command(name="Xp", aliases=["Level"], hidden=True) @leaderboard.command(name="Xp", aliases=["Level"], hidden=True)
async def xp(self, ctx): async def xp(self, ctx):
entries = stats.getAllRows() lb = leaderboards.XPLeaderboard(ctx=ctx)
data = self._generate_embed_data(entries, data_f=lambda x: round(int(x[12]))) await lb.send()
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) @leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True)
async def messages(self, ctx): async def messages(self, ctx):
entries = stats.getAllRows() lb = leaderboards.MessageLeaderboard(ctx=ctx)
message_count = stats.getTotalMessageCount() await lb.send()
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)
@leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True) @leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True)
async def muttn(self, ctx): async def muttn(self, ctx):
entries = muttn.getAllRows() lb = leaderboards.MuttnLeaderboard(ctx=ctx)
data = self._generate_embed_data(entries, data_f=lambda x: round(float(x[1]), 2)) await lb.send()
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)
async def callLeaderboard(self, name, ctx): async def callLeaderboard(self, name, ctx):
command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0] command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0]
await command(ctx) 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): def setup(client):
client.add_cog(Leaderboards(client)) client.add_cog(Leaderboards(client))

View File

@ -1,4 +1,4 @@
from data.menus import paginated_leaderboard from data.menus import leaderboards
from decorators import help from decorators import help
import discord import discord
from discord.ext import commands, menus from discord.ext import commands, menus

View File

@ -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)

View File

@ -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)

View File

@ -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)