Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/bot/constants/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class BotUtils:
)
MESSAGE_REMOVED_FOR_PRIVACY = "Your message was removed for privacy."
DELETE_MESSAGE_NO_PERMISSION = "Bot does not have permission to delete messages."
SEND_MESSAGE_FAILED = "An error occurred while sending the response. Please try again later."


class DiceRolls:
Expand Down Expand Up @@ -338,6 +339,7 @@ class Owner:
DISABLED_DM = BotUtils.DISABLED_DM
MESSAGE_REMOVED_FOR_PRIVACY = BotUtils.MESSAGE_REMOVED_FOR_PRIVACY
DELETE_MESSAGE_NO_PERMISSION = BotUtils.DELETE_MESSAGE_NO_PERMISSION
SEND_MESSAGE_FAILED = BotUtils.SEND_MESSAGE_FAILED

# Dice Rolls
DICE_SIZE_NOT_VALID = DiceRolls.SIZE_NOT_VALID
Expand Down
89 changes: 88 additions & 1 deletion src/bot/tools/bot_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,98 @@ async def send_embed(ctx, embed, dm=False):
))
except (discord.Forbidden, discord.HTTPException):
pass # Can't send to channel either, nothing we can do
# If channel send failed, the error is already logged above
else:
# Channel send failed — notify the user with a simple embed
try:
await ctx.send(embed=discord.Embed(
description=chat_formatting.error(messages.SEND_MESSAGE_FAILED),
color=discord.Color.red(),
))
except (discord.Forbidden, discord.HTTPException):
pass # Can't send anything, nothing we can do
except Exception as e:
ctx.bot.log.error(f"Unexpected error sending message: {e}")


class EmbedPaginatorView(discord.ui.View):
"""Interactive pagination view for embed pages with Previous/Next buttons."""

def __init__(self, pages: list[discord.Embed], author_id: int):
super().__init__(timeout=None)
self.pages = pages
self.current_page = 0
self.author_id = author_id
self.message: discord.Message | None = None
self._update_buttons()

def _update_buttons(self):
self.previous_button.disabled = self.current_page == 0
self.page_indicator.label = f"{self.current_page + 1}/{len(self.pages)}"
self.next_button.disabled = self.current_page == len(self.pages) - 1

@discord.ui.button(label="\u25c0", style=discord.ButtonStyle.secondary)
async def previous_button(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user.id != self.author_id:
return await interaction.response.send_message(
"Only the command invoker can use these buttons.", ephemeral=True
)
self.current_page -= 1
self._update_buttons()
await interaction.response.edit_message(embed=self.pages[self.current_page], view=self)

@discord.ui.button(label="1/1", style=discord.ButtonStyle.secondary, disabled=True)
async def page_indicator(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.defer()

@discord.ui.button(label="\u25b6", style=discord.ButtonStyle.secondary)
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user.id != self.author_id:
return await interaction.response.send_message(
"Only the command invoker can use these buttons.", ephemeral=True
)
self.current_page += 1
self._update_buttons()
await interaction.response.edit_message(embed=self.pages[self.current_page], view=self)


async def send_paginated_embed(ctx, embed: discord.Embed, max_fields: int = 25) -> None:
"""Send an embed with pagination if it exceeds max_fields.

Splits the embed's fields across multiple pages with navigation buttons.
Non-field properties (color, author, description, thumbnail) are preserved on each page.
"""
if len(embed.fields) <= max_fields:
await send_embed(ctx, embed)
return

color = embed.color or ctx.bot.settings["bot"]["EmbedColor"]
total_fields = len(embed.fields)
pages = []

for i in range(0, total_fields, max_fields):
page_embed = discord.Embed(color=color, description=embed.description)
if embed.author:
page_embed.set_author(name=embed.author.name, icon_url=embed.author.icon_url)
if embed.thumbnail:
page_embed.set_thumbnail(url=embed.thumbnail.url)

for field in embed.fields[i : i + max_fields]:
page_embed.add_field(name=field.name, value=field.value, inline=field.inline)

page_number = (i // max_fields) + 1
total_pages = (total_fields + max_fields - 1) // max_fields
page_embed.set_footer(text=f"Page {page_number}/{total_pages}")
pages.append(page_embed)

if len(pages) == 1:
await send_embed(ctx, pages[0])
return

view = EmbedPaginatorView(pages, ctx.message.author.id)
msg = await ctx.send(embed=pages[0], view=view)
view.message = msg


async def delete_message(ctx, warning=False):
if not is_private_message(ctx):
color = None
Expand Down
143 changes: 81 additions & 62 deletions src/gw2/cogs/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,67 +102,66 @@ async def session(ctx):
# Game stopped but end data not saved yet — bot may still be updating
return await gw2_utils.send_msg(ctx, gw2_messages.SESSION_BOT_STILL_UPDATING)

await ctx.message.channel.typing()
color = ctx.bot.settings["gw2"]["EmbedColor"]
start_time = bot_utils.convert_str_to_datetime_short(rs_start["date"])
end_time = bot_utils.convert_str_to_datetime_short(rs_end["date"])

time_passed = gw2_utils.get_time_passed(start_time, end_time)
player_wait_minutes = 1
if time_passed.hours == 0 and time_passed.minutes < player_wait_minutes:
wait_time = str(player_wait_minutes - time_passed.minutes)
m = "minute" if wait_time == "1" else "minutes"
return await gw2_utils.send_msg(
ctx, f"{gw2_messages.SESSION_BOT_STILL_UPDATING}\n {gw2_messages.WAITING_TIME}: `{wait_time} {m}`"
async with ctx.message.channel.typing():
color = ctx.bot.settings["gw2"]["EmbedColor"]
start_time = bot_utils.convert_str_to_datetime_short(rs_start["date"])
end_time = bot_utils.convert_str_to_datetime_short(rs_end["date"])

time_passed = gw2_utils.get_time_passed(start_time, end_time)
player_wait_minutes = 1
if time_passed.hours == 0 and time_passed.minutes < player_wait_minutes:
wait_time = str(player_wait_minutes - time_passed.minutes)
m = "minute" if wait_time == "1" else "minutes"
return await gw2_utils.send_msg(
ctx, f"{gw2_messages.SESSION_BOT_STILL_UPDATING}\n {gw2_messages.WAITING_TIME}: `{wait_time} {m}`"
)

acc_name = rs_session[0]["acc_name"]
embed = discord.Embed(color=color)
embed.set_author(
name=f"{ctx.message.author.display_name}'s {gw2_messages.SESSION_TITLE} ({rs_start['date'].split()[0]})",
icon_url=ctx.message.author.display_avatar.url,
)
embed.add_field(name=gw2_messages.ACCOUNT_NAME, value=chat_formatting.inline(acc_name))
embed.add_field(name=gw2_messages.SERVER, value=chat_formatting.inline(gw2_server))

# Play time from API age (actual in-game time)
start_age = rs_start.get("age", 0)
end_age = rs_end.get("age", 0)
play_time_seconds = end_age - start_age
if play_time_seconds > 0:
play_time_str = gw2_utils.format_seconds_to_time(play_time_seconds)
else:
play_time_str = str(time_passed.timedelta)
embed.add_field(name=gw2_messages.PLAY_TIME, value=chat_formatting.inline(play_time_str))

# Gold (special formatting)
_add_gold_field(embed, rs_start, rs_end)

# Deaths
gw2_session_chars_dal = Gw2SessionCharsDal(ctx.bot.db_session, ctx.bot.log)
rs_chars_start = await gw2_session_chars_dal.get_all_start_characters(user_id)
if rs_chars_start:
rs_chars_end = await gw2_session_chars_dal.get_all_end_characters(user_id)
_add_deaths_field(embed, rs_chars_start, rs_chars_end)

# WvW achievement-based stats
_add_wvw_stats(embed, rs_start, rs_end)

# All wallet currencies (except gold, handled above)
_add_wallet_currency_fields(embed, rs_start, rs_end)

if (
not (isinstance(ctx.channel, discord.DMChannel))
and hasattr(ctx.message.author, "activity")
and ctx.message.author.activity is not None
and "guild wars 2" in str(ctx.message.author.activity.name).lower()
):
still_playing_msg = f"{ctx.message.author.mention}\n {gw2_messages.SESSION_USER_STILL_PLAYING}"
await gw2_utils.end_session(ctx.bot, ctx.message.author, api_key)
await ctx.send(still_playing_msg)

acc_name = rs_session[0]["acc_name"]
embed = discord.Embed(color=color)
embed.set_author(
name=f"{ctx.message.author.display_name}'s {gw2_messages.SESSION_TITLE} ({rs_start['date'].split()[0]})",
icon_url=ctx.message.author.display_avatar.url,
)
embed.add_field(name=gw2_messages.ACCOUNT_NAME, value=chat_formatting.inline(acc_name))
embed.add_field(name=gw2_messages.SERVER, value=chat_formatting.inline(gw2_server))

# Play time from API age (actual in-game time)
start_age = rs_start.get("age", 0)
end_age = rs_end.get("age", 0)
play_time_seconds = end_age - start_age
if play_time_seconds > 0:
play_time_str = gw2_utils.format_seconds_to_time(play_time_seconds)
else:
play_time_str = str(time_passed.timedelta)
embed.add_field(name=gw2_messages.PLAY_TIME, value=chat_formatting.inline(play_time_str))

# Gold (special formatting)
_add_gold_field(embed, rs_start, rs_end)

# Deaths
gw2_session_chars_dal = Gw2SessionCharsDal(ctx.bot.db_session, ctx.bot.log)
rs_chars_start = await gw2_session_chars_dal.get_all_start_characters(user_id)
if rs_chars_start:
rs_chars_end = await gw2_session_chars_dal.get_all_end_characters(user_id)
_add_deaths_field(embed, rs_chars_start, rs_chars_end)

# WvW achievement-based stats
_add_wvw_stats(embed, rs_start, rs_end)

# All wallet currencies (except gold, handled above)
_add_wallet_currency_fields(embed, rs_start, rs_end)

if (
not (isinstance(ctx.channel, discord.DMChannel))
and hasattr(ctx.message.author, "activity")
and ctx.message.author.activity is not None
and "guild wars 2" in str(ctx.message.author.activity.name).lower()
):
still_playing_msg = f"{ctx.message.author.mention}\n {gw2_messages.SESSION_USER_STILL_PLAYING}"
await ctx.message.channel.typing()
await gw2_utils.end_session(ctx.bot, ctx.message.author, api_key)
await ctx.send(still_playing_msg)

await bot_utils.send_embed(ctx, embed)
await bot_utils.send_paginated_embed(ctx, embed)
return None


Expand Down Expand Up @@ -232,7 +231,14 @@ def _add_wvw_stats(embed: discord.Embed, rs_start: dict, rs_end: dict) -> None:


def _add_wallet_currency_fields(embed: discord.Embed, rs_start: dict, rs_end: dict) -> None:
"""Add wallet currency fields to embed (all except gold, which has special formatting)."""
"""Add wallet currency fields to embed (all except gold, which has special formatting).

Currencies are grouped into "Gained Currencies" and "Lost Currencies" fields
to avoid exceeding Discord's 25-field embed limit.
"""
gained_lines = []
lost_lines = []

for stat_key, display_name in WALLET_DISPLAY_NAMES.items():
if stat_key == "gold":
continue
Expand All @@ -242,9 +248,22 @@ def _add_wallet_currency_fields(embed: discord.Embed, rs_start: dict, rs_end: di
if start_val != end_val:
diff = end_val - start_val
if diff > 0:
embed.add_field(name=f"Gained {display_name}", value=chat_formatting.inline(f"+{diff}"))
gained_lines.append(f"+{diff} {display_name}")
else:
embed.add_field(name=f"Lost {display_name}", value=chat_formatting.inline(str(diff)))
lost_lines.append(f"{diff} {display_name}")

if gained_lines:
embed.add_field(
name="Gained Currencies",
value=chat_formatting.inline("\n".join(gained_lines)),
inline=False,
)
if lost_lines:
embed.add_field(
name="Lost Currencies",
value=chat_formatting.inline("\n".join(lost_lines)),
inline=False,
)


async def setup(bot):
Expand Down
Loading