diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..9708dba --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,9 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + \ No newline at end of file diff --git a/.gitignore b/.gitignore index df688a0..29bddb3 100755 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,5 @@ dmypy.json .pyre/ config.json -config.toml \ No newline at end of file +config.toml +.pdm-python \ No newline at end of file diff --git a/README.md b/README.md index 3ed5c63..549eada 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ IB.ai, but in Python ## Running the bot -To run this project, you need to have [Docker](https://docs.docker.com/get-docker/) and [Python 3.11](https://www.python.org/downloads/) installed. +To run this project, you need to have [Docker](https://docs.docker.com/get-docker/) and [Python >=3.11](https://www.python.org/downloads/) installed. 1. Clone this repository @@ -46,3 +46,36 @@ docker-compose -f docker-compose.postgres.yml up --build # Contributing Please read the `CONTRIBUTING.md` file to find out more about contributing towards the project. + +## Development +We use [PDM](https://pdm-project.org/en/stable/) to manage dependencies for this project and [Ruff](https://docs.astral.sh/ruff/) to handle linting and code formatting. + +### 1. Clone this repository + +``` +git clone https://github.com/ib-ai/ib.py.git +``` + +and navigate to the root directory. + +``` +cd ib.py +``` + +### 2. [Install pdm](https://pdm-project.org/en/stable/#recommended-installation-method) + + +### 3. Create virtual environment and install dependencies +(requires either virtualenv, venv, or conda to be installed) + +``` +pdm install +``` + +### 4. Using the virtual environment + +Either [activate virtual environment](https://pdm-project.org/en/stable/usage/venv/#activate-a-virtualenv) if you are running commands in the terminal + +Or + +Choose `.venv\Scripts\python.exe` as the interpreter in your IDE. \ No newline at end of file diff --git a/bot.py b/bot.py index 41130fa..ee27bb6 100755 --- a/bot.py +++ b/bot.py @@ -6,46 +6,52 @@ import logging -logger = logging.getLogger('bot') + +logger = logging.getLogger("bot") logger.setLevel(logging.DEBUG) # TODO: Change back to logging.INFO -cogs_logger = logging.getLogger('cogs') +cogs_logger = logging.getLogger("cogs") cogs_logger.setLevel(logging.DEBUG) # TODO: Change back to logging.INFO -logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S') +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) intents = discord.Intents.all() INITIAL_COGS = ( - 'dev', - 'embeds', - 'filter', - # 'help', - 'helper', - 'moderation', - 'monitor', - 'public', - 'reminder', - 'roles', - 'tags', - 'updates', - 'voting', + "dev", + "embeds", + "filter", + # "help", + "helper", + "moderation", + "monitor", + "public", + "reminder", + "roles", + "tags", + "updates", + "voting", ) + + class IBpy(commands.Bot): def __init__(self): super().__init__( - intents=intents, + intents=intents, command_prefix=config.prefix, description=config.description, - application_id=config.application_id + application_id=config.application_id, ) - + async def setup_hook(self): await db_init() for cog in INITIAL_COGS: try: - await bot.load_extension(f'cogs.{cog}') + await bot.load_extension(f"cogs.{cog}") logger.debug(f'Imported cog "{cog}".') except commands.errors.NoEntryPointError as e: # ! Remove before push @@ -55,26 +61,30 @@ async def setup_hook(self): except commands.errors.ExtensionFailed as e: logger.error(e) logger.info("Loaded all cogs.") - + async def on_ready(self): - await bot.change_presence(activity=discord.Game(name=f"{config.prefix}help"), status=discord.Status.do_not_disturb) + await bot.change_presence( + activity=discord.Game(name=f"{config.prefix}help"), + status=discord.Status.do_not_disturb, + ) bot_name = bot.user.name bot_description = bot.description guild_number = len(bot.guilds) - logger.info(f"Bot \"{bot_name}\" is now connected.") + logger.info(f'Bot "{bot_name}" is now connected.') logger.info(f"Currently serving {guild_number} guilds.") - logger.info(f"Described as \"{bot_description}\".") + logger.info(f'Described as "{bot_description}".') + + await self.get_cog("Reminder").schedule_existing_reminders() + logger.info("Existing reminders queued.") - await self.get_cog('Reminder').schedule_existing_reminders() - logger.info(f'Existing reminders queued.') - async def on_command_error(self, ctx: commands.Context, exception) -> None: # sends the error message as a discord message # uesful for debugging, TODO: remove/edit before pushing to production await super().on_command_error(ctx, exception) await ctx.send(exception) + bot = IBpy() bot.run(config.token) diff --git a/cogs/channelorder.py b/cogs/channelorder.py index 2344ac9..3831d4a 100644 --- a/cogs/channelorder.py +++ b/cogs/channelorder.py @@ -1,32 +1,31 @@ -import discord from discord.ext import commands class ChannelOrder(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - - @commands.hybrid_group(aliases=['co']) + + @commands.hybrid_group(aliases=["co"]) async def channelorder(self, ctx: commands.Context): """ Commands for discord channel arrangement within categories. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @channelorder.command() async def snapshot(self, ctx: commands.Context): """ Save the arrangement of channels in a category. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - - @channelorder.command(aliases=['r']) + raise NotImplementedError("Command requires implementation and permission set-up.") + + @channelorder.command(aliases=["r"]) async def rollback(self, ctx: commands.Context): """ Revert the arrangement of channels in a category. """ - raise NotImplementedError('Command requires implementation and permission set-up.') + raise NotImplementedError("Command requires implementation and permission set-up.") async def setup(bot: commands.Bot): - await bot.add_cog(ChannelOrder(bot)) \ No newline at end of file + await bot.add_cog(ChannelOrder(bot)) diff --git a/cogs/dev.py b/cogs/dev.py index 2a032e2..e240e28 100644 --- a/cogs/dev.py +++ b/cogs/dev.py @@ -35,9 +35,9 @@ async def guilddata(self, ctx: commands.Context): """ Display all guild data. """ - raise NotImplementedError('Command requires implementation and permission set-up.') + raise NotImplementedError("Command requires implementation and permission set-up.") - @commands.command(name='eval') + @commands.command(name="eval") async def evaluate(self, ctx: commands.Context, *, code: str): """ Run python code. @@ -50,7 +50,7 @@ async def evaluate(self, ctx: commands.Context, *, code: str): "channel": ctx.channel, "author": ctx.author, "guild": ctx.guild, - "message": ctx.message + "message": ctx.message, } stdout = io.StringIO() @@ -58,7 +58,8 @@ async def evaluate(self, ctx: commands.Context, *, code: str): try: with contextlib.redirect_stdout(stdout): exec( - f"async def func():\n{textwrap.indent(code, ' ')}", local_variables, + f"async def func():\n{textwrap.indent(code, ' ')}", + local_variables, ) obj = await local_variables["func"]() @@ -69,7 +70,12 @@ async def evaluate(self, ctx: commands.Context, *, code: str): await ctx.send(result[0:2000]) @commands.command() - async def sync(self, ctx: commands.Context, guilds: commands.Greedy[discord.Object], spec: Optional[Literal["~", "*", "^"]] = None) -> None: + async def sync( + self, + ctx: commands.Context, + guilds: commands.Greedy[discord.Object], + spec: Optional[Literal["~", "*", "^"]] = None, + ) -> None: """ Syncs app commands to Discord. """ diff --git a/cogs/embeds.py b/cogs/embeds.py index 4963045..a10d48e 100644 --- a/cogs/embeds.py +++ b/cogs/embeds.py @@ -1,25 +1,24 @@ -import discord from discord.ext import commands class Embeds(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - + @commands.hybrid_command() async def embed(self, ctx: commands.Context): """ Interactively construct a Discord embed. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.command() async def embedraw(self, ctx: commands.Context): """ Create a Discord embed via raw JSON input. """ - raise NotImplementedError('Command requires implementation and permission set-up.') + raise NotImplementedError("Command requires implementation and permission set-up.") async def setup(bot: commands.Bot): - await bot.add_cog(Embeds(bot)) \ No newline at end of file + await bot.add_cog(Embeds(bot)) diff --git a/cogs/filter.py b/cogs/filter.py index 9ab0754..f10eecd 100644 --- a/cogs/filter.py +++ b/cogs/filter.py @@ -11,8 +11,10 @@ from utils.pagination import PaginationView, paginated_embed_menus import logging + logger = logging.getLogger(__name__) + class Filter(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot @@ -50,7 +52,12 @@ async def on_message(self, message: discord.Message): start, end = match.span() formatted_filter = f"{message.content[:start]}**{message.content[start:end]}**{message.content[end:]}" - await log_filtered_message(guild_data.monitor_message_log_id, message, formatted_filter, filter.notify) + await log_filtered_message( + guild_data.monitor_message_log_id, + message, + formatted_filter, + filter.notify, + ) await message.delete() dm_filter_message = ( @@ -73,7 +80,7 @@ async def filter(self, ctx: commands.Context): """ await available_subcommands(ctx) - @filter.command(aliases=['add']) + @filter.command(aliases=["add"]) async def create(self, ctx: commands.Context, *, pattern: RegexConverter): """ Create a filter. @@ -85,16 +92,18 @@ async def create(self, ctx: commands.Context, *, pattern: RegexConverter): return if len(pattern) > 1024: - await ctx.send(f"The pattern provided is longer than 1024 characters.") + await ctx.send("The pattern provided is longer than 1024 characters.") return await StaffFilter.create(trigger=pattern) get_all_filters.cache_clear() logger.debug(f"Added pattern {pattern} to filters.") - await ctx.send(f"The pattern (`{pattern}`) has been successfully added to filtered messages.") + await ctx.send( + f"The pattern (`{pattern}`) has been successfully added to filtered messages." + ) - @filter.command(aliases=['remove']) + @filter.command(aliases=["remove"]) async def delete(self, ctx: commands.Context, filter_id: int): """ Delete a filter. @@ -109,7 +118,9 @@ async def delete(self, ctx: commands.Context, filter_id: int): get_all_filters.cache_clear() logger.debug(f"Removed {filtered_message.trigger} from filters.") - await ctx.send(f"The pattern (`{filtered_message.trigger}`) has been successfully removed from filtered messages.") + await ctx.send( + f"The pattern (`{filtered_message.trigger}`) has been successfully removed from filtered messages." + ) @filter.command() async def list(self, ctx: commands.Context): @@ -118,11 +129,16 @@ async def list(self, ctx: commands.Context): """ filtered_messages = sorted(await get_all_filters(), key=lambda x: x.filter_id) - names = [f"{'[Notify] ' if filter.notify else ''}Filter (ID: `{filter.filter_id}`)" for filter in filtered_messages] - entries =[f"```{filter.trigger}```" for filter in filtered_messages] + names = [ + f"{'[Notify] ' if filter.notify else ''}Filter (ID: `{filter.filter_id}`)" + for filter in filtered_messages + ] + entries = [f"```{filter.trigger}```" for filter in filtered_messages] embeds = paginated_embed_menus(names, entries) - filter_embed, filter_view = await PaginationView(ctx, embeds).return_paginated_embed_view() + filter_embed, filter_view = await PaginationView( + ctx, embeds + ).return_paginated_embed_view() await ctx.send(embed=filter_embed, view=filter_view) @@ -142,8 +158,12 @@ async def notify(self, ctx: commands.Context, filter_id: int): await filtered_message.save() get_all_filters.cache_clear() - logger.debug(f"{'Enabled' if filtered_message.notify else 'Disabled'} notify for filter with ID {filtered_message.filter_id}.") - await ctx.send(f"The ping notification for filter (`{filtered_message.trigger}`) [ID: {filtered_message.filter_id}] has been successfully {'enabled' if filtered_message.notify else 'disabled'}.") + logger.debug( + f"{'Enabled' if filtered_message.notify else 'Disabled'} notify for filter with ID {filtered_message.filter_id}." + ) + await ctx.send( + f"The ping notification for filter (`{filtered_message.trigger}`) [ID: {filtered_message.filter_id}] has been successfully {'enabled' if filtered_message.notify else 'disabled'}." + ) @filter.command() async def toggle(self, ctx: commands.Context): @@ -157,9 +177,14 @@ async def toggle(self, ctx: commands.Context): await guild_data.save() get_guild_data.cache_clear() - await ctx.send(f"Message filtering {'`enabled`' if guild_data.filtering else '`disabled`'} for this guild.") + await ctx.send( + f"Message filtering {'`enabled`' if guild_data.filtering else '`disabled`'} for this guild." + ) -async def log_filtered_message(channel: int, message: discord.Message, formatted_message: str, notify: bool): + +async def log_filtered_message( + channel: int, message: discord.Message, formatted_message: str, notify: bool +): filter_channel = message.guild.get_channel(channel) if not filter_channel: @@ -167,10 +192,16 @@ async def log_filtered_message(channel: int, message: discord.Message, formatted author = f"{message.author.name}#{message.author.discriminator} (ID: {message.author.id})" - description = f"\"{formatted_message}\", sent in **<#{message.channel.id}>** by <@{message.author.id}>" - description = truncate(description, 950) # Accounts for above characters and a lenient snowflake value + description = ( + f'"{formatted_message}", sent in **<#{message.channel.id}>** by <@{message.author.id}>' + ) + description = truncate( + description, 950 + ) # Accounts for above characters and a lenient snowflake value - embed = discord.Embed(title=author, description=description, color=discord.Colour.magenta()) + embed = discord.Embed( + title=author, description=description, color=discord.Colour.magenta() + ) embed.set_author(name="Filter was triggered!", icon_url=message.author.display_avatar.url) @@ -179,5 +210,6 @@ async def log_filtered_message(channel: int, message: discord.Message, formatted if notify: await filter_channel.send("@here") + async def setup(bot: commands.Bot): await bot.add_cog(Filter(bot)) diff --git a/cogs/help.py b/cogs/help.py index bbf9154..efc76ef 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -1,6 +1,5 @@ from typing import Mapping, List, Optional -import discord from discord.ext import commands @@ -8,36 +7,38 @@ class IBpyHelp(commands.HelpCommand): def __init__(self) -> None: super().__init__() - async def send_bot_help(self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]]): + async def send_bot_help( + self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]] + ): """ Send help menu for the bot. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") async def send_cog_help(self, cog: commands.Cog): """ Send help menu for a cog. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") async def send_group_help(self, group: commands.Group): """ Send help menu for a command group. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") async def send_command_help(self, command: commands.Command): """ Send help menu for a command. """ - raise NotImplementedError('Command requires implementation and permission set-up.') + raise NotImplementedError("Command requires implementation and permission set-up.") async def send_error_message(self, error: str): """ Send message when an error is thrown. """ # raise NotImplementedError('Command requires implementation and permission set-up.') - channel = self.get_destination() # this defaults to the command context channel + channel = self.get_destination() # this defaults to the command context channel await channel.send(error) @@ -47,10 +48,10 @@ def __init__(self, bot: commands.Bot) -> None: self.old_help_command = bot.help_command bot.help_command = IBpyHelp() bot.help_command.cog = self - + def cog_unload(self): self.bot.help_command = self.old_help_command async def setup(bot): - await bot.add_cog(Help(bot)) \ No newline at end of file + await bot.add_cog(Help(bot)) diff --git a/cogs/helper.py b/cogs/helper.py index baad8b5..bd04aec 100644 --- a/cogs/helper.py +++ b/cogs/helper.py @@ -5,40 +5,42 @@ class Helper(commands.Cog): def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - self.subjects = toml.load('config.toml')['subjects'] + self.bot = bot + self.subjects = toml.load("config.toml")["subjects"] self.helper_ids = self.subjects.keys() self.subject_channels = [self.subjects[role] for role in self.helper_ids] self.ctx_menu = discord.app_commands.ContextMenu( - name='Toggle Pin', - callback=self.toggle_pin, - ) + name="Toggle Pin", + callback=self.toggle_pin, + ) self.bot.tree.add_command(self.ctx_menu) - + async def cog_unload(self) -> None: self.bot.tree.remove_command(self.ctx_menu.name, type=self.ctx_menu.type) - + async def send_error(self, obj, message): if isinstance(obj, commands.Context): return await obj.send(message) else: return await obj.response.send_message(message, ephemeral=True) - + async def check_permissions(self, obj): user = obj.author if isinstance(obj, commands.Context) else obj.user channel = obj.channel.id user_role_ids = [str(role.id) for role in user.roles] if not any(role in self.helper_ids for role in user_role_ids): - message = 'Only subject helpers can pin messages.' + message = "Only subject helpers can pin messages." await self.send_error(obj, message) return False if channel not in self.subject_channels: - message = 'You may only pin messages in subject channels.' + message = "You may only pin messages in subject channels." await self.send_error(obj, message) return False - valid_channels = [self.subjects[role] for role in user_role_ids if role in self.helper_ids] + valid_channels = [ + self.subjects[role] for role in user_role_ids if role in self.helper_ids + ] if channel not in valid_channels: - message = 'You may only pin messages in your respective subject channel.' + message = "You may only pin messages in your respective subject channel." await self.send_error(obj, message) return False return True @@ -48,33 +50,48 @@ async def toggle_pin(self, interaction: discord.Interaction, message: discord.Me try: if message.pinned: await message.unpin() - return await interaction.response.send_message('The message was successfully unpinned.') + return await interaction.response.send_message( + "The message was successfully unpinned." + ) else: await message.pin() - return await interaction.response.send_message('The message was successfully pinned.') + return await interaction.response.send_message( + "The message was successfully pinned." + ) except discord.Forbidden: - return await interaction.response.send_message('The bot does not have permission to pin/unpin messages.', ephemeral=True) + return await interaction.response.send_message( + "The bot does not have permission to pin/unpin messages.", + ephemeral=True, + ) except discord.NotFound: - return await interaction.response.send_message('Invalid message ID provided.', ephemeral=True) + return await interaction.response.send_message( + "Invalid message ID provided.", ephemeral=True + ) except discord.HTTPException: if not message.pinned: - return await interaction.response.send_message('You have reached the maximum number of pins for this channel.', ephemeral=True) + return await interaction.response.send_message( + "You have reached the maximum number of pins for this channel.", + ephemeral=True, + ) else: - return await interaction.response.send_message('The message could not be unpinned.', ephemeral=True) + return await interaction.response.send_message( + "The message could not be unpinned.", ephemeral=True + ) @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): """ Update helper message based on user helper/dehelper. """ - ... + ... + @commands.hybrid_command() async def helpermessage(self, ctx: commands.Context): """ Send an updating list of helpers for a subject. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() async def pin(self, ctx: commands.Context, message: discord.Message = None): """ @@ -82,34 +99,41 @@ async def pin(self, ctx: commands.Context, message: discord.Message = None): """ if await self.check_permissions(ctx): if message.pinned: - return await ctx.send('The message is already pinned.') + return await ctx.send("The message is already pinned.") try: await message.pin() - return await ctx.send('The message was successfully pinned.') + return await ctx.send("The message was successfully pinned.") except discord.Forbidden: - return await ctx.send('The bot does not have the permission to pin/unpin messages.') + return await ctx.send( + "The bot does not have the permission to pin/unpin messages." + ) except discord.NotFound: - return await ctx.send('Invalid message ID provided.') + return await ctx.send("Invalid message ID provided.") except discord.HTTPException: - return await ctx.send('You have reached the maximum number of pins for this channel.') - + return await ctx.send( + "You have reached the maximum number of pins for this channel." + ) + @commands.hybrid_command() async def unpin(self, ctx: commands.Context, message: discord.Message = None): """ Unpin a message from a channel. """ - if await self.check_permissions(ctx): + if await self.check_permissions(ctx): if not message.pinned: - return await ctx.send('The message is already unpinned.') + return await ctx.send("The message is already unpinned.") try: await message.unpin() - return await ctx.send('The message was successfully unpinned.') + return await ctx.send("The message was successfully unpinned.") except discord.Forbidden: - return await ctx.send('The bot does not have the permission to unpin messages.') + return await ctx.send( + "The bot does not have the permission to unpin messages." + ) except discord.NotFound: - return await ctx.send('Invalid message ID provided.') + return await ctx.send("Invalid message ID provided.") except discord.HTTPException: - return await ctx.send('The message could not be unpinned.') - + return await ctx.send("The message could not be unpinned.") + + async def setup(bot: commands.Bot): await bot.add_cog(Helper(bot)) diff --git a/cogs/moderation.py b/cogs/moderation.py index ec1da04..6367903 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -12,7 +12,9 @@ def __init__(self, bot: commands.Bot) -> None: self.bot = bot @commands.Cog.listener() - async def on_member_ban(self, guild: discord.Guild, user: Union[discord.User, discord.Member]): + async def on_member_ban( + self, guild: discord.Guild, user: Union[discord.User, discord.Member] + ): """ Publish ban to mod-log. """ @@ -53,153 +55,161 @@ async def on_message_delete(self, message: discord.Message): """ ... - @commands.hybrid_command() async def logs(self, ctx: commands.Context): """ Set a channel for log messages. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() async def moderators(self, ctx: commands.Context): """ Set a role for moderators. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") @commands.hybrid_group() async def modlog(self, ctx: commands.Context): """ Commands for setting channels to publish punishment updates to. """ - raise NotImplementedError('Command requires implementation and permission set-up.') + raise NotImplementedError("Command requires implementation and permission set-up.") @modlog.command() async def server(self, ctx: commands.Context): """ Set a channel for punishment updates to be sent to publicly. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") @modlog.command() async def staff(self, ctx: commands.Context): """ Set a channel for punishment updates to be sent internally. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() async def muterole(self, ctx: commands.Context): """ Set a role for mutes. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() - @describe(user='User to ban', reason='Reason for ban') + @describe(user="User to ban", reason="Reason for ban") async def blacklist(self, ctx: commands.Context, user: discord.User, *, reason: str): """ Blacklist a user that is not in the server. - """ + """ if user in ctx.guild.members: - await ctx.send("User is in the server. Please use Discord's built-in moderation tools.") + await ctx.send( + "User is in the server. Please use Discord's built-in moderation tools." + ) return await ctx.guild.ban(user, reason=reason) - await ctx.send(f'Banned {user} for `{reason}`.') - + await ctx.send(f"Banned {user} for `{reason}`.") + @commands.hybrid_command() async def expire(self, ctx: commands.Context): """ Set a duration for a punishment. Equivalently, schedule the revokement of a punishment. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() async def history(self, ctx: commands.Context): """ Display a user's punishment history. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() async def lookup(self, ctx: commands.Context): """ Retrieve punishment case by case number. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.hybrid_command() async def note(self, ctx: commands.Context): """ Save a note on a user. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + @commands.group(invoke_without_command=True) async def purge(self, ctx: commands.Context): """ Commands for bulk deletion. """ await available_subcommands(ctx) - + @purge.command() async def message(self, ctx: commands.Context, number: int): """ Bulk delete messages. - """ + """ async for message in ctx.channel.history(limit=number): try: await message.delete() except discord.Forbidden: - await ctx.send('I do not have permission to delete messages.') + await ctx.send("I do not have permission to delete messages.") return except discord.HTTPException: - await ctx.send('An error occurred while deleting messages.') + await ctx.send("An error occurred while deleting messages.") return - await ctx.send(f'Deleted {number} messages.', delete_after=5) - + await ctx.send(f"Deleted {number} messages.", delete_after=5) + @purge.command() - async def reaction(self, ctx: commands.Context, channel: discord.TextChannel, message_id: int, emoji: discord.Emoji): + async def reaction( + self, + ctx: commands.Context, + channel: discord.TextChannel, + message_id: int, + emoji: discord.Emoji, + ): """ Bulk delete reactions. - """ + """ try: message = await channel.fetch_message(message_id) except discord.NotFound: - await ctx.send('Message not found.') + await ctx.send("Message not found.") return except discord.Forbidden: - await ctx.send('I do not have permission to read messages in that channel.') + await ctx.send("I do not have permission to read messages in that channel.") return except discord.HTTPException: - await ctx.send('An error occurred while fetching the message.') + await ctx.send("An error occurred while fetching the message.") return try: await message.clear_reaction(emoji) except discord.Forbidden: - await ctx.send('I do not have permission to delete reactions.') + await ctx.send("I do not have permission to delete reactions.") return except discord.NotFound: - await ctx.send('The emoji you specifiied was not found.') + await ctx.send("The emoji you specifiied was not found.") return except TypeError: - await ctx.send('The emoji you specified is invalid.') + await ctx.send("The emoji you specified is invalid.") return except discord.HTTPException: - await ctx.send('An error occurred while deleting reactions.') + await ctx.send("An error occurred while deleting reactions.") return - - await ctx.send(f'Deleted reactions to {message_id} with emoji {emoji}.') + + await ctx.send(f"Deleted reactions to {message_id} with emoji {emoji}.") @commands.hybrid_command() async def reason(self, ctx: commands.Context): """ Set a reason for a punishment case. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") + async def setup(bot: commands.Bot): - await bot.add_cog(Moderation(bot)) \ No newline at end of file + await bot.add_cog(Moderation(bot)) diff --git a/cogs/monitor.py b/cogs/monitor.py index 7b56694..fac8396 100644 --- a/cogs/monitor.py +++ b/cogs/monitor.py @@ -3,7 +3,12 @@ import discord from discord.ext import commands from db.cached import get_all_monitor_messages, get_all_monitor_users, get_guild_data -from db.models import GuildData, StaffMonitorMessage, StaffMonitorUser, StaffMonitorMessageGroups +from db.models import ( + GuildData, + StaffMonitorMessage, + StaffMonitorUser, + StaffMonitorMessageGroups, +) from utils.checks import admin_command, cogify, staff_command from utils.commands import available_subcommands @@ -11,8 +16,10 @@ from utils.pagination import paginated_embed_menus, PaginationView import logging + logger = logging.getLogger(__name__) + class Monitor(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot @@ -31,7 +38,7 @@ async def on_message(self, message: discord.Message): # If bot, return if message.author.bot: return - + # TODO Add check for config for NSA Deny List guild_data = await get_guild_data(guild_id=message.guild.id) @@ -39,17 +46,21 @@ async def on_message(self, message: discord.Message): # If no guild data return if not guild_data: return - + # Monitored Users if guild_data.monitoring_user and guild_data.monitor_user_log_id: monitor_users = [user.user_id for user in await get_all_monitor_users()] if message.author.id in monitor_users: await log_suspicious_message(guild_data.monitor_user_log_id, message) return - + # Monitored Messages if guild_data.monitoring_message and guild_data.monitor_message_log_id: - monitor_messages = [pattern.message for pattern in await get_all_monitor_messages() if not pattern.disabled] + monitor_messages = [ + pattern.message + for pattern in await get_all_monitor_messages() + if not pattern.disabled + ] for pattern in monitor_messages: if re.search(pattern, message.content, re.IGNORECASE): await log_suspicious_message(guild_data.monitor_message_log_id, message) @@ -72,35 +83,43 @@ async def channel(self, ctx: commands.Context): """ await available_subcommands(ctx) - @channel.command(name='user') + @channel.command(name="user") @admin_command() - async def channel_user(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): + async def channel_user( + self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None + ): """ Set logger channel for monitored users. """ - values = dict(monitor_user_log_id = channel.id if channel else None) + values = dict(monitor_user_log_id=channel.id if channel else None) await GuildData.update_or_create(values, guild_id=ctx.guild.id) get_guild_data.cache_clear() if channel: - await ctx.send(f'Log channel for monitored users set to <#{channel.id}> for this guild.') + await ctx.send( + f"Log channel for monitored users set to <#{channel.id}> for this guild." + ) else: - await ctx.send('Log channel for monitored users removed for this guild.') - - @channel.command(name='message') + await ctx.send("Log channel for monitored users removed for this guild.") + + @channel.command(name="message") @admin_command() - async def channel_message(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): + async def channel_message( + self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None + ): """ Set logger channel for monitored message patterns. """ - values = dict(monitor_message_log_id = channel.id if channel else None) + values = dict(monitor_message_log_id=channel.id if channel else None) await GuildData.update_or_create(values, guild_id=ctx.guild.id) get_guild_data.cache_clear() if channel: - await ctx.send(f'Log channel for monitored messages set to <#{channel.id}> for this guild.') + await ctx.send( + f"Log channel for monitored messages set to <#{channel.id}> for this guild." + ) else: - await ctx.send('Log channel for monitored messages removed for this guild.') + await ctx.send("Log channel for monitored messages removed for this guild.") @monitor.command() async def cleanup(self, ctx: commands.Context): @@ -117,12 +136,14 @@ async def cleanup(self, ctx: commands.Context): users_removed += 1 get_all_monitor_users.cache_clear() - + logger.debug(f"{users_removed} user(s) were removed from monitor.") await ctx.send(f"{users_removed} user(s) were removed from monitor.") - + @monitor.command() - async def list(self, ctx: commands.Context, type: Optional[Literal['messages', 'users']] = None): + async def list( + self, ctx: commands.Context, type: Optional[Literal["messages", "users"]] = None + ): """ List of monitored message patterns and monitored users. """ @@ -130,8 +151,13 @@ async def list(self, ctx: commands.Context, type: Optional[Literal['messages', ' entries = [] if not type or type == "messages": - monitor_messages = sorted(await get_all_monitor_messages(), key=lambda x: x.monitor_message_id) - names += [f"{'[Disabled] ' if pattern.disabled else ''}Regex (ID: `{pattern.monitor_message_id}`)" for pattern in monitor_messages] + monitor_messages = sorted( + await get_all_monitor_messages(), key=lambda x: x.monitor_message_id + ) + names += [ + f"{'[Disabled] ' if pattern.disabled else ''}Regex (ID: `{pattern.monitor_message_id}`)" + for pattern in monitor_messages + ] entries += [f"```{pattern.message}```" for pattern in monitor_messages] if not type or type == "users": @@ -140,7 +166,9 @@ async def list(self, ctx: commands.Context, type: Optional[Literal['messages', ' entries += [f"<@{user.user_id}>" for user in monitor_users] embeds = paginated_embed_menus(names, entries) - monitor_embed, monitor_view = await PaginationView(ctx, embeds).return_paginated_embed_view() + monitor_embed, monitor_view = await PaginationView( + ctx, embeds + ).return_paginated_embed_view() await ctx.send(embed=monitor_embed, view=monitor_view) @@ -150,8 +178,8 @@ async def message(self, ctx: commands.Context): Commands for monitoring problematic message patterns. """ await available_subcommands(ctx) - - @message.command(aliases=['add'], name='create') + + @message.command(aliases=["add"], name="create") async def message_create(self, ctx: commands.Context, *, pattern: RegexConverter): """ Create a monitor for a message pattern. @@ -167,13 +195,15 @@ async def message_create(self, ctx: commands.Context, *, pattern: RegexConverter logger.debug(f"Added pattern {pattern} to monitor.") await ctx.send(f"The pattern (`{pattern}`) has been successfully added to monitor.") - - @message.command(aliases=['remove'], name='delete') + + @message.command(aliases=["remove"], name="delete") async def message_delete(self, ctx: commands.Context, pattern_id: int): """ Delete a monitor for a message pattern. """ - monitored_message = await StaffMonitorMessage.filter(monitor_message_id=pattern_id).get_or_none() + monitored_message = await StaffMonitorMessage.filter( + monitor_message_id=pattern_id + ).get_or_none() if not monitored_message: await ctx.send(f"The pattern with ID `{pattern_id}` does not exist.") @@ -183,25 +213,36 @@ async def message_delete(self, ctx: commands.Context, pattern_id: int): get_all_monitor_messages.cache_clear() logger.debug(f"Removed pattern {monitored_message.message} from monitor.") - await ctx.send(f"The pattern (`{monitored_message.message}`) has been successfully removed from monitor.") + await ctx.send( + f"The pattern (`{monitored_message.message}`) has been successfully removed from monitor." + ) - @message.command(name='toggle') + @message.command(name="toggle") async def message_toggle(self, ctx: commands.Context, pattern_id: int): """ Toggle a monitor for a message pattern. """ - monitored_message = await StaffMonitorMessage.filter(monitor_message_id=pattern_id).get_or_none() + monitored_message = await StaffMonitorMessage.filter( + monitor_message_id=pattern_id + ).get_or_none() if not monitored_message: await ctx.send(f"Pattern with ID `{pattern_id}` does not exist.") return - + monitored_message.disabled = not monitored_message.disabled await monitored_message.save() get_all_monitor_messages.cache_clear() - logger.debug(f"{'Disabled' if monitored_message.disabled else 'Enabled'} pattern with ID {monitored_message.monitor_message_id} in monitor.") - await ctx.send(f"The pattern (`{monitored_message.message}`) [ID: {monitored_message.monitor_message_id}] has been successfully {'disabled' if monitored_message.disabled else 'enabled'}.") + status = "Disabled" if monitored_message.disabled else "Enabled" + logger.debug( + f"{status} pattern with ID {monitored_message.monitor_message_id} in monitor." + ) + await ctx.send( + f"The pattern (`{monitored_message.message}`) " + f"[ID: {monitored_message.monitor_message_id}] " + f"has been successfully {status.lower()}." + ) @monitor.group() async def user(self, ctx: commands.Context): @@ -209,8 +250,8 @@ async def user(self, ctx: commands.Context): Commands for monitoring problematic users. """ await available_subcommands(ctx) - - @user.command(aliases=['add'], name='create') + + @user.command(aliases=["add"], name="create") async def user_create(self, ctx: commands.Context, member: discord.Member): """ Create a monitor for a user. @@ -226,8 +267,8 @@ async def user_create(self, ctx: commands.Context, member: discord.Member): logger.debug(f"Added user with id {member.id} to monitor.") await ctx.send(f"{member.mention} has been successfully added to monitor.") - - @user.command(aliases=['remove'], name='delete') + + @user.command(aliases=["remove"], name="delete") async def user_delete(self, ctx: commands.Context, member: discord.Member): """ Delete a monitor for a user. @@ -243,13 +284,15 @@ async def user_delete(self, ctx: commands.Context, member: discord.Member): logger.debug(f"Removed user with id {member.id} from monitor.") await ctx.send(f"{member.mention} has been successfully removed from monitor.") - + @monitor.command() @admin_command() - async def toggle(self, ctx: commands.Context, type: Optional[Literal['messages','users']] = None): + async def toggle( + self, ctx: commands.Context, type: Optional[Literal["messages", "users"]] = None + ): """ Toggle if monitoring is active for the guild. - """ + """ guild_data = (await GuildData.get_or_create(guild_id=ctx.guild.id))[0] if not type or type == "messages": @@ -261,7 +304,9 @@ async def toggle(self, ctx: commands.Context, type: Optional[Literal['messages', await guild_data.save() get_guild_data.cache_clear() - await ctx.send(f"Message monitoring {'`enabled`' if guild_data.monitoring_message else '`disabled`'} and user monitoring {'`enabled`' if guild_data.monitoring_user else '`disabled`'} for this guild.") + await ctx.send( + f"Message monitoring {'`enabled`' if guild_data.monitoring_message else '`disabled`'} and user monitoring {'`enabled`' if guild_data.monitoring_user else '`disabled`'} for this guild." + ) @monitor.group() async def group(self, ctx: commands.Context): @@ -270,41 +315,61 @@ async def group(self, ctx: commands.Context): """ await available_subcommands(ctx) - @group.command(name='add') - async def group_add(self, ctx: commands.Context, name: str, monitor_messages: ListConverter): + @group.command(name="add") + async def group_add( + self, ctx: commands.Context, name: str, monitor_messages: ListConverter + ): """ Creates new/adds to existing group message monitor. """ if len(monitor_messages) == 0: - await ctx.send("You must provide at least one monitored message to add to a group.") + await ctx.send( + "You must provide at least one monitored message to add to a group." + ) return if monitor_messages == "*": - await ctx.send("You cannot add all messages to a group. Please specify individual patterns to add to the group.") + await ctx.send( + "You cannot add all messages to a group. Please specify individual patterns to add to the group." + ) return - + db_monitor_messages = [] for pattern_id in monitor_messages: - db_monitor_message = await StaffMonitorMessage.filter(monitor_message_id=pattern_id).get_or_none() + db_monitor_message = await StaffMonitorMessage.filter( + monitor_message_id=pattern_id + ).get_or_none() if not db_monitor_message: await ctx.send(f"The pattern with ID `{pattern_id}` could not be found.") return db_monitor_messages.append(db_monitor_message) - + monitor_group = (await StaffMonitorMessageGroups.get_or_create(name=name))[0] await monitor_group.monitor_messages.add(*db_monitor_messages) - sorted_group_messages = [message.monitor_message_id async for message in monitor_group.monitor_messages.order_by('monitor_message_id')] - - logger.debug(f"Added messages {monitor_messages} to group {name}. Currently contains: {sorted_group_messages}.") - await ctx.send(f"Messages `{monitor_messages}` have been successfully added to group `{name}`. This group currently has the following messages: `{sorted_group_messages}`.") - - @group.command(name='delete') - async def group_delete(self, ctx: commands.Context, name: str, monitor_messages: Optional[ListConverter] = None): + sorted_group_messages = [ + message.monitor_message_id + async for message in monitor_group.monitor_messages.order_by("monitor_message_id") + ] + + logger.debug( + f"Added messages {monitor_messages} to group {name}. Currently contains: {sorted_group_messages}." + ) + await ctx.send( + f"Messages `{monitor_messages}` have been successfully added to group `{name}`. This group currently has the following messages: `{sorted_group_messages}`." + ) + + @group.command(name="delete") + async def group_delete( + self, + ctx: commands.Context, + name: str, + monitor_messages: Optional[ListConverter] = None, + ): """ Deletes monitored messages from group message monitor, and if empty, deletes the entire group. """ @@ -328,14 +393,18 @@ async def group_delete(self, ctx: commands.Context, name: str, monitor_messages: db_monitor_messages = [] for pattern_id in monitor_messages: - db_monitor_message = await StaffMonitorMessage.filter(monitor_message_id=pattern_id).get_or_none() + db_monitor_message = await StaffMonitorMessage.filter( + monitor_message_id=pattern_id + ).get_or_none() if not db_monitor_message: await ctx.send(f"The pattern with ID `{pattern_id}` could not be found.") return - + if db_monitor_message not in monitor_group_patterns: - await ctx.send(f"The pattern with ID `{pattern_id}` is not in the monitor group `{name}`.") + await ctx.send( + f"The pattern with ID `{pattern_id}` is not in the monitor group `{name}`." + ) return db_monitor_messages.append(db_monitor_message) @@ -343,7 +412,9 @@ async def group_delete(self, ctx: commands.Context, name: str, monitor_messages: await monitor_group.monitor_messages.remove(*db_monitor_messages) logger.debug(f"Removed patterns {monitor_messages} from group {name}.") - await ctx.send(f"Patterns `{monitor_messages}` have been successfully removed from group `{name}`.") + await ctx.send( + f"Patterns `{monitor_messages}` have been successfully removed from group `{name}`." + ) else: # Delete all entries but keep group for pattern in monitor_group_patterns: @@ -351,22 +422,27 @@ async def group_delete(self, ctx: commands.Context, name: str, monitor_messages: await ctx.send(f"Removed all patterns from monitor group `{name}`.") logger.debug(f"Removed all patterns from monitor group {name}.") - @group.command(name='list') + @group.command(name="list") async def group_list(self, ctx: commands.Context): """ List of monitored message pattern groups. """ monitor_groups = await StaffMonitorMessageGroups.all() - names = [f"{'[Disabled]' if group.disabled else ''} {group.name}" for group in monitor_groups] + names = [ + f"{'[Disabled]' if group.disabled else ''} {group.name}" + for group in monitor_groups + ] values = await create_formatted_group_message(monitor_groups) embeds = paginated_embed_menus(names, values) - monitor_embed, monitor_view = await PaginationView(ctx, embeds).return_paginated_embed_view() + monitor_embed, monitor_view = await PaginationView( + ctx, embeds + ).return_paginated_embed_view() await ctx.send(embed=monitor_embed, view=monitor_view) - - @group.command(name='toggle') + + @group.command(name="toggle") async def group_toggle(self, ctx: commands.Context, name: str): """ Toggles a group message monitor on or off. @@ -386,9 +462,14 @@ async def group_toggle(self, ctx: commands.Context, name: str): await pattern.save() get_all_monitor_messages.cache_clear() - - logger.debug(f"Monitor group `{name}` (with {len(monitor_group.monitor_messages)} children) has been {'disabled' if monitor_group.disabled else 'enabled'}.") - await ctx.send(f"Monitor group `{name}` (with {len(monitor_group.monitor_messages)} children) has been {'disabled' if monitor_group.disabled else 'enabled'}.") + + logger.debug( + f"Monitor group `{name}` (with {len(monitor_group.monitor_messages)} children) has been {'disabled' if monitor_group.disabled else 'enabled'}." + ) + await ctx.send( + f"Monitor group `{name}` (with {len(monitor_group.monitor_messages)} children) has been {'disabled' if monitor_group.disabled else 'enabled'}." + ) + async def log_suspicious_message(channel: int, message: discord.Message): monitor_channel = message.guild.get_channel(channel) @@ -398,24 +479,38 @@ async def log_suspicious_message(channel: int, message: discord.Message): author = f"{message.author.name}#{message.author.discriminator} (ID: {message.author.id})" - embed = discord.Embed(title=author, description=message.content, color=discord.Colour.red()) + embed = discord.Embed( + title=author, description=message.content, color=discord.Colour.red() + ) - embed.set_author(name="Monitor Trigger", icon_url=message.author.display_avatar.url) \ - .add_field(name="Utilities", value=f"[21 Jump Street]({message.jump_url})\nUser: {message.author.mention} • Channel: <#{message.channel.id}>", inline=False) + embed.set_author( + name="Monitor Trigger", icon_url=message.author.display_avatar.url + ).add_field( + name="Utilities", + value=f"[21 Jump Street]({message.jump_url})\nUser: {message.author.mention} • Channel: <#{message.channel.id}>", + inline=False, + ) await monitor_channel.send(embed=embed) -async def create_formatted_group_message(monitor_groups: list[StaffMonitorMessageGroups]): + +async def create_formatted_group_message( + monitor_groups: list[StaffMonitorMessageGroups], +): formatted_messages = [] - + for group in monitor_groups: - sorted_messages = await group.monitor_messages.all().order_by('monitor_message_id') - message_lines = [f'[ID: {pattern.monitor_message_id}] {pattern.message}' for pattern in sorted_messages] + sorted_messages = await group.monitor_messages.all().order_by("monitor_message_id") + message_lines = [ + f"[ID: {pattern.monitor_message_id}] {pattern.message}" + for pattern in sorted_messages + ] # chr(10) returns \n as backslashes cannot be used in f-string expressions formatted_messages.append(f"```{chr(10).join(message_lines)}```") return formatted_messages + async def setup(bot: commands.Bot): await bot.add_cog(Monitor(bot)) diff --git a/cogs/public.py b/cogs/public.py index ad8b1a4..45152e1 100644 --- a/cogs/public.py +++ b/cogs/public.py @@ -4,10 +4,11 @@ from utils.commands import available_subcommands + class Public(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - + @commands.hybrid_group() async def avatar(self, ctx: commands.Context): """ @@ -16,23 +17,23 @@ async def avatar(self, ctx: commands.Context): await available_subcommands(ctx) @avatar.command() - @describe(member='The user to display the avatar of.') + @describe(member="The user to display the avatar of.") async def guild(self, ctx: commands.Context, member: discord.Member = None): """ Display a user\'s guild-specific avatar, if available. - """ + """ member = member or ctx.author if not member.guild_avatar: - return await ctx.send(f'{member} has no guild avatar.') + return await ctx.send(f"{member} has no guild avatar.") embed = discord.Embed(color=discord.Color.blurple()) embed.set_author(name=f"{member.name}'s avatar") embed.set_image(url=member.guild_avatar.url) await ctx.send(embed=embed) - @avatar.command(name='global') - @describe(member='The user to display the avatar of.') + @avatar.command(name="global") + @describe(member="The user to display the avatar of.") async def _global(self, ctx: commands.Context, member: discord.Member = None): """ Display a user\'s global avatar. @@ -42,45 +43,55 @@ async def _global(self, ctx: commands.Context, member: discord.Member = None): embed.set_author(name=f"{member.name}'s avatar") embed.set_image(url=member.avatar.url if member.avatar else member.default_avatar.url) await ctx.send(embed=embed) - + @commands.hybrid_command() - @describe(user='The user to display the banner of.') + @describe(user="The user to display the banner of.") async def banner(self, ctx: commands.Context, user: discord.User = None): """ Display a user's banner. - """ + """ user = user or ctx.author if not user.banner: - return await ctx.send(f'{user} has no banner.') + return await ctx.send(f"{user} has no banner.") embed = discord.Embed(color=discord.Color.blurple()) embed.set_author(name=f"{user.name}'s banner", icon_url=user.display_avatar.url) embed.set_image(url=user.banner.url) await ctx.send(embed=embed) - + @commands.hybrid_command() async def ping(self, ctx: commands.Context): """ Measure latency. """ - embed = discord.Embed(description=f"I don't see how this will help you, but my ping is `{round(self.bot.latency * 1000)}ms`.", color=discord.Color.orange()) + embed = discord.Embed( + description=f"I don't see how this will help you, but my ping is `{round(self.bot.latency * 1000)}ms`.", + color=discord.Color.orange(), + ) await ctx.send(embed=embed) - - @commands.hybrid_command(aliases=['si']) + + @commands.hybrid_command(aliases=["si"]) async def serverinfo(self, ctx: commands.Context): """ Present server information. """ - embed = discord.Embed(title=f"{ctx.guild.name} ({ctx.guild.id})", color=discord.Color.green()) + embed = discord.Embed( + title=f"{ctx.guild.name} ({ctx.guild.id})", color=discord.Color.green() + ) creation_date = ctx.guild.created_at # unix timestamp timestamp = discord.utils.format_dt(creation_date, style="F") relative = discord.utils.format_dt(creation_date, style="R") - embed.add_field(name="**Creation Date**", value=f"{timestamp} ({relative})", inline=False) + embed.add_field( + name="**Creation Date**", value=f"{timestamp} ({relative})", inline=False + ) num_boosts = ctx.guild.premium_subscription_count embed.add_field(name="**Boosts**", value=f"{num_boosts}") - embed.add_field(name="**Verification Level**", value=f"{str(ctx.guild.verification_level).capitalize()}") + embed.add_field( + name="**Verification Level**", + value=f"{str(ctx.guild.verification_level).capitalize()}", + ) embed.add_field(name="**Owner**", value=f"{ctx.guild.owner.name}") txt = len(ctx.guild.text_channels) @@ -89,7 +100,11 @@ async def serverinfo(self, ctx: commands.Context): stg = len(ctx.guild.stage_channels) frm = len(ctx.guild.forums) total = txt + vc + ctg + stg + frm - embed.add_field(name="**Channels**", value=f"{txt} text, {vc} voice, {ctg} categories, {stg} stage, {frm} forum. {total} total.", inline=False) + embed.add_field( + name="**Channels**", + value=f"{txt} text, {vc} voice, {ctg} categories, {stg} stage, {frm} forum. {total} total.", + inline=False, + ) guild = await self.bot.fetch_guild(ctx.guild.id, with_counts=True) online = guild.approximate_presence_count @@ -101,31 +116,44 @@ async def serverinfo(self, ctx: commands.Context): embed.set_thumbnail(url=ctx.guild.icon.url) await ctx.send(embed=embed) - - @commands.hybrid_command(aliases=['ui']) - @describe(member='The user to display the information of.') + + @commands.hybrid_command(aliases=["ui"]) + @describe(member="The user to display the information of.") async def userinfo(self, ctx: commands.Context, member: discord.Member = None): """ Present user information. """ member = member or ctx.author embed = discord.Embed(color=discord.Color.random()) - embed.set_author(name=f'{member.name}\'s information', icon_url=member.display_avatar.url) - embed.add_field(name='**Nickname**', value=f'{member.display_name}', inline=False) - embed.add_field(name='**Server join date**', value=f'{member.joined_at.strftime("%c")}') - embed.add_field(name='**Account creation date**', value=f'{member.created_at.strftime("%c")}', inline=False) - embed.add_field(name='**Discord ID**', value=f'{member.id}') - embed.add_field(name='**Status**', value=f'{member.status}') + embed.set_author( + name=f"{member.name}'s information", icon_url=member.display_avatar.url + ) + embed.add_field(name="**Nickname**", value=f"{member.display_name}", inline=False) + embed.add_field( + name="**Server join date**", value=f'{member.joined_at.strftime("%c")}' + ) + embed.add_field( + name="**Account creation date**", + value=f'{member.created_at.strftime("%c")}', + inline=False, + ) + embed.add_field(name="**Discord ID**", value=f"{member.id}") + embed.add_field(name="**Status**", value=f"{member.status}") roles = [role.mention for role in member.roles if not role.is_default()] if roles: - rolestext = ', '.join(roles) + rolestext = ", ".join(roles) else: - rolestext = 'No roles' - embed.add_field(name='**Roles**', value=rolestext, inline=False) + rolestext = "No roles" + embed.add_field(name="**Roles**", value=rolestext, inline=False) if member.premium_since: - embed.add_field(name='**Nitro boosting since**', value=f'{member.premium_since.strftime("%c")}', inline=False) + embed.add_field( + name="**Nitro boosting since**", + value=f'{member.premium_since.strftime("%c")}', + inline=False, + ) embed.set_thumbnail(url=member.display_avatar.url) await ctx.send(embed=embed) + async def setup(bot: commands.Bot): - await bot.add_cog(Public(bot)) \ No newline at end of file + await bot.add_cog(Public(bot)) diff --git a/cogs/reminder.py b/cogs/reminder.py index c267fee..0e38286 100644 --- a/cogs/reminder.py +++ b/cogs/reminder.py @@ -14,14 +14,15 @@ from utils.pagination import paginated_embed_menus, PaginationView import logging + logger = logging.getLogger(__name__) -class Reminder(commands.Cog): +class Reminder(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot self.active: Mapping[int, asyncio.Task] = {} - + async def handle_reminder(self, user: discord.User, reminder: MemberReminder): """ Sleep until reminder's timestamp, then send the message to the user and delete the timer. @@ -30,18 +31,20 @@ async def handle_reminder(self, user: discord.User, reminder: MemberReminder): message = reminder.message terminus = reminder.timestamp await long_sleep_until(terminus) - await user.send(f'You asked me to remind you: {message}') + await user.send(f"You asked me to remind you: {message}") await reminder.delete() - + def removal_callback(self, id: int): """ Create callback for removing reminder's task from the internal mapping of active tasks. To be used in asyncio.Task.add_done_callback when scheduling a timer. """ + def callback(task: asyncio.Task): del self.active[id] + return callback - + async def schedule_existing_reminders(self): """ Schedule all timers existing in the database. To be used on bot start-up. @@ -49,36 +52,37 @@ async def schedule_existing_reminders(self): """ reminders = await MemberReminder.all() if not reminders: - logger.debug('No existing reminders found.') - + logger.debug("No existing reminders found.") + # on bot start-up, add a bit of delay between scheduled reminders (to avoid rate-limiting) - dormant = asyncio.create_task( - asyncio.sleep(DEGENERACY_DELAY.total_seconds()) - ) - async def schedule_once_completed(last: asyncio.Task, user: discord.User, reminder: MemberReminder): + dormant = asyncio.create_task(asyncio.sleep(DEGENERACY_DELAY.total_seconds())) + + async def schedule_once_completed( + last: asyncio.Task, user: discord.User, reminder: MemberReminder + ): await asyncio.wait_for(last, timeout=None) await self.handle_reminder(user, reminder) async with asyncio.TaskGroup() as tg: for reminder in reminders: if reminder.reminder_id in self.active: - logger.debug('Reminder already active. (skipping)') + logger.debug("Reminder already active. (skipping)") continue user = self.bot.get_user(reminder.user_id) if not user: - logger.warning(f'User {reminder.user_id} not found. (skipping)') + logger.warning(f"User {reminder.user_id} not found. (skipping)") continue if reminder.timestamp <= timezone.now(): dormant = tg.create_task(schedule_once_completed(dormant, user, reminder)) - logger.debug(f'Dormant timer running: {reminder.reminder_id}') + logger.debug(f"Dormant timer running: {reminder.reminder_id}") else: task = asyncio.create_task(self.handle_reminder(user, reminder)) self.active[reminder.reminder_id] = task task.add_done_callback(self.removal_callback(reminder.reminder_id)) - logger.debug(f'Active timer running: {reminder.reminder_id}') + logger.debug(f"Active timer running: {reminder.reminder_id}") - logger.debug(f'Active reminders: {len(self.active)}') + logger.debug(f"Active reminders: {len(self.active)}") @commands.hybrid_group() async def reminder(self, ctx: commands.Context): @@ -86,18 +90,14 @@ async def reminder(self, ctx: commands.Context): Commands for handling reminders. """ await available_subcommands(ctx) - - @reminder.command(aliases=['add']) - @app_commands.rename(terminus='duration') + + @reminder.command(aliases=["add"]) + @app_commands.rename(terminus="duration") async def create(self, ctx: commands.Context, terminus: DatetimeConverter, *, message): """ Create a reminder. """ - values = dict( - user_id = ctx.author.id, - message = message, - timestamp = terminus - ) + values = dict(user_id=ctx.author.id, message=message, timestamp=terminus) reminder = await MemberReminder.create(**values) logger.debug(f"User {ctx.author.id} scheduled a reminder for {terminus}.") task = asyncio.create_task(self.handle_reminder(ctx.author, reminder)) @@ -105,34 +105,38 @@ async def create(self, ctx: commands.Context, terminus: DatetimeConverter, *, me task.add_done_callback(self.removal_callback(reminder.reminder_id)) await ctx.send(f'Reminder set for {format_dt(terminus)} ({format_dt(terminus, "R")}).') - @reminder.command(aliases=['remove']) + @reminder.command(aliases=["remove"]) async def delete(self, ctx: commands.Context, id: int): """ Delete a reminder. """ - reminder = await MemberReminder.get_or_none(reminder_id = id) + reminder = await MemberReminder.get_or_none(reminder_id=id) if not reminder: - await ctx.send(f'Invalid reminder ID!') + await ctx.send("Invalid reminder ID!") return task = self.active[id] task.cancel() - + await reminder.delete() - await ctx.send(f'Reminder with ID {id} has been deleted.') - + await ctx.send(f"Reminder with ID {id} has been deleted.") + @reminder.command() async def list(self, ctx: commands.Context): """ List your active reminders. - """ + """ user_reminders = await MemberReminder.filter(user_id=ctx.author.id) embed_dict = dict( - title = f'Reminders for {ctx.author.name}#{ctx.author.discriminator}.', - description = f'Here is a list of your active reminders.', + title=f"Reminders for {ctx.author.name}#{ctx.author.discriminator}.", + description="Here is a list of your active reminders.", ) - if ctx.author.accent_color: embed_dict['color'] = ctx.author.accent_color.value - names = [f'[ID: {reminder.reminder_id}] {format_dt(reminder.timestamp)}' for reminder in user_reminders] + if ctx.author.accent_color: + embed_dict["color"] = ctx.author.accent_color.value + names = [ + f"[ID: {reminder.reminder_id}] {format_dt(reminder.timestamp)}" + for reminder in user_reminders + ] values = [reminder.message for reminder in user_reminders] embeds = paginated_embed_menus(names, values, embed_dict=embed_dict) @@ -141,4 +145,4 @@ async def list(self, ctx: commands.Context): async def setup(bot: commands.Bot): - await bot.add_cog(Reminder(bot)) \ No newline at end of file + await bot.add_cog(Reminder(bot)) diff --git a/cogs/roles.py b/cogs/roles.py index 94bb74e..54f0b7f 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -4,32 +4,41 @@ import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # TODO: change back to logging.info +logger.setLevel(logging.DEBUG) # TODO: change back to logging.info class Roles(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - + cog_check = cogify(admin_command()) @commands.hybrid_command() - async def giverole(self, ctx: commands.Context, existing_role: discord.Role, new_role: discord.Role): + async def giverole( + self, ctx: commands.Context, existing_role: discord.Role, new_role: discord.Role + ): """ Assign a new role to all members with a specific role. """ - failure_count = 0 - role_members = [member for member in ctx.guild.members if existing_role in member.roles] + failure_count = 0 + role_members = [ + member for member in ctx.guild.members if existing_role in member.roles + ] for member in role_members: try: await member.add_roles(new_role) except discord.Forbidden: - return await ctx.send('I do not have permission to add roles to members.') + return await ctx.send("I do not have permission to add roles to members.") except discord.HTTPException: - logger.info(f'Could not add role to {member.name}.') + logger.info(f"Could not add role to {member.name}.") failure_count += 1 - await ctx.send(f"Successfully added roles to {len(role_members) - failure_count} members.") - logger.info(f'Given {new_role.name} role to {len(role_members) - failure_count} members.') + await ctx.send( + f"Successfully added roles to {len(role_members) - failure_count} members." + ) + logger.info( + f"Given {new_role.name} role to {len(role_members) - failure_count} members." + ) + async def setup(bot: commands.Bot): await bot.add_cog(Roles(bot)) diff --git a/cogs/tags.py b/cogs/tags.py index 4ad723b..8ea223d 100644 --- a/cogs/tags.py +++ b/cogs/tags.py @@ -10,13 +10,17 @@ import logging -from utils.pagination import NAME_SIZE_LIMIT, VALUE_SIZE_LIMIT, PaginationView, paginated_embed_menus +from utils.pagination import ( + NAME_SIZE_LIMIT, + VALUE_SIZE_LIMIT, + PaginationView, + paginated_embed_menus, +) logger = logging.getLogger(__name__) class Tags(commands.Cog): - def __init__(self, bot: commands.Bot) -> None: self.bot = bot @@ -56,15 +60,14 @@ async def tag(self, ctx: commands.Context): """ await available_subcommands(ctx) - @tag.command(name="create", aliases=['add']) - async def tag_create(self, ctx: commands.Context, trigger: RegexConverter, - output: str): + @tag.command(name="create", aliases=["add"]) + async def tag_create(self, ctx: commands.Context, trigger: RegexConverter, output: str): """ Create a tag. """ - if len( - trigger - ) > NAME_SIZE_LIMIT - 13: # 2 for backticks and 11 for [Disabled] with trailing whitespace + if ( + len(trigger) > NAME_SIZE_LIMIT - 13 + ): # 2 for backticks and 11 for [Disabled] with trailing whitespace await ctx.send( f"The tag trigger is too long (should not exceed {NAME_SIZE_LIMIT - 13} characters)." ) @@ -84,7 +87,7 @@ async def tag_create(self, ctx: commands.Context, trigger: RegexConverter, logger.debug(f"Created tag {trigger} with output {output}.") await ctx.send(f"Created tag `{trigger}` with output `{output}`.") - @tag.command(name="delete", aliases=['remove']) + @tag.command(name="delete", aliases=["remove"]) async def tag_delete(self, ctx: commands.Context, trigger: RegexConverter): """ Delete a tag. @@ -102,28 +105,20 @@ async def tag_delete(self, ctx: commands.Context, trigger: RegexConverter): await ctx.send(f"Deleted tag `{trigger}` with output `{tag.output}`.") @tag.command(name="list") - async def tag_list(self, - ctx: commands.Context, - trigger: Optional[RegexConverter] = None): + async def tag_list(self, ctx: commands.Context, trigger: Optional[RegexConverter] = None): """ List of tags with specified filter, or all tags if none specified. """ tags = await get_all_tags() if trigger: - tags = [ - tag for tag in tags if trigger.lower() in tag.trigger.lower() - ] - - names = [ - f"{'[Disabled] ' if tag.disabled else ''}`{tag.trigger}`" - for tag in tags - ] + tags = [tag for tag in tags if trigger.lower() in tag.trigger.lower()] + + names = [f"{'[Disabled] ' if tag.disabled else ''}`{tag.trigger}`" for tag in tags] values = [tag.output for tag in tags] embeds = paginated_embed_menus(names, values) - tag_embed, tag_view = await PaginationView( - ctx, embeds).return_paginated_embed_view() + tag_embed, tag_view = await PaginationView(ctx, embeds).return_paginated_embed_view() await ctx.send(embed=tag_embed, view=tag_view) @@ -142,12 +137,8 @@ async def tag_toggle(self, ctx: commands.Context, trigger: RegexConverter): await tag.save() get_all_tags.cache_clear() - logger.debug( - f"Tag {trigger} is now {'disabled' if tag.disabled else 'enabled'}." - ) - await ctx.send( - f"Tag `{trigger}` is now {'disabled' if tag.disabled else 'enabled'}." - ) + logger.debug(f"Tag {trigger} is now {'disabled' if tag.disabled else 'enabled'}.") + await ctx.send(f"Tag `{trigger}` is now {'disabled' if tag.disabled else 'enabled'}.") @commands.hybrid_group() async def reply(self, ctx: commands.Context): @@ -161,18 +152,19 @@ async def reply_list(self, ctx: commands.Context): """ List of disabled reply channels. """ - suppressed_channels = (await get_guild_data(guild_id=ctx.guild.id - )).suppressed_channels - channels = [f"<#{channel_id}>" for channel_id in suppressed_channels - ] if suppressed_channels else ["`None`"] + suppressed_channels = (await get_guild_data(guild_id=ctx.guild.id)).suppressed_channels + channels = ( + [f"<#{channel_id}>" for channel_id in suppressed_channels] + if suppressed_channels + else ["`None`"] + ) await ctx.send( f"Replies for the following channels are disabled: {', '.join(channels)}" ) @reply.command(name="toggle") - async def reply_toggle(self, ctx: commands.Context, - channel: discord.TextChannel): + async def reply_toggle(self, ctx: commands.Context, channel: discord.TextChannel): """ Toggle if bot replies are disabled for specified channel. """ diff --git a/cogs/updates.py b/cogs/updates.py index f2532da..c03b13a 100644 --- a/cogs/updates.py +++ b/cogs/updates.py @@ -16,13 +16,11 @@ logger = logging.getLogger(__name__) # updates operates by UTC dates -UTC = timezone(offset=timedelta(), name='UTC') +UTC = timezone(offset=timedelta(), name="UTC") class UpdatesMessage(commands.Converter): - - async def convert(self, ctx: commands.Context, - message_id: str) -> discord.Message: + async def convert(self, ctx: commands.Context, message_id: str) -> discord.Message: guild_data = await get_guild_data(guild_id=ctx.guild.id) updates_channel = ctx.bot.get_channel(guild_data.updates_id) update_message = await updates_channel.fetch_message(message_id) @@ -30,7 +28,6 @@ async def convert(self, ctx: commands.Context, class Updates(commands.Cog): - def __init__(self, bot: commands.Bot) -> None: self.bot = bot @@ -45,9 +42,7 @@ async def update(self, ctx: commands.Context): @update.command() @admin_command() - async def set(self, - ctx: commands.Context, - channel: Optional[discord.TextChannel] = None): + async def set(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): """ Set an updates channel. """ @@ -55,39 +50,38 @@ async def set(self, await GuildData.update_or_create(values, guild_id=ctx.guild.id) get_guild_data.cache_clear() if channel: - await ctx.send( - f'Updates channel set to <#{channel.id}> for this guild.') + await ctx.send(f"Updates channel set to <#{channel.id}> for this guild.") else: - await ctx.send(f'Updates channel removed for this guild.') - - @update.command(aliases=['add']) - async def create(self, - ctx: commands.Context, - update1: str, - update2: Optional[str] = None, - update3: Optional[str] = None): + await ctx.send("Updates channel removed for this guild.") + + @update.command(aliases=["add"]) + async def create( + self, + ctx: commands.Context, + update1: str, + update2: Optional[str] = None, + update3: Optional[str] = None, + ): """ Create (up to 3) update entries. """ - logger.debug(f'Update create - {update1}, {update2}, {update3}') + logger.debug(f"Update create - {update1}, {update2}, {update3}") # note: currently, discord does not support variable-length arguments in hybrid commands updates = [update1] - if update2: updates.append(update2) - if update3: updates.append(update3) + if update2: + updates.append(update2) + if update3: + updates.append(update3) guild_data = await get_guild_data(guild_id=ctx.guild.id) updates_channel = self.bot.get_channel(guild_data.updates_id) # updates operates by UTC dates - last_midnight = datetime.now(tz=UTC).replace(hour=0, - minute=0, - second=0, - microsecond=0) - async for message in updates_channel.history(limit=5, - after=last_midnight): + last_midnight = datetime.now(tz=UTC).replace(hour=0, minute=0, second=0, microsecond=0) + async for message in updates_channel.history(limit=5, after=last_midnight): if message.author == self.bot.user: - update_content = '\n- '.join([message.content, *updates]) + update_content = "\n- ".join([message.content, *updates]) await message.edit(content=update_content) break else: @@ -95,40 +89,41 @@ async def create(self, d = last_midnight.date() fmt = f"**{ordinal(d.day)} of %B, %Y**" update_date = d.strftime(fmt) - update_content = '\n- '.join([update_date, *updates]) + update_content = "\n- ".join([update_date, *updates]) await updates_channel.send(content=update_content) await ctx.send(f'Added update(s): `{"`,`".join(updates)}`') - @update.command(aliases=['remove']) - async def delete(self, ctx: commands.Context, message: UpdatesMessage, - entries: commands.Greedy[IndexConverter]): + @update.command(aliases=["remove"]) + async def delete( + self, + ctx: commands.Context, + message: UpdatesMessage, + entries: commands.Greedy[IndexConverter], + ): """ Delete update entries. """ if not entries: raise commands.BadArgument( - 'Please provide a valid update entry. (Updates entries must be a positive integer.)' + "Please provide a valid update entry. (Updates entries must be a positive integer.)" ) - logger.debug(f'Update delete - {entries}, {message.content}') + logger.debug(f"Update delete - {entries}, {message.content}") # note: the zeroth entry is always the date # update entry indices are supplied with 1-based indexing - updates = message.content.split('\n- ') + updates = message.content.split("\n- ") try: removed = [updates[i] for i in entries] - except IndexError as e: - raise commands.BadArgument( - 'Updates entries provided go out of range.') - remaining = [ - updates[i] for i in range(len(updates)) if i not in entries - ] + except IndexError: + raise commands.BadArgument("Updates entries provided go out of range.") + remaining = [updates[i] for i in range(len(updates)) if i not in entries] # if no updates remain, delete message if len(remaining) < 2: await message.delete() else: - new = '\n- '.join(remaining) + new = "\n- ".join(remaining) await message.edit(content=new) await ctx.send(f'Removed update(s): `{"`,`".join(removed)}`') diff --git a/cogs/voting.py b/cogs/voting.py index 95c99af..3282be3 100644 --- a/cogs/voting.py +++ b/cogs/voting.py @@ -20,77 +20,76 @@ async def on_raw_reaction_delete(self, message: discord.Message): """ ... - @commands.hybrid_group() async def voteladder(self, ctx: commands.Context): """ Commands for handling voteladders. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @voteladder.command() async def channel(self, ctx: commands.Context): """ Assign a channel to a voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - - @voteladder.command(aliases=['add']) + raise NotImplementedError("Command requires implementation and permission set-up.") + + @voteladder.command(aliases=["add"]) async def create(self, ctx: commands.Context): """ Create a voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - - @voteladder.command(aliases=['remove']) + raise NotImplementedError("Command requires implementation and permission set-up.") + + @voteladder.command(aliases=["remove"]) async def delete(self, ctx: commands.Context): """ Delete a voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @voteladder.command() async def duration(self, ctx: commands.Context): """ Assign a vote duration to a voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @voteladder.command() async def minimum(self, ctx: commands.Context): """ Assign a minimum upvote count for passing to a voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @voteladder.command() async def role(self, ctx: commands.Context): """ Assign a role to the voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @voteladder.command() async def threshold(self, ctx: commands.Context): """ Assign a vote passing theshold to a voteladder. """ - raise NotImplementedError('Command requires implementation and permission set-up.') - + raise NotImplementedError("Command requires implementation and permission set-up.") + @voteladder.command() async def list(self, ctx: commands.Context): """ List available voteladders. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") @commands.command() async def vote(self, ctx: commands.Context): """ Hold a vote within a particular voteladder. - """ - raise NotImplementedError('Command requires implementation and permission set-up.') + """ + raise NotImplementedError("Command requires implementation and permission set-up.") async def setup(bot: commands.Bot): - await bot.add_cog(Voting(bot)) \ No newline at end of file + await bot.add_cog(Voting(bot)) diff --git a/db/cached.py b/db/cached.py index 83ac702..3e9829c 100644 --- a/db/cached.py +++ b/db/cached.py @@ -1,10 +1,14 @@ -from typing import Optional from async_lru import alru_cache -from db.models import GuildData, StaffFilter, StaffMonitorMessage, StaffMonitorUser, StaffTag +from db.models import ( + GuildData, + StaffFilter, + StaffMonitorMessage, + StaffMonitorUser, + StaffTag, +) def model_cache_factory(Model): - @alru_cache async def model_cache() -> list[Model]: return await Model.all() diff --git a/db/db.py b/db/db.py index ce1f840..1ebcabb 100644 --- a/db/db.py +++ b/db/db.py @@ -6,7 +6,7 @@ TORTOISE_ORM = { "connections": { - "default": f'postgres://{config.db_user}:{config.db_password}@{config.db_host}:5432/{config.db_name}' + "default": f"postgres://{config.db_user}:{config.db_password}@{config.db_host}:5432/{config.db_name}" }, "apps": { "models": { diff --git a/db/models.py b/db/models.py index ba73f28..0dfab3e 100644 --- a/db/models.py +++ b/db/models.py @@ -22,8 +22,7 @@ class ChannelType(str, enum.Enum): class GuildData(Model): - - class Meta(): + class Meta: table = "guild_data" guild_id = fields.BigIntField(pk=True, unique=True) @@ -37,9 +36,7 @@ class Meta(): helper_id = fields.BigIntField(null=True) filtering = fields.BooleanField(default=False) removal = fields.BooleanField(default=False) - suppressed_channels = ArrayField(element_type="bigint", - null=True, - default=list) + suppressed_channels = ArrayField(element_type="bigint", null=True, default=list) monitoring_user = fields.BooleanField(default=False) monitoring_message = fields.BooleanField(default=False) monitor_user_log_id = fields.BigIntField(null=True) @@ -47,8 +44,7 @@ class Meta(): class GuildSnapshot(Model): - - class Meta(): + class Meta: table = "snapshot" snapshot_id = fields.IntField(pk=True) @@ -58,8 +54,7 @@ class Meta(): class GuildCassowary(Model): - - class Meta(): + class Meta: table = "cassowary" cassowary_id = fields.IntField(pk=True) @@ -68,18 +63,16 @@ class Meta(): class GuildCassowaryRoles(Model): - - class Meta(): + class Meta: table = "cassowary_roles" category_role_id = fields.BigIntField(pk=True) role_id = fields.BigIntField() - cassowary_id = fields.OneToOneField('models.GuildCassowary') + cassowary_id = fields.OneToOneField("models.GuildCassowary") class GuildVoteLadder(Model): - - class Meta(): + class Meta: table = "vote_ladder" vote_ladder_id = fields.IntField(pk=True) @@ -92,8 +85,7 @@ class Meta(): class GuildVote(Model): - - class Meta(): + class Meta: table = "vote" vote_id = fields.IntField(pk=True) @@ -103,15 +95,14 @@ class Meta(): negative = fields.IntField(default=0) expiry = fields.IntField(default=604800) # 1 week in seconds finished = fields.BooleanField(default=False) - vote_ladder_id = fields.OneToOneField('models.GuildVoteLadder') + vote_ladder_id = fields.OneToOneField("models.GuildVoteLadder") # Staff Tables class StaffTag(Model): - - class Meta(): + class Meta: table = "tag" tag_id = fields.IntField(pk=True) @@ -121,8 +112,7 @@ class Meta(): class StaffNote(Model): - - class Meta(): + class Meta: table = "note" note_id = fields.IntField(pk=True) @@ -133,8 +123,7 @@ class Meta(): class StaffMonitorUser(Model): - - class Meta(): + class Meta: table = "monitor_user" monitor_user_id = fields.IntField(pk=True) @@ -142,21 +131,19 @@ class Meta(): class StaffMonitorMessageGroups(Model): - - class Meta(): + class Meta: table = "monitor_message_groups" group_id = fields.IntField(pk=True) name = fields.CharField(max_length=256) disabled = fields.BooleanField(default=False) - monitor_messages: fields.ManyToManyRelation[ - "StaffMonitorMessage"] = fields.ManyToManyField( - 'models.StaffMonitorMessage', related_name="groups") + monitor_messages: fields.ManyToManyRelation["StaffMonitorMessage"] = ( + fields.ManyToManyField("models.StaffMonitorMessage", related_name="groups") + ) class StaffMonitorMessage(Model): - - class Meta(): + class Meta: table = "monitor_message" monitor_message_id = fields.IntField(pk=True) @@ -165,8 +152,7 @@ class Meta(): class StaffFilter(Model): - - class Meta(): + class Meta: table = "filter" filter_id = fields.IntField(pk=True) @@ -175,8 +161,7 @@ class Meta(): class StaffReaction(Model): - - class Meta(): + class Meta: table = "reaction" reaction_id = fields.IntField(pk=True) @@ -185,20 +170,18 @@ class Meta(): class StaffButtonRole(Model): - - class Meta(): + class Meta: table = "buttonrole" button_role_id = fields.IntField(pk=True) emoji_id = fields.BigIntField() label = fields.CharField(max_length=256) role_ids = ArrayField() - reaction_id = fields.OneToOneField('models.StaffReaction') + reaction_id = fields.OneToOneField("models.StaffReaction") class StaffPunishment(Model): - - class Meta(): + class Meta: table = "punishment" punishment_id = fields.IntField(pk=True) @@ -218,8 +201,7 @@ class Meta(): class HelperMessage(Model): - - class Meta(): + class Meta: table = "helper_message" helper_message_id = fields.IntField(pk=True) @@ -232,8 +214,7 @@ class Meta(): class MemberRole(Model): - - class Meta(): + class Meta: table = "member_role" user_id = fields.BigIntField(pk=True) @@ -241,8 +222,7 @@ class Meta(): class MemberOpt(Model): - - class Meta(): + class Meta: table = "member_opt" opt_id = fields.IntField(pk=True) @@ -251,8 +231,7 @@ class Meta(): class MemberReminder(Model): - - class Meta(): + class Meta: table = "member_reminder" reminder_id = fields.IntField(pk=True) diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..b95350d --- /dev/null +++ b/pdm.lock @@ -0,0 +1,560 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:1512bcd81771060f91e176845e8f22030ef0eccaef566ffb985a6a6b474092e0" + +[[package]] +name = "aerich" +version = "0.7.2" +requires_python = ">=3.7,<4.0" +summary = "A database migrations tool for Tortoise ORM." +groups = ["default"] +dependencies = [ + "click", + "dictdiffer", + "pydantic", + "tomlkit", + "tortoise-orm", +] +files = [ + {file = "aerich-0.7.2-py3-none-any.whl", hash = "sha256:84c78c07d45436b89ca4db5411eca4e9292a591fb7d6fd4282fa4a7d0c6d2af1"}, + {file = "aerich-0.7.2.tar.gz", hash = "sha256:31d67de7b96184636b89de99062e059e5e6204b6251d24c33eb21fc9cf982e09"}, +] + +[[package]] +name = "aiohttp" +version = "3.9.5" +requires_python = ">=3.8" +summary = "Async http client/server framework (asyncio)" +groups = ["default"] +dependencies = [ + "aiosignal>=1.1.2", + "attrs>=17.3.0", + "frozenlist>=1.1.1", + "multidict<7.0,>=4.5", + "yarl<2.0,>=1.0", +] +files = [ + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +requires_python = ">=3.7" +summary = "aiosignal: a list of registered asynchronous callbacks" +groups = ["default"] +dependencies = [ + "frozenlist>=1.1.0", +] +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[[package]] +name = "aiosqlite" +version = "0.17.0" +requires_python = ">=3.6" +summary = "asyncio bridge to the standard sqlite3 module" +groups = ["default"] +dependencies = [ + "typing-extensions>=3.7.2", +] +files = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "async-lru" +version = "2.0.4" +requires_python = ">=3.8" +summary = "Simple LRU cache for asyncio" +groups = ["default"] +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +requires_python = ">=3.7" +summary = "Timeout context manager for asyncio programs" +groups = ["default"] +marker = "python_version < \"3.12.0\"" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncpg" +version = "0.29.0" +requires_python = ">=3.8.0" +summary = "An asyncio PostgreSQL driver" +groups = ["default"] +dependencies = [ + "async-timeout>=4.0.3; python_version < \"3.12.0\"", +] +files = [ + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default"] +marker = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "dictdiffer" +version = "0.9.0" +summary = "Dictdiffer is a library that helps you to diff and patch dictionaries." +groups = ["default"] +files = [ + {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, + {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, +] + +[[package]] +name = "discord-py" +version = "2.4.0" +requires_python = ">=3.8" +summary = "A Python wrapper for the Discord API" +groups = ["default"] +dependencies = [ + "aiohttp<4,>=3.7.4", +] +files = [ + {file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"}, + {file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"}, +] + +[[package]] +name = "frozenlist" +version = "1.4.1" +requires_python = ">=3.8" +summary = "A list-like structure which implements collections.abc.MutableSequence" +groups = ["default"] +files = [ + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "idna" +version = "3.7" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iso8601" +version = "1.1.0" +requires_python = ">=3.6.2,<4.0" +summary = "Simple module to parse ISO 8601 dates" +groups = ["default"] +files = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] + +[[package]] +name = "multidict" +version = "6.0.5" +requires_python = ">=3.7" +summary = "multidict implementation" +groups = ["default"] +files = [ + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "pydantic" +version = "2.7.1" +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.4.0", + "pydantic-core==2.18.2", + "typing-extensions>=4.6.1", +] +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[[package]] +name = "pypika-tortoise" +version = "0.1.6" +requires_python = ">=3.7,<4.0" +summary = "Forked from pypika and streamline just for tortoise-orm" +groups = ["default"] +files = [ + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, +] + +[[package]] +name = "pytz" +version = "2024.1" +summary = "World timezone definitions, modern and historical" +groups = ["default"] +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "ruff" +version = "0.5.0" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, + {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, + {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, + {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, + {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, + {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, + {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, + {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, + {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python Library for Tom's Obvious, Minimal Language" +groups = ["default"] +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.5" +requires_python = ">=3.7" +summary = "Style preserving TOML library" +groups = ["default"] +files = [ + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, +] + +[[package]] +name = "tortoise-orm" +version = "0.21.3" +requires_python = "<4.0,>=3.8" +summary = "Easy async ORM for python, built with relations in mind" +groups = ["default"] +dependencies = [ + "aiosqlite<0.18.0,>=0.16.0", + "iso8601<2.0.0,>=1.0.2", + "pydantic!=2.7.0,<3.0,>=2.0", + "pypika-tortoise<0.2.0,>=0.1.6", + "pytz", +] +files = [ + {file = "tortoise_orm-0.21.3-py3-none-any.whl", hash = "sha256:9b8f8f8ba23a51f3407bfdc76cf9b2e5bc901ff07c7bec71250a83fa7724dab4"}, + {file = "tortoise_orm-0.21.3.tar.gz", hash = "sha256:d6e3a627915d4037d312f6ca0cb7d0bf6593630cf1da466df60c7c4c3128398e"}, +] + +[[package]] +name = "tortoise-orm" +version = "0.21.3" +extras = ["asyncpg"] +requires_python = "<4.0,>=3.8" +summary = "Easy async ORM for python, built with relations in mind" +groups = ["default"] +dependencies = [ + "asyncpg", + "tortoise-orm==0.21.3", +] +files = [ + {file = "tortoise_orm-0.21.3-py3-none-any.whl", hash = "sha256:9b8f8f8ba23a51f3407bfdc76cf9b2e5bc901ff07c7bec71250a83fa7724dab4"}, + {file = "tortoise_orm-0.21.3.tar.gz", hash = "sha256:d6e3a627915d4037d312f6ca0cb7d0bf6593630cf1da466df60c7c4c3128398e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "yarl" +version = "1.9.4" +requires_python = ">=3.7" +summary = "Yet another URL library" +groups = ["default"] +dependencies = [ + "idna>=2.0", + "multidict>=4.0", +] +files = [ + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] diff --git a/pyproject.toml b/pyproject.toml index 5b54e7f..349fe74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,3 +2,31 @@ tortoise_orm = "db.db.TORTOISE_ORM" location = "db/migrations" src_folder = "./." + +[tool.pdm] +distribution = false + +[tool.pdm.dev-dependencies] +dev = [ + "ruff>=0.4.4", +] +[project] +name = "ib.py" +version = "0.1.0" +description = "Discord bot for International Baccalaureate Discord server." +authors = [ + {name = "ib-ai", email = ""}, +] +dependencies = [ + "discord-py>=2.3.2", + "async-lru>=2.0.4", + "toml>=0.10.2", + "aerich>=0.7.2", + "tortoise-orm[asyncpg]>=0.20.1", +] +requires-python = ">=3.11" +readme = "README.md" +license = {text = "GPL-3.0-only"} + +[tool.ruff] +line-length = 95 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100755 index 33863ec..0000000 --- a/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -aerich==0.7.1 -aiohttp==3.8.3 -aiosignal==1.3.1 -aiosqlite==0.17.0 -async-lru==1.0.3 -async-timeout==4.0.2 -asyncpg==0.27.0 -attrs==22.1.0 -charset-normalizer==2.1.1 -click==8.1.3 -dictdiffer==0.9.0 -discord.py==2.1.0 -emojis==0.7.0 -frozenlist==1.3.3 -idna==3.4 -iso8601==1.1.0 -multidict==6.0.3 -pydantic==1.10.2 -pypika-tortoise==0.1.6 -python-dateutil==2.8.2 -pytz==2022.6 -six==1.16.0 -SQLAlchemy==1.3.24 -tomlkit==0.11.6 -tortoise-orm==0.19.2 -typing_extensions==4.4.0 -yarl==1.8.2 diff --git a/utils/checks.py b/utils/checks.py index f4e367f..7a259cd 100644 --- a/utils/checks.py +++ b/utils/checks.py @@ -1,22 +1,25 @@ -from functools import cache from discord.ext import commands from db.cached import get_guild_data + def cogify(check): return lambda self, ctx: check.predicate(ctx) + async def is_moderator(ctx: commands.Context): assert await commands.guild_only().predicate(ctx) guild_data = await get_guild_data(ctx.guild.id) moderator_role_id = guild_data.moderator_id if guild_data else None - return any(role.id == moderator_role_id for role in ctx.author.roles) if moderator_role_id else False + return ( + any(role.id == moderator_role_id for role in ctx.author.roles) + if moderator_role_id + else False + ) def admin_command(): return commands.has_guild_permissions(manage_guild=True) + def staff_command(): - return commands.check_any( - commands.check(is_moderator), - admin_command() - ) + return commands.check_any(commands.check(is_moderator), admin_command()) diff --git a/utils/commands.py b/utils/commands.py index 82f32de..f149162 100644 --- a/utils/commands.py +++ b/utils/commands.py @@ -1,6 +1,7 @@ import logging from discord.ext import commands + async def available_subcommands(ctx: commands.Context): """ Send a list of available subcommands in a group command. @@ -10,7 +11,10 @@ async def available_subcommands(ctx: commands.Context): try: usable = await cmd.can_run(ctx) if usable: - subcmds.append('`'+cmd.name+'`') + subcmds.append("`" + cmd.name + "`") except commands.CommandError: - logging.debug(f'Command "{cmd.name}" check threw error, discarded in {ctx.command.name} group subcommand list.', exc_info=True) + logging.debug( + f'Command "{cmd.name}" check threw error, discarded in {ctx.command.name} group subcommand list.', + exc_info=True, + ) await ctx.send(f'Available subcommands: {", ".join(subcmds)}.') diff --git a/utils/config.py b/utils/config.py index 746d0c7..6543441 100644 --- a/utils/config.py +++ b/utils/config.py @@ -3,14 +3,22 @@ from pathlib import Path _path = Path(__file__).parent / "../config.json" -_config = json.load(open(_path, 'r')) +_config = json.load(open(_path, "r")) # Create global config (override with environment variables) -token = os.getenv("TOKEN") if "TOKEN" in os.environ else _config['token'] -prefix = os.getenv("PREFIX") if "PREFIX" in os.environ else _config['prefix'] -description = os.getenv("DESCRIPTION") if "DESCRIPTION" in os.environ else _config['description'] -application_id = os.getenv("APPLICATION_ID") if "APPLICATION_ID" in os.environ else _config['application_id'] -db_host = os.getenv("DB_HOST") if "DB_HOST" in os.environ else _config['db_host'] -db_user = os.getenv("DB_USER") if "DB_USER" in os.environ else _config['db_user'] -db_name = os.getenv("DB_NAME") if "DB_NAME" in os.environ else _config['db_name'] -db_password = os.getenv("DB_PASSWORD") if "DB_PASSWORD" in os.environ else _config['db_password'] \ No newline at end of file +token = os.getenv("TOKEN") if "TOKEN" in os.environ else _config["token"] +prefix = os.getenv("PREFIX") if "PREFIX" in os.environ else _config["prefix"] +description = ( + os.getenv("DESCRIPTION") if "DESCRIPTION" in os.environ else _config["description"] +) +application_id = ( + os.getenv("APPLICATION_ID") + if "APPLICATION_ID" in os.environ + else _config["application_id"] +) +db_host = os.getenv("DB_HOST") if "DB_HOST" in os.environ else _config["db_host"] +db_user = os.getenv("DB_USER") if "DB_USER" in os.environ else _config["db_user"] +db_name = os.getenv("DB_NAME") if "DB_NAME" in os.environ else _config["db_name"] +db_password = ( + os.getenv("DB_PASSWORD") if "DB_PASSWORD" in os.environ else _config["db_password"] +) diff --git a/utils/converters.py b/utils/converters.py index 014c74c..d74a29c 100644 --- a/utils/converters.py +++ b/utils/converters.py @@ -12,19 +12,17 @@ class IndexConverter(commands.Converter): - async def convert(self, ctx: commands.Context, arg: str) -> int: """ Checks if provided index is a valid and positive integer. """ n = int(arg) if n <= 0: - raise commands.BadArgument('Index must be a positive integer.') + raise commands.BadArgument("Index must be a positive integer.") return n class DatetimeConverter(commands.Converter): - async def convert(self, ctx: commands.Context, arg: str) -> datetime: """ Convert string (timestamp or duration into the future) into a python datetime object. @@ -32,28 +30,27 @@ async def convert(self, ctx: commands.Context, arg: str) -> datetime: now = timezone.now() now_timestamp = now.timestamp() try: - match = re.search('', arg) + match = re.search("", arg) if match is None: timestamp = int(arg) else: timestamp = int(match.group(1)) if timestamp < now_timestamp: raise commands.BadArgument( - "Timestamp cannot correspond to a time in the past.") - return datetime.fromtimestamp(timestamp, - tz=timezone.get_default_timezone()) - except (ValueError, OSError, OverflowError) as e: - logger.debug('Direct timestamp conversion failed.') + "Timestamp cannot correspond to a time in the past." + ) + return datetime.fromtimestamp(timestamp, tz=timezone.get_default_timezone()) + except (ValueError, OSError, OverflowError): + logger.debug("Direct timestamp conversion failed.") try: delta = parse_time(arg) return now + delta - except (ValueError, KeyError) as e: - raise commands.BadArgument('Invalid duration format.') + except (ValueError, KeyError): + raise commands.BadArgument("Invalid duration format.") class RegexConverter(commands.Converter): - async def convert(self, ctx: commands.Context, arg: str) -> str: """ Checks if provided regex pattern is valid. @@ -61,22 +58,19 @@ async def convert(self, ctx: commands.Context, arg: str) -> str: try: re.compile(arg) except re.error: - raise commands.BadArgument( - "The regex pattern provided is invalid.") + raise commands.BadArgument("The regex pattern provided is invalid.") return arg class ListConverter(commands.Converter): - - async def convert(self, ctx: commands.Context, - argument: str) -> list[int] | str: + async def convert(self, ctx: commands.Context, argument: str) -> list[int] | str: """ Checks if provided list can be separated and parsed. """ - if argument == '*': - return '*' + if argument == "*": + return "*" try: - return list(map(int, re.split('\s*[,;\s]\s*', argument))) + return list(map(int, re.split("\s*[,;\s]\s*", argument))) except ValueError: raise commands.BadArgument("The list provided is invalid.") diff --git a/utils/misc.py b/utils/misc.py index 100650b..fb0d8f5 100644 --- a/utils/misc.py +++ b/utils/misc.py @@ -1,13 +1,17 @@ def ordinal(n: int) -> str: if n < 0: - raise ValueError('Negative ordinal - ordinal must be passed a non-negative integer.') + raise ValueError("Negative ordinal - ordinal must be passed a non-negative integer.") - if n%100 in (11, 12, 13): return f'{n}th' - if n%10 == 1: return f'{n}st' - if n%10 == 2: return f'{n}nd' - if n%10 == 3: return f'{n}rd' - return f'{n}th' + if n % 100 in (11, 12, 13): + return f"{n}th" + if n % 10 == 1: + return f"{n}st" + if n % 10 == 2: + return f"{n}nd" + if n % 10 == 3: + return f"{n}rd" + return f"{n}th" def truncate(text: str, limit: int = 2000) -> str: - return text[:limit-3] + '...' if len(text) > limit else text + return text[: limit - 3] + "..." if len(text) > limit else text diff --git a/utils/pagination.py b/utils/pagination.py index 48ca021..bd626b5 100644 --- a/utils/pagination.py +++ b/utils/pagination.py @@ -8,18 +8,19 @@ from utils.misc import truncate - NAME_SIZE_LIMIT = 256 VALUE_SIZE_LIMIT = 1024 + + def paginated_embed_menus( - names: Collection[str], - values: Collection[str], - pagesize: int = 10, - *, - inline: Union[Collection[bool], bool] = False, - embed_dict: Optional[dict] = None, - empty_desc: str = 'No entries found.', - ) -> Collection[discord.Embed]: + names: Collection[str], + values: Collection[str], + pagesize: int = 10, + *, + inline: Union[Collection[bool], bool] = False, + embed_dict: Optional[dict] = None, + empty_desc: str = "No entries found.", +) -> Collection[discord.Embed]: """ Generates embeds for a paginated embed view. @@ -39,23 +40,31 @@ def paginated_embed_menus( Description to be set when names/values is empty. """ N = len(names) - if N != len(values): raise ValueError('names and values for paginated embed menus must be of equal length.') + if N != len(values): + raise ValueError("names and values for paginated embed menus must be of equal length.") if isinstance(inline, bool): - inline = [inline]*N - elif N != len(inline): raise ValueError('"inline" must be boolean or a collection of booleans of equal length to names/values for paginated embed menus.') + inline = [inline] * N + elif N != len(inline): + raise ValueError( + '"inline" must be boolean or a collection of booleans of equal length to names/values for paginated embed menus.' + ) if embed_dict: - if 'title' in embed_dict and len(embed_dict['title']) > 256: raise ValueError('title cannot be over 256 characters') - if 'description' in embed_dict and len(embed_dict['description']) > 4096: raise ValueError('description cannot be over 4096 characters') - if 'footer' in embed_dict: raise ValueError('embed_dict "footer" key must not be set.') - if 'fields' in embed_dict: raise ValueError('embed_dict "fields" key must not be set.') + if "title" in embed_dict and len(embed_dict["title"]) > 256: + raise ValueError("title cannot be over 256 characters") + if "description" in embed_dict and len(embed_dict["description"]) > 4096: + raise ValueError("description cannot be over 4096 characters") + if "footer" in embed_dict: + raise ValueError('embed_dict "footer" key must not be set.') + if "fields" in embed_dict: + raise ValueError('embed_dict "fields" key must not be set.') else: embed_dict = { # default - 'description': 'Here is a list of entries.' + "description": "Here is a list of entries." } - + if N == 0: - embed_dict['description'] = empty_desc + embed_dict["description"] = empty_desc return [discord.Embed.from_dict(embed_dict)] embeds: Collection[discord.Embed] = [] @@ -63,7 +72,9 @@ def paginated_embed_menus( pages = 1 items = 0 for name, value, inline_field in zip(names, values, inline): - if items == pagesize or len(current) + len(name) + len(value) > 5090: # leave 10 chars for footers + if ( + items == pagesize or len(current) + len(name) + len(value) > 5090 + ): # leave 10 chars for footers embeds.append(current) current = discord.Embed.from_dict(embed_dict) pages += 1 @@ -84,6 +95,7 @@ class PaginationView(ui.View): """ A class that handles pagination of embeds using Discord buttons. """ + def __init__(self, ctx: commands.Context, embeds: Collection[discord.Embed]): """ Parameters @@ -98,12 +110,12 @@ def __init__(self, ctx: commands.Context, embeds: Collection[discord.Embed]): current_page : int Current page index. """ - super().__init__(timeout = 60) + super().__init__(timeout=60) self.ctx = ctx self.embeds = embeds self.current_page = 0 - @ui.button(emoji=u"\u23EA", style=discord.ButtonStyle.blurple) + @ui.button(emoji="\u23ea", style=discord.ButtonStyle.blurple) async def first_page(self, interaction: discord.Interaction, _): """ Goes to the first page. @@ -111,8 +123,8 @@ async def first_page(self, interaction: discord.Interaction, _): self.current_page = 0 self.update_buttons() await self.update_view(interaction) - - @ui.button(emoji=u"\u2B05", style=discord.ButtonStyle.blurple) + + @ui.button(emoji="\u2b05", style=discord.ButtonStyle.blurple) async def before_page(self, interaction: discord.Interaction, _): """ Goes to the previous page. @@ -121,8 +133,8 @@ async def before_page(self, interaction: discord.Interaction, _): self.current_page -= 1 self.update_buttons() await self.update_view(interaction) - - @ui.button(emoji=u"\u27A1", style=discord.ButtonStyle.blurple) + + @ui.button(emoji="\u27a1", style=discord.ButtonStyle.blurple) async def next_page(self, interaction: discord.Interaction, _): """ Goes to the next page. @@ -131,8 +143,8 @@ async def next_page(self, interaction: discord.Interaction, _): self.current_page += 1 self.update_buttons() await self.update_view(interaction) - - @ui.button(emoji=u"\u23E9", style=discord.ButtonStyle.blurple) + + @ui.button(emoji="\u23e9", style=discord.ButtonStyle.blurple) async def last_page(self, interaction: discord.Interaction, _): """ Goes to the last page. @@ -145,7 +157,8 @@ def update_buttons(self): """ Updates the buttons based on the current page. """ - for i in self.children: i.disabled = False + for i in self.children: + i.disabled = False if self.current_page == 0: self.children[0].disabled = True self.children[1].disabled = True @@ -158,11 +171,12 @@ async def update_view(self, interaction: discord.Interaction): Updates the embed and view. """ await interaction.response.edit_message( - embed = self.embeds[self.current_page], - view = self + embed=self.embeds[self.current_page], view=self ) - async def return_paginated_embed_view(self) -> tuple[discord.Embed, discord.ui.View | None]: + async def return_paginated_embed_view( + self, + ) -> tuple[discord.Embed, discord.ui.View | None]: """ Returns the first embed and containing view. """ @@ -170,6 +184,6 @@ async def return_paginated_embed_view(self) -> tuple[discord.Embed, discord.ui.V no_data_embed = discord.Embed(description="No data available.") return [no_data_embed, None] - self.update_buttons() # Disable buttons if there's only one embed + self.update_buttons() # Disable buttons if there's only one embed return self.embeds[self.current_page], self diff --git a/utils/time.py b/utils/time.py index be4bba2..349f24e 100644 --- a/utils/time.py +++ b/utils/time.py @@ -4,8 +4,12 @@ from tortoise import timezone -DEGENERACY_DELAY = timedelta(seconds=1) # when datetime is in the past, a small amount of time is slept for regardless +DEGENERACY_DELAY = timedelta( + seconds=1 +) # when datetime is in the past, a small amount of time is slept for regardless MAX_DELTA = timedelta(days=40) # asyncio.sleep is faulty for longer periods of time + + async def long_sleep_until(terminus: datetime): """ Sleep until the datetime object given. @@ -21,40 +25,44 @@ async def long_sleep_until(terminus: datetime): ENDMONTH_BUFFER = timedelta(days=5) -CODES = {'w': 'weeks', 'd': 'days', 'h': 'hours', 'm': 'minutes', 's': 'seconds'} +CODES = {"w": "weeks", "d": "days", "h": "hours", "m": "minutes", "s": "seconds"} + + def parse_time(s: str) -> timedelta: """ Convert short-hand duration time string into a python timedelta object. """ - if not re.fullmatch('(?:\d+\D+)*', s): + if not re.fullmatch("(?:\d+\D+)*", s): raise ValueError() total_delta = timedelta() - for value, key in re.findall('(\d+)(\D+)', s): + for value, key in re.findall("(\d+)(\D+)", s): value = int(value) - if key.startswith('y'): + if key.startswith("y"): # python timedelta objects don't work with years # implemented this way to preserve day and month of year now = timezone.now() + total_delta - if now.month == 2 and now.day == 29: # for when you add 1 year to the 29th of February - now -= ENDMONTH_BUFFER # subtract a few days so datetime initialization doesn't throw errors + if ( + now.month == 2 and now.day == 29 + ): # for when you add 1 year to the 29th of February + now -= ENDMONTH_BUFFER # subtract a few days so datetime initialization doesn't throw errors for i in range(value): next = datetime( - year = now.year + 1, - month = now.month, - day = now.day, - hour = now.hour, - minute = now.minute, - second = now.second, - microsecond = now.microsecond, - tzinfo = timezone.get_default_timezone(), + year=now.year + 1, + month=now.month, + day=now.day, + hour=now.hour, + minute=now.minute, + second=now.second, + microsecond=now.microsecond, + tzinfo=timezone.get_default_timezone(), ) total_delta += next - now now = next - elif key.startswith('mo'): + elif key.startswith("mo"): # python timedelta objects don't work with months # implemented this way to preserve the day of month now = timezone.now() + total_delta - if now.day >= 28: # for when you add 1 month to the 31st of January, and similar + if now.day >= 28: # for when you add 1 month to the 31st of January, and similar now -= ENDMONTH_BUFFER # subtract a few days so datetime initialization doesn't throw errors for i in range(value): year = now.year @@ -63,18 +71,18 @@ def parse_time(s: str) -> timedelta: month = 0 year += 1 next = datetime( - year = year, - month = month + 1, - day = now.day, - hour = now.hour, - minute = now.minute, - second = now.second, - microsecond = now.microsecond, - tzinfo = timezone.get_default_timezone(), + year=year, + month=month + 1, + day=now.day, + hour=now.hour, + minute=now.minute, + second=now.second, + microsecond=now.microsecond, + tzinfo=timezone.get_default_timezone(), ) total_delta += next - now now = next else: # remaining cases are handled with "codes" dictionary total_delta += timedelta(**{CODES[key[0]]: value}) - return total_delta \ No newline at end of file + return total_delta