From 0e8ff2d9520dcf374ceb59911f607228d0cb3677 Mon Sep 17 00:00:00 2001 From: No767 <73260931+No767@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:07:14 -0800 Subject: [PATCH] Migrate to hybrids The rationale is that we want to keep parity with the current modmail impl, which is just pure prefixed. Hybrids allow the choice of prefixed and slash commands so best of both worlds --- bot/cogs/dev_tools.py | 5 ++- bot/cogs/meta.py | 23 +++++----- bot/launcher.py | 12 +++--- bot/libs/utils/modals.py | 16 +++++-- bot/libs/utils/pages/paginator.py | 63 +++++++++++++--------------- bot/libs/utils/pages/simple_pages.py | 9 ++-- bot/libs/utils/views.py | 20 +++++---- bot/rodhaj.py | 9 ++-- poetry.lock | 54 +++++++++++++++++++++++- pyproject.toml | 1 + 10 files changed, 138 insertions(+), 74 deletions(-) diff --git a/bot/cogs/dev_tools.py b/bot/cogs/dev_tools.py index a754c27..41fcdc0 100644 --- a/bot/cogs/dev_tools.py +++ b/bot/cogs/dev_tools.py @@ -14,11 +14,13 @@ class DevTools(commands.Cog, command_attrs=dict(hidden=True)): def __init__(self, bot: Rodhaj): self.bot = bot + async def cog_check(self, ctx: commands.Context) -> bool: + return await self.bot.is_owner(ctx.author) + # Umbra's sync command # To learn more about it, see the link below (and ?tag ass on the dpy server): # https://about.abstractumbra.dev/discord.py/2023/01/29/sync-command-example.html @commands.guild_only() - @commands.is_owner() @commands.command(name="sync") async def sync( self, @@ -64,7 +66,6 @@ async def sync( await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.") @commands.guild_only() - @commands.is_owner() @commands.command(name="reload-all") async def reload_all(self, ctx: commands.Context) -> None: """Reloads all cogs. Used in production to not produce any downtime""" diff --git a/bot/cogs/meta.py b/bot/cogs/meta.py index 3ca1a0c..f81b7e3 100644 --- a/bot/cogs/meta.py +++ b/bot/cogs/meta.py @@ -5,7 +5,6 @@ import discord import psutil import pygit2 -from discord import app_commands from discord.ext import commands from discord.utils import format_dt from libs.utils import Embed, human_timedelta @@ -16,7 +15,7 @@ # A cog houses a category of commands # Unlike djs, think of commands being stored as a category, # which the cog is that category -class Meta(commands.Cog): +class Utilities(commands.Cog): def __init__(self, bot: Rodhaj) -> None: self.bot = bot self.process = psutil.Process() @@ -49,8 +48,8 @@ def get_last_commits(self, count: int = 5): ) return "\n".join(self.format_commit(c) for c in commits) - @app_commands.command(name="about") - async def about(self, interaction: discord.Interaction) -> None: + @commands.hybrid_command(name="about") + async def about(self, ctx: commands.Context) -> None: """Shows some stats for Rodhaj""" total_members = 0 total_unique = len(self.bot.users) @@ -82,20 +81,20 @@ async def about(self, interaction: discord.Interaction) -> None: embed.add_field(name="Python Version", value=platform.python_version()) embed.add_field(name="Version", value=str(self.bot.version)) embed.add_field(name="Uptime", value=self.get_bot_uptime(brief=True)) - await interaction.response.send_message(embed=embed) + await ctx.send(embed=embed) - @app_commands.command(name="uptime") - async def uptime(self, interaction: discord.Interaction) -> None: + @commands.hybrid_command(name="uptime") + async def uptime(self, ctx: commands.Context) -> None: """Displays the bot's uptime""" uptime_message = f"Uptime: {self.get_bot_uptime()}" - await interaction.response.send_message(uptime_message) + await ctx.send(uptime_message) - @app_commands.command(name="version") - async def version(self, interaction: discord.Interaction) -> None: + @commands.hybrid_command(name="version") + async def version(self, ctx: commands.Context) -> None: """Displays the current build version""" version_message = f"Version: {self.bot.version}" - await interaction.response.send_message(version_message) + await ctx.send(version_message) async def setup(bot: Rodhaj) -> None: - await bot.add_cog(Meta(bot)) + await bot.add_cog(Utilities(bot)) diff --git a/bot/launcher.py b/bot/launcher.py index 9b05ba9..6339f4c 100644 --- a/bot/launcher.py +++ b/bot/launcher.py @@ -4,7 +4,7 @@ import asyncpg import discord from aiohttp import ClientSession -from dotenv import load_dotenv +from environs import Env from libs.utils import RodhajLogger from rodhaj import Rodhaj @@ -14,11 +14,13 @@ else: from uvloop import install -load_dotenv() +# Hope not to trip pyright +env = Env() +env.read_env() -TOKEN = os.environ["TOKEN"] -DEV_MODE = os.getenv("DEV_MODE") in ("True", "TRUE") -POSTGRES_URI = os.environ["POSTGRES_URI"] +TOKEN = env("TOKEN") +DEV_MODE = env.bool("DEV_MODE", False) +POSTGRES_URI = env("POSTGRES_URI") intents = discord.Intents.default() intents.message_content = True diff --git a/bot/libs/utils/modals.py b/bot/libs/utils/modals.py index a765f49..d0b6fc9 100644 --- a/bot/libs/utils/modals.py +++ b/bot/libs/utils/modals.py @@ -1,21 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import discord +from discord.ext import commands from .errors import produce_error_embed +if TYPE_CHECKING: + from bot.rodhaj import Rodhaj + NO_CONTROL_MSG = "This modal cannot be controlled by you, sorry!" class RoboModal(discord.ui.Modal): """Subclassed `discord.ui.Modal` that includes sane default configs""" - def __init__(self, interaction: discord.Interaction, *args, **kwargs): + def __init__(self, ctx: commands.Context[Rodhaj], *args, **kwargs): super().__init__(*args, **kwargs) - self.interaction = interaction + self.ctx = ctx async def interaction_check(self, interaction: discord.Interaction, /) -> bool: if interaction.user and interaction.user.id in ( - self.interaction.client.application.owner.id, # type: ignore - self.interaction.user.id, + self.ctx.bot.application.owner.id, # type: ignore + self.ctx.author.id, ): return True await interaction.response.send_message(NO_CONTROL_MSG, ephemeral=True) diff --git a/bot/libs/utils/pages/paginator.py b/bot/libs/utils/pages/paginator.py index 7c8ca02..6b16cb1 100644 --- a/bot/libs/utils/pages/paginator.py +++ b/bot/libs/utils/pages/paginator.py @@ -4,24 +4,26 @@ import discord from discord.ext import menus +from discord.ext.commands import Context from .modals import NumberedPageModal +# This is originally from RoboDanny's Paginator class (RoboPages) class HajPages(discord.ui.View): def __init__( self, source: menus.PageSource, *, - interaction: discord.Interaction, + ctx: Context, check_embeds: bool = True, compact: bool = False, ): super().__init__() self.source: menus.PageSource = source self.check_embeds: bool = check_embeds - self.interaction: discord.Interaction = interaction - self.followup: Optional[discord.InteractionMessage] = None + self.ctx: Context = ctx + self.message: Optional[discord.Message] = None self.current_page: int = 0 self.compact: bool = compact self.clear_items() @@ -47,7 +49,7 @@ def fill_items(self) -> None: self.add_item(self.numbered_page) self.add_item(self.stop_pages) - async def _get_kwargs_from_page(self, page: int) -> Dict[str, Any]: + async def get_kwargs_from_page(self, page: int) -> Dict[str, Any]: value = await discord.utils.maybe_coroutine(self.source.format_page, self, page) if isinstance(value, dict): return value @@ -63,12 +65,12 @@ async def show_page( ) -> None: page = await self.source.get_page(page_number) self.current_page = page_number - kwargs = await self._get_kwargs_from_page(page) + kwargs = await self.get_kwargs_from_page(page) self._update_labels(page_number) if kwargs: if interaction.response.is_done(): - if self.followup: - await self.followup.edit(**kwargs, view=self) + if self.message: + await self.message.edit(**kwargs, view=self) else: await interaction.response.edit_message(**kwargs, view=self) @@ -118,8 +120,8 @@ async def show_checked_page( async def interaction_check(self, interaction: discord.Interaction) -> bool: if interaction.user and interaction.user.id in ( - self.interaction.client.application.owner.id, # type: ignore - self.interaction.user.id, + self.ctx.bot.owner_id, + self.ctx.author.id, ): return True await interaction.response.send_message( @@ -128,45 +130,39 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool: return False async def on_timeout(self) -> None: - if self.followup: - await self.interaction.edit_original_response(view=None) + if self.message: + await self.message.edit(view=None) async def on_error( self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item ) -> None: - await interaction.followup.send( - "An unknown error occurred, sorry", ephemeral=True - ) + if interaction.response.is_done(): + await interaction.followup.send( + "An unknown error occurred, sorry", ephemeral=True + ) + else: + await interaction.response.send_message( + "An unknown error occurred, sorry", ephemeral=True + ) async def start( self, *, content: Optional[str] = None, ephemeral: bool = False ) -> None: - if self.check_embeds and not self.interaction.permissions.embed_links: - await self.interaction.response.send_message( - "Bot doesn't have embed link perms in this channel", ephemeral=True + if self.check_embeds and not self.ctx.channel.permissions_for(self.ctx.me).embed_links: # type: ignore + await self.ctx.send( + "Bot does not have embed links permission in this channel.", + ephemeral=True, ) return await self.source._prepare_once() page = await self.source.get_page(0) - kwargs = await self._get_kwargs_from_page(page) + kwargs = await self.get_kwargs_from_page(page) if content: kwargs.setdefault("content", content) self._update_labels(0) - - # Fixes the issue of somehow the interaction failing for /pronouns profile - if self.interaction.response.is_done(): - await self.interaction.followup.send( - **kwargs, view=self, ephemeral=ephemeral - ) - self.followup = await self.interaction.original_response() - return - - await self.interaction.response.send_message( - **kwargs, view=self, ephemeral=ephemeral - ) - self.followup = await self.interaction.original_response() + 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( @@ -182,12 +178,11 @@ async def go_to_previous_page( """go to the previous page""" await self.show_checked_page(interaction, self.current_page - 1) - # Shows the current button @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 ): - """show current page""" + pass @discord.ui.button(label="Next", style=discord.ButtonStyle.blurple) async def go_to_next_page( @@ -209,7 +204,7 @@ async def numbered_page( self, interaction: discord.Interaction, button: discord.ui.Button ): """lets you type a page number to go to""" - if self.followup is None: + if self.message is None: return modal = NumberedPageModal(self.source.get_max_pages()) diff --git a/bot/libs/utils/pages/simple_pages.py b/bot/libs/utils/pages/simple_pages.py index e25f151..975387a 100644 --- a/bot/libs/utils/pages/simple_pages.py +++ b/bot/libs/utils/pages/simple_pages.py @@ -1,4 +1,5 @@ import discord +from discord.ext import commands from .paginator import HajPages from .sources import SimplePageSource @@ -10,10 +11,6 @@ class SimplePages(HajPages): Basically an embed with some normal formatting. """ - def __init__( - self, entries, *, interaction: discord.Interaction, per_page: int = 12 - ): - super().__init__( - SimplePageSource(entries, per_page=per_page), interaction=interaction - ) + def __init__(self, entries, *, ctx: commands.Context, per_page: int = 12): + super().__init__(SimplePageSource(entries, per_page=per_page), ctx=ctx) self.embed = discord.Embed(colour=discord.Colour.from_rgb(200, 168, 255)) diff --git a/bot/libs/utils/views.py b/bot/libs/utils/views.py index 8b97435..c8c4560 100644 --- a/bot/libs/utils/views.py +++ b/bot/libs/utils/views.py @@ -1,23 +1,29 @@ -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any import discord +from discord.ext import commands from .errors import produce_error_embed +if TYPE_CHECKING: + from bot.rodhaj import Rodhaj + NO_CONTROL_MSG = "This view cannot be controlled by you, sorry!" class RoboView(discord.ui.View): """Subclassed `discord.ui.View` that includes sane default configs""" - def __init__(self, interaction: discord.Interaction): + def __init__(self, ctx: commands.Context[Rodhaj]): super().__init__() - self.interaction = interaction + self.ctx = ctx async def interaction_check(self, interaction: discord.Interaction, /) -> bool: if interaction.user and interaction.user.id in ( - self.interaction.client.application.owner.id, # type: ignore - self.interaction.user.id, + self.ctx.bot.application.owner.id, # type: ignore + self.ctx.author.id, ): return True await interaction.response.send_message(NO_CONTROL_MSG, ephemeral=True) @@ -36,5 +42,5 @@ async def on_error( self.stop() async def on_timeout(self) -> None: - if self.interaction.response.is_done(): - await self.interaction.edit_original_response(view=None) + self.clear_items() + self.stop() diff --git a/bot/rodhaj.py b/bot/rodhaj.py index 2ecb32a..3bb0cfb 100644 --- a/bot/rodhaj.py +++ b/bot/rodhaj.py @@ -32,7 +32,7 @@ def __init__( activity=discord.Activity( type=discord.ActivityType.watching, name="a game" ), - command_prefix="r>", + command_prefix=["r>", "?", "!"], help_command=None, intents=intents, tree_cls=RodhajCommandTree, @@ -64,9 +64,12 @@ def stop(): for extension in EXTENSIONS: await self.load_extension(extension) + # Load Jishaku during production as this is what Umbra, Jeyy and others do + # Useful for debugging purposes + await self.load_extension("jishaku") + if self._dev_mode is True and _fsw is True: - self.logger.info("Dev mode is enabled. Loading Jishaku and FSWatcher") - await self.load_extension("jishaku") + self.logger.info("Dev mode is enabled. Loading FSWatcher") self.loop.create_task(self.fs_watcher()) async def on_ready(self): diff --git a/poetry.lock b/poetry.lock index ed5975f..ad44a4b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -607,6 +607,27 @@ files = [ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] +[[package]] +name = "environs" +version = "9.5.0" +description = "simplified environment variable parsing" +optional = false +python-versions = ">=3.6" +files = [ + {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, + {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, +] + +[package.dependencies] +marshmallow = ">=3.0.0" +python-dotenv = "*" + +[package.extras] +dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] +tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] + [[package]] name = "exceptiongroup" version = "1.1.3" @@ -795,6 +816,26 @@ publish = ["Jinja2 (>=3.0.3)"] test = ["coverage (>=6.3.2)", "flake8 (>=4.0.1)", "isort (>=5.10.1)", "pylint (>=2.11.1)", "pytest (>=7.0.1)", "pytest-asyncio (>=0.18.1)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)"] voice = ["yt-dlp (>=2022.3.8)"] +[[package]] +name = "marshmallow" +version = "3.20.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +optional = false +python-versions = ">=3.8" +files = [ + {file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"}, + {file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"}, +] + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] +tests = ["pytest", "pytz", "simplejson"] + [[package]] name = "multidict" version = "6.0.4" @@ -951,6 +992,17 @@ files = [ {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"}, ] +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + [[package]] name = "platformdirs" version = "3.11.0" @@ -1584,4 +1636,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "de32bd6b391e5231030ac7c0aad474577dc856b8060c02407b9973117ee7a73d" +content-hash = "d6d7c95f3e5d774496f559a8a5c2c1dbea2b8d9af976c85d3bea2f1ced763370" diff --git a/pyproject.toml b/pyproject.toml index 6c7c40d..cf30203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ pygit2 = "^1.13.2" python-dateutil = "^2.8.2" click = "^8.1.7" typing-extensions = "^4.8.0" +environs = "^9.5.0" # These are pinned by major version # To not get dependabot to constantly update them