diff --git a/ballsdex/core/bot.py b/ballsdex/core/bot.py index a259de68..6a97e8bf 100755 --- a/ballsdex/core/bot.py +++ b/ballsdex/core/bot.py @@ -48,6 +48,9 @@ def owner_check(ctx: commands.Context[BallsDexBot]): + """ + Checks who the owner of the bot is. + """ return ctx.bot.is_owner(ctx.author) @@ -55,6 +58,9 @@ class Translator(app_commands.Translator): async def translate( self, string: locale_str, locale: Locale, context: TranslationContextTypes ) -> str | None: + """ + Translate the given string to the specified locale. + """ return string.message.replace("countryball", settings.collectible_name).replace( "BallsDex", settings.bot_name ) @@ -175,6 +181,9 @@ def __init__( self.owner_ids: set async def start_prometheus_server(self): + """ + Start the Prometheus server for metrics. + """ self.prometheus_server = PrometheusServer( self, settings.prometheus_host, settings.prometheus_port ) @@ -183,6 +192,9 @@ async def start_prometheus_server(self): def assign_ids_to_app_groups( self, group: app_commands.Group, synced_commands: list[app_commands.AppCommandGroup] ): + """ + Assign the IDs to the app commands in the group. + """ for synced_command in synced_commands: bot_command = group.get_command(synced_command.name) if not bot_command: @@ -194,6 +206,9 @@ def assign_ids_to_app_groups( ) def assign_ids_to_app_commands(self, synced_commands: list[app_commands.AppCommand]): + """ + Assign the IDs to the app commands. + """ for synced_command in synced_commands: bot_command = self.tree.get_command(synced_command.name, type=synced_command.type) if not bot_command: @@ -205,6 +220,9 @@ def assign_ids_to_app_commands(self, synced_commands: list[app_commands.AppComma ) async def load_cache(self): + """ + Load the cache with database models and format a summary table. + """ table = Table(box=box.SIMPLE) table.add_column("Model", style="cyan") table.add_column("Count", justify="right", style="green") @@ -244,7 +262,9 @@ async def load_cache(self): console.print(table) async def gateway_healthy(self) -> bool: - """Check whether or not the gateway proxy is ready and healthy.""" + """ + Check whether or not the gateway proxy is ready and healthy. + """ if settings.gateway_url is None: raise RuntimeError("This is only available on the production bot instance.") @@ -261,6 +281,9 @@ async def gateway_healthy(self) -> bool: return False async def setup_hook(self) -> None: + """ + Setup the bot before starting up. + """ await self.tree.set_translator(Translator()) log.info("Starting up with %s shards...", self.shard_count) if settings.gateway_url is None: @@ -361,6 +384,9 @@ async def on_ready(self): ) async def blacklist_check(self, interaction: discord.Interaction) -> bool: + """ + Check if the user or guild is blacklisted from the bot. + """ if interaction.user.id in self.blacklist: if interaction.type != discord.InteractionType.autocomplete: await interaction.response.send_message( @@ -391,6 +417,9 @@ async def blacklist_check(self, interaction: discord.Interaction) -> bool: async def on_command_error( self, context: commands.Context, exception: commands.errors.CommandError ): + """ + Handle command errors, and give feedback to the user. + """ if isinstance(exception, (commands.CommandNotFound, commands.DisabledCommand)): return @@ -433,6 +462,10 @@ async def on_command_error( async def on_application_command_error( self, interaction: discord.Interaction, error: app_commands.AppCommandError ): + """ + Handle errors in application commands and give feedback to the user. + """ + async def send(content: str): if interaction.response.is_done(): await interaction.followup.send(content, ephemeral=True) diff --git a/ballsdex/core/commands.py b/ballsdex/core/commands.py index 17829515..bb9adff3 100755 --- a/ballsdex/core/commands.py +++ b/ballsdex/core/commands.py @@ -30,12 +30,15 @@ async def ping(self, ctx: commands.Context): @commands.is_owner() async def reloadtree(self, ctx: commands.Context): """ - Sync the application commands with Discord + Sync the application commands with Discord. """ await self.bot.tree.sync() await ctx.send("Application commands tree reloaded.") async def reload_package(self, package: str, *, with_prefix=False): + """ + Reload a package by name. + """ try: try: await self.bot.reload_extension(package) @@ -50,7 +53,7 @@ async def reload_package(self, package: str, *, with_prefix=False): @commands.is_owner() async def reload(self, ctx: commands.Context, package: str): """ - Reload an extension + Reload an extension. """ try: await self.reload_package(package) diff --git a/ballsdex/core/dev.py b/ballsdex/core/dev.py index 17db0929..a52cc7ea 100755 --- a/ballsdex/core/dev.py +++ b/ballsdex/core/dev.py @@ -53,12 +53,18 @@ def box(text: str, lang: str = "") -> str: + """ + Formats a given text string inside a code block for Discord. + """ return f"```{lang}\n{text}\n```" def text_to_file( text: str, filename: str = "file.txt", *, spoiler: bool = False, encoding: str = "utf-8" ) -> discord.File: + """ + Converts a string of text into a Discord file object for attachment. + """ file = BytesIO(text.encode(encoding)) return discord.File(file, filename, spoiler=spoiler) @@ -103,6 +109,9 @@ async def send_interactive( result = 0 def predicate(m: discord.Message): + """ + Evaluates whether a given Discord message satisfies specific criteria. + """ nonlocal result if (ctx.author.id != m.author.id) or ctx.channel.id != m.channel.id: return False @@ -163,7 +172,9 @@ def predicate(m: discord.Message): class Dev(commands.Cog): - """Various development focused utilities.""" + """ + Various development focused utilities. + """ def __init__(self): super().__init__() @@ -173,10 +184,17 @@ def __init__(self): @staticmethod def async_compile(source, filename, mode): + """ + Compiles the given source code into a code object that supports `await` expressions. + """ return compile(source, filename, mode, flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT, optimize=0) @staticmethod async def maybe_await(coro): + """ + Resolves a coroutine or value, returning the final result. If the input is a coroutine, + it is awaited up to two times. + """ for i in range(2): if inspect.isawaitable(coro): coro = await coro @@ -186,7 +204,9 @@ async def maybe_await(coro): @staticmethod def cleanup_code(content): - """Automatically removes code blocks from the code.""" + """ + Automatically removes code blocks from the code. + """ # remove ```py\n``` if content.startswith("```") and content.endswith("```"): return START_CODE_BLOCK_RE.sub("", content)[:-3] @@ -196,7 +216,8 @@ def cleanup_code(content): @classmethod def get_syntax_error(cls, e): - """Format a syntax error to send to the user. + """ + Format a syntax error to send to the user. Returns a string representation of the error formatted as a codeblock. """ @@ -208,16 +229,24 @@ def get_syntax_error(cls, e): @staticmethod def get_pages(msg: str): - """Pagify the given message for output to the user.""" + """ + Pagify the given message for output to the user. + """ return pagify(msg, delims=["\n", " "], priority=True, shorten_by=10) @staticmethod def sanitize_output(ctx: commands.Context, input_: str) -> str: - """Hides the bot's token from a string.""" + """ + Hides the bot's token from a string. + """ token = ctx.bot.http.token return re.sub(re.escape(token), "[EXPUNGED]", input_, re.I) def get_environment(self, ctx: commands.Context) -> dict: + """ + Constructs an execution environment for dynamic code evaluation. The environment includes + references to various bot-related objects, models, policies, and utility functions. + """ env = { "bot": ctx.bot, "ctx": ctx, @@ -263,7 +292,8 @@ def get_environment(self, ctx: commands.Context) -> dict: @commands.command() @commands.is_owner() async def debug(self, ctx: commands.Context, *, code): - """Evaluate a statement of python code. + """ + Evaluate a statement of python code. The bot will always respond with the return value of the code. If the return value of the code is a coroutine, it will be awaited, @@ -305,7 +335,8 @@ async def debug(self, ctx: commands.Context, *, code): @commands.command(name="eval") @commands.is_owner() async def _eval(self, ctx: commands.Context, *, body: str): - """Execute asynchronous code. + """ + Execute asynchronous code. This command wraps code into the body of an async function and then calls and awaits it. The bot will respond with anything printed to @@ -359,7 +390,8 @@ async def _eval(self, ctx: commands.Context, *, body: str): @commands.command() @commands.is_owner() async def mock(self, ctx: commands.Context, user: discord.Member, *, command): - """Mock another user invoking a command. + """ + Mock another user invoking a command. The prefix must not be entered. """ @@ -372,7 +404,8 @@ async def mock(self, ctx: commands.Context, user: discord.Member, *, command): @commands.command(name="mockmsg") @commands.is_owner() async def mock_msg(self, ctx: commands.Context, user: discord.Member, *, content: str): - """Dispatch a message event as if it were sent by a different user. + """ + Dispatch a message event as if it were sent by a different user. Only reads the raw content of the message. Attachments, embeds etc. are ignored. diff --git a/ballsdex/core/image_generator/image_gen.py b/ballsdex/core/image_generator/image_gen.py index 514ed062..134203da 100755 --- a/ballsdex/core/image_generator/image_gen.py +++ b/ballsdex/core/image_generator/image_gen.py @@ -27,6 +27,9 @@ def draw_card(ball_instance: "BallInstance", media_path: str = "./admin_panel/media/"): + """ + Draw the card for a countryball instance. + """ ball = ball_instance.countryball ball_health = (237, 115, 101, 255) ball_credits = ball.credits diff --git a/ballsdex/core/metrics.py b/ballsdex/core/metrics.py index 2f8d96a6..1ab185e5 100644 --- a/ballsdex/core/metrics.py +++ b/ballsdex/core/metrics.py @@ -65,6 +65,11 @@ def __init__(self, bot: "BallsDexBot", host: str = "localhost", port: int = 1526 ) async def collect_metrics(self): + """ + This function collects various metrics about the bot's performance + and activity for Prometheus monitoring. It categorizes the guilds the bot is in based + on their member count, grouping them by powers of ten. + """ guilds: dict[int, int] = defaultdict(int) for guild in self.bot.guilds: if not guild.member_count: @@ -82,6 +87,9 @@ async def collect_metrics(self): self.asyncio_delay.observe((t2 - t1).total_seconds() - 1) async def get(self, request: web.Request) -> web.Response: + """ + This function handles incoming HTTP GET requests to the Prometheus metrics endpoint. + """ log.debug("Request received") await self.collect_metrics() response = web.Response(body=generate_latest()) @@ -89,17 +97,27 @@ async def get(self, request: web.Request) -> web.Response: return response async def setup(self): + """ + This function initializes and configures the Prometheus server. + """ self.runner = web.AppRunner(self.app) await self.runner.setup() self.site = web.TCPSite(self.runner, host=self.host, port=self.port) self._inited = True async def run(self): + """ + This function starts the Prometheus server after setting it up. It ensures the server is + non-blocking, allowing other operations to continue while the server runs. + """ await self.setup() await self.site.start() # this call isn't blocking log.info(f"Prometheus server started on http://{self.site._host}:{self.site._port}/") async def stop(self): + """ + This function stops the Prometheus server gracefully. + """ if self._inited: await self.site.stop() await self.runner.cleanup() diff --git a/ballsdex/logging.py b/ballsdex/logging.py index edffe4db..daf290bd 100644 --- a/ballsdex/logging.py +++ b/ballsdex/logging.py @@ -8,6 +8,10 @@ def init_logger(disable_rich: bool = False, debug: bool = False) -> logging.handlers.QueueListener: + """ + Initializes and configures the logging system for the application. This includes setting up + handlers for streaming logs to the console and saving them to a file. + """ formatter = logging.Formatter( "[{asctime}] {levelname} {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{" ) diff --git a/ballsdex/packages/admin/balls.py b/ballsdex/packages/admin/balls.py index c4777d65..92969105 100644 --- a/ballsdex/packages/admin/balls.py +++ b/ballsdex/packages/admin/balls.py @@ -27,6 +27,11 @@ async def save_file(attachment: discord.Attachment) -> Path: + """ + Saves an uploaded Discord attachment to the server's file system. + Ensures that the file does not overwrite any existing files by appending + a numeric suffix to the filename if necessary. + """ path = Path(f"./static/uploads/{attachment.filename}") match = FILENAME_RE.match(attachment.filename) if not match: diff --git a/ballsdex/packages/admin/menu.py b/ballsdex/packages/admin/menu.py index bc80c403..2017d776 100644 --- a/ballsdex/packages/admin/menu.py +++ b/ballsdex/packages/admin/menu.py @@ -19,6 +19,9 @@ def __init__(self, entries: Iterable[BlacklistHistory], user_id: int, bot: "Ball super().__init__(entries, per_page=1) async def format_page(self, menu: Pages, blacklist: BlacklistHistory) -> discord.Embed: + """ + Formats an embed page to display blacklist history for a user. + """ embed = discord.Embed( title=f"Blacklist History for {self.header}", description=f"Type: {blacklist.action_type}\nReason: {blacklist.reason}", diff --git a/ballsdex/packages/balls/cog.py b/ballsdex/packages/balls/cog.py index 1d8dd1c9..909a471c 100644 --- a/ballsdex/packages/balls/cog.py +++ b/ballsdex/packages/balls/cog.py @@ -30,6 +30,10 @@ class DonationRequest(View): + """ + Formats the view for donation requests. + """ + def __init__( self, bot: "BallsDexBot", @@ -44,6 +48,9 @@ def __init__( self.new_player = new_player async def interaction_check(self, interaction: discord.Interaction, /) -> bool: + """ + Validates whether the user interacting with a menu is authorized to do so. + """ if interaction.user.id != self.new_player.discord_id: await interaction.response.send_message( "You are not allowed to interact with this menu.", ephemeral=True @@ -52,6 +59,9 @@ async def interaction_check(self, interaction: discord.Interaction, /) -> bool: return True async def on_timeout(self): + """ + Handles the event when the menu times out due to inactivity. + """ for item in self.children: item.disabled = True # type: ignore try: @@ -66,6 +76,9 @@ async def on_timeout(self): style=discord.ButtonStyle.success, emoji="\N{HEAVY CHECK MARK}\N{VARIATION SELECTOR-16}" ) async def accept(self, interaction: discord.Interaction, button: Button): + """ + Handles the acceptance of a donation. + """ self.stop() for item in self.children: item.disabled = True # type: ignore @@ -89,6 +102,9 @@ async def accept(self, interaction: discord.Interaction, button: Button): emoji="\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}", ) async def deny(self, interaction: discord.Interaction, button: Button): + """ + Handles the rejection of a donation. + """ self.stop() for item in self.children: item.disabled = True # type: ignore diff --git a/ballsdex/packages/balls/countryballs_paginator.py b/ballsdex/packages/balls/countryballs_paginator.py index 504fc0c0..18d67bcd 100644 --- a/ballsdex/packages/balls/countryballs_paginator.py +++ b/ballsdex/packages/balls/countryballs_paginator.py @@ -13,6 +13,13 @@ class CountryballsSource(menus.ListPageSource): + """ + A data source for paginating a list of BallInstance objects. + + This class provides logic for formatting and managing a paginated view + of countryballs for display in Discord embeds. + """ + def __init__(self, entries: List[BallInstance]): super().__init__(entries, per_page=25) @@ -22,6 +29,13 @@ async def format_page(self, menu: CountryballsSelector, balls: List[BallInstance class CountryballsSelector(Pages): + """ + A pagination menu for displaying and selecting countryballs. + + This class uses the `Pages` paginator and integrates a dropdown menu + for users to select a countryball. + """ + def __init__(self, interaction: discord.Interaction["BallsDexBot"], balls: List[BallInstance]): self.bot = interaction.client source = CountryballsSource(balls) @@ -29,6 +43,9 @@ def __init__(self, interaction: discord.Interaction["BallsDexBot"], balls: List[ self.add_item(self.select_ball_menu) def set_options(self, balls: List[BallInstance]): + """ + Formats a page of countryballs and signals the selector to update. + """ options: List[discord.SelectOption] = [] for ball in balls: emoji = self.bot.get_emoji(int(ball.countryball.emoji_id)) @@ -50,6 +67,9 @@ def set_options(self, balls: List[BallInstance]): @discord.ui.select() async def select_ball_menu(self, interaction: discord.Interaction, item: discord.ui.Select): + """ + Handles the selection of a countryball from the dropdown menu. + """ await interaction.response.defer(thinking=True) ball_instance = await BallInstance.get( id=int(interaction.data.get("values")[0]) # type: ignore @@ -57,11 +77,24 @@ async def select_ball_menu(self, interaction: discord.Interaction, item: discord await self.ball_selected(interaction, ball_instance) async def ball_selected(self, interaction: discord.Interaction, ball_instance: BallInstance): + """ + A placeholder method for handling selected countryballs. + """ raise NotImplementedError() class CountryballsViewer(CountryballsSelector): + """ + A specialized version of CountryballsSelector for viewing countryballs. + + Overrides the `ball_selected` method to handle displaying information + about a selected countryball. + """ + async def ball_selected(self, interaction: discord.Interaction, ball_instance: BallInstance): + """ + Handles the display of a selected countryball. + """ content, file = await ball_instance.prepare_for_message(interaction) await interaction.followup.send(content=content, file=file) file.close() diff --git a/ballsdex/packages/config/components.py b/ballsdex/packages/config/components.py index 4a5212eb..9c2dc6b1 100644 --- a/ballsdex/packages/config/components.py +++ b/ballsdex/packages/config/components.py @@ -40,6 +40,9 @@ def __init__( ) async def interaction_check(self, interaction: discord.Interaction) -> bool: + """ + Validates whether the user interacting with a menu is authorized to do so. + """ if interaction.user.id != self.new_player.id: await interaction.response.send_message( "You are not allowed to interact with this menu.", ephemeral=True @@ -53,6 +56,9 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool: emoji="\N{HEAVY CHECK MARK}\N{VARIATION SELECTOR-16}", ) async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button): + """ + Handles the acceptance of the configuration embed. + """ config, created = await GuildConfig.get_or_create(guild_id=interaction.guild_id) config.spawn_channel = self.channel.id # type: ignore config.enabled = True @@ -74,6 +80,9 @@ async def accept_button(self, interaction: discord.Interaction, button: discord. ) async def on_timeout(self) -> None: + """ + Handles the event when the menu times out due to inactivity. + """ if self.message: for item in self.children: if isinstance(item, discord.ui.Button): diff --git a/ballsdex/packages/countryballs/ab_spawn.py b/ballsdex/packages/countryballs/ab_spawn.py index dbcf7a79..7dfe9332 100644 --- a/ballsdex/packages/countryballs/ab_spawn.py +++ b/ballsdex/packages/countryballs/ab_spawn.py @@ -49,6 +49,10 @@ def get_manager(self, guild: "discord.Guild") -> BaseSpawnManager: return self.manager_b async def handle_message(self, message: "discord.Message") -> bool | tuple[Literal[True], str]: + """ + Handles an incoming message, processes it through the manager, + and returns the result along with a message. + """ assert message.guild manager = self.get_manager(message.guild) result = await manager.handle_message(message) @@ -64,6 +68,10 @@ async def handle_message(self, message: "discord.Message") -> bool | tuple[Liter async def admin_explain( self, interaction: "discord.Interaction[BallsDexBot]", guild: "discord.Guild" ): + """ + Explains the server's manager assignment and sends a follow-up message + to the admin with the relevant information. + """ manager = self.get_manager(guild) await manager.admin_explain(interaction, guild) if manager == self.manager_a: diff --git a/ballsdex/packages/countryballs/cog.py b/ballsdex/packages/countryballs/cog.py index d0a60406..6d3d3be7 100644 --- a/ballsdex/packages/countryballs/cog.py +++ b/ballsdex/packages/countryballs/cog.py @@ -32,6 +32,10 @@ def __init__(self, bot: "BallsDexBot"): self.spawn_manager = spawn_manager(bot) async def load_cache(self): + """ + Loads the cache with guild spawn channel data for all enabled guilds + with a non-null spawn channel. + """ i = 0 async for config in GuildConfig.filter(enabled=True, spawn_channel__isnull=False).only( "guild_id", "spawn_channel" diff --git a/ballsdex/packages/countryballs/components.py b/ballsdex/packages/countryballs/components.py index af7323c0..40b7da20 100755 --- a/ballsdex/packages/countryballs/components.py +++ b/ballsdex/packages/countryballs/components.py @@ -35,6 +35,9 @@ def __init__(self, ball: "CountryBall", button: Button): self.button = button async def on_error(self, interaction: discord.Interaction, error: Exception, /) -> None: + """ + Gracefully handle the case where an error occurs. + """ log.exception("An error occured in countryball catching prompt", exc_info=error) if interaction.response.is_done(): await interaction.followup.send( @@ -46,6 +49,9 @@ async def on_error(self, interaction: discord.Interaction, error: Exception, /) ) async def on_submit(self, interaction: discord.Interaction["BallsDexBot"]): + """ + Handle the submitted guesses of users. + """ # TODO: use lock await interaction.response.defer(thinking=True) @@ -97,6 +103,9 @@ async def on_submit(self, interaction: discord.Interaction["BallsDexBot"]): async def catch_ball( self, bot: "BallsDexBot", user: discord.Member ) -> tuple[BallInstance, bool]: + """ + This functions gives the countryball to the fastest user to catch. + """ player, created = await Player.get_or_create(discord_id=user.id) # stat may vary by +/- 20% of base stat @@ -167,6 +176,9 @@ async def interaction_check(self, interaction: discord.Interaction["BallsDexBot" return await interaction.client.blacklist_check(interaction) async def on_timeout(self): + """ + Handle the timeout of the view. + """ self.catch_button.disabled = True if self.ball.message: try: @@ -176,6 +188,9 @@ async def on_timeout(self): @button(style=discord.ButtonStyle.primary, label="Catch me!") async def catch_button(self, interaction: discord.Interaction["BallsDexBot"], button: Button): + """ + Format the 'Catch me!' button and handle the catching of the countryball. + """ if self.ball.catched: await interaction.response.send_message("I was caught already!", ephemeral=True) else: diff --git a/ballsdex/packages/countryballs/countryball.py b/ballsdex/packages/countryballs/countryball.py index a974f745..f2edd1e4 100755 --- a/ballsdex/packages/countryballs/countryball.py +++ b/ballsdex/packages/countryballs/countryball.py @@ -13,6 +13,12 @@ class CountryBall: + """ + Represents a collectible entity associated with a specific country. Each instance corresponds + to a unique ball model, containing attributes such as its name, associated bonuses, and + the time of its creation. + """ + def __init__(self, model: Ball): self.name = model.country self.model = model @@ -26,6 +32,9 @@ def __init__(self, model: Ball): @classmethod async def get_random(cls): + """ + Selects and returns a random enabled ball model, based on their rarity distribution. + """ countryballs = list(filter(lambda m: m.enabled, balls.values())) if not countryballs: raise RuntimeError("No ball to spawn") diff --git a/ballsdex/packages/countryballs/spawn.py b/ballsdex/packages/countryballs/spawn.py index 712e53bc..4099ee35 100755 --- a/ballsdex/packages/countryballs/spawn.py +++ b/ballsdex/packages/countryballs/spawn.py @@ -107,6 +107,9 @@ class SpawnCooldown: message_cache: deque[CachedMessage] = field(default_factory=lambda: deque(maxlen=100)) def reset(self, time: datetime): + """ + Resets the manager's state to default values. + """ self.scaled_message_count = 1.0 self.threshold = random.randint(*SPAWN_CHANCE_RANGE) try: @@ -116,6 +119,10 @@ def reset(self, time: datetime): self.time = time async def increase(self, message: discord.Message) -> bool: + """ + Increases the message count based on various conditions, affecting the + spawn manager's behavior. + """ # this is a deque, not a list # its property is that, once the max length is reached (100 for us), # the oldest element is removed, thus we only have the last 100 messages in memory @@ -144,11 +151,19 @@ async def increase(self, message: discord.Message) -> bool: class SpawnManager(BaseSpawnManager): + """ + The default spawn manager for countryballs. + """ + def __init__(self, bot: "BallsDexBot"): super().__init__(bot) self.cooldowns: dict[int, SpawnCooldown] = {} async def handle_message(self, message: discord.Message) -> bool: + """ + Handles a message by checking if spawn conditions are met and whether + a spawn should occur. + """ guild = message.guild if not guild: return False @@ -191,6 +206,10 @@ async def handle_message(self, message: discord.Message) -> bool: async def admin_explain( self, interaction: discord.Interaction["BallsDexBot"], guild: discord.Guild ): + """ + Provides an explanation to the admin regarding spawn conditions and + the current state of the spawn manager for a guild. + """ cooldown = self.cooldowns.get(guild.id) if not cooldown: await interaction.response.send_message( diff --git a/ballsdex/packages/info/cog.py b/ballsdex/packages/info/cog.py index bc8c0b6f..b5589af7 100644 --- a/ballsdex/packages/info/cog.py +++ b/ballsdex/packages/info/cog.py @@ -47,6 +47,9 @@ def __init__(self, bot: "BallsDexBot"): self.bot = bot async def _get_10_balls_emojis(self) -> list[discord.Emoji]: + """ + Get 10 random enabled countryball emojis. + """ balls: list[Ball] = random.choices( [x for x in countryballs.values() if x.enabled], k=min(10, len(countryballs)) ) diff --git a/ballsdex/packages/trade/display.py b/ballsdex/packages/trade/display.py index 0c218f9a..e673e404 100644 --- a/ballsdex/packages/trade/display.py +++ b/ballsdex/packages/trade/display.py @@ -12,6 +12,10 @@ class TradeViewFormat(menus.ListPageSource): + """ + Formats the trade history for a user. + """ + def __init__( self, entries: Iterable[TradeModel], @@ -47,6 +51,9 @@ async def format_page(self, menu: Pages, trade: TradeModel) -> discord.Embed: def _get_prefix_emote(trader: TradingUser) -> str: + """ + Determine the appropriate emoji prefix for a trader based on their status. + """ if trader.cancelled: return "\N{NO ENTRY SIGN}" elif trader.accepted: @@ -68,6 +75,13 @@ def _get_trader_name(trader: TradingUser, is_admin: bool = False) -> str: def _build_list_of_strings( trader: TradingUser, bot: "BallsDexBot", short: bool = False ) -> list[str]: + """ + Build a list of strings representing a trader's proposal, ensuring the total length + does not exceed 1024 characters per field for Discord embeds. + + This function avoids breaking a line in the middle of a description. The output is + split across multiple lines if necessary, ensuring no line exceeds the character limit. + """ # this builds a list of strings always lower than 1024 characters # while not cutting in the middle of a line proposal: list[str] = [""] diff --git a/ballsdex/packages/trade/menu.py b/ballsdex/packages/trade/menu.py index 23045300..fdd8e532 100644 --- a/ballsdex/packages/trade/menu.py +++ b/ballsdex/packages/trade/menu.py @@ -30,6 +30,10 @@ class InvalidTradeOperation(Exception): class TradeView(View): + """ + Handles the buttons appearing below the trade embed. + """ + def __init__(self, trade: TradeMenu): super().__init__(timeout=60 * 30) self.trade = trade @@ -47,6 +51,9 @@ async def interaction_check(self, interaction: discord.Interaction, /) -> bool: @button(label="Lock proposal", emoji="\N{LOCK}", style=discord.ButtonStyle.primary) async def lock(self, interaction: discord.Interaction, button: Button): + """ + Format and handle the 'Lock proposal' button. + """ trader = self.trade._get_trader(interaction.user) if trader.locked: await interaction.response.send_message( @@ -69,6 +76,9 @@ async def lock(self, interaction: discord.Interaction, button: Button): @button(label="Reset", emoji="\N{DASH SYMBOL}", style=discord.ButtonStyle.secondary) async def clear(self, interaction: discord.Interaction, button: Button): + """ + Format and handle the 'Reset' button. + """ trader = self.trade._get_trader(interaction.user) if trader.locked: await interaction.response.send_message( @@ -88,11 +98,18 @@ async def clear(self, interaction: discord.Interaction, button: Button): style=discord.ButtonStyle.danger, ) async def cancel(self, interaction: discord.Interaction, button: Button): + """ + Format and handle the 'Cancel trade' button. + """ await self.trade.user_cancel(self.trade._get_trader(interaction.user)) await interaction.response.send_message("Trade has been cancelled.", ephemeral=True) class ConfirmView(View): + """ + Handles the buttons below the trade embed on the last phase of a trade. + """ + def __init__(self, trade: TradeMenu): super().__init__(timeout=90) self.trade = trade @@ -113,6 +130,9 @@ async def interaction_check(self, interaction: discord.Interaction, /) -> bool: style=discord.ButtonStyle.success, emoji="\N{HEAVY CHECK MARK}\N{VARIATION SELECTOR-16}" ) async def accept_button(self, interaction: discord.Interaction, button: Button): + """ + Handle the 'Accept' button. + """ trader = self.trade._get_trader(interaction.user) if self.trade.cooldown_start_time is None: return @@ -151,6 +171,9 @@ async def accept_button(self, interaction: discord.Interaction, button: Button): emoji="\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}", ) async def deny_button(self, interaction: discord.Interaction, button: Button): + """ + Handle the 'Deny' button. + """ await self.trade.user_cancel(self.trade._get_trader(interaction.user)) await interaction.response.send_message("Trade has been cancelled.", ephemeral=True) @@ -182,6 +205,9 @@ def _get_trader(self, user: discord.User | discord.Member) -> TradingUser: raise RuntimeError(f"User with ID {user.id} cannot be found in the trade") def _generate_embed(self): + """ + Generate the trade embed with the initial content. + """ add_command = self.cog.add.extras.get("mention", "`/trade add`") remove_command = self.cog.remove.extras.get("mention", "`/trade remove`") view_command = self.cog.view.extras.get("mention", "`/trade view`") @@ -293,6 +319,9 @@ async def user_cancel(self, trader: TradingUser): await self.cancel() async def perform_trade(self): + """ + Perform the trade if possible, else handle the errors. + """ valid_transferable_countryballs: list[BallInstance] = [] trade = await Trade.create(player1=self.trader1.player, player2=self.trader2.player)