Skip to content
Open
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
230 changes: 188 additions & 42 deletions techsupport_bot/commands/burn.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import discord
from core import auxiliary, cogs
from discord.ext import commands
from discord import app_commands

if TYPE_CHECKING:
import bot
Expand All @@ -31,6 +31,9 @@ class Burn(cogs.BaseCog):

Attributes:
PHRASES (list[str]): The list of phrases to pick from

Args:
bot (bot.TechSupportBot): The bot instance
"""

PHRASES: list[str] = [
Expand All @@ -42,81 +45,224 @@ class Burn(cogs.BaseCog):
"Was that message a hot pan? BECAUSE IT BURNS!",
]

def __init__(self: Self, bot: bot.TechSupportBot) -> None:
super().__init__(bot=bot)
self.ctx_menu = app_commands.ContextMenu(
name="Declare Burn",
callback=self.burn_from_message,
extras={"module": "burn"},
)
if getattr(self.bot, "tree", None):
self.bot.tree.add_command(self.ctx_menu)

async def handle_burn(
self: Self,
ctx: commands.Context,
user: discord.Member,
interaction: discord.Interaction,
user: discord.Member | discord.User,
message: discord.Message,
) -> None:
"""The core logic to handle the burn command
"""The core logic to handle burn execution.

Args:
ctx (commands.Context): The context in which the command was run in
user (discord.Member): The user that was called in the burn command
interaction (discord.Interaction): The interaction that called this command
user (discord.Member | discord.User): The user that was called in the burn command
message (discord.Message): The message to react to.
Will be None if no message could be found
"""
if not message:
await auxiliary.send_deny_embed(
message="I could not a find a message to reply to", channel=ctx.channel
await interaction.followup.send(
embed=auxiliary.prepare_deny_embed(build_burn_not_found_message())
)
return

await auxiliary.add_list_of_reactions(
message=message, reactions=["🔥", "🚒", "👨‍🚒"]
message=message, reactions=build_burn_reactions()
)

phrase_pool = normalize_phrase_pool(self.PHRASES)
invalid_phrase_message = validate_phrase_pool(phrase_pool)
if invalid_phrase_message:
await interaction.followup.send(
embed=auxiliary.prepare_deny_embed(invalid_phrase_message)
)
return

phrase_index = choose_phrase_index(phrase_pool)
burn_description = build_burn_description(phrase_pool, phrase_index)

embed = auxiliary.generate_basic_embed(
title="Burn Alert!",
description=f"🔥🔥🔥 {random.choice(self.PHRASES)} 🔥🔥🔥",
description=burn_description,
color=discord.Color.red(),
)
await ctx.send(embed=embed, content=auxiliary.construct_mention_string([user]))
await interaction.followup.send(
embed=embed, content=auxiliary.construct_mention_string([user])
)

async def burn_command(
self: Self, ctx: commands.Context, user_to_match: discord.Member
self: Self,
interaction: discord.Interaction,
user_to_match: discord.Member | discord.User,
) -> None:
"""This the core logic of the burn command
This is a command and should be accessed via discord
"""The core logic of the slash burn command.

Args:
ctx (commands.Context): The context in which the command was run
user_to_match (discord.Member): The user in which to burn
interaction (discord.Interaction): The interaction in which the command was run
user_to_match (discord.Member | discord.User): The user in which to burn
"""
if ctx.message.reference is None:
prefix = await self.bot.get_prefix(ctx.message)
message = await auxiliary.search_channel_for_message(
channel=ctx.channel, prefix=prefix, member_to_match=user_to_match
)
else:
message = ctx.message.reference.resolved
prefix = self.bot.guild_configs[str(interaction.guild.id)].command_prefix

await self.handle_burn(ctx, user_to_match, message)
message = await auxiliary.search_channel_for_message(
channel=interaction.channel,
prefix=prefix,
member_to_match=user_to_match,
)

await self.handle_burn(interaction, user_to_match, message)

@auxiliary.with_typing
@commands.guild_only()
@commands.command(
brief="Declares a BURN!",
description="Declares mentioned user's message as a BURN!",
usage="@user",
@app_commands.command(
name="burn",
description="Declares a user's message as a burn",
extras={"module": "burn"},
)
async def burn(
self: Self, ctx: commands.Context, user_to_match: discord.Member = None
self: Self, interaction: discord.Interaction, user_to_match: discord.Member
) -> None:
"""The only purpose of this function is to accept input from discord
"""The slash command entry point for burn.

Args:
ctx (commands.Context): The context in which the command was run
interaction (discord.Interaction): The interaction that called this command
user_to_match (discord.Member): The user in which to burn
"""
if user_to_match is None:
if ctx.message.reference is None:
await auxiliary.send_deny_embed(
message="You need to mention someone to declare a burn.",
channel=ctx.channel,
)
return
await interaction.response.defer(ephemeral=False)
await self.burn_command(interaction, user_to_match)

async def burn_from_message(
self: Self, interaction: discord.Interaction, message: discord.Message
) -> None:
"""Context menu callback to declare a burn on a selected message.

user_to_match = ctx.message.reference.resolved.author
Args:
interaction (discord.Interaction): The interaction that called this command
message (discord.Message): The selected message for the context menu command
"""
await interaction.response.defer(ephemeral=False)

target_author_id = resolve_burn_target_for_context_menu(
message_author_id=getattr(message.author, "id", 0),
interaction_user_id=interaction.user.id,
)
if target_author_id == interaction.user.id:
target_user = interaction.user
else:
target_user = message.author

await self.handle_burn(interaction, target_user, message)


def build_burn_reactions() -> list[str]:
"""Builds the ordered list of burn reactions.

Returns:
list[str]: The emoji reactions to add to the target message
"""
reactions = ["🔥", "🚒"]
reactions.append("👨‍🚒")
return reactions


def normalize_phrase_pool(phrases: list[str]) -> list[str]:
"""Normalizes and deduplicates burn phrases.

Args:
phrases (list[str]): Raw phrases to normalize

Returns:
list[str]: A deduplicated list of clean phrases
"""
normalized_phrases = []
seen = set()
for phrase in phrases:
cleaned_phrase = phrase.strip()
if len(cleaned_phrase) == 0:
continue
if cleaned_phrase in seen:
continue
seen.add(cleaned_phrase)
normalized_phrases.append(cleaned_phrase)

return normalized_phrases


def validate_phrase_pool(phrases: list[str]) -> str | None:
"""Validates that there are usable phrases to render.

Args:
phrases (list[str]): The normalized phrase list

Returns:
str | None: A deny message if invalid, otherwise None
"""
if len(phrases) == 0:
return "There are no burn phrases configured"

return None


def choose_phrase_index(phrases: list[str]) -> int:
"""Chooses a random index from a phrase pool.

Args:
phrases (list[str]): The available phrase pool

Raises:
ValueError: Raised if an empty list is supplied

Returns:
int: The index of the chosen phrase
"""
if len(phrases) == 0:
raise ValueError("phrase list cannot be empty")

return random.randint(0, len(phrases) - 1)


def build_burn_description(phrases: list[str], chosen_index: int) -> str:
"""Builds the burn embed description from phrase data.

Args:
phrases (list[str]): The available phrase pool
chosen_index (int): The index selected by the phrase chooser

Returns:
str: A formatted burn phrase description
"""
phrase = phrases[chosen_index]
return f"🔥🔥🔥 {phrase} 🔥🔥🔥"


def build_burn_not_found_message() -> str:
"""Builds the deny message for missing target messages.

Returns:
str: A user-facing deny message
"""
return "I could not find a message to reply to"


def resolve_burn_target_for_context_menu(
message_author_id: int, interaction_user_id: int
) -> int:
"""Resolves the target user id for context menu burn calls.

Args:
message_author_id (int): The selected message author id
interaction_user_id (int): The user id of the command invoker

Returns:
int: The chosen target user id
"""
if message_author_id <= 0:
return interaction_user_id

await self.burn_command(ctx, user_to_match)
return message_author_id
Loading
Loading