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