diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..428bd6f --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,42 @@ +## Contributing to discord.py +[comment]: <> (Thanks to Rapptz -maybe, this is where i took inspiration from- for making this template I've adapted for my bot) + +First off, thanks for taking the time to contribute. It makes the bot substantially better. :+1: + +The following is a set of guidelines for contributing to the repository. These are guidelines, not hard rules. + +## This is too much to read! I want to ask a question! + +Generally speaking questions are better suited in our resources below. + +- The official support server: https://discord.gg/m9UsWuaYDT +- The GitHub issues page: https://github.com/SpaceBot-Development-Team/space/issues + +Please try your best not to ask questions in our issue tracker. Most of them don't belong there unless they provide value to a larger audience. + +## Good Bug Reports + +Please be aware of the following things when filing bug reports. + +1. Don't open duplicate issues. Please search your issue to see if it has been asked already. Duplicate issues will be closed. +2. When filing a bug about exceptions or tracebacks, please include the *complete* traceback. Without the complete traceback the issue might be **unsolvable** and you will be asked to provide more information. +3. Make sure to provide enough information to make the issue workable. The issue template will generally walk you through the process but they are enumerated here as well: + - A **summary** of your bug report. This is generally a quick sentence or two to describe the issue in human terms. + - Guidance on **how to reproduce the issue**. Ideally, this should have a small set of steps on how to reproduce this bug. + - Tell us **what you expected to happen**. That way we can meet that expectation. + - Tell us **what actually happens**. What ends up happening in reality? It's not helpful to say "it fails" or "it doesn't work". Say *how* it failed, does it hang? How are the expectations different from reality? + +If the bug report is missing this information then it'll take us longer to fix the issue. We will probably ask for clarification, and barring that if no response was given then the issue will be closed. + +## Submitting a Pull Request + +Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows PEP-8 guidelines (mostly) with a column limit of 125. + +### Git Commit Guidelines + +- Use present tense (e.g. "Add feature" not "Added feature") +- Limit all lines to 72 characters or less. +- Reference issues or pull requests outside of the first line. + - Please use the shorthand `#123` and not the full URL. + +If you do not meet any of these guidelines, don't fret. Chances are they will be fixed upon rebasing but please do try to meet them to remove some of the workload. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..43826f9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +buy_me_a_coffee: dev_anony +patreon: Developer_Anonymous +github: SpaceBot-Development-Team diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..694b9c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug Report +description: Report broken or incorrect behaviour +labels: unconfirmed bug +body: + - type: markdown + attributes: + value: > + Thanks for taking the time to fill out a bug. + If you want real-time support, consider joining our Discord at https://discord.gg/m9UsWuaYDT instead. + + Please note that this form is for bugs only! + - type: input + attributes: + label: Summary + description: A simple summary of your bug report + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: > + What you did to make it happen. + validations: + required: true + - type: textarea + attributes: + label: Expected Results + description: > + What did you expect to happen? + validations: + required: true + - type: textarea + attributes: + label: Actual Results + description: > + What actually happened? + validations: + required: true + - type: checkboxes + attributes: + label: Checklist + description: > + Let's make sure you've properly done due diligence when reporting this issue! + options: + - label: I have searched the open issues for duplicates. + required: true + - type: textarea + attributes: + label: Additional Context + description: If there is anything else to say, please do so here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a490cf1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + about: Ask questions and discuss with other users of the library. + url: https://github.com/SpaceBot-Development-Team/space/discussions + - name: Discord Server + about: Use our official Discord server to ask for help and questions as well. + url: https://discord.gg/m9UsWuaYDT diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..c27a61a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,47 @@ +name: Feature Request +description: Suggest a feature for the bot +labels: feature request +body: + - type: input + attributes: + label: Summary + description: > + A short description of what your feature request is. + validations: + required: true + - type: dropdown + attributes: + multiple: false + label: What is this feature request for? + options: + - The bot + - The documentation + validations: + required: true + - type: textarea + attributes: + label: The Problem + description: > + What problem if your feature trying to solve? + What becomes easier or possible when this feature is implemented? + validations: + required: true + - type: textarea + attributes: + label: The Ideal Solution + description: > + What is your ideal solution to the problem? + What would you like this feature to do? + validations: + required: true + - type: textarea + attributes: + label: The Current Solution + description: > + What is the current solution to the problem, if any? + validations: + required: false + - type: textarea + attributes: + label: Additional Context + description: If there is anything else to say, please do so here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..15043b6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +### Summary + + + +## Checklist + + + +- [ ] If code changes were made, then they have been tested. +- [ ] This PR fixes an issue. +- [ ] This PR adds something new (e.g. new commands or modules). +- [ ] This PR is a breaking change (e.g. commands or modules renamed or removed). +- [ ] This PR is **not** a code change (e.g. documentation, docstrings, README, changelog, ...). +- [ ] I have searched for duplicates. +- [ ] If ``type: ignore``, ``pyright: ignore``, or similar, were used, then a comment explaining why was also left. + - [ ] I have formatted the code using ``black==25.1.0``. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ac7984c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +name: Lint + +on: + push: + pull_request: + types: [ opened, reopened, synchronize ] + +jobs: + check: + runs-on: ubuntu-latest + + name: check + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up CPython + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + id: install-deps + run: | + python -m pip install --upgrade pip setuptools wheel black==25.1.0 requests + pip install -U -r requirements.txt + + - name: Setup node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Run PyRight + uses: jakebailey/pyright-action@v1 + with: + version: '1.1.394' + warnings: false + no-comments: true + + - name: Run Black + if: ${{ always() && steps.install-deps.outcome == 'success' }} + run: | + black --check cogs store bot.py errors.py main.py paginator.py utils.py diff --git a/.gitignore b/.gitignore index 4482010..87f8bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,6 @@ cython_debug/ # Configuration files *.config + +# SSH +space-bot* diff --git a/README.md b/README.md index 352cf1c..8d841fb 100644 --- a/README.md +++ b/README.md @@ -9,5 +9,4 @@ through [this link](https://discord.com/api/oauth2/authorize?client_id=118850123 # Contributing to Development -The only requirement is to have a good understanding of Python and **know what you are writing, meaning that any attempt to create a vulnerability, joke changes, or anything that does not benefit** -**the bot will result in a contribution ban**. +For more information on how to contribute, look at [CONTRIBUTING.md](/.github/CONTRIBUTING.md) diff --git a/bot.py b/bot.py index 55fd554..fa7ab10 100644 --- a/bot.py +++ b/bot.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import datetime @@ -44,14 +45,14 @@ PREMIUM_SKU_ID: Final[int] = 1256218013930094682 log = logging.getLogger(__name__) + class LegacyBotContext(commands.Context["LegacyBot"]): - __slots__ = ( - '_cs_premium', - ) + __slots__ = ('_cs_premium',) guild: discord.Guild + me: discord.Member author: discord.Member - channel: discord.TextChannel | discord.VoiceChannel | discord.StageChannel | discord.ForumChannel + channel: discord.TextChannel | discord.VoiceChannel | discord.StageChannel | discord.Thread @property def reference(self) -> discord.MessageReference | None: @@ -61,7 +62,9 @@ def reference(self) -> discord.MessageReference | None: @property def resolved_reference(self) -> discord.Message | None: """:class:`discord.Message`: The resolved reference message, or ``None``.""" - return self.reference and (self.reference.resolved or self.reference.cached_message) # pyright: ignore[reportReturnType] + return self.reference and ( + self.reference.resolved or self.reference.cached_message + ) # pyright: ignore[reportReturnType] @property def session(self) -> aiohttp.ClientSession: @@ -103,13 +106,21 @@ async def is_premium(self) -> bool: return self._cs_premium if self.interaction: - prem = ( - (PREMIUM_SKU_ID in [e.sku_id for e in self.interaction.entitlements]) - or - (PREMIUM_SKU_ID in self.interaction.entitlement_sku_ids) + prem = (PREMIUM_SKU_ID in [e.sku_id for e in self.interaction.entitlements]) or ( + PREMIUM_SKU_ID in self.interaction.entitlement_sku_ids ) else: - prem = len([_ async for _ in self.bot.entitlements(user=self.author, guild=self.guild, skus=[discord.Object(PREMIUM_SKU_ID)])]) > 0 + prem = ( + len( + [ + _ + async for _ in self.bot.entitlements( + user=self.author, guild=self.guild, skus=[discord.Object(PREMIUM_SKU_ID)] + ) + ] + ) + > 0 + ) self._cs_premium = prem return self._cs_premium @@ -267,7 +278,8 @@ async def update_disabled_modules(self, guild_id: int, disabled_modules: list[st async with conn.transaction(): await conn.execute( 'UPDATE guilds SET disabled_modules = $1::text[] WHERE "id" = $2;', - disabled_modules, guild_id, + disabled_modules, + guild_id, ) self._disabled_modules[guild_id] = disabled_modules @@ -284,7 +296,8 @@ async def _create_or_cache_guild_config(self, guild_id: int) -> None: 'WITH inserted AS ' '(INSERT INTO guilds ("id", prefixes) VALUES ($1, $2::varchar[]) ON CONFLICT ("id") DO NOTHING RETURNING prefixes) ' 'SELECT prefixes FROM inserted UNION ALL SELECT prefixes FROM guilds WHERE "id" = $1 LIMIT 1;', - guild_id, ['?'], + guild_id, + ['?'], ) if data is None: @@ -409,8 +422,7 @@ async def on_guild_join(self, guild: discord.Guild) -> None: guild.id, ) await conn.execute( - 'INSERT INTO claimtimes_config (guild_id) VALUES ($1) ON CONFLICT (id) DO NOTHING;', - guild.id + 'INSERT INTO claimtimes_config (guild_id) VALUES ($1) ON CONFLICT (id) DO NOTHING;', guild.id ) # ensure context manager closure pass @@ -455,17 +467,13 @@ async def on_command_error(self, context: LegacyBotContext, error: commands.Comm embed.description = f'You need the {r} role to execute this command!' elif isinstance(error, commands.BotMissingPermissions): fmt = discord.utils._human_join( - [m.replace('_', ' ').replace('guild', 'server').title() - for m in error.missing_permissions - ], + [m.replace('_', ' ').replace('guild', 'server').title() for m in error.missing_permissions], final='and', ) embed.description = f'I need {fmt} permissions to execute this command!' elif isinstance(error, commands.MissingPermissions): fmt = discord.utils._human_join( - [m.replace('_', ' ').replace('guild', 'server').title() - for m in error.missing_permissions - ], + [m.replace('_', ' ').replace('guild', 'server').title() for m in error.missing_permissions], final='and', ) embed.description = f'You need {fmt} permissions to execute this command!' @@ -504,7 +512,9 @@ async def on_command_error(self, context: LegacyBotContext, error: commands.Comm elif isinstance(error, commands.ThreadNotFound): embed.description = f'Thread with name or ID "{error.argument}" was not found!' elif isinstance(error, commands.ChannelNotReadable): - embed.description = f'I do not have Read Messages permissions on {error.argument.mention}, and I need it to execute the command!' + embed.description = ( + f'I do not have Read Messages permissions on {error.argument.mention}, and I need it to execute the command!' + ) elif isinstance(error, commands.ChannelNotFound): embed.description = f'Channel with name or ID "{error.argument}" was not found!' elif isinstance(error, commands.UserNotFound): @@ -537,7 +547,9 @@ async def on_command_error(self, context: LegacyBotContext, error: commands.Comm elif isinstance(error, commands.PrivateMessageOnly): embed.description = 'This command can only be used on private messages!' elif isinstance(error, commands.BadLiteralArgument): - embed.description = f'"{error.argument}" is not a valid choice available in {discord.utils._human_join(error.literals)}' + embed.description = ( + f'"{error.argument}" is not a valid choice available in {discord.utils._human_join(error.literals)}' + ) elif isinstance(error, commands.BadUnionArgument): embed.description = f'"{error.param.name}" value was not valid!' elif isinstance(error, commands.ExpectedClosingQuoteError): @@ -550,7 +562,9 @@ async def on_command_error(self, context: LegacyBotContext, error: commands.Comm embed.description = 'You are missing one attachment to execute this command!' elif isinstance(error, commands.MissingRequiredArgument): assert context.command - embed.description = f'`{error.param.name}` is missing! Make sure you follow the command syntax: `{context.command.signature}`' + embed.description = ( + f'`{error.param.name}` is missing! Make sure you follow the command syntax: `{context.command.signature}`' + ) elif isinstance(error, ModuleDisabled): embed.description = str(error) else: @@ -563,7 +577,7 @@ async def on_command_error(self, context: LegacyBotContext, error: commands.Comm if send_debug_log: await self.send_debug_message( embed=discord.Embed( - title=f'An unknown error occurred on {context.command.name}', + title=f'An unknown error occurred on {context.command.name if context.command else "a non-command context"}', description=f'Executed by: {context.author} ({context.author.id})\nExecution date: {discord.utils.format_dt(context.created_at)}', colour=discord.Colour.red(), ).add_field( diff --git a/cogs/config.py b/cogs/config.py index b1c65dc..c027b58 100644 --- a/cogs/config.py +++ b/cogs/config.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio @@ -51,27 +52,33 @@ class ConfigRecord(TypedDict): messages_enabled: bool disabled_modules: list[str] + PREMIUM_SKU_ID = 1256218013930094682 DURATION_REGEX = re.compile(r'(\d{1,5}(?:[.,]?\d{1,5})?)([smhd])') def replace_greet_message_vars(string: str, member: discord.Member) -> str: - return string.replace( - "{mention}", - member.mention, - ).replace( - "{mc}", - str( - member.guild.member_count - or member.guild.approximate_member_count - or len(member.guild.members) - ), - ).replace( - '{server_name}', member.guild.name, - ).replace( - '{member(tag)}', member.name, - ).replace( - '{member(name)}', member.display_name, + return ( + string.replace( + "{mention}", + member.mention, + ) + .replace( + "{mc}", + str(member.guild.member_count or member.guild.approximate_member_count or len(member.guild.members)), + ) + .replace( + '{server_name}', + member.guild.name, + ) + .replace( + '{member(tag)}', + member.name, + ) + .replace( + '{member(name)}', + member.display_name, + ) ) @@ -198,7 +205,8 @@ async def save_prefixes(self, interaction: Interaction, button: discord.ui.Butto async with conn.transaction(): await conn.execute( 'UPDATE guilds SET prefixes = $1::varchar[] WHERE "id" = $2;', - self.view.record['prefixes'], interaction.guild_id, + self.view.record['prefixes'], + interaction.guild_id, ) interaction.client._guild_prefixes[interaction.guild_id] = self.view.record['prefixes'] @@ -237,7 +245,8 @@ async def edit_greet_message(self, interaction: Interaction, button: discord.ui. async with conn.transaction(): await conn.execute( 'UPDATE guilds SET greets = $1::jsonb WHERE "id" = $2;', - self.view.record['greets'], interaction.guild_id, + self.view.record['greets'], + interaction.guild_id, ) await interaction.edit_original_response(view=self.view.update_self()) @@ -284,7 +293,8 @@ async def edit_greet_delafter(self, interaction: Interaction, button: discord.ui async with conn.transaction(): await conn.execute( 'UPDATE guilds SET greets = $1::jsonb WHERE "id" = $2;', - self.view.record['greets'], interaction.guild_id, + self.view.record['greets'], + interaction.guild_id, ) await interaction.edit_original_response(view=self.view.update_self()) @@ -312,7 +322,7 @@ def __init__(self, record: asyncpg.Record, context: Context, *, premium: bool) - if prefixes is None: prefixes = self.record['prefixes'] = ['?'] - super().__init__(timeout=60*15) # 15 minutes + super().__init__(timeout=60 * 15) # 15 minutes self.info_container = discord.ui.Container(accent_colour=discord.Colour.blurple()) self.prefixes_action_row = ConfigViewPrefixesActionRow() @@ -328,7 +338,7 @@ def __init__(self, record: asyncpg.Record, context: Context, *, premium: bool) - async def on_timeout(self) -> None: for child in self.walk_children(): if hasattr(child, 'disabled'): - child.disabled = True + child.disabled = True # pyright: ignore[reportAttributeAccessIssue] if self.message: await self.message.edit(view=self) @@ -359,10 +369,12 @@ def get_greet_delafter(self) -> float: return ret def construct_buy_premium_view(self) -> discord.ui.View: - return discord.ui.View(timeout=.1).add_item(discord.ui.Button(sku_id=PREMIUM_SKU_ID)) + return discord.ui.View(timeout=0.1).add_item(discord.ui.Button(sku_id=PREMIUM_SKU_ID)) def update_premium(self, interaction: Interaction): - self.premium = (PREMIUM_SKU_ID in interaction.entitlement_sku_ids) or (discord.utils.get(interaction.entitlements, sku_id=PREMIUM_SKU_ID) is not None) + self.premium = (PREMIUM_SKU_ID in interaction.entitlement_sku_ids) or ( + discord.utils.get(interaction.entitlements, sku_id=PREMIUM_SKU_ID) is not None + ) def update_self(self) -> 'ConfigView': self.clear_items() @@ -389,18 +401,13 @@ def update_self(self) -> 'ConfigView': # -- prefixes -- self.info_container.add_item( discord.ui.TextDisplay( - ( - f'# Server Config\n\n' - f'**Prefixes:** {", ".join(f"``{pre}``" for pre in self.record["prefixes"])}' - ), + (f'# Server Config\n\n' f'**Prefixes:** {", ".join(f"``{pre}``" for pre in self.record["prefixes"])}'), ), ) self.info_container.add_item( self.prefixes_action_row, ) - self.info_container.add_item( - discord.ui.Separator(visible=True, spacing=discord.SeparatorSize.small) - ) + self.info_container.add_item(discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small)) # -- greet -- self.info_container.add_item( @@ -415,9 +422,7 @@ def update_self(self) -> 'ConfigView': self.info_container.add_item( discord.ui.ActionRow(ModifyGreetChannelsSelect(greets.keys(), self.premium)), ) - self.info_container.add_item( - discord.ui.Separator(visible=True, spacing=discord.SeparatorSize.small) - ) + self.info_container.add_item(discord.ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small)) # -- modules -- @@ -495,10 +500,7 @@ def __init__(self, default_channels: Iterable[str | int], premium: bool, /) -> N discord.ChannelType.private_thread, discord.ChannelType.voice, ], - default_values=[ - discord.SelectDefaultValue.from_channel(discord.Object(int(c))) - for c in default_channels - ], + default_values=[discord.SelectDefaultValue.from_channel(discord.Object(int(c))) for c in default_channels], min_values=0, max_values=10 if premium else 5, ) @@ -522,7 +524,8 @@ async def callback(self, interaction: Interaction) -> None: async with conn.transaction(): await conn.execute( 'UPDATE guilds SET greets = $1::jsonb WHERE "id" = $2;', - self.view.record['greets'], interaction.guild_id, + self.view.record['greets'], + interaction.guild_id, ) await interaction.edit_original_response(view=self.view.update_self()) @@ -571,14 +574,16 @@ class GConfigView(discord.ui.View): 'message', ) - def __init__(self, claimtime: asyncpg.Record, *, premium: bool, author: discord.abc.Snowflake, bot: LegacyBot, guild_id: int) -> None: + def __init__( + self, claimtime: asyncpg.Record, *, premium: bool, author: discord.abc.Snowflake, bot: LegacyBot, guild_id: int + ) -> None: self.claimtime: dict[str, Any] = dict(claimtime) self.premium: bool = premium self.author: discord.abc.Snowflake = author self.message: discord.Message | None = None self.bot: LegacyBot = bot self.guild_id: int = guild_id - super().__init__(timeout=60*15) + super().__init__(timeout=60 * 15) if claimtime["winmsg_enabled"] is True: self.enable_win_message.label = 'Disable' @@ -587,7 +592,7 @@ def __init__(self, claimtime: asyncpg.Record, *, premium: bool, author: discord. async def on_timeout(self) -> None: for child in self.walk_children(): if hasattr(child, 'disabled'): - child.disabled = True + child.disabled = True # pyright: ignore[reportAttributeAccessIssue] if self.message: await self.message.edit(view=self) @@ -620,7 +625,10 @@ def get_embed(self) -> discord.Embed: embed.add_field( name='Claimtimes:', value='\n'.join( - [f'<@&{role_id}>: {claimtime["time"]:.2f} seconds | Override: {claimtime["override"]}' for role_id, claimtime in self.claimtime["roles"].items()], + [ + f'<@&{role_id}>: {claimtime["time"]:.2f} seconds | Override: {claimtime["override"]}' + for role_id, claimtime in self.claimtime["roles"].items() + ], ), ) return embed @@ -646,7 +654,8 @@ async def edit_win_message(self, interaction: Interaction, button: discord.ui.Bu async with conn.transaction(): await conn.execute( 'INSERT INTO claimtimes_config (guild_id, win_message) VALUES ($1, $2) ON CONFLICT (guild_id) DO UPDATE SET win_message = EXCLUDED.win_message;', - interaction.guild_id, self.claimtime["win_message"] + interaction.guild_id, + self.claimtime["win_message"], ) await interaction.client.claimtime_store.load() @@ -676,7 +685,8 @@ async def enable_win_message(self, interaction: Interaction, button: discord.ui. async with conn.transaction(): await conn.execute( 'INSERT INTO claimtimes_config (guild_id, winmsg_enabled) VALUES ($1, $2) ON CONFLICT (guild_id) DO UPDATE SET winmsg_enabled = EXCLUDED.winmsg_enabled;', - interaction.guild_id, self.claimtime["winmsg_enabled"], + interaction.guild_id, + self.claimtime["winmsg_enabled"], ) await interaction.client.claimtime_store.load() @@ -764,7 +774,8 @@ async def role_select(self, interaction: Interaction, select: discord.ui.Select[ async with conn.transaction(): await conn.execute( 'INSERT INTO claimtimes_config (guild_id, roles) VALUES ($1, $2::jsonb) ON CONFLICT (guild_id) DO UPDATE SET roles = EXCLUDED.roles;', - guild_id, self.roles, + guild_id, + self.roles, ) await interaction.edit_original_response( @@ -813,16 +824,12 @@ async def role_select( await msg.delete() if msg.content.lower() == 'cancel': - await interaction.edit_original_response( - content='Cancelled.' - ) + await interaction.edit_original_response(content='Cancelled.') return matches = DURATION_REGEX.findall(msg.content.lower()) if not matches or len(matches) > 1: - await interaction.edit_original_response( - content='Invalid duration provided!' - ) + await interaction.edit_original_response(content='Invalid duration provided!') return time, fmt = matches[0] @@ -836,7 +843,7 @@ async def role_select( msg = await interaction.client.wait_for( 'message', check=lambda m: m.author.id == interaction.user.id and m.channel.id == interaction.channel_id, - timeout=60.0 + timeout=60.0, ) except asyncio.TimeoutError: await interaction.edit_original_response( @@ -852,9 +859,7 @@ async def role_select( elif low in ('no', 'n', 'false', 'f', '0', 'disable', 'off'): ret = False else: - await interaction.edit_original_response( - content='Invalid response provided! Defaulting to ``False``.' - ) + await interaction.edit_original_response(content='Invalid response provided! Defaulting to ``False``.') ret = False self.parent.claimtime["roles"][str(select.values[0].id)] = {"time": value, "override": ret} @@ -863,7 +868,8 @@ async def role_select( async with conn.transaction(): await conn.execute( 'INSERT INTO claimtimes_config (guild_id, roles) VALUES ($1, $2::jsonb) ON CONFLICT (guild_id) DO UPDATE SET roles = EXCLUDED.roles;', - interaction.guild_id, self.parent.claimtime["roles"], + interaction.guild_id, + self.parent.claimtime["roles"], ) await interaction.client.claimtime_store.load() @@ -919,10 +925,7 @@ async def get_guild_config(self, guild: discord.Guild) -> asyncpg.Record | None: async def insert_guild_config(self, guild: discord.Guild) -> asyncpg.Record: async with self.bot.get_connection() as conn: async with conn.transaction(): - row = await conn.fetchrow( - 'INSERT INTO guilds ("id") VALUES ($1) RETURNING *;', - guild.id - ) + row = await conn.fetchrow('INSERT INTO guilds ("id") VALUES ($1) RETURNING *;', guild.id) if row is None: raise RuntimeError('row returned none') return row @@ -1030,5 +1033,6 @@ async def gconfig(self, ctx: Context) -> None: async def setup(bot: LegacyBot) -> None: await bot.add_cog(Configuration(bot)) + async def teardown(bot: LegacyBot) -> None: await bot.remove_cog(Configuration.__cog_name__) diff --git a/cogs/giveaways.py b/cogs/giveaways.py index 01b375e..3af9f85 100644 --- a/cogs/giveaways.py +++ b/cogs/giveaways.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio @@ -58,9 +59,11 @@ class WinMessageVariablesData(TypedDict): host: discord.abc.User winner: discord.Member + DURATION_REGEX = re.compile(r'(\d{1,5}(?:[.,]?\d{1,5})?)([smhd])') log = logging.getLogger(__name__) + class WinnersConverter(commands.Converter[int]): __slots__ = () @@ -92,7 +95,7 @@ class Duration(commands.Converter["Duration"]): 'resolved', ) - CONVERT_MAP = {'d': 86400,'h': 3600, 'm': 60, 's': 1} + CONVERT_MAP = {'d': 86400, 'h': 3600, 'm': 60, 's': 1} if TYPE_CHECKING: relative_float: float @@ -124,26 +127,47 @@ def replace_vars(string: str, data: EmbedVariablesData, /) -> str: winner_list = ', '.join([f'<@{user}>' for user in data['winner_list']]) if data['winner_list'] else None host = data['host'] ends_at = data['ends_at'] - return string.replace( - '{prize}', data['prize'], - ).replace( - '{host(username)}', host.name, - ).replace( - '{host(mention)}', host.mention, - ).replace( - '{time_left}', discord.utils.format_dt(ends_at, 'R'), - ).replace( - '{end_time}', discord.utils.format_dt(ends_at, 'F'), - ).replace( - '{num_winners}', str(data['winner_amount']), - ).replace( - '{ends}', ends, - ).replace( - '{server_name}', data['guild'].name, - ).replace( - '{winner_list}', winner_list or 'Not decided', - ).replace( - '{winners}', winner_list if winner_list is not None else str(data['winner_amount']), + return ( + string.replace( + '{prize}', + data['prize'], + ) + .replace( + '{host(username)}', + host.name, + ) + .replace( + '{host(mention)}', + host.mention, + ) + .replace( + '{time_left}', + discord.utils.format_dt(ends_at, 'R'), + ) + .replace( + '{end_time}', + discord.utils.format_dt(ends_at, 'F'), + ) + .replace( + '{num_winners}', + str(data['winner_amount']), + ) + .replace( + '{ends}', + ends, + ) + .replace( + '{server_name}', + data['guild'].name, + ) + .replace( + '{winner_list}', + winner_list or 'Not decided', + ) + .replace( + '{winners}', + winner_list if winner_list is not None else str(data['winner_amount']), + ) ) @@ -152,26 +176,47 @@ def replace_win_message_vars(string: str, data: WinMessageVariablesData, /) -> s winner = data["winner"] created_at = winner.created_at joined_at = winner.joined_at or datetime.datetime.now(datetime.timezone.utc) - return string.replace( - '{claim_time}', f'{data["claimtime"]} seconds', - ).replace( - '{host(username)}', host.name, - ).replace( - '{host(mention)}', host.mention, - ).replace( - '{winner(username)}', winner.name, - ).replace( - '{winner(mention)}', winner.mention, - ).replace( - '{winner(created_ago)}', discord.utils.format_dt(created_at, 'R'), - ).replace( - '{winner(created_date)}', discord.utils.format_dt(created_at, 'f'), - ).replace( - '{winner(joined_ago)}', discord.utils.format_dt(joined_at, 'R'), - ).replace( - '{winner(joined_date)}', discord.utils.format_dt(joined_at, 'f'), - ).replace( - '{prize}', data["prize"], + return ( + string.replace( + '{claim_time}', + f'{data["claimtime"]} seconds', + ) + .replace( + '{host(username)}', + host.name, + ) + .replace( + '{host(mention)}', + host.mention, + ) + .replace( + '{winner(username)}', + winner.name, + ) + .replace( + '{winner(mention)}', + winner.mention, + ) + .replace( + '{winner(created_ago)}', + discord.utils.format_dt(created_at, 'R'), + ) + .replace( + '{winner(created_date)}', + discord.utils.format_dt(created_at, 'f'), + ) + .replace( + '{winner(joined_ago)}', + discord.utils.format_dt(joined_at, 'R'), + ) + .replace( + '{winner(joined_date)}', + discord.utils.format_dt(joined_at, 'f'), + ) + .replace( + '{prize}', + data["prize"], + ) ) @@ -225,7 +270,8 @@ async def callback(self, interaction: discord.Interaction[LegacyBot]) -> None: async with conn.transaction(): await conn.execute( 'DELETE FROM giveaway_participants WHERE message_id=$1 AND user_id=$2;', - self.message_id, interaction.user.id, + self.message_id, + interaction.user.id, ) await interaction.edit_original_response( content='You have left the giveaway!', @@ -288,7 +334,10 @@ async def callback(self, interaction: discord.Interaction[LegacyBot]) -> None: await conn.execute( 'INSERT INTO giveaway_participants (guild_id, channel_id, message_id, user_id) VALUES ' '($1, $2, $3, $4)', - interaction.message.id, interaction.message.channel.id, interaction.message.id, interaction.user.id, + interaction.message.id, + interaction.message.channel.id, + interaction.message.id, + interaction.user.id, ) except asyncpg.UniqueViolationError: await interaction.followup.send( @@ -306,6 +355,7 @@ async def callback(self, interaction: discord.Interaction[LegacyBot]) -> None: ephemeral=True, ) + PartialMessage = Annotated[discord.PartialMessage | None, commands.PartialMessageConverter] @@ -421,7 +471,7 @@ def format_embed_variables(self, embed: EmbedPayload, data: EmbedVariablesData, new[key] = { 'text': replace_vars(value['text'], data), } - + icon_url = value.get('icon_url') if icon_url: new[key]['icon_url'] = replace_url(icon_url, data) # type: ignore @@ -520,19 +570,27 @@ async def gstart(self, ctx: Context, duration: Duration, winners: Winners | None 'guild': ctx.guild, 'winner_amount': winners, 'winner_list': None, - } + }, ) view = discord.ui.View(timeout=None) view.add_item(JoinGiveaway()) - msg = await ctx.channel.send(content=':tada: **GIVEAWAY!** :tada:', embed=discord.Embed.from_dict(embed), view=view) + msg = await ctx.channel.send( + content=':tada: **GIVEAWAY!** :tada:', embed=discord.Embed.from_dict(embed), view=view + ) async with ctx.get_connection() as conn: async with conn.transaction(): await conn.execute( 'INSERT INTO giveaways (guild_id, channel_id, message_id, winner_amount, ends_at, prize, host_id) ' 'VALUES ($1, $2, $3, $4, $5, $6, $7)', - ctx.guild.id, ctx.channel.id, msg.id, winners, duration.resolved, prize, ctx.author.id, + ctx.guild.id, + ctx.channel.id, + msg.id, + winners, + duration.resolved, + prize, + ctx.author.id, ) if ctx.interaction: @@ -572,7 +630,7 @@ async def greroll(self, ctx: Context, *, giveaway: PartialMessage = None) -> Non channel = (giveaway and giveaway.channel) or ctx.channel - if not isinstance(channel, discord.abc.GuildChannel): + if not isinstance(channel, discord.abc.GuildChannel) or isinstance(channel, discord.ForumChannel): await ctx.reply('Non-valid giveaway message channel provided!') return @@ -590,7 +648,9 @@ async def greroll(self, ctx: Context, *, giveaway: PartialMessage = None) -> Non try: ret = await self.bot.get_or_fetch_members(ctx.guild, winner_id, record['host_id']) except (ValueError, discord.NotFound): - await ctx.reply('Could not resolve the winner into a member. This is most likely an error on Discord side.', ephemeral=True) + await ctx.reply( + 'Could not resolve the winner into a member. This is most likely an error on Discord side.', ephemeral=True + ) return if len(ret) < 2: @@ -641,9 +701,7 @@ async def gend(self, ctx: Context, *, giveaway: PartialMessage = None) -> None: record = await self.get_latest_giveaway(ctx.channel) # pyright: ignore[reportArgumentType] if record is None: - await ctx.reply( - "Could not find any giveaways in this channel!", ephemeral=True - ) + await ctx.reply("Could not find any giveaways in this channel!", ephemeral=True) return await self.end_giveaway(record, wait=False) @@ -667,7 +725,9 @@ async def get_latest_giveaway(self, channel: discord.abc.GuildChannel) -> asyncp gw = await self.bot.pool.fetchrow( 'SELECT * FROM giveaways WHERE guild_id=$1 AND channel_id=$2 AND ends_at < $3 ORDER BY ends_at LIMIT 1;', - guild, chid, datetime.datetime.now(datetime.timezone.utc), + guild, + chid, + datetime.datetime.now(datetime.timezone.utc), ) if gw is None: @@ -682,11 +742,7 @@ async def end_giveaway(self, record: asyncpg.Record, *, wait: bool = True) -> No guild_id = record['guild_id'] guild = self.bot.get_guild(guild_id) host_id = record['host_id'] - host = ( - (guild and guild.get_member(host_id)) or - self.bot.get_user(host_id) or - (await self.bot.fetch_user(host_id)) - ) + host = (guild and guild.get_member(host_id)) or self.bot.get_user(host_id) or (await self.bot.fetch_user(host_id)) prize = record["prize"] channel = self.bot.get_partial_messageable( @@ -723,9 +779,7 @@ async def end_giveaway(self, record: asyncpg.Record, *, wait: bool = True) -> No ) for winner in winner_list: - await message.reply( - f'Congrats <@{winner}>! You have won **{record["prize"]}**!' - ) + await message.reply(f'Congrats <@{winner}>! You have won **{record["prize"]}**!') win_message = self.claimtimes.get_win_message(guild.id) @@ -742,9 +796,7 @@ async def end_giveaway(self, record: asyncpg.Record, *, wait: bool = True) -> No 'winner': winner, 'prize': prize, } - task = asyncio.create_task( - self.send_and_end_claimtime(claimtime, data, message, win_message) - ) + task = asyncio.create_task(self.send_and_end_claimtime(claimtime, data, message, win_message)) self._claimtime_tasks.add(task) task.add_done_callback(self._claimtime_tasks.remove) @@ -756,13 +808,15 @@ async def end_giveaway(self, record: asyncpg.Record, *, wait: bool = True) -> No async with conn.transaction(): await conn.execute( 'UPDATE giveaways SET ended=$1, winner_list=$2::bigint[] WHERE message_id=$3;', - True, winner_list, message.id, + True, + winner_list, + message.id, ) - async def send_and_end_claimtime(self, time: float, data: WinMessageVariablesData, message: discord.PartialMessage, win_message: str) -> None: - ret = await message.reply( - replace_win_message_vars(win_message, data) - ) + async def send_and_end_claimtime( + self, time: float, data: WinMessageVariablesData, message: discord.PartialMessage, win_message: str + ) -> None: + ret = await message.reply(replace_win_message_vars(win_message, data)) await asyncio.sleep(time) await ret.reply(f'{time:.2f} seconds finished!') @@ -782,7 +836,9 @@ async def kill_old_giveaways(self) -> None: for gw in gws: await conn.execute( 'DELETE FROM giveaway_participants WHERE guild_id=$1 AND channel_id=$2 AND message_id=$3;', - gw['guild_id'], gw['channel_id'], gw['message_id'], + gw['guild_id'], + gw['channel_id'], + gw['message_id'], ) @tasks.loop(seconds=5) @@ -813,11 +869,14 @@ async def on_raw_member_remove(self, payload: discord.RawMemberRemoveEvent) -> N async with conn.transaction(): await conn.execute( 'DELETE FROM giveaway_participants WHERE guild_id=$1 AND user_id=$2;', - guild_id, user_id, + guild_id, + user_id, ) + async def setup(bot: LegacyBot) -> None: await bot.add_cog(Giveaways(bot)) + async def teardown(bot: LegacyBot) -> None: await bot.remove_cog(Giveaways.__cog_name__) diff --git a/cogs/lyrics_generator.py b/cogs/lyrics_generator.py index 443e9c6..97e38d5 100644 --- a/cogs/lyrics_generator.py +++ b/cogs/lyrics_generator.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from collections.abc import Callable, ItemsView, Iterator, KeysView, ValuesView @@ -43,6 +44,7 @@ from bot import LegacyBot, LegacyBotContext as Context from _typeshed import SupportsRichComparison + K = TypeVar('K', bound=SupportsRichComparison) else: K = TypeVar('K') @@ -71,7 +73,7 @@ async def create_token(self) -> None: 'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret, - } + }, ) as response: data = await response.json(loads=json.loads) @@ -80,7 +82,9 @@ async def create_token(self) -> None: self.access_token = data['access_token'] self.token_type = data['token_type'] - self.expires_at = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=int(data['expires_in'])) + self.expires_at = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( + seconds=int(data['expires_in']) + ) async def search_songs(self, query: str, /) -> list[Song]: async with self.bot.session.get( @@ -92,7 +96,7 @@ async def search_songs(self, query: str, /) -> list[Song]: }, headers={ 'Authorization': f'{self.token_type} {self.access_token}', - } + }, ) as response: data = await response.json(loads=json.loads) @@ -160,7 +164,7 @@ def __len__(self) -> int: def __getitem__(self, key: K) -> V: return self._data[key] - def __contains__(self, key: K) -> bool: + def __contains__(self, key: Any) -> bool: return key in self._data def __iter__(self) -> Iterator[K]: @@ -210,7 +214,9 @@ def filename(self) -> str: return f'{self._parent_name}-{self.height}x{self.width}.jpeg' @classmethod - def _from_images(cls, images: list[dict[str, Any]], parent_name: str, session: aiohttp.ClientSession) -> list[SpotifyAsset]: + def _from_images( + cls, images: list[dict[str, Any]], parent_name: str, session: aiohttp.ClientSession + ) -> list[SpotifyAsset]: return [cls(d, parent_name, session) for d in images] @@ -294,7 +300,7 @@ def add_items_to_container(self) -> None: f'**Name:** {song.name} | **Artist(s):** {", ".join(a.name for a in song.artists)}', f'**Album:** {song.album.name} | **Track:** {song.track}', f'**Explicit:** {"Yes" if song.explicit else "No"} | **Popularity:** {song.popularity}/100', - accessory=discord.ui.Thumbnail(song.thumbnail_url) + accessory=discord.ui.Thumbnail(song.thumbnail_url), ) select_options.append( discord.SelectOption( @@ -310,12 +316,11 @@ def add_items_to_container(self) -> None: ), ) - async def on_timeout(self) -> None: if self.message is not MISSING: for child in self.walk_children(): if hasattr(child, 'disabled'): - child.disabled = True + child.disabled = True # pyright: ignore[reportAttributeAccessIssue] await self.message.edit(view=self) @@ -431,10 +436,8 @@ def __init__(self, lyrics: list[str], bot: LegacyBot, author_id: int, song: Song self.lyrics: list[str] = lyrics self.paged_lyrics: dict[int, list[str]] = { page_id: page - for page_id, page - in enumerate( - discord.utils.as_chunks([l for l in self.lyrics if l], max_size=10) - ) if page + for page_id, page in enumerate(discord.utils.as_chunks([l for l in self.lyrics if l], max_size=10)) + if page } self.current_page: int = 0 self.selected_lyrics: OrderedDict[int, str] = OrderedDict() @@ -552,9 +555,11 @@ async def callback(self, interaction: discord.Interaction[LegacyBot]) -> None: return await interaction.response.edit_message( - view=discord.ui.LayoutView().add_item( + view=discord.ui.LayoutView() + .add_item( discord.ui.TextDisplay('Generating lyrics, please wait...'), - ).add_item( + ) + .add_item( discord.ui.TextDisplay('This message will be automatically updated when the process is done!'), ) ) @@ -801,7 +806,7 @@ def update_view(self) -> None: ) self.container.add_item( discord.ui.ActionRow( - #ToggleConfigButton(self.spotify_logo, 'spotify_logo', label='Toggle Spotify Logo'), + # ToggleConfigButton(self.spotify_logo, 'spotify_logo', label='Toggle Spotify Logo'), ToggleConfigButton(self.light_text, 'light_text', label='Toggle Light Text'), FinishButton(self), ReturnToLyricsSelector(self, self.parent), @@ -821,8 +826,7 @@ async def callback(self, interaction: discord.Interaction[LegacyBot]) -> None: ret = await self.parent.generate_image() file = discord.File(ret, filename='image.png') view = discord.ui.LayoutView().add_item( - discord.ui.MediaGallery(discord.MediaGalleryItem('attachment://image.png') - ), + discord.ui.MediaGallery(discord.MediaGalleryItem('attachment://image.png')), ) await self.parent.on_timeout() self.parent.stop() @@ -956,7 +960,9 @@ async def refresh_token(self) -> None: @commands.hybrid_command(name='generate-lyrics') @commands.cooldown( - 1, 30, commands.BucketType.user, + 1, + 30, + commands.BucketType.user, ) async def generate_lyrics(self, ctx: Context, *, query: str) -> None: """Generates a song's lyrics. diff --git a/cogs/meta.py b/cogs/meta.py index d67512c..a78bb07 100644 --- a/cogs/meta.py +++ b/cogs/meta.py @@ -52,9 +52,7 @@ def __init__( self.title: str = f"Commands in `{self.group.qualified_name}`" self.description: str = self.group.description - async def format_page( # type: ignore - self, menu: Paginator, commands: list[commands.HybridCommand] - ) -> discord.Embed: + async def format_page(self, menu: Paginator, commands: list[commands.HybridCommand]) -> discord.Embed: # type: ignore embed = discord.Embed( title=self.title, description=self.description, @@ -72,20 +70,14 @@ async def format_page( # type: ignore maximum = self.get_max_pages() if maximum > 1: - embed.set_author( - name=f"Page {menu.current_page + 1}/{maximum} ({len(self.entries)} commands)" - ) + embed.set_author(name=f"Page {menu.current_page + 1}/{maximum} ({len(self.entries)} commands)") - embed.set_footer( - text=f'Use "{self.prefix}help " for more information on a command.' - ) + embed.set_footer(text=f'Use "{self.prefix}help " for more information on a command.') return embed class HelpSelectMenu(discord.ui.Select["HelpMenu"]): - def __init__( - self, entries: dict[commands.Cog, list[commands.HybridCommand]], bot: LegacyBot - ): + def __init__(self, entries: dict[commands.Cog, list[commands.HybridCommand]], bot: LegacyBot): super().__init__( placeholder="Choose a category...", min_values=1, @@ -123,9 +115,7 @@ async def callback(self, interaction: discord.Interaction): else: cog = self.bot.get_cog(value) if cog is None: - await interaction.response.send_message( - "This category somehow does not exist!", ephemeral=True - ) + await interaction.response.send_message("This category somehow does not exist!", ephemeral=True) return commands = self.commands[cog] @@ -136,9 +126,7 @@ async def callback(self, interaction: discord.Interaction): ) return - source = GroupHelpPageSource( - cog, commands, prefix=self.view.ctx.clean_prefix - ) + source = GroupHelpPageSource(cog, commands, prefix=self.view.ctx.clean_prefix) await self.view.rebind(source, interaction) @@ -225,16 +213,12 @@ class HelpMenu(Paginator): def __init__(self, source: menus.PageSource, ctx: Context): super().__init__(source, context=ctx, compact=True) - def add_categories( - self, commands: dict[commands.Cog, list[commands.HybridCommand]] - ) -> None: + def add_categories(self, commands: dict[commands.Cog, list[commands.HybridCommand]]) -> None: self.clear_items() self.add_item(HelpSelectMenu(commands, self.ctx.bot)) self.fill_items() - async def rebind( - self, source: menus.PageSource, interaction: discord.Interaction - ) -> None: + async def rebind(self, source: menus.PageSource, interaction: discord.Interaction) -> None: self.source = source self.current_page = 0 @@ -252,9 +236,7 @@ class PaginatedHelpCommand(commands.HelpCommand): def __init__(self): super().__init__( command_attrs={ - "cooldown": commands.CooldownMapping.from_cooldown( - 1, 3.0, commands.BucketType.member - ), + "cooldown": commands.CooldownMapping.from_cooldown(1, 3.0, commands.BucketType.member), "help": "Shows a command, group or category help", } ) @@ -262,10 +244,7 @@ def __init__(self): async def on_help_command_error(self, ctx: Context, error: commands.CommandError): # type: ignore if isinstance(error, commands.CommandInvokeError): # Ignore missing permission errors - if ( - isinstance(error.original, discord.HTTPException) - and error.original.code == 50013 - ): + if isinstance(error.original, discord.HTTPException) and error.original.code == 50013: return await ctx.send(str(error.original)) @@ -350,9 +329,7 @@ def common_command_formatting( else: embed_like.description = command.help or "No short help" - async def send_command_help( # type: ignore - self, command: commands.HybridCommand | commands.HybridGroup - ): + async def send_command_help(self, command: commands.HybridCommand | commands.HybridGroup): # type: ignore # No pagination necessary for a single command. embed = discord.Embed(colour=discord.Colour.blurple()) self.common_command_formatting(embed, command) @@ -377,14 +354,14 @@ class ShowVarsView(discord.ui.View): def __init__(self, author: discord.abc.User) -> None: self.author: discord.abc.User = author self.message: discord.Message | None = None - super().__init__(timeout=60*15) + super().__init__(timeout=60 * 15) def enable_all(self) -> None: for child in self.children: if hasattr(child, 'disabled'): - child.disabled = False + child.disabled = False # pyright: ignore[reportAttributeAccessIssue] if hasattr(child, 'style'): - child.style = discord.ButtonStyle.blurple + child.style = discord.ButtonStyle.blurple # pyright: ignore[reportAttributeAccessIssue] async def on_timeout(self) -> None: if self.message: @@ -400,14 +377,18 @@ def get_win_message_variables_embed(self) -> discord.Embed: embed.add_field( name='\u200b', value='- `{claim_time}` - The amount of seconds the winner (e.g.: 10 seconds)\n' - '- `{host(username)}` - The giveaway host username (e.g.: ' f'{self.author.name})\n' - '- `{host(mention)}` - The giveaway host mention (e.g.: ' f'{self.author.mention})\n' - '- `{winner(username)}` - The giveaway winner username (e.g.: ' f'{self.author.name})\n' - '- `{winner(mention)}` - The giveaway winner mention (e.g.: ' f'{self.author.mention})\n' + '- `{host(username)}` - The giveaway host username (e.g.: ' + f'{self.author.name})\n' + '- `{host(mention)}` - The giveaway host mention (e.g.: ' + f'{self.author.mention})\n' + '- `{winner(username)}` - The giveaway winner username (e.g.: ' + f'{self.author.name})\n' + '- `{winner(mention)}` - The giveaway winner mention (e.g.: ' + f'{self.author.mention})\n' '- `{winner(created_ago)}` - When was the winner account created, in relative (e.g.: `2 years ago`)\n' '- `{winner(created_date)}` - When was the winner account created, in full date (e.g.: `17 May 2016 22:57`)\n' '- `{winner(joined_ago)}` - When the winner joined the server, in relative (e.g.: `3 months ago`)\n' - '- `{winner(joined_date)}` - When the winner joined the server, in full date (e.g.: `20 May 2016 11:43`)' + '- `{winner(joined_date)}` - When the winner joined the server, in full date (e.g.: `20 May 2016 11:43`)', ) return embed @@ -421,8 +402,10 @@ def get_gw_embed_variables_embed(self) -> discord.Embed: embed.add_field( name='\u200b', value='- `{prize}` - The giveaway prize\n' - '- `{host(username)}` - The giveaway host username (e.g: ' f'{self.author.name})\n' - '- `{host(mention)}` - The giveaway host mention (e.g.: ' f'{self.author.mention})\n' + '- `{host(username)}` - The giveaway host username (e.g: ' + f'{self.author.name})\n' + '- `{host(mention)}` - The giveaway host mention (e.g.: ' + f'{self.author.mention})\n' '- `{time_left}` - The remaining time until the giveaway ends, or how much time ended (e.g.: `in 10 minutes`/`10 minutes ago`)\n' '- `{end_time}` - The date when the giveaway will end (e.g.: `Tuesday, 17 May 2016 22:57`)\n' '- `{num_winners}` - The amount of winners of the giveaway (e.g.: `1`)\n' @@ -431,7 +414,7 @@ def get_gw_embed_variables_embed(self) -> discord.Embed: '- `{server_name}` - The server name.\n' '- `{winner_list}` - The giveaway winners, or `Not decided` if they have not yet been decided.\n' '- `{winners}` - It will work as `{num_winners}` if the giveaway has not ended and no winners are decided, when winners are decided, ' - 'it works as `{winner_list}`.' + 'it works as `{winner_list}`.', ) return embed @@ -443,23 +426,31 @@ def get_greet_message_variables_embed(self, server_name: str) -> discord.Embed: ) embed.add_field( name='\u200b', - value='- `{mention}` - The member mention (e.g.: ' f'{self.author.mention})\n' + value='- `{mention}` - The member mention (e.g.: ' + f'{self.author.mention})\n' '- `{mc}` - The server member count (e.g.: 1)\n' - '- `{server(name)}` - The server name (e.g.: ' f'{server_name})\n' - '- `{member(tag)}` - The member username (e.g: ' f'{self.author.name})\n' - '- `{member(name)}` - The member name (e.g.: ' f'{self.author.display_name})', + '- `{server(name)}` - The server name (e.g.: ' + f'{server_name})\n' + '- `{member(tag)}` - The member username (e.g: ' + f'{self.author.name})\n' + '- `{member(name)}` - The member name (e.g.: ' + f'{self.author.display_name})', ) return embed @discord.ui.button(label='Win Message Variables', style=discord.ButtonStyle.blurple) - async def win_message_variables(self, interaction: discord.Interaction[LegacyBot], button: discord.ui.Button[ShowVarsView]) -> None: + async def win_message_variables( + self, interaction: discord.Interaction[LegacyBot], button: discord.ui.Button[ShowVarsView] + ) -> None: self.enable_all() button.disabled = True button.style = discord.ButtonStyle.grey await interaction.response.edit_message(embed=self.get_win_message_variables_embed(), view=self) @discord.ui.button(label='Giveaway Embed Variables', style=discord.ButtonStyle.blurple) - async def gw_embed_variables(self, interaction: discord.Interaction[LegacyBot], button: discord.ui.Button[ShowVarsView]) -> None: + async def gw_embed_variables( + self, interaction: discord.Interaction[LegacyBot], button: discord.ui.Button[ShowVarsView] + ) -> None: self.enable_all() button.disabled = True button.style = discord.ButtonStyle.grey @@ -469,7 +460,9 @@ async def gw_embed_variables(self, interaction: discord.Interaction[LegacyBot], label='Greet Message Variables', style=discord.ButtonStyle.blurple, ) - async def greet_msg_vars(self, interaction: discord.Interaction[LegacyBot], button: discord.ui.Button[ShowVarsView]) -> None: + async def greet_msg_vars( + self, interaction: discord.Interaction[LegacyBot], button: discord.ui.Button[ShowVarsView] + ) -> None: assert interaction.guild self.enable_all() button.disabled = True @@ -483,7 +476,8 @@ async def start(self, ctx: Context) -> None: self.win_message_variables.disabled = True self.win_message_variables.style = discord.ButtonStyle.grey self.message = await ctx.reply( - embed=self.get_win_message_variables_embed(), view=self, + embed=self.get_win_message_variables_embed(), + view=self, ) @@ -503,9 +497,7 @@ def __init__(self, bot: LegacyBot) -> None: @discord.app_commands.command(name="help") @discord.app_commands.checks.cooldown(1, 30, key=lambda i: (i.guild_id, i.user.id)) - async def slash_help( - self, interaction: discord.Interaction, *, command: str | None = None - ) -> None: + async def slash_help(self, interaction: discord.Interaction, *, command: str | None = None) -> None: """Shows a command, group or category help. Parameters diff --git a/cogs/tools.py b/cogs/tools.py index 1d81f67..2b7d496 100644 --- a/cogs/tools.py +++ b/cogs/tools.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING, Annotated, Literal @@ -218,5 +219,6 @@ async def timediff(self, ctx: Context, object_1: Object, *, object_2: Object | N async def setup(bot: LegacyBot) -> None: await bot.add_cog(Tools(bot)) + async def teardown(bot: LegacyBot) -> None: await bot.remove_cog(Tools.__cog_name__) diff --git a/errors.py b/errors.py index 3e0d5e6..b487fc6 100644 --- a/errors.py +++ b/errors.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from discord.ext import commands @@ -43,8 +44,7 @@ def __init__(self) -> None: class BadWinnersArgument(commands.BadArgument): - """Error raised when, in a giveaway, a bad winners amount is provided. - """ + """Error raised when, in a giveaway, a bad winners amount is provided.""" __slots__ = () diff --git a/main.py b/main.py index 531b7e0..23deda3 100644 --- a/main.py +++ b/main.py @@ -83,7 +83,7 @@ async def runner(): debug_webhook_url=os.environ["DEBUG_WEBHOOK_URL"], ) as bot: async with asyncpg.create_pool( - #os.environ['DB_URI'].format(os.environ['DB_PASSWORD']), + # os.environ['DB_URI'].format(os.environ['DB_PASSWORD']), user=os.environ["db_user"], host=os.environ["db_host"], port=int(os.environ["db_port"]), @@ -112,7 +112,7 @@ async def runner(): await bot.send_debug_message( embed=discord.Embed( title='Error When Booting Up Bot!', - description=f'```py\n{traceback.format_exception(type(exc), exc, exc.__traceback__)[:3996]}```' + description=f'```py\n{traceback.format_exception(type(exc), exc, exc.__traceback__)[:3996]}```', ) ) diff --git a/paginator.py b/paginator.py index 792db59..39cdf92 100644 --- a/paginator.py +++ b/paginator.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import traceback @@ -87,7 +88,7 @@ def __init__( def fill_items(self) -> None: if not self.compact: - self.numbered_page.row = 1 + self.numbered_page.row = 1 self.stop_pages.row = 1 if self.source.is_paginating(): @@ -105,9 +106,7 @@ def fill_items(self) -> None: self.add_item(self.stop_pages) async def _get_kwargs_from_page(self, page: int) -> dict[str, Any]: - value = await discord.utils.maybe_coroutine( - self.source.format_page, self, page - ) + value = await discord.utils.maybe_coroutine(self.source.format_page, self, page) if isinstance(value, dict): return value @@ -135,12 +134,8 @@ def _update_labels(self, page_number: int) -> None: self.go_to_first_page.disabled = page_number == 0 if self.compact: max_pages = self.source.get_max_pages() - self.go_to_last_page.disabled = ( - max_pages is None or (page_number + 1) >= max_pages - ) - self.go_to_next_page.disabled = ( - max_pages is not None and (page_number + 1) >= max_pages - ) + self.go_to_last_page.disabled = max_pages is None or (page_number + 1) >= max_pages + self.go_to_next_page.disabled = max_pages is not None and (page_number + 1) >= max_pages self.go_to_previous_page.disabled = page_number == 0 return @@ -162,7 +157,9 @@ def _update_labels(self, page_number: int) -> None: self.go_to_previous_page.label = "…" async def show_checked_page( - self, interaction: discord.Interaction, page_number: int, + self, + interaction: discord.Interaction, + page_number: int, ) -> None: max_pages = self.source.get_max_pages() @@ -209,7 +206,9 @@ async def on_error(self, interaction: discord.Interaction, error: Exception, ite ) async def start(self, *, content: str | None = None, ephemeral: bool = False) -> None: - if self.check_embeds and not self.ctx.channel.permissions_for(self.ctx.me).embed_links: # pyright: ignore[reportArgumentType] + if ( + self.check_embeds and not self.ctx.channel.permissions_for(self.ctx.me).embed_links + ): # pyright: ignore[reportArgumentType] await self.ctx.send( 'I donnot have Embed Links permissions!', ephemeral=True, @@ -226,44 +225,32 @@ async def start(self, *, content: str | None = None, ephemeral: bool = False) -> self.message = await self.ctx.send(**kwargs, view=self, ephemeral=ephemeral) @discord.ui.button(label="≪", style=discord.ButtonStyle.grey) - async def go_to_first_page( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def go_to_first_page(self, interaction: discord.Interaction, button: discord.ui.Button): """go to the first page""" await self.show_page(interaction, 0) @discord.ui.button(label="Back", style=discord.ButtonStyle.blurple) - async def go_to_previous_page( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def go_to_previous_page(self, interaction: discord.Interaction, button: discord.ui.Button): """go to the previous page""" await self.show_checked_page(interaction, self.current_page - 1) @discord.ui.button(label="Current", style=discord.ButtonStyle.grey, disabled=True) - async def go_to_current_page( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def go_to_current_page(self, interaction: discord.Interaction, button: discord.ui.Button): pass @discord.ui.button(label="Next", style=discord.ButtonStyle.blurple) - async def go_to_next_page( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def go_to_next_page(self, interaction: discord.Interaction, button: discord.ui.Button): """go to the next page""" await self.show_checked_page(interaction, self.current_page + 1) @discord.ui.button(label="≫", style=discord.ButtonStyle.grey) - async def go_to_last_page( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def go_to_last_page(self, interaction: discord.Interaction, button: discord.ui.Button): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(interaction, self.source.get_max_pages() - 1) # type: ignore @discord.ui.button(label="Go To Page", style=discord.ButtonStyle.grey) - async def numbered_page( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def numbered_page(self, interaction: discord.Interaction, button: discord.ui.Button): """lets you type a page number to go to""" if self.message is None: return @@ -276,16 +263,12 @@ async def numbered_page( await interaction.followup.send("You took too much!", ephemeral=True) return elif self.is_finished(): - await modal.interaction.response.send_message( - "You took too much!", ephemeral=True - ) + await modal.interaction.response.send_message("You took too much!", ephemeral=True) return value = str(modal.page.value) if not value.isdigit(): - await modal.interaction.response.send_message( - f"Expected a number, not {value!r}", ephemeral=True - ) + await modal.interaction.response.send_message(f"Expected a number, not {value!r}", ephemeral=True) return value = int(value) @@ -295,9 +278,7 @@ async def numbered_page( await modal.interaction.response.send_message(error, ephemeral=True) @discord.ui.button(label="Close", style=discord.ButtonStyle.red) - async def stop_pages( - self, interaction: discord.Interaction, button: discord.ui.Button - ): + async def stop_pages(self, interaction: discord.Interaction, button: discord.ui.Button): """stops the pagination session.""" await interaction.response.defer() await interaction.delete_original_response() @@ -320,9 +301,7 @@ def __init__( self.clear_description: bool = clear_description self.inline: bool = inline - async def format_page( # type: ignore - self, menu: Paginator, entries: list[tuple[Any, Any]] - ) -> discord.Embed: + async def format_page(self, menu: Paginator, entries: list[tuple[Any, Any]]) -> discord.Embed: # type: ignore self.embed.clear_fields() if self.clear_description: self.embed.description = None diff --git a/store/claimtime.py b/store/claimtime.py index 1b2ab12..1279bd2 100644 --- a/store/claimtime.py +++ b/store/claimtime.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING @@ -32,9 +33,7 @@ if TYPE_CHECKING: from bot import LegacyBot -__all__ = ( - 'ClaimtimeDBStore', -) +__all__ = ('ClaimtimeDBStore',) class ClaimtimeDBStore(DBStore): @@ -76,7 +75,9 @@ def get_member_claimtime(self, member: discord.Member, /) -> float | None: base = 0 roles = [r.id for r in member.roles] - for role, claimtime in sorted(config['roles'].items(), key=lambda r: member._roles.index(int(r[0])) if int(r[0]) in member._roles else -1): + for role, claimtime in sorted( + config['roles'].items(), key=lambda r: member._roles.index(int(r[0])) if int(r[0]) in member._roles else -1 + ): if int(role) in roles: if not claimtime['override']: base += claimtime['time'] diff --git a/store/core.py b/store/core.py index 78cea0a..0ad6ba5 100644 --- a/store/core.py +++ b/store/core.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload @@ -36,9 +37,11 @@ D = TypeVar('D') MISSING = discord.utils.MISSING + def default_value_factory(name: str, value: Any, mode: Literal['save', 'load', 'delete']) -> Any: return value + def default_replaceable_factory(name: str, value: Any, pos: int) -> str: return f'${pos}' @@ -88,7 +91,7 @@ def __init__( keys: list[str] | str, *, value_factory: ValueFactory = default_value_factory, - query_replaceable_factory: ReplaceableFactory = default_replaceable_factory, + query_replaceable_factory: ReplaceableFactory = default_replaceable_factory, ) -> None: self.bot: LegacyBot = bot self.table: str = table @@ -107,9 +110,7 @@ def _get_where_query(self, fields: dict[str, Any], start: int = 1) -> str: checks = [] for no, (field, value) in enumerate(fields.items(), start): - checks.append( - f'{field} = {self._replaceable_factory(field, value, no)}' - ) + checks.append(f'{field} = {self._replaceable_factory(field, value, no)}') return ' AND '.join(checks) @@ -123,9 +124,7 @@ def _run_factories(self, values: dict[str, Any], mode: Literal['save', 'load', ' ret = [] for name, value in values.items(): - ret.append( - self._factory(name, value, mode) - ) + ret.append(self._factory(name, value, mode)) return ret async def _remove_key(self, values: dict[str, Any]) -> None: @@ -174,20 +173,16 @@ def clear_cache(self) -> None: self._data.clear() @overload - def get(self, key: tuple[Any, ...]) -> dict[str, Any] | None: - ... + def get(self, key: tuple[Any, ...]) -> dict[str, Any] | None: ... @overload - def get(self, key: tuple[Any, ...], default: D) -> dict[str, Any] | D: - ... + def get(self, key: tuple[Any, ...], default: D) -> dict[str, Any] | D: ... @overload - def get(self, key: Any) -> dict[str, Any] | None: - ... + def get(self, key: Any) -> dict[str, Any] | None: ... @overload - def get(self, key: Any, default: D) -> dict[str, Any] | D: - ... + def get(self, key: Any, default: D) -> dict[str, Any] | D: ... def get(self, key: Any, default: D = None) -> dict[str, Any] | D: """Gets a key from the store. @@ -209,12 +204,10 @@ def get(self, key: Any, default: D = None) -> dict[str, Any] | D: return self._data.get(key, default) @overload - async def pop(self, key: tuple[Any, ...]) -> dict[str, Any]: - ... + async def pop(self, key: tuple[Any, ...]) -> dict[str, Any]: ... @overload - async def pop(self, key: Any) -> Any: - ... + async def pop(self, key: Any) -> Any: ... async def pop(self, key: Any) -> dict[str, Any]: """Pops a key from the store. @@ -247,12 +240,10 @@ async def pop(self, key: Any) -> dict[str, Any]: return data @overload - async def set(self, key: tuple[Any, ...], value: dict[str, Any]) -> None: - ... + async def set(self, key: tuple[Any, ...], value: dict[str, Any]) -> None: ... @overload - async def set(self, key: Any, value: dict[str, Any]) -> None: - ... + async def set(self, key: Any, value: dict[str, Any]) -> None: ... async def set(self, key: Any, value: dict[str, Any]) -> None: """Updates a key's value. @@ -288,12 +279,10 @@ async def set(self, key: Any, value: dict[str, Any]) -> None: await conn.execute(query, *values) @overload - async def update(self, mapping: dict[tuple[Any, ...], dict[str, Any]]) -> None: - ... + async def update(self, mapping: dict[tuple[Any, ...], dict[str, Any]]) -> None: ... @overload - async def update(self, mapping: dict[Any, dict[str, Any]]) -> None: - ... + async def update(self, mapping: dict[Any, dict[str, Any]]) -> None: ... async def update(self, mapping: dict[Any, dict[str, Any]]) -> None: """Bulk updates the store with a mapping. @@ -317,12 +306,10 @@ async def update(self, mapping: dict[Any, dict[str, Any]]) -> None: await self.set(key, value) @overload - async def delete(self, key: tuple[Any, ...]) -> None: - ... + async def delete(self, key: tuple[Any, ...]) -> None: ... @overload - async def delete(self, key: Any) -> None: - ... + async def delete(self, key: Any) -> None: ... async def delete(self, key: Any) -> None: """Delets a key from the store. diff --git a/utils.py b/utils.py index c3f0365..3753172 100644 --- a/utils.py +++ b/utils.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import datetime @@ -28,9 +29,7 @@ import discord from discord.ext import commands -__slots__ = ( - 'format_dt', -) +__slots__ = ('format_dt',) def format_td(td: datetime.timedelta) -> str: @@ -58,7 +57,8 @@ def format_td(td: datetime.timedelta) -> str: f'{seconds} second{"s" if seconds != 1 else ""}', ) return discord.utils._human_join( - fmts, final='and', + fmts, + final='and', ) @@ -68,4 +68,5 @@ def decorator(cmd): commands.check_any(commands.has_guild_permissions(**perms), commands.is_owner())(cmd) discord.app_commands.default_permissions(**perms)(cmd) return cmd + return decorator