Skip to content

Commit

Permalink
Migrate to hybrids
Browse files Browse the repository at this point in the history
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
  • Loading branch information
No767 committed Nov 18, 2023
1 parent f99251e commit 0e8ff2d
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 74 deletions.
5 changes: 3 additions & 2 deletions bot/cogs/dev_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"""
Expand Down
23 changes: 11 additions & 12 deletions bot/cogs/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
12 changes: 7 additions & 5 deletions bot/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 12 additions & 4 deletions bot/libs/utils/modals.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
63 changes: 29 additions & 34 deletions bot/libs/utils/pages/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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())
Expand Down
9 changes: 3 additions & 6 deletions bot/libs/utils/pages/simple_pages.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import discord
from discord.ext import commands

from .paginator import HajPages
from .sources import SimplePageSource
Expand All @@ -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))
20 changes: 13 additions & 7 deletions bot/libs/utils/views.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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()
9 changes: 6 additions & 3 deletions bot/rodhaj.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
Loading

0 comments on commit 0e8ff2d

Please sign in to comment.