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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 52 additions & 25 deletions techsupport_bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import json
import os
import threading
from typing import Self

import botlogging
import discord
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion techsupport_bot/botlogging/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion techsupport_bot/botlogging/delayed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion techsupport_bot/botlogging/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions techsupport_bot/commands/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions techsupport_bot/commands/commandcontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
"""
Expand Down
8 changes: 4 additions & 4 deletions techsupport_bot/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand All @@ -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)]
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
"""
Expand Down
24 changes: 10 additions & 14 deletions techsupport_bot/commands/duck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
"""
Expand Down
4 changes: 2 additions & 2 deletions techsupport_bot/commands/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions techsupport_bot/commands/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading