Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement deletion process #16

Merged
merged 5 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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