Skip to content

Commit b9340cf

Browse files
authored
Implement deletion process (#16)
* Fix views timing out issue * Implement deletion process * Add error handler * Implement subclassed `commands.Context` (`RoboContext`)" * Include docs deps and align to best practices
1 parent e680378 commit b9340cf

11 files changed

+840
-37
lines changed

bot/cogs/config.py

+97-14
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,37 @@
55
import msgspec
66
from async_lru import alru_cache
77
from discord.ext import commands
8-
from libs.utils import is_manager
8+
from libs.utils import RoboContext, is_manager
99

1010
from rodhaj import Rodhaj
1111

12+
UNKNOWN_ERROR_MESSAGE = (
13+
"An unknown error happened. Please contact the dev team for assistance"
14+
)
15+
1216

1317
# Msgspec Structs are usually extremely fast compared to slotted classes
1418
class GuildConfig(msgspec.Struct):
1519
bot: Rodhaj
1620
id: int
21+
category_id: int
1722
ticket_channel_id: int
1823
logging_channel_id: int
1924
logging_broadcast_url: str
2025
locked: bool = False
2126

27+
@property
28+
def category_channel(self) -> Optional[discord.CategoryChannel]:
29+
guild = self.bot.get_guild(self.id)
30+
return guild and guild.get_channel(self.category_id) # type: ignore
31+
2232
@property
2333
def logging_channel(self) -> Optional[discord.TextChannel]:
2434
guild = self.bot.get_guild(self.id)
2535
return guild and guild.get_channel(self.logging_channel_id) # type: ignore
2636

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

@@ -48,7 +58,7 @@ async def get_webhook(self) -> Optional[discord.Webhook]:
4858
@alru_cache()
4959
async def get_config(self) -> Optional[GuildConfig]:
5060
query = """
51-
SELECT id, ticket_channel_id, logging_channel_id, logging_broadcast_url, locked
61+
SELECT id, category_id, ticket_channel_id, logging_channel_id, logging_broadcast_url, locked
5262
FROM guild_config
5363
WHERE id = $1;
5464
"""
@@ -63,8 +73,8 @@ async def get_config(self) -> Optional[GuildConfig]:
6373
class SetupFlags(commands.FlagConverter):
6474
ticket_name: str = commands.flag(
6575
name="ticket_name",
66-
default="modmail",
67-
description="The name of the ticket forum. Defaults to modmail",
76+
default="tickets",
77+
description="The name of the ticket forum. Defaults to tickets",
6878
)
6979
log_name: str = commands.flag(
7080
name="log_name",
@@ -80,17 +90,29 @@ def __init__(self, bot: Rodhaj) -> None:
8090
self.bot = bot
8191
self.pool = self.bot.pool
8292

93+
@alru_cache()
94+
async def get_guild_config(self, guild_id: int) -> Optional[GuildConfig]:
95+
# Normally using the star is bad practice but...
96+
# Since I don't want to write out every single column to select,
97+
# we are going to use the star
98+
# The guild config roughly maps to it as well
99+
query = "SELECT * FROM guild_config WHERE guild_id = $1;"
100+
rows = await self.pool.fetchrow(query, guild_id)
101+
if rows is None:
102+
return None
103+
config = GuildConfig(bot=self.bot, **dict(rows))
104+
return config
105+
83106
@is_manager()
84107
@commands.guild_only()
85108
@commands.hybrid_group(name="config")
86-
async def config(self, ctx: commands.Context) -> None:
109+
async def config(self, ctx: RoboContext) -> None:
87110
"""Commands to configure, setup, or delete Rodhaj"""
88111
if ctx.invoked_subcommand is None:
89112
await ctx.send_help(ctx.command)
90113

91-
# TODO: Make a delete command (just in case but shouldn't really be needed)
92114
@config.command(name="setup")
93-
async def setup(self, ctx: commands.Context, *, flags: SetupFlags) -> None:
115+
async def setup(self, ctx: RoboContext, *, flags: SetupFlags) -> None:
94116
"""First-time setup for Rodhaj
95117
96118
You only need to run this once
@@ -198,19 +220,26 @@ async def setup(self, ctx: commands.Context, *, flags: SetupFlags) -> None:
198220
available_tags=forum_tags,
199221
)
200222
except discord.Forbidden:
201-
await ctx.send("Missing permissions to either")
223+
await ctx.send(
224+
"\N{NO ENTRY SIGN} Rodhaj is missing permissions: Manage Channels and Manage Webhooks"
225+
)
202226
return
203227
except discord.HTTPException:
204-
await ctx.send("Some error happened")
228+
await ctx.send(UNKNOWN_ERROR_MESSAGE)
205229
return
206230

207231
query = """
208-
INSERT INTO guild_config (id, ticket_channel_id, logging_channel_id, logging_broadcast_url)
209-
VALUES ($1, $2, $3, $4);
232+
INSERT INTO guild_config (id, category_id, ticket_channel_id, logging_channel_id, logging_broadcast_url)
233+
VALUES ($1, $2, $3, $4, $5);
210234
"""
211235
try:
212236
await self.pool.execute(
213-
query, guild_id, ticket_channel.id, logging_channel.id, lgc_webhook.url
237+
query,
238+
guild_id,
239+
rodhaj_category.id,
240+
ticket_channel.id,
241+
logging_channel.id,
242+
lgc_webhook.url,
214243
)
215244
except asyncpg.UniqueViolationError:
216245
await ticket_channel.delete(reason=delete_reason)
@@ -220,11 +249,65 @@ async def setup(self, ctx: commands.Context, *, flags: SetupFlags) -> None:
220249
"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)"
221250
)
222251
else:
223-
# Invalidate LRU cache
252+
# Invalidate LRU cache just to clear it out
224253
dispatcher.get_config.cache_invalidate()
225254
msg = f"Rodhaj channels successfully created! The ticket channel can be found under {ticket_channel.mention}"
226255
await ctx.send(msg)
227256

257+
@config.command(name="delete")
258+
async def delete(self, ctx: RoboContext) -> None:
259+
"""Permanently deletes Rodhaj channels and tickets."""
260+
if ctx.guild is None:
261+
await ctx.send("Really... This module is meant to be ran in a server")
262+
return
263+
264+
guild_id = ctx.guild.id
265+
266+
dispatcher = GuildWebhookDispatcher(self.bot, guild_id)
267+
guild_config = await self.get_guild_config(guild_id)
268+
269+
msg = "Are you really sure that you want to delete the Rodhaj channels?"
270+
confirm = await ctx.prompt(msg, timeout=300.0)
271+
if confirm:
272+
if guild_config is None:
273+
msg = (
274+
"Could not find the guild config. Perhaps Rodhaj is not set up yet?"
275+
)
276+
await ctx.send(msg)
277+
return
278+
279+
reason = f"Requested by {ctx.author.name} (ID: {ctx.author.id}) to purge Rodhaj channels"
280+
281+
if (
282+
guild_config.logging_channel is not None
283+
and guild_config.ticket_channel is not None
284+
and guild_config.category_channel is not None
285+
):
286+
try:
287+
await guild_config.logging_channel.delete(reason=reason)
288+
await guild_config.ticket_channel.delete(reason=reason)
289+
await guild_config.category_channel.delete(reason=reason)
290+
except discord.Forbidden:
291+
await ctx.send(
292+
"\N{NO ENTRY SIGN} Rodhaj is missing permissions: Manage Channels"
293+
)
294+
return
295+
except discord.HTTPException:
296+
await ctx.send(UNKNOWN_ERROR_MESSAGE)
297+
return
298+
299+
query = """
300+
DELETE FROM guild_config WHERE guild_id = $1;
301+
"""
302+
await self.pool.execute(query, guild_id)
303+
dispatcher.get_config.cache_invalidate()
304+
self.get_guild_config.cache_invalidate()
305+
await ctx.send("Successfully deleted channels")
306+
elif confirm is None:
307+
await ctx.send("Not removing Rodhaj channels. Canceling.")
308+
else:
309+
await ctx.send("Cancelling.")
310+
228311

229312
async def setup(bot: Rodhaj) -> None:
230313
await bot.add_cog(Config(bot))

bot/cogs/dev_tools.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@
44
from cogs import EXTENSIONS
55
from discord.ext import commands
66
from discord.ext.commands import Context, Greedy
7+
from libs.utils import RoboContext, RoboView
78

89
from rodhaj import Rodhaj
910

1011

12+
class MaybeView(RoboView):
13+
def __init__(self, ctx: RoboContext) -> None:
14+
super().__init__(ctx)
15+
16+
@discord.ui.button(label="eg")
17+
async def eg(
18+
self, interaction: discord.Interaction, button: discord.ui.Button
19+
) -> None:
20+
await interaction.response.send_message("yo nice oen", ephemeral=True)
21+
22+
1123
class DevTools(commands.Cog, command_attrs=dict(hidden=True)):
1224
"""Tools for developing RodHaj"""
1325

1426
def __init__(self, bot: Rodhaj):
1527
self.bot = bot
1628

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

2032
# Umbra's sync command
@@ -67,7 +79,7 @@ async def sync(
6779

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

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

8198
async def setup(bot: Rodhaj):
8299
await bot.add_cog(DevTools(bot))

bot/cogs/utilities.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pygit2
88
from discord.ext import commands
99
from discord.utils import format_dt
10-
from libs.utils import Embed, human_timedelta
10+
from libs.utils import Embed, RoboContext, human_timedelta
1111

1212
from rodhaj import Rodhaj
1313

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

5151
@commands.hybrid_command(name="about")
52-
async def about(self, ctx: commands.Context) -> None:
52+
async def about(self, ctx: RoboContext) -> None:
5353
"""Shows some stats for Rodhaj"""
5454
total_members = 0
5555
total_unique = len(self.bot.users)
@@ -83,13 +83,13 @@ async def about(self, ctx: commands.Context) -> None:
8383
await ctx.send(embed=embed)
8484

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

9191
@commands.hybrid_command(name="version")
92-
async def version(self, ctx: commands.Context) -> None:
92+
async def version(self, ctx: RoboContext) -> None:
9393
"""Displays the current build version"""
9494
version_message = f"Version: {self.bot.version}"
9595
await ctx.send(version_message)

bot/libs/utils/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
is_manager as is_manager,
44
is_mod as is_mod,
55
)
6+
from .context import RoboContext as RoboContext
67
from .embeds import Embed as Embed, ErrorEmbed as ErrorEmbed
8+
from .errors import send_error_embed as send_error_embed
79
from .logger import RodhajLogger as RodhajLogger
810
from .modals import RoboModal as RoboModal
911
from .time import human_timedelta as human_timedelta

bot/libs/utils/context.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Optional
4+
5+
import discord
6+
from discord.ext import commands
7+
8+
from .views import RoboView
9+
10+
if TYPE_CHECKING:
11+
from bot.rodhaj import Rodhaj
12+
13+
14+
class ConfirmationView(RoboView):
15+
def __init__(self, ctx, timeout: float, delete_after: bool):
16+
super().__init__(ctx, timeout)
17+
self.value: Optional[bool] = None
18+
self.delete_after = delete_after
19+
self.message: Optional[discord.Message] = None
20+
21+
async def on_timeout(self) -> None:
22+
if self.delete_after and self.message:
23+
await self.message.delete()
24+
elif self.message:
25+
await self.message.edit(view=None)
26+
27+
async def delete_response(self, interaction: discord.Interaction):
28+
await interaction.response.defer()
29+
if self.delete_after:
30+
await interaction.delete_original_response()
31+
32+
self.stop()
33+
34+
@discord.ui.button(
35+
label="Confirm",
36+
style=discord.ButtonStyle.green,
37+
emoji="<:greenTick:596576670815879169>",
38+
)
39+
async def confirm(
40+
self, interaction: discord.Interaction, button: discord.ui.Button
41+
) -> None:
42+
self.value = True
43+
await self.delete_response(interaction)
44+
45+
@discord.ui.button(
46+
label="Cancel",
47+
style=discord.ButtonStyle.red,
48+
emoji="<:redTick:596576672149667840>",
49+
)
50+
async def cancel(
51+
self, interaction: discord.Interaction, button: discord.ui.Button
52+
) -> None:
53+
self.value = False
54+
await interaction.response.defer()
55+
await interaction.delete_original_response()
56+
self.stop()
57+
58+
59+
class RoboContext(commands.Context):
60+
bot: Rodhaj
61+
62+
def __init__(self, **kwargs):
63+
super().__init__(**kwargs)
64+
65+
async def prompt(
66+
self, message: str, *, timeout: float = 60.0, delete_after: bool = False
67+
) -> Optional[bool]:
68+
view = ConfirmationView(ctx=self, timeout=timeout, delete_after=delete_after)
69+
view.message = await self.send(message, view=view, ephemeral=delete_after)
70+
await view.wait()
71+
return view.value

0 commit comments

Comments
 (0)