Skip to content

Commit

Permalink
Implement deletion process (#16)
Browse files Browse the repository at this point in the history
* Fix views timing out issue

* Implement deletion process

* Add error handler

* Implement subclassed `commands.Context` (`RoboContext`)"

* Include docs deps and align to best practices
  • Loading branch information
No767 authored Nov 25, 2023
1 parent e680378 commit b9340cf
Show file tree
Hide file tree
Showing 11 changed files with 840 additions and 37 deletions.
111 changes: 97 additions & 14 deletions bot/cogs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,37 @@
import msgspec
from async_lru import alru_cache
from discord.ext import commands
from libs.utils import is_manager
from libs.utils import RoboContext, is_manager

from rodhaj import Rodhaj

UNKNOWN_ERROR_MESSAGE = (
"An unknown error happened. Please contact the dev team for assistance"
)


# Msgspec Structs are usually extremely fast compared to slotted classes
class GuildConfig(msgspec.Struct):
bot: Rodhaj
id: int
category_id: int
ticket_channel_id: int
logging_channel_id: int
logging_broadcast_url: str
locked: bool = False

@property
def category_channel(self) -> Optional[discord.CategoryChannel]:
guild = self.bot.get_guild(self.id)
return guild and guild.get_channel(self.category_id) # type: ignore

@property
def logging_channel(self) -> Optional[discord.TextChannel]:
guild = self.bot.get_guild(self.id)
return guild and guild.get_channel(self.logging_channel_id) # type: ignore

@property
def ticket_channel(self) -> Optional[discord.TextChannel]:
def ticket_channel(self) -> Optional[discord.ForumChannel]:
guild = self.bot.get_guild(self.id)
return guild and guild.get_channel(self.ticket_channel_id) # type: ignore

Expand All @@ -48,7 +58,7 @@ async def get_webhook(self) -> Optional[discord.Webhook]:
@alru_cache()
async def get_config(self) -> Optional[GuildConfig]:
query = """
SELECT id, ticket_channel_id, logging_channel_id, logging_broadcast_url, locked
SELECT id, category_id, ticket_channel_id, logging_channel_id, logging_broadcast_url, locked
FROM guild_config
WHERE id = $1;
"""
Expand All @@ -63,8 +73,8 @@ async def get_config(self) -> Optional[GuildConfig]:
class SetupFlags(commands.FlagConverter):
ticket_name: str = commands.flag(
name="ticket_name",
default="modmail",
description="The name of the ticket forum. Defaults to modmail",
default="tickets",
description="The name of the ticket forum. Defaults to tickets",
)
log_name: str = commands.flag(
name="log_name",
Expand All @@ -80,17 +90,29 @@ def __init__(self, bot: Rodhaj) -> None:
self.bot = bot
self.pool = self.bot.pool

@alru_cache()
async def get_guild_config(self, guild_id: int) -> Optional[GuildConfig]:
# Normally using the star is bad practice but...
# Since I don't want to write out every single column to select,
# we are going to use the star
# The guild config roughly maps to it as well
query = "SELECT * FROM guild_config WHERE guild_id = $1;"
rows = await self.pool.fetchrow(query, guild_id)
if rows is None:
return None
config = GuildConfig(bot=self.bot, **dict(rows))
return config

@is_manager()
@commands.guild_only()
@commands.hybrid_group(name="config")
async def config(self, ctx: commands.Context) -> None:
async def config(self, ctx: RoboContext) -> None:
"""Commands to configure, setup, or delete Rodhaj"""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)

# TODO: Make a delete command (just in case but shouldn't really be needed)
@config.command(name="setup")
async def setup(self, ctx: commands.Context, *, flags: SetupFlags) -> None:
async def setup(self, ctx: RoboContext, *, flags: SetupFlags) -> None:
"""First-time setup for Rodhaj
You only need to run this once
Expand Down Expand Up @@ -198,19 +220,26 @@ async def setup(self, ctx: commands.Context, *, flags: SetupFlags) -> None:
available_tags=forum_tags,
)
except discord.Forbidden:
await ctx.send("Missing permissions to either")
await ctx.send(
"\N{NO ENTRY SIGN} Rodhaj is missing permissions: Manage Channels and Manage Webhooks"
)
return
except discord.HTTPException:
await ctx.send("Some error happened")
await ctx.send(UNKNOWN_ERROR_MESSAGE)
return

query = """
INSERT INTO guild_config (id, ticket_channel_id, logging_channel_id, logging_broadcast_url)
VALUES ($1, $2, $3, $4);
INSERT INTO guild_config (id, category_id, ticket_channel_id, logging_channel_id, logging_broadcast_url)
VALUES ($1, $2, $3, $4, $5);
"""
try:
await self.pool.execute(
query, guild_id, ticket_channel.id, logging_channel.id, lgc_webhook.url
query,
guild_id,
rodhaj_category.id,
ticket_channel.id,
logging_channel.id,
lgc_webhook.url,
)
except asyncpg.UniqueViolationError:
await ticket_channel.delete(reason=delete_reason)
Expand All @@ -220,11 +249,65 @@ async def setup(self, ctx: commands.Context, *, flags: SetupFlags) -> None:
"Failed to create the channels. Please contact Noelle to figure out why (it's more than likely that the channels exist and bypassed checking the lru cache for some reason)"
)
else:
# Invalidate LRU cache
# Invalidate LRU cache just to clear it out
dispatcher.get_config.cache_invalidate()
msg = f"Rodhaj channels successfully created! The ticket channel can be found under {ticket_channel.mention}"
await ctx.send(msg)

@config.command(name="delete")
async def delete(self, ctx: RoboContext) -> None:
"""Permanently deletes Rodhaj channels and tickets."""
if ctx.guild is None:
await ctx.send("Really... This module is meant to be ran in a server")
return

guild_id = ctx.guild.id

dispatcher = GuildWebhookDispatcher(self.bot, guild_id)
guild_config = await self.get_guild_config(guild_id)

msg = "Are you really sure that you want to delete the Rodhaj channels?"
confirm = await ctx.prompt(msg, timeout=300.0)
if confirm:
if guild_config is None:
msg = (
"Could not find the guild config. Perhaps Rodhaj is not set up yet?"
)
await ctx.send(msg)
return

reason = f"Requested by {ctx.author.name} (ID: {ctx.author.id}) to purge Rodhaj channels"

if (
guild_config.logging_channel is not None
and guild_config.ticket_channel is not None
and guild_config.category_channel is not None
):
try:
await guild_config.logging_channel.delete(reason=reason)
await guild_config.ticket_channel.delete(reason=reason)
await guild_config.category_channel.delete(reason=reason)
except discord.Forbidden:
await ctx.send(
"\N{NO ENTRY SIGN} Rodhaj is missing permissions: Manage Channels"
)
return
except discord.HTTPException:
await ctx.send(UNKNOWN_ERROR_MESSAGE)
return

query = """
DELETE FROM guild_config WHERE guild_id = $1;
"""
await self.pool.execute(query, guild_id)
dispatcher.get_config.cache_invalidate()
self.get_guild_config.cache_invalidate()
await ctx.send("Successfully deleted channels")
elif confirm is None:
await ctx.send("Not removing Rodhaj channels. Canceling.")
else:
await ctx.send("Cancelling.")


async def setup(bot: Rodhaj) -> None:
await bot.add_cog(Config(bot))
21 changes: 19 additions & 2 deletions bot/cogs/dev_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
from cogs import EXTENSIONS
from discord.ext import commands
from discord.ext.commands import Context, Greedy
from libs.utils import RoboContext, RoboView

from rodhaj import Rodhaj


class MaybeView(RoboView):
def __init__(self, ctx: RoboContext) -> None:
super().__init__(ctx)

@discord.ui.button(label="eg")
async def eg(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_message("yo nice oen", ephemeral=True)


class DevTools(commands.Cog, command_attrs=dict(hidden=True)):
"""Tools for developing RodHaj"""

def __init__(self, bot: Rodhaj):
self.bot = bot

async def cog_check(self, ctx: commands.Context) -> bool:
async def cog_check(self, ctx: RoboContext) -> bool:
return await self.bot.is_owner(ctx.author)

# Umbra's sync command
Expand Down Expand Up @@ -67,7 +79,7 @@ async def sync(

@commands.guild_only()
@commands.command(name="reload-all")
async def reload_all(self, ctx: commands.Context) -> None:
async def reload_all(self, ctx: RoboContext) -> None:
"""Reloads all cogs. Used in production to not produce any downtime"""
if not hasattr(self.bot, "uptime"):
await ctx.send("Bot + exts must be up and loaded before doing this")
Expand All @@ -77,6 +89,11 @@ async def reload_all(self, ctx: commands.Context) -> None:
await self.bot.reload_extension(extension)
await ctx.send("Successfully reloaded all extensions live")

@commands.command(name="view-test", hidden=True)
async def view_test(self, ctx: RoboContext) -> None:
view = MaybeView(ctx)
view.message = await ctx.send("yeo", view=view)


async def setup(bot: Rodhaj):
await bot.add_cog(DevTools(bot))
8 changes: 4 additions & 4 deletions bot/cogs/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pygit2
from discord.ext import commands
from discord.utils import format_dt
from libs.utils import Embed, human_timedelta
from libs.utils import Embed, RoboContext, human_timedelta

from rodhaj import Rodhaj

Expand Down Expand Up @@ -49,7 +49,7 @@ def get_last_commits(self, count: int = 5):
return "\n".join(self.format_commit(c) for c in commits)

@commands.hybrid_command(name="about")
async def about(self, ctx: commands.Context) -> None:
async def about(self, ctx: RoboContext) -> None:
"""Shows some stats for Rodhaj"""
total_members = 0
total_unique = len(self.bot.users)
Expand Down Expand Up @@ -83,13 +83,13 @@ async def about(self, ctx: commands.Context) -> None:
await ctx.send(embed=embed)

@commands.hybrid_command(name="uptime")
async def uptime(self, ctx: commands.Context) -> None:
async def uptime(self, ctx: RoboContext) -> None:
"""Displays the bot's uptime"""
uptime_message = f"Uptime: {self.get_bot_uptime()}"
await ctx.send(uptime_message)

@commands.hybrid_command(name="version")
async def version(self, ctx: commands.Context) -> None:
async def version(self, ctx: RoboContext) -> None:
"""Displays the current build version"""
version_message = f"Version: {self.bot.version}"
await ctx.send(version_message)
Expand Down
2 changes: 2 additions & 0 deletions bot/libs/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
is_manager as is_manager,
is_mod as is_mod,
)
from .context import RoboContext as RoboContext
from .embeds import Embed as Embed, ErrorEmbed as ErrorEmbed
from .errors import send_error_embed as send_error_embed
from .logger import RodhajLogger as RodhajLogger
from .modals import RoboModal as RoboModal
from .time import human_timedelta as human_timedelta
Expand Down
71 changes: 71 additions & 0 deletions bot/libs/utils/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Optional

import discord
from discord.ext import commands

from .views import RoboView

if TYPE_CHECKING:
from bot.rodhaj import Rodhaj


class ConfirmationView(RoboView):
def __init__(self, ctx, timeout: float, delete_after: bool):
super().__init__(ctx, timeout)
self.value: Optional[bool] = None
self.delete_after = delete_after
self.message: Optional[discord.Message] = None

async def on_timeout(self) -> None:
if self.delete_after and self.message:
await self.message.delete()
elif self.message:
await self.message.edit(view=None)

async def delete_response(self, interaction: discord.Interaction):
await interaction.response.defer()
if self.delete_after:
await interaction.delete_original_response()

self.stop()

@discord.ui.button(
label="Confirm",
style=discord.ButtonStyle.green,
emoji="<:greenTick:596576670815879169>",
)
async def confirm(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
self.value = True
await self.delete_response(interaction)

@discord.ui.button(
label="Cancel",
style=discord.ButtonStyle.red,
emoji="<:redTick:596576672149667840>",
)
async def cancel(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
self.value = False
await interaction.response.defer()
await interaction.delete_original_response()
self.stop()


class RoboContext(commands.Context):
bot: Rodhaj

def __init__(self, **kwargs):
super().__init__(**kwargs)

async def prompt(
self, message: str, *, timeout: float = 60.0, delete_after: bool = False
) -> Optional[bool]:
view = ConfirmationView(ctx=self, timeout=timeout, delete_after=delete_after)
view.message = await self.send(message, view=view, ephemeral=delete_after)
await view.wait()
return view.value
Loading

0 comments on commit b9340cf

Please sign in to comment.