diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index 97e608277..1f7764e59 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -10,6 +10,7 @@ import json import os import threading +from typing import Self import botlogging import discord @@ -28,7 +29,15 @@ class TechSupportBot(commands.Bot): - """The main bot object.""" + """Sets up a new TechSupportBot object. + This does NOT start the bot, the start function must be called for that + + Args: + intents (discord.Intents): The list of intents that + the bot needs to request from discord + allowed_mentions (discord.AllowedMentions): What the bot is, or is not, + allowed to mention + """ CONFIG_PATH: str = "./config.yml" EXTENSIONS_DIR_NAME: str = "commands" @@ -43,15 +52,6 @@ class TechSupportBot(commands.Bot): def __init__( self, intents: discord.Intents, allowed_mentions: discord.AllowedMentions ) -> None: - """Sets up a new TechSupportBot object. - This does NOT start the bot, the start function must be called for that - - Args: - intents (discord.Intents): The list of intents that - the bot needs to request from discord - allowed_mentions (discord.AllowedMentions): What the bot is, or is not, - allowed to mention - """ # Sets a few properires to None to avoid ValueErrors later on self.startup_time: datetime = None self.owner: discord.User = None @@ -120,7 +120,7 @@ async def start(self) -> None: asyncio.create_task(self.logger.run()) # Start the IRC bot in an asynchronous task - irc_config = getattr(self.file_config.api, "irc") + irc_config = self.file_config.api.irc if irc_config.enable_irc: await self.logger.send_log( message="Connecting to IRC...", level=LogLevel.DEBUG, console_only=True @@ -187,7 +187,7 @@ async def on_guild_join(self, guild: discord.Guild) -> None: """Configures a new guild upon joining. This registers a new guild config, and starts any loop jobs that are configured - parameters: + Args: guild (discord.Guild): the guild that was joined """ self.register_new_guild_config(str(guild.id)) @@ -237,7 +237,7 @@ async def log_DM(self, sent_from: str, source: str, content: str) -> None: async def on_message(self, message: discord.Message) -> None: """Logs DMs and ensure that commands are processed - parameters: + Args: message (discord.Message): the message object """ owner = await self.get_owner() @@ -282,7 +282,7 @@ async def register_new_guild_config(self, guild_id: str) -> bool: async def create_new_context_config(self, guild_id: str) -> munch.Munch: """Creates a new guild config for a given guild. - parameters: + Args: guild_id (str): The guild ID the config will be for. Only used for storing the config """ extensions_config = munch.DefaultMunch(None) @@ -438,9 +438,9 @@ def validate_bot_config_subsection(self, section: str, subsection: str) -> None: if value is None: error_key = key elif isinstance(value, dict): - for k, v in value.items(): - if v is None: - error_key = k + for dict_key, dict_value in value.items(): + if dict_value is None: + error_key = dict_key if error_key: raise ValueError( f"Config key {error_key} from {section}.{subsection} not supplied" @@ -573,7 +573,7 @@ async def get_postgres_ref(self) -> None: db_ref = gino.Gino() # Pull information from postgres out of the file config - config_child = getattr(self.file_config.database, "postgres") + config_child = self.file_config.database.postgres user = config_child.user password = config_child.password name = config_child.name @@ -624,8 +624,14 @@ async def load_extensions(self, graceful: bool = True) -> None: """Loads all extensions currently in the extensions directory. Args: - graceful (bool): True if extensions should gracefully fail to load + graceful (bool, optional): True if extensions should gracefully fail to load. + Defaults to True. + + Raises: + exception: If graceful is false, this will raise ANY + exception generated by loading extensions """ + self.logger.console.debug("Retrieving commands") for extension_name in await self.get_potential_extensions(): if extension_name in self.file_config.bot_config.disabled_extensions: @@ -686,6 +692,9 @@ async def register_file_extension( Args: extension_name (str): the name of the extension to register fp (io.BufferedIOBase): the file-like object to save to disk + + Raises: + NameError: Raised if no extension name is provided """ if not extension_name: raise NameError("Invalid extension name") @@ -755,7 +764,7 @@ async def get_prefix(self, message: discord.Message) -> str: """Gets the appropriate prefix for a command. This is called by discord.py and must be async - parameters: + Args: message (discord.Message): the message to check against """ guild_config = self.guild_configs[str(message.guild.id)] @@ -847,9 +856,16 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool: Args: interaction (discord.Interaction): The interaction that started the command + Raises: + AppCommandExtensionDisabled: Raised if the guild config hasn't enabled + the extension belonging to this command + AppCommandRateLimit: Raised if the command is enabled, + but the user is under rate limit restrictions + Returns: bool: True if the command should be run, false if it shouldn't be run """ + # Since we can't do it anywhere else, log slash command here await self.slash_command_log(interaction) @@ -926,16 +942,27 @@ async def slash_command_log(self, interaction: discord.Interaction) -> None: embed=embed, ) - async def can_run(self, ctx: commands.Context, *, call_once=False) -> bool: + async def can_run( + self: Self, ctx: commands.Context, *, call_once: bool = False + ) -> bool: """Wraps the default can_run check to: Evaluate bot admin permissions Add a rate limiter Check if extension is disabled - parameters: - ctx (commands.Context): the context associated with the command - call_once (bool): True if the check should be retrieved from the call_once attribute + Args: + ctx (commands.Context): The context associated with the command + call_once (bool, optional): True if the check should be retrieved from the + call_once attribute. Defaults to False. + + Raises: + ExtensionDisabled: Raised if the extension holding the command is disabled + CommandRateLimit: Raised if the user is under rate limit + + Returns: + bool: True if the user can run the command, False otherwise """ + await self.logger.send_log( message="Checking if prefix command can run", level=LogLevel.DEBUG, @@ -971,7 +998,7 @@ async def can_run(self, ctx: commands.Context, *, call_once=False) -> bool: async def start_irc(self) -> None: """Starts the IRC connection in a seperate thread""" - irc_config = getattr(self.file_config.api, "irc") + irc_config = self.file_config.api.irc main_loop = asyncio.get_running_loop() irc_bot = ircrelay.IRCBot( diff --git a/techsupport_bot/botlogging/common.py b/techsupport_bot/botlogging/common.py index 772246791..827c29e46 100644 --- a/techsupport_bot/botlogging/common.py +++ b/techsupport_bot/botlogging/common.py @@ -22,7 +22,7 @@ class LogContext: """A very simple class to store a few contextual items about the log This is used to determine if some guild settings means the log shouldn't be logged - parameters: + Args: guild (discord.Guild): The guild the log occured with. Optional channel (discord.abc.Messageble): The channel, DM, thread, or other messagable the log occured in diff --git a/techsupport_bot/botlogging/delayed.py b/techsupport_bot/botlogging/delayed.py index 81258dd50..0fe2c1edb 100644 --- a/techsupport_bot/botlogging/delayed.py +++ b/techsupport_bot/botlogging/delayed.py @@ -10,7 +10,7 @@ class DelayedLogger(logger.BotLogger): """Logging interface that queues log events to be sent over time. - parameters: + Args: bot (bot.TechSupportBot): the bot object name (str): the name of the logging channel send (bool): Whether or not to allow sending of logs to discord diff --git a/techsupport_bot/botlogging/logger.py b/techsupport_bot/botlogging/logger.py index 3823af97e..1c1b203c4 100644 --- a/techsupport_bot/botlogging/logger.py +++ b/techsupport_bot/botlogging/logger.py @@ -19,7 +19,7 @@ class BotLogger: """Logging interface for Discord bots. - parameters: + Args: bot (bot.TechSupportBot): the bot object name (str): the name of the logging channel send (bool): Whether or not to allow sending of logs to discord diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 8c42dce97..623a90db0 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -120,8 +120,8 @@ async def command_permission_check(interaction: discord.Interaction) -> bool: interaction (discord.Interaction): The interaction that was generated from the slash command Raises: - app_commands.AppCommandError: If there are no roles configured - app_commands.MissingAnyRole: If the executing user is missing the required roles + AppCommandError: If there are no roles configured + MissingAnyRole: If the executing user is missing the required roles Returns: bool: Will return true if the command is allowed to execute, false if it should not execute diff --git a/techsupport_bot/commands/bot.py b/techsupport_bot/commands/botinfo.py similarity index 97% rename from techsupport_bot/commands/bot.py rename to techsupport_bot/commands/botinfo.py index edf2e48dd..60b499112 100644 --- a/techsupport_bot/commands/bot.py +++ b/techsupport_bot/commands/botinfo.py @@ -42,7 +42,7 @@ async def get_bot_data(self, ctx) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the calling message """ embed = discord.Embed(title=self.bot.user.name, color=discord.Color.blurple()) @@ -65,7 +65,7 @@ async def get_bot_data(self, ctx) -> None: value=", ".join(f"{guild.name} ({guild.id})" for guild in self.bot.guilds), inline=True, ) - irc_config = getattr(self.bot.file_config.api, "irc") + irc_config = self.bot.file_config.api.irc if not irc_config.enable_irc: embed.add_field( name="IRC", diff --git a/techsupport_bot/commands/commandcontrol.py b/techsupport_bot/commands/commandcontrol.py index 02ec9c402..6cbb615db 100644 --- a/techsupport_bot/commands/commandcontrol.py +++ b/techsupport_bot/commands/commandcontrol.py @@ -58,7 +58,7 @@ async def enable_command(self, ctx, *, command_name: str) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message command_name (str): the name of the command """ @@ -91,7 +91,7 @@ async def disable_command(self, ctx, *, command_name: str) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message command_name (str): the name of the command """ diff --git a/techsupport_bot/commands/config.py b/techsupport_bot/commands/config.py index d6ebd9c4f..acaedcd38 100644 --- a/techsupport_bot/commands/config.py +++ b/techsupport_bot/commands/config.py @@ -38,7 +38,7 @@ async def config_command(self, ctx) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message """ @@ -58,7 +58,7 @@ async def patch_config(self, ctx: commands.Context) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message """ config = self.bot.guild_configs[str(ctx.guild.id)] @@ -129,7 +129,7 @@ async def enable_extension( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message extension_name (str): the extension subname to enable """ @@ -179,7 +179,7 @@ async def disable_extension( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message extension_name (str): the extension subname to disable """ diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index b080c2623..b1b221cfe 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -211,23 +211,23 @@ async def got_away(self: Self, channel: discord.TextChannel) -> None: await channel.send(embed=embed) async def handle_winner( - self, + self: Self, winner: discord.Member, guild: discord.Guild, action: str, raw_duration: datetime.datetime, channel: discord.abc.Messageable, ) -> None: + """This is a function to update the database based on a winner + + Args: + winner (discord.Member): A discord.Member object for the winner + guild (discord.Guild): A discord.Guild object for the guild the winner is a part of + action (str): A string, either "befriended" or "killed", depending on the action + raw_duration (datetime.datetime): A datetime object of the time since the duck spawned + channel (discord.abc.Messageable): The channel in which the duck game happened in """ - This is a function to update the database based on a winner - - Parameters: - winner -> A discord.Member object for the winner - guild -> A discord.Guild object for the guild the winner is a part of - action -> A string, either "befriended" or "killed", depending on the action - raw_duration -> A datetime object of the time since the duck spawned - channel -> The channel in which the duck game happened in - """ + config_ = self.bot.guild_configs[str(guild.id)] log_channel = config_.get("logging_channel") await self.bot.logger.send_log( @@ -386,7 +386,6 @@ async def get_duck_user( """If it exists, will return the duck winner database entry Args: - self (Self): _description_ user_id (int): The integer ID of the user guild_id (int): The guild ID of where the user belongs to @@ -452,7 +451,6 @@ async def stats( """Discord command for getting duck stats for a given user Args: - self (Self): _description_ ctx (commands.Context): The context in which the command was run user (discord.Member, optional): The member to lookup stats for. Defaults to ctx.message.author. @@ -775,7 +773,6 @@ async def donate(self: Self, ctx: commands.Context, user: discord.Member) -> Non This is a discord command Args: - self (Self): _description_ ctx (commands.Context): The context in which the command was run user (discord.Member): The user to donate a duck to """ @@ -854,7 +851,6 @@ async def reset(self: Self, ctx: commands.Context, user: discord.Member) -> None This is a discord command Args: - self (Self): _description_ ctx (commands.Context): The context in which the command was run user (discord.Member): The user to reset """ diff --git a/techsupport_bot/commands/echo.py b/techsupport_bot/commands/echo.py index f262d31ad..c9b0d3b23 100644 --- a/techsupport_bot/commands/echo.py +++ b/techsupport_bot/commands/echo.py @@ -60,7 +60,7 @@ async def echo_channel( This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the calling message channel_id (int): the ID of the channel to send the echoed message message (str): the message to echo @@ -89,7 +89,7 @@ async def echo_user( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the calling message user_id (int): the ID of the user to send the echoed message message (str): the message to echo diff --git a/techsupport_bot/commands/embed.py b/techsupport_bot/commands/embed.py index 8a35eba89..62c6d3095 100644 --- a/techsupport_bot/commands/embed.py +++ b/techsupport_bot/commands/embed.py @@ -49,8 +49,8 @@ async def has_embed_role(ctx: commands.Context) -> bool: ctx (commands.Context): Context of the invokation Raises: - commands.CommandError: Raised if embed_roles isn't set up - commands.MissingAnyRole: Raised if the invoker is missing a role + CommandError: Raised if embed_roles isn't set up + MissingAnyRole: Raised if the invoker is missing a role Returns: bool: Whether the invoker has the role diff --git a/techsupport_bot/commands/emoji.py b/techsupport_bot/commands/emoji.py index d32a98f52..0dd17b632 100644 --- a/techsupport_bot/commands/emoji.py +++ b/techsupport_bot/commands/emoji.py @@ -34,7 +34,7 @@ class Emojis(cogs.BaseCog): KEY_MAP = {"?": "question", "!": "exclamation"} @classmethod - def emoji_from_char(cls, char: str): + def emoji_from_char(cls: Self, char: str) -> str: """Gets an unicode emoji from a character Args: @@ -53,7 +53,7 @@ def emoji_from_char(cls, char: str): return emoji.emojize(f":{cls.KEY_MAP[char]}:", language="alias") return None - def check_if_all_unique(self, string: str) -> bool: + def check_if_all_unique(self: Self, string: str) -> bool: """Checks, using the set function, if a string has duplicates or not Args: @@ -65,7 +65,9 @@ def check_if_all_unique(self, string: str) -> bool: return len(set(string.lower())) == len(string.lower()) @classmethod - def generate_emoji_string(cls, string: str, only_emoji: bool = False): + def generate_emoji_string( + cls: Self, string: str, only_emoji: bool = False + ) -> list[str]: """This takes a string and returns a string or list of emojis Args: @@ -74,7 +76,7 @@ def generate_emoji_string(cls, string: str, only_emoji: bool = False): Defaults to False. Returns: - List: The string or list of emojis + list[str]: The string or list of emojis """ emoji_list = [] diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index b43ffa54f..18bf3e5d4 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -66,7 +66,7 @@ async def extension_status( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message extension_name (str): the name of the extension """ @@ -105,7 +105,7 @@ async def load_extension( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message extension_name (str): the name of the extension """ @@ -130,7 +130,7 @@ async def unload_extension( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message extension_name (str): the name of the extension """ @@ -155,7 +155,7 @@ async def register_extension( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message extension_name (str): the name of the extension """ diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index b0a50a563..a265dcc0e 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -123,10 +123,11 @@ async def has_given_factoids_role( Args: ctx (commands.Context): Context used for getting the config file + check_roles (list[str]): The list of string names of roles Raises: - commands.CommandError: No management roles assigned in the config - commands.MissingAnyRole: Invoker doesn't have a factoid management role + CommandError: No management roles assigned in the config + MissingAnyRole: Invoker doesn't have a factoid management role Returns: bool: Whether the invoker has a factoid management role @@ -242,7 +243,7 @@ async def create_factoid_call( alias (str, optional): The parent factoid. Defaults to None. Raises: - custom_errors.TooLongFactoidMessageError: + TooLongFactoidMessageError: When the message argument is over 2k chars, discords limit """ if len(message) > 2000: @@ -272,7 +273,7 @@ async def modify_factoid_call( factoid (bot.models.Factoid): Factoid to modify. Raises: - custom_errors.TooLongFactoidMessageError: + TooLongFactoidMessageError: When the message argument is over 2k chars, discords limit """ if len(factoid.message) > 2000: @@ -525,6 +526,9 @@ async def get_raw_factoid_entry( factoid_name (str): The name of the factoid to get guild (str): The id of the guild for the factoid + Raises: + FactoidNotFoundError: Raised when the provided factoid doesn't exist + Returns: bot.models.Factoid: The factoid """ @@ -559,7 +563,7 @@ async def get_factoid( guild (str): The id of the guild for the factoid Raises: - custom_errors.FactoidNotFoundError: If the factoid wasn't found + FactoidNotFoundError: If the factoid wasn't found Returns: bot.models.Factoid: The factoid @@ -704,11 +708,13 @@ async def delete_factoid( return True # -- Getting and responding with a factoid -- - async def match(self, config, _: commands.Context, message_contents: str) -> bool: + async def match( + self: Self, config: munch.Munch, _: commands.Context, message_contents: str + ) -> bool: """Checks if a message started with the prefix from the config Args: - config (Config): The config to get the prefix from + config (munch.Munch): The config to get the prefix from message_contents (str): The message to check Returns: @@ -731,8 +737,8 @@ async def response( message_content (str): Content of the call Raises: - custom_errors.FactoidNotFoundError: Raised if a broken alias is present in the DB - custom_errors.TooLongFactoidMessageError: + FactoidNotFoundError: Raised if a broken alias is present in the DB + TooLongFactoidMessageError: Raised when the raw message content is over discords 2000 char limit """ if not ctx.guild: @@ -829,7 +835,7 @@ async def send_to_irc( factoid_message (str): The text of the factoid to send """ # Don't attempt to send a message if irc if irc is disabled - irc_config = getattr(self.bot.file_config.api, "irc") + irc_config = self.bot.file_config.api.irc if not irc_config.enable_irc: return None @@ -1484,7 +1490,7 @@ async def all_(self: Self, ctx: commands.Context, *, flag: str = "") -> None: Defaults to an empty string. Raises: - commands.MissingPermission: Raised when someone tries to call .factoid all with + MissingPermissions: Raised when someone tries to call .factoid all with the hidden flag without administrator permissions """ flags = flag.lower().split() diff --git a/techsupport_bot/commands/gate.py b/techsupport_bot/commands/gate.py index f9cbf0248..d36d8151a 100644 --- a/techsupport_bot/commands/gate.py +++ b/techsupport_bot/commands/gate.py @@ -155,7 +155,6 @@ async def get_roles( but are listed in the gate config roles to be applied Args: - self (Self): _description_ config (munch.Munch): The config of the guild ctx (commands.Context): The context of the message that triggered the gate diff --git a/techsupport_bot/commands/github.py b/techsupport_bot/commands/github.py index 950978d94..605de24be 100644 --- a/techsupport_bot/commands/github.py +++ b/techsupport_bot/commands/github.py @@ -66,7 +66,7 @@ async def issue(self: Self, ctx, title: str, description: str): This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the calling message title: the title of the issue description: the description of the issue diff --git a/techsupport_bot/commands/google.py b/techsupport_bot/commands/google.py index f7133294e..d6e2dfe07 100644 --- a/techsupport_bot/commands/google.py +++ b/techsupport_bot/commands/google.py @@ -1,13 +1,26 @@ """Module for the google extension for the discord bot.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import ui from core import auxiliary, cogs, extensionconfig from discord.ext import commands +if TYPE_CHECKING: + import bot + -async def setup(bot): - """Adding google extension config to the config file.""" +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Google plugin into the bot + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to + + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ # Don't load without the API key try: if not bot.file_config.api.api_keys.google: @@ -90,10 +103,13 @@ async def search(self, ctx, *, query: str): for index, item in enumerate(items): link = item.get("link") snippet = item.get("snippet", "
").replace("\n", "") - if field_counter == 1: - embed = auxiliary.generate_basic_embed( + embed = ( + auxiliary.generate_basic_embed( title=f"Results for {query}", url=self.ICON_URL ) + if field_counter == 1 + else embed + ) embed.add_field(name=link, value=snippet, inline=False) if ( diff --git a/techsupport_bot/commands/grab.py b/techsupport_bot/commands/grab.py index 9c702a6db..54e8090e0 100644 --- a/techsupport_bot/commands/grab.py +++ b/techsupport_bot/commands/grab.py @@ -3,6 +3,7 @@ """ import random +from typing import Self import discord import ui @@ -33,13 +34,21 @@ async def setup(bot): bot.add_extension_config("grab", config) -async def invalid_channel(ctx): - """ - A method to check channels against the whitelist +async def invalid_channel(ctx: commands.Context) -> bool: + """A method to check channels against the whitelist If the channel is not in the whitelist, the command execution is halted - This is expected to be used in a @commands.check call + + Args: + ctx (commands.Context): The context in which the command was run in + + Raises: + CommandError: Raised if grabs aren't allowed in the given channel + + Returns: + bool: If the grabs are allowed in the channel the command was run in """ + config = ctx.bot.guild_configs[str(ctx.guild.id)] # Check if list is empty. If it is, allow all channels if not config.extensions.grab.allowed_channels.value: @@ -65,14 +74,17 @@ class Grabber(cogs.BaseCog): description="Grabs a message by ID and saves it", usage="[username-or-user-ID]", ) - async def grab_user(self, ctx, user_to_grab: discord.Member): - """ - This is the grab by user function. Accessible by .grab + async def grab_user( + self: Self, ctx: commands.Context, user_to_grab: discord.Member + ) -> None: + """This is the grab by user function. Accessible by .grab This will only search for 20 messages - Parameters: - user_to_grab: discord.Member. The user to search for grabs from + Args: + ctx (commands.Context): The context in which the command was run in + user_to_grab (discord.Member): The user to search for grabs from """ + if user_to_grab.bot: await auxiliary.send_deny_embed( message="Ain't gonna catch me slipping!", channel=ctx.channel @@ -271,8 +283,19 @@ async def random_grab(self, ctx, user_to_grab: discord.Member): description="Deleted a specific grab from a user by the message", usage="[user] [message]", ) - async def delete_grab(self, ctx, target_user: discord.Member, *, message: str): - """Deletes a specific grab from an user""" + async def delete_grab( + self: Self, ctx: commands.Context, target_user: discord.Member, *, message: str + ) -> None: + """Deletes a given grab by exact string + + Args: + ctx (commands.Context): The context in which the command was run in + target_user (discord.Member): The user to delete a grab from + message (str): The exact string of the grab to delete + + Raises: + CommandError: Raised if the grab cannot be found for the given user + """ # Stop execution if the invoker isn't the target or an admin if ( not ctx.message.author.id == target_user.id diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 870e8d712..027ef6a32 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -2,6 +2,7 @@ import datetime import uuid +from typing import Self import discord import ui @@ -25,7 +26,11 @@ async def setup(bot): class HangmanGame: - """Class for the game hangman.""" + """Class for the game hangman. + + Raises: + ValueError: A valid alphabetic word wasn't provided + """ HANG_PICS = [ """ @@ -109,8 +114,19 @@ def draw_hang_state(self): """Method to draw the current state of the game.""" return self.HANG_PICS[self.step] - def guess(self, letter): - """Method to define a guess.""" + def guess(self: Self, letter: str) -> bool: + """Registers a guess to the given game + + Args: + letter (str): The single letter to guess + + Raises: + ValueError: Raised if letter isn't a single character + RuntimeError: Raised if the game is finished + + Returns: + bool: True if the letter is in the game, false if it isn't + """ found = True if len(letter) > 1: raise ValueError("guess must be letter") @@ -139,8 +155,19 @@ def failed(self): return True return False - def guessed(self, letter): - """Method to know if a guess has already been guessed.""" + def guessed(self: Self, letter: str) -> bool: + """Method to know if a letter has already been guessed + + Args: + letter (str): The letter to check if it has been guessed + + Raises: + ValueError: Raised if the letter isn't a single character + + Returns: + bool: True if it's been guessed, False if it hasn't + """ + if len(letter) > 1: raise ValueError("guess must be letter") if letter.lower() in self.guesses: @@ -148,8 +175,20 @@ def guessed(self, letter): return False -async def can_stop_game(ctx): - """Method to stop the game of hangman at any time.""" +async def can_stop_game(ctx: commands.Context) -> bool: + """Checks if a user has the ability to stop the running game + + Args: + ctx (commands.Context): The context in which the stop command was run + + Raises: + AttributeError: The Hangman game could not be found + CommandError: No admin roles have been defined in the config + MissingAnyRole: The doesn't have the admin roles needed to stop the game + + Returns: + bool: True the user can stop the game, False they cannot + """ cog = ctx.bot.get_cog("HangmanCog") if not cog: raise AttributeError("could not find hangman cog when checking game states") diff --git a/techsupport_bot/commands/help.py b/techsupport_bot/commands/help.py index b1e9a0c10..0df7f3fc9 100644 --- a/techsupport_bot/commands/help.py +++ b/techsupport_bot/commands/help.py @@ -43,9 +43,9 @@ async def help_command(self, ctx: commands.Context, search_term: str = "") -> No This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message - search_term (str) [Optional]; The term to search command name and descriptions for. + search_term (str, Optional): The term to search command name and descriptions for. Will default to empty string """ # Build raw lists of commands diff --git a/techsupport_bot/commands/htd.py b/techsupport_bot/commands/htd.py index 67c5750d7..1f2849959 100644 --- a/techsupport_bot/commands/htd.py +++ b/techsupport_bot/commands/htd.py @@ -2,6 +2,8 @@ Convert a value or evaluate a mathematical expression to decimal, hex, binary, and ascii encoding """ +from typing import Self + import discord from core import auxiliary, cogs from discord.ext import commands @@ -57,30 +59,30 @@ def split_nicely(self, str_to_split: str) -> list: return parsed_list - def convert_value_to_integer(self, val: str) -> int: + def convert_value_to_integer(self, value_to_convert: str) -> int: """Converts a given value as hex, binary, or decimal into an integer type Args: - val (str): The given value to convert + value_to_convert (str): The given value to convert Returns: int: The value represented as an integer """ - if val.replace("-", "").startswith("0x"): + if value_to_convert.replace("-", "").startswith("0x"): # input detected as hex num_base = 16 - elif val.replace("-", "").startswith("0b"): + elif value_to_convert.replace("-", "").startswith("0b"): # input detected as binary num_base = 2 else: # assume the input is detected as an int num_base = 10 # special handling is needed for floats - if "." in val: - return int(float(val)) + if "." in value_to_convert: + return int(float(value_to_convert)) - return int(val, num_base) + return int(value_to_convert, num_base) def perform_op_on_list(self, equation_list: list) -> int: """This will compute an equation if passed as a list @@ -140,22 +142,22 @@ async def htd(self, ctx: commands.Context, *, val_to_convert: str): """ await self.htd_command(ctx, val_to_convert) - def clean_input(self, input: str) -> str: + def clean_input(self: Self, user_input: str) -> str: """A method to clean up input to be better processed by later functions This replaces "#" with "0x" to recognized "#" as hex It also removes quotes and spaces Args: - input (str): The raw input from the user + user_input (str): The raw input from the user Returns: str: The cleaned up string """ - input = input.replace("#", "0x") - input = input.replace("'", "") - input = input.replace('"', "") - input = input.replace(" ", "") - return input + user_input = user_input.replace("#", "0x") + user_input = user_input.replace("'", "") + user_input = user_input.replace('"', "") + user_input = user_input.replace(" ", "") + return user_input def convert_list_to_ints(self, raw_list: list) -> list: """This converts the values in an equation list into ints diff --git a/techsupport_bot/commands/ipinfo.py b/techsupport_bot/commands/ipinfo.py index df7f9410f..8ebf1603f 100644 --- a/techsupport_bot/commands/ipinfo.py +++ b/techsupport_bot/commands/ipinfo.py @@ -46,7 +46,7 @@ async def get_info(self, ctx, ip_address: str): def generate_embed(self, ip: str, fields: dict[str, str]) -> discord.Embed: """Generates an embed from a set of key, values. - parameters: + Args: ip (str): the ip address fields (dict): dictionary containing embed field titles and their contents diff --git a/techsupport_bot/commands/leave.py b/techsupport_bot/commands/leave.py index bc3fd9cb4..d632830df 100644 --- a/techsupport_bot/commands/leave.py +++ b/techsupport_bot/commands/leave.py @@ -31,7 +31,7 @@ async def leave(self, ctx: commands.Context, *, guild_id: int) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the calling message guild_id (int): the ID of the guild to leave """ diff --git a/techsupport_bot/commands/linter.py b/techsupport_bot/commands/linter.py index 16c03f522..e643cd8d8 100644 --- a/techsupport_bot/commands/linter.py +++ b/techsupport_bot/commands/linter.py @@ -11,6 +11,7 @@ """ import json +from typing import Self import discord from core import auxiliary, cogs @@ -25,13 +26,14 @@ async def setup(bot): class Lint(cogs.BaseCog): """Class to add the lint command on the discord bot.""" - async def check_syntax(self, message: discord.Message) -> str: + async def check_syntax(self: Self, message: discord.Message) -> str: """Checks if the json syntax is valid by trying to load it. + Args: - message (discord.Message) - The message to check the json file of + message (discord.Message): The message to check the json file of Returns: - (str) - The thrown error + str: The thrown error """ # The called method returns JSONDecodeError if the syntax is not valid. try: @@ -46,10 +48,11 @@ async def check_syntax(self, message: discord.Message) -> str: description="Checks the syntax of an attached json file", usage="|json-file|", ) - async def lint(self, ctx: commands.Context): + async def lint(self: Self, ctx: commands.Context) -> None: """Method to add the lint command to the discord bot. + Args: - ctx (commands.Context) - The context in which the command was run + ctx (commands.Context): The context in which the command was run """ await self.lint_command(ctx) diff --git a/techsupport_bot/commands/listen.py b/techsupport_bot/commands/listen.py index 7ee759217..76e1723fb 100644 --- a/techsupport_bot/commands/listen.py +++ b/techsupport_bot/commands/listen.py @@ -28,7 +28,7 @@ class ListenChannel(commands.Converter): async def convert(self, ctx, argument: int): """Convert method for the converter. - parameters: + Args: ctx (discord.ext.commands.Context): the context object argument (int): the channel ID to convert """ @@ -80,7 +80,7 @@ async def preconfig(self): async def get_destinations(self, src): """Gets channel object destinations for a given source channel. - parameters: + Args: src (discord.TextChannel): the source channel to build for """ destinations = self.destination_cache.get(src.id) @@ -94,7 +94,7 @@ async def get_destinations(self, src): async def build_destinations_from_src(self, src): """Builds channel objects for a given src. - parameters: + Args: src (discord.TextChannel): the source channel to build for """ destination_data = await self.get_destination_data(src) @@ -108,7 +108,7 @@ async def build_destinations( ) -> list[discord.abc.Messageable]: """Converts destination ID's to their actual channels objects. - parameters: + Args: destination_ids (list[int]): the destination ID's to reference """ destinations = set() @@ -130,7 +130,7 @@ async def build_destinations( async def get_destination_data(self, src: discord.TextChannel) -> list[str]: """Retrieves raw destination data given a source channel. - parameters: + Args: src (discord.TextChannel): the source channel to build for """ destination_data = await self.bot.models.Listener.query.where( @@ -212,7 +212,7 @@ async def update_destinations( ) -> None: """Updates destinations in Postgres given a src. - parameters: + Args: src (discord.TextChannel): the source channel to build for dst (discord.TextChannel): the destination channel to build for """ @@ -247,7 +247,7 @@ async def start( This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the message src (ListenChannel): the source channel ID dst (ListenChannel): the destination channel ID @@ -280,7 +280,7 @@ async def stop(self, ctx, src: ListenChannel, dst: ListenChannel): This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message src (ListenChannel): the source channel ID dst (ListenChannel): the destination channel ID @@ -313,7 +313,7 @@ async def clear(self, ctx): This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message """ all_listens = await self.bot.models.Listener.query.gino.all() @@ -333,7 +333,7 @@ async def jobs(self, ctx): This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message """ source_objects = await self.get_all_sources() @@ -369,7 +369,7 @@ async def jobs(self, ctx): async def on_message(self, message: discord.Message): """Listens to message events. - parameters: + Args: message (discord.Message): the message that triggered the event """ if message.author.bot: @@ -387,7 +387,7 @@ async def on_message(self, message: discord.Message): async def on_extension_listener_event(self, payload): """Listens for custom extension-based events. - parameters: + Args: payload (dict): the data associated with the event """ if not isinstance(getattr(payload, "embed", None), discord.Embed): diff --git a/techsupport_bot/commands/members.py b/techsupport_bot/commands/members.py index b8f83331c..2d1191d11 100644 --- a/techsupport_bot/commands/members.py +++ b/techsupport_bot/commands/members.py @@ -12,6 +12,7 @@ import datetime import io +from typing import Self, Sequence import discord import yaml @@ -28,14 +29,17 @@ class Members(cogs.BaseCog): """Class for the Member command on the discord bot.""" async def get_members_with_role( - self, ctx: commands.Context, member_list: list, role_name: str + self: Self, + ctx: commands.Context, + member_list: Sequence[discord.Member], + role_name: str, ): """ Gets a list of members with role_name for the invokers guild. Args: - ctx (command.Context): Used to return a message - member_list (list): A list of members to parse + ctx (commands.Context): Used to return a message + member_list (Sequence[discord.Member]): A list of members to parse role_name (str): The role to check for """ # All roles are handled using a shorthand for loop because all diff --git a/techsupport_bot/commands/modmail.py b/techsupport_bot/commands/modmail.py index d1468a109..690fdf157 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -16,7 +16,7 @@ import asyncio import re from datetime import datetime -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Self, Tuple import discord import expiringdict @@ -40,8 +40,8 @@ async def has_modmail_management_role( config (munch.Munch): Can be defined manually to run this without providing actual ctx Raises: - commands.CommandError: No modmail management roles were assigned in the config - commands.MissingAnyRole: Invoker doesn't have a modmail role + CommandError: No modmail management roles were assigned in the config + MissingAnyRole: Invoker doesn't have a modmail role Returns: bool: Whether the invoker has a modmail management role @@ -128,12 +128,12 @@ async def on_message(self, message: discord.Message) -> None: @commands.Cog.listener() async def on_typing( - self, channel: discord.DMChannel, user: discord.User, _: datetime + self: Self, channel: discord.DMChannel, user: discord.User, _: datetime ) -> None: """When someone starts typing in modmails dms, start typing in the corresponding thread Args: - channel (discord.Channel): The channel where someone started typing + channel (discord.DMChannel): The channel where someone started typing user (discord.User): The user who started typing """ if isinstance(channel, discord.DMChannel) and user.id in active_threads: @@ -259,7 +259,6 @@ async def build_attachments( """Returns a list of as many files from a message as the bot can send to the given channel Args: - thread (discord.Thread): The thread the attachments are going to be sent to (To get the maximum file size) message (discord.Message): The message to get the attachments from @@ -858,10 +857,14 @@ async def setup(bot): class Modmail(cogs.BaseCog): - """The modmail cog class""" + """The modmail cog class + + Raises: + AttributeError: Modmail aborting loading due to being disabled + """ - def __init__(self, bot: bot.TechSupportBot): - """Init is used to make variables global so they can be used on the modmail side""" + def __init__(self: Self, bot: bot.TechSupportBot): + # Init is used to make variables global so they can be used on the modmail side super().__init__(bot=bot) # Only runs if modmail is enabled diff --git a/techsupport_bot/commands/news.py b/techsupport_bot/commands/news.py index 93495b6ce..f009bac49 100644 --- a/techsupport_bot/commands/news.py +++ b/techsupport_bot/commands/news.py @@ -1,16 +1,29 @@ """Module for the news extension for the discord bot.""" +from __future__ import annotations + import enum import random +from typing import TYPE_CHECKING import aiocron from botlogging import LogContext, LogLevel from core import auxiliary, cogs, extensionconfig from discord.ext import commands +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the News plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to -async def setup(bot): - """Adding the news config to the config file.""" + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ # Don't load without the API key try: diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index d223bdd25..f94aa0b7e 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -4,6 +4,7 @@ import io import re from datetime import timedelta +from typing import Self import dateparser import discord @@ -649,9 +650,12 @@ async def create_linx_embed(self, config, ctx, content): "Linx-Randomize": "yes", "Accept": "application/json", } - file = {"file": io.StringIO(content)} + file_to_paste = {"file": io.StringIO(content)} response = await self.bot.http_functions.http_call( - "post", self.bot.file_config.api.api_url.linx, headers=headers, data=file + "post", + self.bot.file_config.api.api_url.linx, + headers=headers, + data=file_to_paste, ) url = response.get("url") @@ -787,16 +791,21 @@ async def get_warnings_command(self, ctx, user: discord.User): usage="@user [time] [reason]", aliases=["timeout"], ) - async def mute(self, ctx, user: discord.Member, *, duration: str = None): - """ - Method to mute a user in discord using the native timeout. + async def mute( + self: Self, ctx: commands.Context, user: discord.Member, *, duration: str = None + ) -> None: + """Method to mute a user in discord using the native timeout. This should be run via discord - Parameters: - user: The discord.Member to be timed out. Required - duration: A string (# [s|m|h|d]) that declares how long. - Max time is 28 days by discord API. Defaults to 1 hour + Args: + ctx (commands.Context): _description_ + user (discord.Member): The discord.Member to be timed out. + duration (str, optional): Max time is 28 days by discord API. Defaults to 1 hour + + Raises: + ValueError: Raised if the provided duration string cannot be converted into a time """ + can_execute = await self.can_execute(ctx, user) if not can_execute: return diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 254f4e819..86831f98c 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -1,6 +1,8 @@ """This is the discord side of the IRC->Discord relay""" -from typing import Dict, List, Union +from __future__ import annotations + +from typing import TYPE_CHECKING, Dict, List, Self, Union import discord import irc.client @@ -10,18 +12,24 @@ from core import auxiliary, cogs from discord.ext import commands +if TYPE_CHECKING: + import bot + -async def setup(bot: commands.Bot) -> None: +async def setup(bot: bot.TechSupportBot) -> None: """Setup function for the IRC relay This is sets up the IRC postgres table, adds the irc cog, and adds a refernce to it to the irc file Args: - bot (commands.Bot): The bot object + bot (bot.TechSupportBot): The bot object + + Raises: + AttributeError: Raised if IRC is disabled """ # Don't load relay if irc is disabled - irc_config = getattr(bot.file_config.api, "irc") + irc_config = bot.file_config.api.irc if not irc_config.enable_irc: raise AttributeError("Relay was not loaded due to IRC being disabled") @@ -36,14 +44,16 @@ class DiscordToIRC(cogs.MatchCog): mapping = None # bidict - discord:irc - async def preconfig(self): + async def preconfig(self: Self) -> None: """The preconfig setup for the discord side This maps the database to a bidict for quick lookups, and allows lookups in threads """ allmaps = await self.bot.models.IRCChannelMapping.query.gino.all() self.mapping = bidict({}) - for map in allmaps: - self.mapping.put(map.discord_channel_id, map.irc_channel_id) + for irc_discord_map in allmaps: + self.mapping.put( + irc_discord_map.discord_channel_id, irc_discord_map.irc_channel_id + ) async def match( self, config: munch.Munch, ctx: commands.Context, content: str @@ -59,7 +69,7 @@ async def match( str: The string representation of the IRC channel. Will be None if no IRC mapping """ # Check if IRC is enabled - irc_config = getattr(self.bot.file_config.api, "irc") + irc_config = self.bot.file_config.api.irc if not irc_config.enable_irc: return None @@ -71,9 +81,9 @@ async def match( return None # If there is a map, find it and return it - map = self.mapping[str(ctx.channel.id)] - if map: - return map + irc_discord_map = self.mapping[str(ctx.channel.id)] + if irc_discord_map: + return irc_discord_map # If no conditions are met, do nothing return None @@ -106,7 +116,7 @@ async def handle_factoid( discord_message (discord.Message): The original containing the invocation of the factoid factoid_message (str): The string representation of the factoid """ - irc_config = getattr(self.bot.file_config.api, "irc") + irc_config = self.bot.file_config.api.irc if not irc_config.enable_irc: return @@ -124,6 +134,7 @@ async def handle_factoid( ) @commands.group( + name="irc", brief="Executes an irc command", description="Executes an irc command", ) @@ -135,7 +146,7 @@ async def irc_base(self, ctx: commands.Context) -> None: """ await auxiliary.extension_help(self, ctx, self.__module__[9:]) - @irc.command(name="maps", description="List all the maps for IRC") + @irc_base.command(name="maps", description="List all the maps for IRC") async def irc_maps(self, ctx: commands.Context) -> None: """Show the current IRC maps @@ -160,7 +171,7 @@ async def irc_maps(self, ctx: commands.Context) -> None: await ctx.send(embed=embed) @commands.has_permissions(administrator=True) - @irc.command(name="disconnect", description="Disconnect from IRC") + @irc_base.command(name="disconnect", description="Disconnect from IRC") async def irc_disconnect(self, ctx: commands.Context) -> None: """Disconnects from IRC @@ -180,7 +191,7 @@ async def irc_disconnect(self, ctx: commands.Context) -> None: ) @commands.has_permissions(administrator=True) - @irc.command(name="reconnect", description="Reconnects to IRC") + @irc_base.command(name="reconnect", description="Reconnects to IRC") async def irc_reconnect(self, ctx: commands.Context) -> None: """Reconnects to IRC @@ -193,7 +204,7 @@ async def irc_reconnect(self, ctx: commands.Context) -> None: message="Reconnected to IRC", channel=ctx.channel ) - @irc.command(name="status", description="Check status") + @irc_base.command(name="status", description="Check status") async def irc_status(self, ctx: commands.Context) -> None: """Prints some basic status of the IRC bot This same info is available in .bot @@ -208,7 +219,7 @@ async def irc_status(self, ctx: commands.Context) -> None: ) irc_status = self.bot.irc.get_irc_status() - irc_config = getattr(self.bot.file_config.api, "irc") + irc_config = self.bot.file_config.api.irc if not irc_config.enable_irc: embed.description = "IRC is not enabled" embed.description = ( @@ -219,7 +230,7 @@ async def irc_status(self, ctx: commands.Context) -> None: await ctx.send(embed=embed) @commands.has_permissions(ban_members=True) - @irc.command(name="ban", description="Ban a user on IRC") + @irc_base.command(name="ban", description="Ban a user on IRC") async def irc_ban(self, ctx: commands.Context, *, user: str) -> None: """A discord command to ban someone on the linked IRC channel @@ -227,26 +238,26 @@ async def irc_ban(self, ctx: commands.Context, *, user: str) -> None: ctx (commands.Context): The context in which the command was run user (str): The hostmask of the user to ban """ - map = self.mapping[str(ctx.channel.id)] - if not map: + irc_discord_map = self.mapping[str(ctx.channel.id)] + if not irc_discord_map: await auxiliary.send_deny_embed( message="This channel is not linked to IRC", channel=ctx.channel ) return - if not self.bot.irc.is_bot_op_on_channel(channel_name=map): + if not self.bot.irc.is_bot_op_on_channel(channel_name=irc_discord_map): await auxiliary.send_deny_embed( message="The IRC bot does not have permissions to ban", channel=ctx.channel, ) return - self.bot.irc.ban_on_irc(user=user, channel=map, action="+b") + self.bot.irc.ban_on_irc(user=user, channel=irc_discord_map, action="+b") await auxiliary.send_confirm_embed( - message=f"Sucessfully sent ban command for {user} from {map}", + message=f"Sucessfully sent ban command for {user} from {irc_discord_map}", channel=ctx.channel, ) @commands.has_permissions(ban_members=True) - @irc.command(name="unban", description="Unban a user on IRC") + @irc_base.command(name="unban", description="Unban a user on IRC") async def irc_unban(self, ctx: commands.Context, *, user: str) -> None: """A discord command to unban someone on the linked IRC channel @@ -254,26 +265,26 @@ async def irc_unban(self, ctx: commands.Context, *, user: str) -> None: ctx (commands.Context): The context in which the command was run user (str): The hostmask of the user to ban """ - map = self.mapping[str(ctx.channel.id)] - if not map: + irc_discord_map = self.mapping[str(ctx.channel.id)] + if not irc_discord_map: await auxiliary.send_deny_embed( message="This channel is not linked to IRC", channel=ctx.channel ) return - if not self.bot.irc.is_bot_op_on_channel(channel_name=map): + if not self.bot.irc.is_bot_op_on_channel(channel_name=irc_discord_map): await auxiliary.send_deny_embed( message="The IRC bot does not have permissions to unban", channel=ctx.channel, ) return - self.bot.irc.ban_on_irc(user=user, channel=map, action="-b") + self.bot.irc.ban_on_irc(user=user, channel=irc_discord_map, action="-b") await auxiliary.send_confirm_embed( - message=f"Sucessfully sent unban command for {user} from {map}", + message=f"Sucessfully sent unban command for {user} from {irc_discord_map}", channel=ctx.channel, ) @commands.has_permissions(administrator=True) - @irc.command(name="link", description="Add a link between IRC and discord") + @irc_base.command(name="link", description="Add a link between IRC and discord") async def irc_link(self, ctx: commands.Context, irc_channel: str) -> None: """Create a new link between discord and IRC @@ -298,7 +309,7 @@ async def irc_link(self, ctx: commands.Context, irc_channel: str) -> None: ) return - joined_channels = getattr(self.bot.file_config.api.irc, "channels") + joined_channels = self.bot.file_config.api.irc.channels if irc_channel not in joined_channels: await auxiliary.send_deny_embed( @@ -306,15 +317,17 @@ async def irc_link(self, ctx: commands.Context, irc_channel: str) -> None: ) return - map = self.bot.models.IRCChannelMapping( + irc_discord_map = self.bot.models.IRCChannelMapping( guild_id=str(ctx.guild.id), discord_channel_id=str(ctx.channel.id), irc_channel_id=irc_channel, ) - self.mapping.put(map.discord_channel_id, map.irc_channel_id) + self.mapping.put( + irc_discord_map.discord_channel_id, irc_discord_map.irc_channel_id + ) - await map.create() + await irc_discord_map.create() await auxiliary.send_confirm_embed( message=( f"New link established between <#{ctx.channel.id}> and {irc_channel}" @@ -323,7 +336,9 @@ async def irc_link(self, ctx: commands.Context, irc_channel: str) -> None: ) @commands.has_permissions(administrator=True) - @irc.command(name="unlink", description="Remove a link between IRC and discord") + @irc_base.command( + name="unlink", description="Remove a link between IRC and discord" + ) async def irc_unlink(self, ctx: commands.Context) -> None: """Deletes the link in the current discord channel @@ -380,9 +395,9 @@ async def send_message_from_irc(self, split_message: Dict[str, str]) -> None: if split_message["channel"] not in self.mapping.inverse: return - map = self.mapping.inverse[split_message["channel"]] + irc_discord_map = self.mapping.inverse[split_message["channel"]] - discord_channel = await self.bot.fetch_channel(map) + discord_channel = await self.bot.fetch_channel(irc_discord_map) mentions = self.get_mentions( message=split_message["content"], channel=discord_channel @@ -437,11 +452,10 @@ def generate_sent_message_embed( embed.set_footer( text=( f"{split_message['hostmask']} •" - f" {getattr(self.bot.file_config.api.irc, 'server')}" + f" {self.bot.file_config.api.irc.server}" ) ) embed.color = discord.Color.blurple() - return embed @commands.Cog.listener() diff --git a/techsupport_bot/commands/restart.py b/techsupport_bot/commands/restart.py index 745826d2c..726d24481 100644 --- a/techsupport_bot/commands/restart.py +++ b/techsupport_bot/commands/restart.py @@ -33,14 +33,14 @@ async def restart(self, ctx: commands.Context) -> None: This is a command and should be accessed via Discord. - parameters: + Args: ctx (commands.Context): the context object for the calling message """ await auxiliary.send_confirm_embed( message="Rebooting! Beep boop!", channel=ctx.channel ) # Exit IRC if it's enabled - irc_config = getattr(self.bot.file_config.api, "irc") + irc_config = self.bot.file_config.api.irc if irc_config.enable_irc: self.bot.irc.exit_irc() diff --git a/techsupport_bot/commands/roll.py b/techsupport_bot/commands/roll.py index 6cd181fd7..25c787fbb 100644 --- a/techsupport_bot/commands/roll.py +++ b/techsupport_bot/commands/roll.py @@ -1,6 +1,7 @@ """Module for the roll extension for the discord bot.""" import random +from typing import Self import discord from core import auxiliary, cogs @@ -24,25 +25,29 @@ class Roller(cogs.BaseCog): description="Rolls a random number in a given range", usage="[minimum] [maximum] (defaults to 1-100)", ) - async def roll(self, ctx: commands.Context, min: int = 1, max: int = 100): + async def roll( + self: Self, ctx: commands.Context, min_value: int = 1, max_value: int = 100 + ): """The function that is called when .roll is run on discord Args: ctx (commands.Context): The context in which the command was run in - min (int, optional): The mininum value of the dice. Defaults to 1. - max (int, optional): The maximum value of the dice. Defaults to 100. + min_value (int, optional): The mininum value of the dice. Defaults to 1. + max_value (int, optional): The maximum value of the dice. Defaults to 100. """ - await self.roll_command(ctx=ctx, min=min, max=max) + await self.roll_command(ctx=ctx, min_value=min_value, max_value=max_value) - async def roll_command(self, ctx: commands.Context, min: int, max: int): + async def roll_command( + self: Self, ctx: commands.Context, min_value: int, max_value: int + ): """The core logic for the roll command Args: ctx (commands.Context): The context in which the command was run in - min (int, optional): The mininum value of the dice. - max (int, optional): The maximum value of the dice. + min_value (int, optional): The mininum value of the dice. + max_value (int, optional): The maximum value of the dice. """ - number = self.get_roll_number(min=min, max=max) + number = self.get_roll_number(min_value=min_value, max_value=max_value) embed = auxiliary.generate_basic_embed( title="RNG Roller", description=f"You rolled a {number}", @@ -51,14 +56,14 @@ async def roll_command(self, ctx: commands.Context, min: int, max: int): ) await ctx.send(embed=embed) - def get_roll_number(self, min: int, max: int) -> int: + def get_roll_number(self, min_value: int, max_value: int) -> int: """A function to get a random number based on min and max values Args: - min (int, optional): The mininum value of the dice. - max (int, optional): The maximum value of the dice. + min_value (int, optional): The mininum value of the dice. + max_value (int, optional): The maximum value of the dice. Returns: int: The random number """ - return random.randint(min, max) + return random.randint(min_value, max_value) diff --git a/techsupport_bot/commands/set.py b/techsupport_bot/commands/set.py index 491560b2b..1f272e751 100644 --- a/techsupport_bot/commands/set.py +++ b/techsupport_bot/commands/set.py @@ -48,7 +48,7 @@ async def set_game(self, ctx, *, game_name: str): This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message game_name (str): the name of the game """ @@ -66,7 +66,7 @@ async def set_nick(self, ctx, *, nick: str): This is a command and should be accessed via Discord. - parameters: + Args: ctx (discord.ext.Context): the context object for the message nick (str): the bot nickname """ diff --git a/techsupport_bot/commands/spotify.py b/techsupport_bot/commands/spotify.py index ef15246b4..5c51448fc 100644 --- a/techsupport_bot/commands/spotify.py +++ b/techsupport_bot/commands/spotify.py @@ -1,13 +1,27 @@ """Module for the Spotify extension of the discord bot.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import aiohttp import ui from core import auxiliary, cogs from discord.ext import commands +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Spotify plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to -async def setup(bot): - """Adding the Spotify configuration to the config file.""" + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ # Don't load without the API key try: diff --git a/techsupport_bot/commands/weather.py b/techsupport_bot/commands/weather.py index bc62dc29a..5f7f946a8 100644 --- a/techsupport_bot/commands/weather.py +++ b/techsupport_bot/commands/weather.py @@ -1,13 +1,27 @@ """Module for the weather extension for the discord bot.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import discord import munch from core import auxiliary, cogs from discord.ext import commands +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Weather plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to -async def setup(bot): - """Adding the weather configuration to the config file.""" + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ # Don't load without the API key try: diff --git a/techsupport_bot/commands/who.py b/techsupport_bot/commands/who.py index b9d8f3228..aa101824a 100644 --- a/techsupport_bot/commands/who.py +++ b/techsupport_bot/commands/who.py @@ -54,7 +54,18 @@ class Who(cogs.BaseCog): @staticmethod async def is_reader(interaction: discord.Interaction) -> bool: """Checks whether invoker can read notes. If at least one reader - role is not set, all members can read notes.""" + role is not set, all members can read notes + + Args: + interaction (discord.Interaction): The interaction in which the whois command occured + + Raises: + CommandError: Raised if there are no note_readers set in the config + + Returns: + bool: True if the user can run, False if they cannot + """ + config = interaction.client.guild_configs[str(interaction.guild.id)] if reader_roles := config.extensions.who.note_readers.value: roles = ( diff --git a/techsupport_bot/commands/winerror.py b/techsupport_bot/commands/winerror.py index 5ee933a6b..317f40f56 100644 --- a/techsupport_bot/commands/winerror.py +++ b/techsupport_bot/commands/winerror.py @@ -4,7 +4,7 @@ import json from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self import discord from core import auxiliary, cogs @@ -226,36 +226,38 @@ def handle_hex_errors(self, hex_code: int) -> ErrorCategory: ) return category - def twos_comp(self, val: int, bits: int) -> int: + def twos_comp(self: Self, original_value: int, bits: int) -> int: """compute the 2's complement of int value val""" - if val & (1 << (bits - 1)) != 0: # if sign bit is set e.g., 8bit: 128-255 - val = val - (1 << bits) # compute negative value - return val + if ( + original_value & (1 << (bits - 1)) != 0 + ): # if sign bit is set e.g., 8bit: 128-255 + original_value = original_value - (1 << bits) # compute negative value + return original_value - def reverse_twos_comp(self, val: int, bits: int) -> int: + def reverse_twos_comp(self: Self, original_value: int, bits: int) -> int: """Gets the reverse twos complement for the given input Args: - val (int): The value to find the unsigned value for + original_value (int): The value to find the unsigned value for bits (int): How many bits need to be shifted Returns: int: The value with a reverse twos complement """ - return (1 << bits) - 1 - ~val + return (1 << bits) - 1 - ~original_value - def try_parse_decimal(self, val: str) -> int: + def try_parse_decimal(self: Self, original_value: str) -> int: """Parse a string input into a decimal value. If this fails, 0 is returned Args: - val (str): The string input to turn into a decimal + original_value (str): The string input to turn into a decimal Returns: int: 0, or the properly converted string """ # check if the target error code is a base 10 integer try: - return_val = int(val, 10) + return_val = int(original_value, 10) # Error codes are unsigned. If a negative number is input, the only code we're # interested in is the hex equivalent. if return_val <= 0: @@ -266,11 +268,11 @@ def try_parse_decimal(self, val: str) -> int: # Check if the error code is a valid hex number. # Returns 0xFFFF, or CDERR_DIALOGFAILURE upon invalid code. - def try_parse_hex(self, val: str) -> int: + def try_parse_hex(self: Self, original_value: str) -> int: """Parse a string input into a hex value. If this fails, 0xFFFF is returned Args: - val (str): The string input to turn into a hex + original_value (str): The string input to turn into a hex Returns: int: 0xFFFF, or the properly converted string @@ -279,33 +281,33 @@ def try_parse_hex(self, val: str) -> int: try: # if the number is a negative decimal number, we need its unsigned hexadecimal # equivalent. - if int(val, 16) < 0: - if abs(int(val)) > 0xFFFFFFFF: + if int(original_value, 16) < 0: + if abs(int(original_value)) > 0xFFFFFFFF: return 0xFFFF # the integer conversion here is deliberately a base 10 conversion. The command # should fail if a negative hex number is queried. - return self.reverse_twos_comp(int(val), 32) + return self.reverse_twos_comp(int(original_value), 32) # check if the number is larger than 32 bits - if abs(int(val, 16)) > 0xFFFFFFFF: + if abs(int(original_value, 16)) > 0xFFFFFFFF: return 0xFFFF - return int(val, 16) + return int(original_value, 16) except ValueError: return 0xFFFF - def pad_hex(self, input: str) -> str: + def pad_hex(self: Self, hex_code_input: str) -> str: """Pads a hex value Args: - input (str): The input value to add 0s to + hex_code_input (str): The input value to add 0s to Returns: str: The padded value """ # this string should never be over 10 characters, however this check is here # just in case there's a bug in the code. - if len(input) > 10: + if len(hex_code_input) > 10: return "0xFFFF" - return "0x" + input[2:].zfill(8) + return "0x" + hex_code_input[2:].zfill(8) diff --git a/techsupport_bot/commands/wolfram.py b/techsupport_bot/commands/wolfram.py index a3d989c6b..ecc70c047 100644 --- a/techsupport_bot/commands/wolfram.py +++ b/techsupport_bot/commands/wolfram.py @@ -1,12 +1,26 @@ """Module for the wolfram extension for the discord bot.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + import discord from core import auxiliary, cogs from discord.ext import commands +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Wolfram Alpha plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to -async def setup(bot): - """Adding the wolfram configuration to the config file.""" + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ # Don't load without the API key try: diff --git a/techsupport_bot/core/auxiliary.py b/techsupport_bot/core/auxiliary.py index fa9a435b0..2136dc96d 100644 --- a/techsupport_bot/core/auxiliary.py +++ b/techsupport_bot/core/auxiliary.py @@ -93,7 +93,7 @@ async def add_list_of_reactions(message: discord.Message, reactions: list) -> No def construct_mention_string(targets: list[discord.User]) -> str: """Builds a string of mentions from a list of users. - parameters: + Args: targets ([]discord.User): the list of users to mention """ constructed = set() @@ -204,11 +204,20 @@ async def get_json_from_attachments( ) -> munch.Munch | str | None: """Returns concatted JSON from a message's attachments. - parameters: - message (Message): the message object - as_string (bool): True if the serialized JSON should be returned - allow_failure (bool): True if an exception should be ignored when parsing attachments + Args: + message (discord.Message): the message object + as_string (bool, optional): True if the serialized JSON should be returned. + Defaults to False. + allow_failure (bool, optional): True if an exception should be ignored when + parsing attachments. Defaults to False. + + Raises: + exception: If allow_failure is False, this raises ANY exception caught while parsing + + Returns: + munch.Munch | str | None: The json formatted as requested by as_string and allow_failure """ + if not message.attachments: return None @@ -233,7 +242,7 @@ async def get_json_from_attachments( def config_schema_matches(input_config: dict, current_config: dict) -> list[str] | None: """Performs a schema check on an input guild config. - parameters: + Args: input_config (dict): the config to be added current_config (dict): the current config """ @@ -269,7 +278,7 @@ def with_typing(command: commands.Command) -> commands.Command: This will show the bot as typing... until the command completes - parameters: + Args: command (commands.Command): the command object to modify """ original_callback = command.callback @@ -331,7 +340,7 @@ def get_object_diff( def add_diff_fields(embed: discord.Embed, diff: dict) -> discord.Embed: """Adds fields to an embed based on diff data. - parameters: + Args: embed (discord.Embed): the embed object diff (dict): the diff data for an object """ @@ -390,7 +399,7 @@ def get_help_embed_for_extension(self, extension_name, command_prefix): Defined so it doesn't have to be written out twice - parameters: + Args: extension_name (str): the name of the extension to show the help for command_prefix (str): passed to the func as it has to be awaited @@ -443,7 +452,7 @@ async def extension_help(self, ctx: commands.Context, extension_name: str) -> No all extensions have the value set to extension., it's the most reliable way to get the extension name regardless of aliases - parameters: + Args: ctx (commands.Context): context of the message extension_name (str): the name of the extension to show the help for """ @@ -496,7 +505,7 @@ async def bot_admin_check_context(ctx: commands.Context) -> bool: ctx (commands.Context): The context that the command was called in Raises: - commands.MissingPermissions: If the user is not a bot admin + MissingPermissions: If the user is not a bot admin Returns: bool: True if can run diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index 7cef9ed21..1b71100db 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any, List, Self import discord import gino @@ -18,7 +18,7 @@ class BaseCog(commands.Cog): """The base cog to use when making extensions. - parameters: + Args: bot (Bot): the bot object models (List[gino.Model]): the Postgres models for the extension no_guild (bool): True if the extension should run globally @@ -51,7 +51,7 @@ async def _handle_preconfig(self, handler) -> None: This makes the extension unload when there is an error. - parameters: + Args: handler (asyncio.coroutine): the preconfig handler """ await self.bot.wait_until_ready() @@ -77,7 +77,7 @@ async def preconfig(self) -> None: def extension_enabled(self, config: munch.Munch) -> bool: """Checks if an extension is currently enabled for a given config. - parameters: + Args: config (dict): the context/guild config """ if config is None: @@ -100,7 +100,7 @@ class MatchCog(BaseCog): async def on_message(self, message: discord.Message) -> None: """Listens for a message and passes it to the response handler if valid. - parameters: + Args: message (message): the message object """ if message.author == self.bot.user: @@ -142,7 +142,7 @@ async def match( ) -> bool: """Runs a boolean check on message content. - parameters: + Args: _config (dict): the config associated with the context _ctx (context): the context object _content (str): the message content @@ -154,7 +154,7 @@ async def response( ) -> None: """Performs a response if the match is valid. - parameters: + Args: _config (dict): the config associated with the context _ctx (context): the context object _content (str): the message content @@ -166,7 +166,7 @@ class LoopCog(BaseCog): This currently doesn't utilize the tasks library. - parameters: + Args: bot (Bot): the bot object """ @@ -184,7 +184,7 @@ def __init__(self, *args: tuple, **kwargs: dict[str, Any]): async def register_new_tasks(self, guild: discord.Guild) -> None: """Creates the configured loop tasks for a given guild. - parameters: + Args: guild (discord.Guild): the guild to add the tasks for """ config = self.bot.guild_configs[str(guild.id)] @@ -306,11 +306,15 @@ async def _track_new_channels(self) -> None: async def loop_preconfig(self) -> None: """Preconfigures the environment before starting the loop.""" - async def _loop_execute(self, guild: discord.Guild, target_channel=None) -> None: + async def _loop_execute( + self: Self, guild: discord.Guild, target_channel: discord.abc.Messageable = None + ) -> None: """Loops through the execution method. - parameters: + Args: guild (discord.Guild): the guild associated with the execution + target_channel (discord.abc.Messageable): The channel to run the loop in, + if the loop is channel specific """ config = self.bot.guild_configs[str(guild.id)] @@ -373,7 +377,7 @@ async def execute( ) -> None: """Runs sequentially after each wait method. - parameters: + Args: _config (munch.Munch): the config object for the guild _guild (discord.Guild): the guild associated with the execution _target_channel (discord.Channel): the channel object to use @@ -386,7 +390,7 @@ async def _default_wait(self) -> None: async def wait(self, _config: munch.Munch, _guild: discord.Guild) -> None: """The default wait method. - parameters: + Args: _config (munch.Munch): the config object for the guild _guild (discord.Guild): the guild associated with the execution """ diff --git a/techsupport_bot/core/custom_errors.py b/techsupport_bot/core/custom_errors.py index 7fd346ec1..1938d7b7a 100644 --- a/techsupport_bot/core/custom_errors.py +++ b/techsupport_bot/core/custom_errors.py @@ -58,7 +58,7 @@ def __init__(self, wait): class ErrorResponse: """Object for generating a custom error message from an exception. - parameters: + Args: message_format (str): the substition formatted (%s) message lookups (Union[str, list]): the lookup objects to reference """ @@ -85,7 +85,7 @@ def __init__(self, message_format=None, lookups=None, dont_print_trace=False): def default_message(self, exception=None): """Handles default message generation. - parameters: + Args: exception (Exception): the exception to reference """ return ( @@ -97,7 +97,7 @@ def default_message(self, exception=None): def get_message(self, exception=None): """Gets a response message from a given exception. - parameters: + Args: exception (Exception): the exception to reference """ if not self.message_format: diff --git a/techsupport_bot/core/extensionconfig.py b/techsupport_bot/core/extensionconfig.py index 172d7d411..8684342c9 100644 --- a/techsupport_bot/core/extensionconfig.py +++ b/techsupport_bot/core/extensionconfig.py @@ -14,7 +14,7 @@ def add(self, key, datatype, title, description, default): This is usually used in the extensions's setup function. - parameters: + Args: key (str): the lookup key for the entry datatype (str): the datatype metadata for the entry title (str): the title of the entry diff --git a/techsupport_bot/core/http.py b/techsupport_bot/core/http.py index 5c634f851..a1005c3cc 100644 --- a/techsupport_bot/core/http.py +++ b/techsupport_bot/core/http.py @@ -9,7 +9,7 @@ import urllib from collections import deque from json import JSONDecodeError -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self from urllib.parse import urlparse import aiohttp @@ -80,18 +80,25 @@ def __init__(self, bot: bot.TechSupportBot) -> None: print("No linx API URL found. Not rate limiting linx") async def http_call( - self, method: str, url: str, *args: tuple, **kwargs: dict[str, Any] + self: Self, method: str, url: str, *args: tuple, **kwargs: dict[str, Any] ): """Makes an HTTP request. By default this returns JSON/dict with the status code injected. - parameters: + Args: method (str): the HTTP method to use url (str): the URL to call - use_cache (bool): True if the GET result should be grabbed from cache + use_cache (bool): True if the GET result should be grabbed from cache get_raw_response (bool): True if the actual response object should be returned + + Raises: + HTTPRateLimit: Raised if the API is currently on cooldown + + Returns: + _type_: _description_ """ + # Get the URL not the endpoint being called ignore_rate_limit = False root_url = urlparse(url).netloc diff --git a/techsupport_bot/functions/events.py b/techsupport_bot/functions/events.py index b7db5353e..6022c2667 100644 --- a/techsupport_bot/functions/events.py +++ b/techsupport_bot/functions/events.py @@ -462,7 +462,7 @@ async def on_guild_remove(self, guild: discord.Guild): async def on_guild_join(self, guild: discord.Guild): """Configures a new guild upon joining. - parameters: + Args: guild (discord.Guild): the guild that was joined """ embed = discord.Embed() @@ -702,7 +702,7 @@ async def on_command(self, ctx: commands.Context): async def on_error(self, event_method): """Catches non-command errors and sends them to the error logger for processing. - parameters: + Args: event_method (str): the event method name associated with the error (eg. on_message) """ _, exception, _ = sys.exc_info() diff --git a/techsupport_bot/ircrelay/formatting.py b/techsupport_bot/ircrelay/formatting.py index 9d8c24823..be9d503ff 100644 --- a/techsupport_bot/ircrelay/formatting.py +++ b/techsupport_bot/ircrelay/formatting.py @@ -167,7 +167,7 @@ def get_file_links(message_attachments: List[discord.Attachment]) -> str: Args: message_attachments (List[discord.Attachment]): The list of attachments from a - discord.Message object + discord.Message object Returns: str: The str containing space a seperated list of urls diff --git a/techsupport_bot/ircrelay/irc.py b/techsupport_bot/ircrelay/irc.py index 78e481321..af9f41386 100644 --- a/techsupport_bot/ircrelay/irc.py +++ b/techsupport_bot/ircrelay/irc.py @@ -16,7 +16,17 @@ class IRCBot(ib3.auth.SASL, irc.bot.SingleServerIRCBot): - """The IRC bot class. This is the class that runs the entire IRC side of the bot""" + """The IRC bot class. This is the class that runs the entire IRC side of the bot + The class to start the entire IRC bot + + Args: + loop (asyncio.AbstractEventLoop): The running event loop for the discord API. + server (str): The string server domain/IP + port (int): The port the IRC server is running on + channels (List[str]): The list of channels to join + username (str): The username of the IRC bot account + password (str): The password of the IRC bot account + """ irc_cog = None loop = None @@ -35,16 +45,7 @@ def __init__( username: str, password: str, ) -> None: - """The function to start the entire IRC bot - Args: - loop (asyncio.AbstractEventLoop): The running event loop for the discord API. - server (str): The string server domain/IP - port (int): The port the IRC server is running on - channels (List[str]): The list of channels to join - username (str): The username of the IRC bot account - password (str): The password of the IRC bot account - """ self.loop = loop super().__init__( server_list=[(server, port)], diff --git a/techsupport_bot/tests/commands_tests/test_extensions_htd.py b/techsupport_bot/tests/commands_tests/test_extensions_htd.py index d83460161..2f9dda0c6 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_htd.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_htd.py @@ -10,15 +10,15 @@ import pytest from commands import htd from core import auxiliary -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the htd extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: HTD: The instance of the htd class diff --git a/techsupport_bot/tests/commands_tests/test_extensions_hug.py b/techsupport_bot/tests/commands_tests/test_extensions_hug.py index ed73e908d..15233a89c 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_hug.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_hug.py @@ -9,15 +9,15 @@ import pytest from commands import hug from core import auxiliary -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the hug extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: Hugger: The instance of the Hugger class diff --git a/techsupport_bot/tests/commands_tests/test_extensions_lenny.py b/techsupport_bot/tests/commands_tests/test_extensions_lenny.py index d94147757..ba2391792 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_lenny.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_lenny.py @@ -7,15 +7,15 @@ import pytest from commands import lenny -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the htd extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: HTD: The instance of the htd class diff --git a/techsupport_bot/tests/commands_tests/test_extensions_linter.py b/techsupport_bot/tests/commands_tests/test_extensions_linter.py index 497c1db95..809e361ef 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_linter.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_linter.py @@ -10,15 +10,15 @@ import pytest from commands import linter from core import auxiliary -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the linter extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: Lint: The instance of the Lint class diff --git a/techsupport_bot/tests/commands_tests/test_extensions_mock.py b/techsupport_bot/tests/commands_tests/test_extensions_mock.py index 9fa618d17..44a591c06 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_mock.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_mock.py @@ -11,15 +11,15 @@ from core import auxiliary from hypothesis import given from hypothesis.strategies import text -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the mock extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: Mocker: The instance of the Mocker class diff --git a/techsupport_bot/tests/commands_tests/test_extensions_roll.py b/techsupport_bot/tests/commands_tests/test_extensions_roll.py index 352f49405..6cd0d407f 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_roll.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_roll.py @@ -12,15 +12,15 @@ from core import auxiliary from hypothesis import given from hypothesis.strategies import integers -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the roll extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: Roller: The instance of the Roller class @@ -43,7 +43,7 @@ async def test_generate_embed(self): discord_env.context.send = AsyncMock() # Step 2 - Call the function - await roller.roll_command(ctx=discord_env.context, min=1, max=10) + await roller.roll_command(ctx=discord_env.context, min_value=1, max_value=10) # Step 3 - Assert that everything works auxiliary.generate_basic_embed.assert_called_once_with( @@ -67,7 +67,7 @@ async def test_roll_calls_send(self): discord_env.context.send = AsyncMock() # Step 2 - Call the function - await roller.roll_command(ctx=discord_env.context, min=1, max=10) + await roller.roll_command(ctx=discord_env.context, min_value=1, max_value=10) # Step 3 - Assert that everything works discord_env.context.send.assert_called_once_with(embed="embed") @@ -80,18 +80,18 @@ class Test_RandomNumber: """A single test to test get_roll_number""" @given(integers(), integers()) - def test_random_numbers(self, min, max): + def test_random_numbers(self, min_value, max_value): """A property test to ensure that random number doesn't return anything unexpected""" # Step 1 - Setup env roller = setup_local_extension() - if min > max: - temp = min - max = min - min = temp + if min_value > max_value: + temp = min_value + max_value = min_value + min_value = temp # Step 2 - Call the function - result = roller.get_roll_number(min=min, max=max) + result = roller.get_roll_number(min_value=min_value, max_value=max_value) # Step 3 - Assert that everything works assert isinstance(result, int) - assert min <= result <= max + assert min_value <= result <= max_value diff --git a/techsupport_bot/tests/commands_tests/test_extensions_wyr.py b/techsupport_bot/tests/commands_tests/test_extensions_wyr.py index 1410d7bd8..af960fa05 100644 --- a/techsupport_bot/tests/commands_tests/test_extensions_wyr.py +++ b/techsupport_bot/tests/commands_tests/test_extensions_wyr.py @@ -11,15 +11,15 @@ import pytest from commands import wyr from core import auxiliary -from tests import config_for_tests +from tests import config_for_tests, helpers -def setup_local_extension(bot=None): +def setup_local_extension(bot: helpers.MockBot = None): """A simple function to setup an instance of the wyr extension Args: - bot (MockBot, optional): A fake bot object. Should be used if using a - fake_discord_env in the test. Defaults to None. + bot (helpers.MockBot, optional): A fake bot object. Should be used if using a + fake_discord_env in the test. Defaults to None. Returns: WouldYouRather: The instance of the WouldYouRather class diff --git a/techsupport_bot/tests/config_for_tests.py b/techsupport_bot/tests/config_for_tests.py index 978fb6fc8..1d9055596 100644 --- a/techsupport_bot/tests/config_for_tests.py +++ b/techsupport_bot/tests/config_for_tests.py @@ -59,13 +59,13 @@ def __init__(self): # member objects self.person1 = MockMember( - bot=False, id=1, name="person1", display_avatar=self.asset1 + bot=False, input_id=1, name="person1", display_avatar=self.asset1 ) self.person2 = MockMember( - bot=False, id=2, name="person2", display_avatar=self.asset2 + bot=False, input_id=2, name="person2", display_avatar=self.asset2 ) self.person3_bot = MockMember( - bot=True, id=3, name="bot", display_avatar=self.asset1 + bot=True, input_id=3, name="bot", display_avatar=self.asset1 ) # attachment objects diff --git a/techsupport_bot/tests/helpers/bot.py b/techsupport_bot/tests/helpers/bot.py index 9f4361717..b9562bc7c 100644 --- a/techsupport_bot/tests/helpers/bot.py +++ b/techsupport_bot/tests/helpers/bot.py @@ -14,8 +14,8 @@ class MockBot: wait_until_ready() -> always returns true """ - def __init__(self, id=None): - self.id = id + def __init__(self, input_id=None): + self.id = input_id async def get_prefix(self, message=None): """A mock function to get the prefix of the bot""" diff --git a/techsupport_bot/tests/helpers/channel.py b/techsupport_bot/tests/helpers/channel.py index c6fd4ab5b..886824775 100644 --- a/techsupport_bot/tests/helpers/channel.py +++ b/techsupport_bot/tests/helpers/channel.py @@ -26,7 +26,7 @@ async def history(self, limit: int) -> AsyncGenerator[str, None]: limit (int): The represents a limit. This is currently not used Yields: - AsyncGenerator[str, None, None] : This represents a single message in the history + str: This represents a single message in the history """ if limit == 0: return diff --git a/techsupport_bot/tests/helpers/member.py b/techsupport_bot/tests/helpers/member.py index 6d14f91c9..9a283b049 100644 --- a/techsupport_bot/tests/helpers/member.py +++ b/techsupport_bot/tests/helpers/member.py @@ -15,9 +15,9 @@ class MockMember: display_avatar -> The MockAsset object for the avatar """ - def __init__(self, id=None, bot=False, name=None, display_avatar=None): - self.id = id + def __init__(self, input_id=None, bot=False, name=None, display_avatar=None): + self.id = input_id self.bot = bot - self.mention = f"<@{id}>" + self.mention = f"<@{input_id}>" self.name = name self.display_avatar = display_avatar diff --git a/techsupport_bot/ui/confirm.py b/techsupport_bot/ui/confirm.py index a79481601..b98c86a55 100644 --- a/techsupport_bot/ui/confirm.py +++ b/techsupport_bot/ui/confirm.py @@ -48,6 +48,12 @@ async def send( author (discord.Member): The original author of the command triggering this timeout (int, optional): The amount of seconds to wait for a response before returning ConfirmResponse.TIMEOUT. Defaults to 60. + interaction (discord.Interaction | None, optional): If this is in an + application command, what is the interaction to reply or followup to. + Defaults to None + ephemeral (bool, optional): If this is an application command, + should replies be ephemeral? + Will do nothing without interaction being passed. Defaults to False """ embed = auxiliary.generate_basic_embed( title="Please confirm!", description=message, color=discord.Color.green() diff --git a/techsupport_bot/ui/roleselect.py b/techsupport_bot/ui/roleselect.py index 9e60b1bdb..8968a70cc 100644 --- a/techsupport_bot/ui/roleselect.py +++ b/techsupport_bot/ui/roleselect.py @@ -4,14 +4,14 @@ class RoleSelect(discord.ui.Select): - """This holds the select object for a list of roles""" + """This holds the select object for a list of roles + + Args: + role_list (list[str]): A list of SelectOption to be in the dropdown + """ def __init__(self, role_list: list[str]): - """A function to set some defaults - Args: - role_list (list[str]): A list of SelectOption to be in the dropdown - """ super().__init__( placeholder="Select roles...", min_values=0, @@ -36,14 +36,13 @@ async def on_timeout(self): class SelectView(discord.ui.View): - """This is the view that will hold only the dropdown""" + """This is the view that will hold only the dropdown + Adds the dropdown and does nothing else + Args: + role_list (list[str]): The list of SelectOptions to add to the dropdown + """ def __init__(self, role_list: list[str]): - """Adds the dropdown and does nothing else - - Args: - role_list (list[str]): The list of SelectOptions to add to the dropdown - """ super().__init__() # Adds the dropdown to our view object. self.select = RoleSelect(role_list)