Skip to content

Commit

Permalink
address mrcat review comments
Browse files Browse the repository at this point in the history
- support show all <-> show less
- add comment about font choice

other things:
- remove all buttons on timeout
- send text file as a separate message, keeping image in original
  reponse
- tweak some comments and rename print_leaderboard
  • Loading branch information
katrinafyi committed Dec 1, 2024
1 parent 4e2fcad commit 344c0f6
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 36 deletions.
81 changes: 50 additions & 31 deletions uqcsbot/advent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from uqcsbot.models import AOCRegistrations, AOCWinners
from uqcsbot.utils.err_log_utils import FatalErrorWithLog
from uqcsbot.utils.advent_utils import (
Leaderboard,
Member,
Day,
Json,
Expand All @@ -22,7 +23,7 @@
CACHE_TIME,
HL_COLOUR,
parse_leaderboard_column_string,
print_leaderboard,
build_leaderboard,
render_leaderboard_to_image,
render_leaderboard_to_text,
)
Expand Down Expand Up @@ -139,46 +140,51 @@


class LeaderboardView(discord.ui.View):
INITIAL_VISIBLE_ROWS = 20
TRUNCATED_COUNT = 20
TIMEOUT = 180 # seconds

def __init__(
self,
bot: UQCSBot,
inter: discord.Interaction,
code: int,
year: int,
day: Optional[Day],
members: list[Member],
leaderboard_style: str,
sortby: Optional[SortingMethod],
):
super().__init__(timeout=300)
super().__init__(timeout=self.TIMEOUT)

# constant within one embed
self.bot = bot
self.inter = inter
self.code = code
self.year = year
self.day = day
self.all_members = members
self.leaderboard_style = leaderboard_style
self.sortby = sortby
self.timestamp = datetime.now()
self.basename = f"advent_{self.code}_{self.year}_{self.day}"

# can be changed by interaction
self.visible_members = members[: self.INITIAL_VISIBLE_ROWS]
self.show_text = False
self._visible_members = members[: self.TRUNCATED_COUNT]

@property
def is_truncated(self):
return len(self.visible_members) < len(self.all_members)
return len(self._visible_members) < len(self.all_members)

def make_message_arguments(self) -> Dict[str, Any]:
view_url = LEADERBOARD_VIEW_URL.format(year=self.year, code=self.code)

leaderboard = print_leaderboard(
def _build_leaderboard(self, members: List[Member]) -> Leaderboard:
return build_leaderboard(
parse_leaderboard_column_string(self.leaderboard_style, self.bot),
self.visible_members,
members,
self.day,
)

def make_message_arguments(self) -> Dict[str, Any]:
view_url = LEADERBOARD_VIEW_URL.format(year=self.year, code=self.code)

title = (
"Advent of Code UQCS Leaderboard"
if self.code == UQCS_LEADERBOARD
Expand All @@ -193,12 +199,10 @@ def make_message_arguments(self) -> Dict[str, Any]:
notes.append(f"sorted by {self.sortby}")
if self.is_truncated:
notes.append(
f"top {len(self.visible_members)} shown out of {len(self.all_members)}"
f"top {len(self._visible_members)} shown out of {len(self.all_members)}"
)
body = f"({', '.join(notes)})" if notes else ""

basename = f"advent_{self.code}_{self.year}_{self.day}"

embed = discord.Embed(
title=title,
url=view_url,
Expand All @@ -207,19 +211,16 @@ def make_message_arguments(self) -> Dict[str, Any]:
timestamp=self.timestamp,
)

if not self.show_text:
scoreboard_image = render_leaderboard_to_image(leaderboard)
file = discord.File(io.BytesIO(scoreboard_image), basename + ".png")
embed.set_image(url=f"attachment://{file.filename}")
else:
scoreboard_text = render_leaderboard_to_text(leaderboard)
file = discord.File(
io.BytesIO(scoreboard_text.encode("utf-8")), basename + ".txt"
)
leaderboard = self._build_leaderboard(self._visible_members)
scoreboard_image = render_leaderboard_to_image(leaderboard)
file = discord.File(io.BytesIO(scoreboard_image), self.basename + ".png")
embed.set_image(url=f"attachment://{file.filename}")

self.show_all_interaction.disabled = not self.is_truncated
self.get_text_interaction.label = (
"Show as image" if self.show_text else "Show as text"
self.show_all_interaction.disabled = (
len(self.all_members) <= self.TRUNCATED_COUNT
)
self.show_all_interaction.label = (
"Show all" if self.is_truncated else "Show less"
)

return {
Expand All @@ -232,15 +233,33 @@ def make_message_arguments(self) -> Dict[str, Any]:
async def show_all_interaction(
self, inter: discord.Interaction, btn: discord.ui.Button["LeaderboardView"]
):
self.visible_members = self.all_members
self._visible_members = (
self.all_members
if self.is_truncated
else self.all_members[: self.TRUNCATED_COUNT]
)
await inter.response.edit_message(**self.make_message_arguments())

@discord.ui.button(label="Show text", style=discord.ButtonStyle.gray)
@discord.ui.button(label="Export as text", style=discord.ButtonStyle.gray)
async def get_text_interaction(
self, inter: discord.Interaction, btn: discord.ui.Button["LeaderboardView"]
):
self.show_text = not self.show_text
await inter.response.edit_message(**self.make_message_arguments())
"""
Sends the text leaderboard as a file attachment within a new reply.
"""
leaderboard = self._build_leaderboard(self.all_members)
text = render_leaderboard_to_text(leaderboard)
file = discord.File(io.BytesIO(text.encode("utf-8")), self.basename + ".txt")
await inter.response.send_message(file=file)

btn.disabled = True
await self.inter.edit_original_response(view=self)

async def on_timeout(self) -> None:
"""
Detach interactable view on timeout.
"""
await self.inter.edit_original_response(view=None)


class Advent(commands.Cog):
Expand Down Expand Up @@ -629,7 +648,7 @@ async def leaderboard_command(
return

view = LeaderboardView(
self.bot, code, year, day, members, leaderboard_style, sortby
self.bot, interaction, code, year, day, members, leaderboard_style, sortby
)
await interaction.edit_original_response(**view.make_message_arguments())

Expand Down
16 changes: 11 additions & 5 deletions uqcsbot/utils/advent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Callable,
NamedTuple,
Tuple,
cast,
)
from collections import defaultdict
from datetime import datetime, timedelta
Expand Down Expand Up @@ -43,6 +44,7 @@
CACHE_TIME = timedelta(minutes=15)

# Colours borrowed from adventofcode.com website
# https://adventofcode.com/static/style.css
BG_COLOUR = "#0f0f23"
FG_COLOUR = "#cccccc"
HL_COLOUR = "#009900"
Expand Down Expand Up @@ -428,14 +430,17 @@ def _isolate_leaderboard_layers(
continue
layers[k] += "".join(c if c.isspace() else " " for c in text)

spaces = layers[None]
del layers[None]
return spaces, layers # type: ignore
spaces_str = layers.pop(None)
return spaces_str, cast(Dict[str, Any], layers)


def render_leaderboard_to_image(leaderboard: Leaderboard) -> bytes:
spaces, layers = _isolate_leaderboard_layers(leaderboard)

# NOTE: font choice should support as wide a range of glyphs as possible,
# since discord display names are arbitrary and pillow does not support
# fallback fonts.
# font must also be monospace, in order for the colour layers to be aligned.
font = PIL.ImageFont.truetype("./uqcsbot/static/NotoSansMono-Regular.ttf", 20)

img = PIL.Image.new("RGB", (1, 1))
Expand All @@ -457,11 +462,12 @@ def render_leaderboard_to_image(leaderboard: Leaderboard) -> bytes:
return buf.getvalue() # XXX: why do we need to getvalue()?


def print_leaderboard(
def build_leaderboard(
columns: List[LeaderboardColumn], members: List[Member], day: Optional[Day]
):
"""
Returns a string of the leaderboard of the given format.
Returns a leaderboard made up of fragments, with the given column configuration
and member rows.
"""
header = "".join(column.title[0] for column in columns)
header += "\n"
Expand Down

0 comments on commit 344c0f6

Please sign in to comment.