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

Add exit call to kill command #202

55 changes: 36 additions & 19 deletions cogs/kill.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import contextlib
import logging
from logging import Logger
from typing import Final

import discord
from discord.ui import View
Expand All @@ -21,14 +22,28 @@
class ConfirmKillView(View):
"""A discord.View containing two buttons to confirm shutting down TeX-Bot."""

@classmethod
async def _delete_message(cls, response: discord.InteractionResponse) -> None:
e: discord.NotFound
try:
await response.edit_message(delete_after=0)
except discord.NotFound as e:
MESSAGE_WAS_ALREADY_DELETED: Final[bool] = (
e.code == 10008
or ("unknown" in e.text.lower() and "message" in e.text.lower())
)
if not MESSAGE_WAS_ALREADY_DELETED:
raise e from e

@discord.ui.button( # type: ignore[misc]
label="SHUTDOWN",
style=discord.ButtonStyle.red,
custom_id="shutdown_confirm",
)
async def confirm_shutdown_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501
"""When the shutdown button is pressed, delete the message."""
await interaction.response.edit_message(delete_after=0)
# noinspection PyTypeChecker
await self._delete_message(interaction.response)

@discord.ui.button( # type: ignore[misc]
label="CANCEL",
Expand All @@ -37,7 +52,8 @@ async def confirm_shutdown_button_callback(self, _: discord.Button, interaction:
)
async def cancel_shutdown_button_callback(self, _: discord.Button, interaction: discord.Interaction) -> None: # noqa: E501
"""When the cancel button is pressed, delete the message."""
await interaction.response.edit_message(delete_after=0)
# noinspection PyTypeChecker
await self._delete_message(interaction.response)


class KillCommandCog(TeXBotBaseCog):
Expand All @@ -60,46 +76,47 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None:
with contextlib.suppress(BaseDoesNotExistError):
committee_role = await self.bot.committee_role

confirmation_message: discord.Message | discord.Interaction = await ctx.respond(
response: discord.Message | discord.Interaction = await ctx.respond(
content=(
f"{f"Hi {committee_role.mention}, a" if committee_role else "A"}"
"re you sure you want to kill me?\n"
"This action is irreversible "
"and will prevent me from performing any further actions.\n\n"
"and will prevent me from performing any further actions "
"until I am manually restarted.\n\n"
"Please confirm using the buttons below."
),
view=ConfirmKillView(),
)
confirmation_message: discord.Message = (
response
if isinstance(response, discord.Message)
else await response.original_response()
)

button_interaction: discord.Interaction = await self.bot.wait_for(
"interaction",
check=lambda interaction: (
interaction.type == discord.InteractionType.component
and interaction.message == confirmation_message
and interaction.message.id == confirmation_message.id
and ((committee_role in interaction.user.roles) if committee_role else True)
and "custom_id" in interaction.data
and interaction.data["custom_id"] in {"shutdown_confirm", "shutdown_cancel"}
),
)

await confirmation_message.delete()

if button_interaction.data["custom_id"] == "shutdown_confirm": # type: ignore[index, typeddict-item]
if isinstance(confirmation_message, discord.Message):
await confirmation_message.delete()
elif confirmation_message.message is not None:
await confirmation_message.message.delete()

await ctx.respond(content="My battery is low and it's getting dark...")
logger.info("Manual shutdown initiated by %s.", ctx.interaction.user)
await self.bot.close()
await button_interaction.respond(
content="My battery is low and it's getting dark...",
)
await self.bot.perform_kill_and_close(initiated_by_user=ctx.interaction.user)
return

CarrotManMatt marked this conversation as resolved.
Show resolved Hide resolved
if button_interaction.data["custom_id"] == "shutdown_cancel": # type: ignore[index, typeddict-item]
if isinstance(confirmation_message, discord.Message):
await confirmation_message.delete()
elif confirmation_message.message is not None:
await confirmation_message.message.delete()

await ctx.respond(content="Shutdown has been cancelled.")
await button_interaction.respond(
content="Shutdown has been cancelled.",
)
logger.info("Manual shutdown cancelled by %s.", ctx.interaction.user)
return

Expand Down
5 changes: 5 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@

if __name__ == "__main__":
bot.run(settings["DISCORD_BOT_TOKEN"])

if bot.EXIT_WAS_DUE_TO_KILL_COMMAND:
raise SystemExit(0)

raise SystemExit(1)
29 changes: 29 additions & 0 deletions utils/tex_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
__all__: Sequence[str] = ("TeXBot",)


import logging
import re
from logging import Logger
from typing import Final, TypeAlias

import discord
Expand Down Expand Up @@ -33,6 +35,8 @@
| None
)

logger: Logger = logging.getLogger("TeX-Bot")


class TeXBot(discord.Bot):
"""
Expand All @@ -54,11 +58,18 @@ def __init__(self, *args: object, **options: object) -> None:
self._roles_channel: discord.TextChannel | None = None
self._general_channel: discord.TextChannel | None = None
self._rules_channel: discord.TextChannel | None = None
self._exit_was_due_to_kill_command: bool = False

self._main_guild_set: bool = False

super().__init__(*args, **options) # type: ignore[no-untyped-call]

# noinspection PyPep8Naming
@property
def EXIT_WAS_DUE_TO_KILL_COMMAND(self) -> bool: # noqa: N802
"""Return whether the TeX-Bot exited due to the kill command being used."""
return self._exit_was_due_to_kill_command

@property
def main_guild(self) -> discord.Guild:
"""
Expand Down Expand Up @@ -341,6 +352,24 @@ async def _fetch_text_channel(self, name: str) -> discord.TextChannel | None:

return text_channel

async def perform_kill_and_close(self, initiated_by_user: discord.User | discord.Member | None = None) -> None: # noqa: E501
"""
Shutdown TeX-Bot by using the "/kill" command.

A log message will also be sent, announcing the user that requested the shutdown.
"""
if self.EXIT_WAS_DUE_TO_KILL_COMMAND:
EXIT_FLAG_ALREADY_SET_MESSAGE: Final[str] = (
"The kill & close command has already been used. Invalid state."
)
raise RuntimeError(EXIT_FLAG_ALREADY_SET_MESSAGE)

if initiated_by_user:
logger.info("Manual shutdown initiated by %s.", initiated_by_user)

self._exit_was_due_to_kill_command = True
await self.close()

async def get_everyone_role(self) -> discord.Role:
"""
Util method to retrieve the "@everyone" role from your group's Discord guild.
Expand Down