From 6c14132edaae85a409e2981da386e2e08d8d542b Mon Sep 17 00:00:00 2001 From: Endkind Date: Sat, 12 Jul 2025 13:55:18 +0200 Subject: [PATCH] refactor: move checks and messages to utils with deprecation notices - moved `check_*` decorators from `ModubotDiscord.commands.__init__` to `ModubotDiscord.utils.checks.permissions` and `ModubotDiscord.utils.checks.owner` - moved `send_message` and `send_error` to `ModubotDiscord.utils.messages` - added deprecation warnings for old `check_*` `send_message`, and `send_error` usages - introduced `ErrorType` enum for standardized error titles --- ModuBotDiscord/checks/owner/__init__.py | 1 + ModuBotDiscord/checks/owner/check.py | 73 ++++++++ ModuBotDiscord/checks/permissions/__init__.py | 2 + ModuBotDiscord/checks/permissions/check.py | 93 ++++++++++ ModuBotDiscord/checks/permissions/enum.py | 61 +++++++ ModuBotDiscord/commands/__init__.py | 163 ++++++------------ ModuBotDiscord/enums/permission.py | 8 + ModuBotDiscord/utils/enums.py | 9 + ModuBotDiscord/utils/messages.py | 81 +++++++++ pyproject.toml | 2 +- 10 files changed, 380 insertions(+), 113 deletions(-) create mode 100644 ModuBotDiscord/checks/owner/__init__.py create mode 100644 ModuBotDiscord/checks/owner/check.py create mode 100644 ModuBotDiscord/checks/permissions/__init__.py create mode 100644 ModuBotDiscord/checks/permissions/check.py create mode 100644 ModuBotDiscord/checks/permissions/enum.py create mode 100644 ModuBotDiscord/utils/enums.py create mode 100644 ModuBotDiscord/utils/messages.py diff --git a/ModuBotDiscord/checks/owner/__init__.py b/ModuBotDiscord/checks/owner/__init__.py new file mode 100644 index 0000000..6f3353a --- /dev/null +++ b/ModuBotDiscord/checks/owner/__init__.py @@ -0,0 +1 @@ +from .check import * diff --git a/ModuBotDiscord/checks/owner/check.py b/ModuBotDiscord/checks/owner/check.py new file mode 100644 index 0000000..0a2eb8e --- /dev/null +++ b/ModuBotDiscord/checks/owner/check.py @@ -0,0 +1,73 @@ +import functools +from typing import Awaitable, Callable, TypeVar, Union + +from discord import Interaction +from ModuBotDiscord.config import DiscordConfig +from ModuBotDiscord.utils.messages import ErrorType, send_error + +T = TypeVar("T", bound=Callable[..., Awaitable[None]]) + + +def check_bot_owner() -> Callable[[T], T]: + def decorator(func: T) -> T: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + interaction: Union[Interaction, None] = None + + for arg in args: + if isinstance(arg, Interaction): + interaction = arg + break + + if interaction is None: + interaction = kwargs.get("interaction") + + if interaction is None: + raise TypeError("No Interaction found for bot owner check.") + + if interaction.user.id != DiscordConfig.OWNER_ID: + await send_error( + interaction, + title=ErrorType.ACTION_NOT_ALLOWED, + description="You must be the bot owner to use this command.", + ) + return None + return await func(*args, **kwargs) + + return wrapper + + return decorator + + +def check_guild_owner() -> Callable[[T], T]: + def decorator(func: T) -> T: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + interaction: Union[Interaction, None] = None + + for arg in args: + if isinstance(arg, Interaction): + interaction = arg + break + + if interaction is None: + interaction = kwargs.get("interaction") + + if interaction is None: + raise TypeError("No Interaction found for guild owner check.") + + if ( + not interaction.guild + or interaction.user.id != interaction.guild.owner_id + ): + await send_error( + interaction, + title=ErrorType.ACTION_NOT_ALLOWED, + description="You must be the server owner to use this command.", + ) + return None + return await func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/ModuBotDiscord/checks/permissions/__init__.py b/ModuBotDiscord/checks/permissions/__init__.py new file mode 100644 index 0000000..2a3dff8 --- /dev/null +++ b/ModuBotDiscord/checks/permissions/__init__.py @@ -0,0 +1,2 @@ +from .check import * +from .enum import * diff --git a/ModuBotDiscord/checks/permissions/check.py b/ModuBotDiscord/checks/permissions/check.py new file mode 100644 index 0000000..9ecbed2 --- /dev/null +++ b/ModuBotDiscord/checks/permissions/check.py @@ -0,0 +1,93 @@ +import functools +from typing import Awaitable, Callable, TypeVar, Union + +from discord import Interaction +from ModuBotDiscord.utils.messages import ErrorType, send_error + +from .enum import Permission + +T = TypeVar("T", bound=Callable[..., Awaitable[None]]) + + +def check_permission(*permissions: Permission) -> Callable[[T], T]: + def decorator(func: T) -> T: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + interaction: Union[Interaction, None] = None + + for arg in args: + if isinstance(arg, Interaction): + interaction = arg + break + + if interaction is None: + interaction = kwargs.get("interaction") + + if interaction is None: + raise ValueError("No Interaction found for user permission check.") + + missing = [ + perm.value + for perm in permissions + if not getattr(interaction.user.guild_permissions, perm.value, False) + ] + if missing: + missing_permissions = ", ".join(f"`{m}`" for m in missing) + await send_error( + interaction, + title=ErrorType.ACTION_NOT_ALLOWED, + description=f"You are missing the following permissions: {missing_permissions}", + ) + return None + return await func(*args, **kwargs) + + return wrapper + + return decorator + + +def check_bot_permission(*permissions: Permission) -> Callable[[T], T]: + def decorator(func: T) -> T: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + interaction: Union[Interaction, None] = None + + for arg in args: + if isinstance(arg, Interaction): + interaction = arg + break + + if interaction is None: + interaction = kwargs.get("interaction") + + if interaction is None: + raise ValueError("No Interaction found for bot permission check.") + + if not interaction.guild: + await send_error( + interaction, + title=ErrorType.ACTION_NOT_ALLOWED, + description="This command can only be used in a server.", + ) + return None + + bot_permissions = interaction.guild.me.guild_permissions + missing = [ + perm.value + for perm in permissions + if not getattr(bot_permissions, perm.value, False) + ] + if missing: + missing_permissions = ", ".join(f"`{m}`" for m in missing) + await send_error( + interaction, + title=ErrorType.ACTION_NOT_ALLOWED, + description=f"The bot is missing the following permissions: {missing_permissions}", + ) + return None + + return await func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/ModuBotDiscord/checks/permissions/enum.py b/ModuBotDiscord/checks/permissions/enum.py new file mode 100644 index 0000000..c63ba83 --- /dev/null +++ b/ModuBotDiscord/checks/permissions/enum.py @@ -0,0 +1,61 @@ +from enum import Enum + + +class Permission(str, Enum): + ADD_REACTIONS = "add_reactions" + ADMINISTRATOR = "administrator" + ATTACH_FILES = "attach_files" + BAN_MEMBERS = "ban_members" + CHANGE_NICKNAME = "change_nickname" + CONNECT = "connect" + CREATE_EVENTS = "create_events" + CREATE_EXPRESSIONS = "create_expressions" + CREATE_INSTANT_INVITE = "create_instant_invite" + CREATE_POLLS = "create_polls" + CREATE_PRIVATE_THREADS = "create_private_threads" + CREATE_PUBLIC_THREADS = "create_public_threads" + DEAFEN_MEMBERS = "deafen_members" + EMBED_LINKS = "embed_links" + EXTERNAL_EMOJIS = "external_emojis" + EXTERNAL_STICKERS = "external_stickers" + KICK_MEMBERS = "kick_members" + MANAGE_CHANNELS = "manage_channels" + MANAGE_EMOJIS = "manage_emojis" + MANAGE_EMOJIS_AND_STICKERS = "manage_emojis_and_stickers" + MANAGE_EVENTS = "manage_events" + MANAGE_EXPRESSIONS = "manage_expressions" + MANAGE_GUILD = "manage_guild" + MANAGE_MESSAGES = "manage_messages" + MANAGE_NICKNAMES = "manage_nicknames" + MANAGE_PERMISSIONS = "manage_permissions" + MANAGE_ROLES = "manage_roles" + MANAGE_THREADS = "manage_threads" + MANAGE_WEBHOOKS = "manage_webhooks" + MENTION_EVERYONE = "mention_everyone" + MODERATE_MEMBERS = "moderate_members" + MOVE_MEMBERS = "move_members" + MUTE_MEMBERS = "mute_members" + PRIORITY_SPEAKER = "priority_speaker" + READ_MESSAGE_HISTORY = "read_message_history" + READ_MESSAGES = "read_messages" + REQUEST_TO_SPEAK = "request_to_speak" + SEND_MESSAGES = "send_messages" + SEND_MESSAGES_IN_THREADS = "send_messages_in_threads" + SEND_POLLS = "send_polls" + SEND_TTS_MESSAGES = "send_tts_messages" + SEND_VOICE_MESSAGES = "send_voice_messages" + SPEAK = "speak" + STREAM = "stream" + USE_APPLICATION_COMMANDS = "use_application_commands" + USE_EMBEDDED_ACTIVITIES = "use_embedded_activities" + USE_EXTERNAL_APPS = "use_external_apps" + USE_EXTERNAL_EMOJIS = "use_external_emojis" + USE_EXTERNAL_SOUNDS = "use_external_sounds" + USE_EXTERNAL_STICKERS = "use_external_stickers" + USE_SOUNDBOARD = "use_soundboard" + USE_VOICE_ACTIVATION = "use_voice_activation" + VALUE = "value" + VIEW_AUDIT_LOG = "view_audit_log" + VIEW_CHANNEL = "view_channel" + VIEW_CREATOR_MONETIZATION_ANALYTICS = "view_creator_monetization_analytics" + VIEW_GUILD_INSIGHTS = "view_guild_insights" diff --git a/ModuBotDiscord/commands/__init__.py b/ModuBotDiscord/commands/__init__.py index 4940078..f418461 100644 --- a/ModuBotDiscord/commands/__init__.py +++ b/ModuBotDiscord/commands/__init__.py @@ -1,13 +1,19 @@ -import functools import logging import warnings from abc import ABC, abstractmethod -from typing import Awaitable, Callable, Dict, List, Optional, Sequence, TypeVar, Union +from typing import Awaitable, Callable, List, Optional, TypeVar, Union import discord from discord import Embed, Interaction from discord.utils import MISSING, _MissingSentinel -from ModuBotDiscord.config import DiscordConfig +from ModuBotDiscord.checks.owner import check_bot_owner as _check_bot_owner_new +from ModuBotDiscord.checks.owner import check_guild_owner as _check_guild_owner_new +from ModuBotDiscord.checks.permissions import ( + check_bot_permission as _check_bot_permission_new, +) +from ModuBotDiscord.checks.permissions import check_permission as _check_permission_new +from ModuBotDiscord.utils.messages import send_error as _send_error_new +from ModuBotDiscord.utils.messages import send_message as _send_message_new from ..enums import PermissionEnum @@ -38,6 +44,12 @@ async def send_message( discord.interactions.InteractionMessage, discord.webhook.async_.WebhookMessage ] ]: + warnings.warn( + "`ModuBotDiscord.commands.send_message` is deprecated, use `ModuBotDiscord.utils.messages.send_message` instead", + DeprecationWarning, + stacklevel=2, + ) + if msg is not None: warnings.warn( "`msg` is deprecated, use `content` instead", @@ -48,27 +60,8 @@ async def send_message( if content is None: content = msg - if interaction.is_expired(): - logger.warning("Interaction is expired. Skipping send_message().") - return None - - if interaction.response.is_done(): - return await interaction.followup.send( - content=content, - embed=embed, - embeds=embeds, - file=file, - files=files, - view=view, - tts=tts, - ephemeral=ephemeral, - allowed_mentions=allowed_mentions, - suppress_embeds=suppress_embeds, - silent=silent, - poll=poll, - ) - - await interaction.response.send_message( + return await _send_message_new( + interaction=interaction, content=content, embed=embed, embeds=embeds, @@ -84,8 +77,6 @@ async def send_message( poll=poll, ) - return await interaction.original_response() - async def send_error( interaction: Interaction, @@ -93,6 +84,12 @@ async def send_error( description: Optional[str] = None, msg: Optional[str] = None, ) -> None: + warnings.warn( + "`ModuBotDiscord.commands.send_error` is deprecated, use `ModuBotDiscord.utils.messages.send_error` instead", + DeprecationWarning, + stacklevel=2, + ) + if msg is not None: warnings.warn( "`msg` is deprecated, use `title` or `description` instead", @@ -104,105 +101,47 @@ async def send_error( else: title = msg - embed: Embed = Embed(title=title, description=description, color=0xFF0000) - await send_message(interaction, embed=embed, ephemeral=True) + await _send_error_new(interaction=interaction, title=title, description=description) def check_permission(*permissions: PermissionEnum) -> Callable[[T], T]: - def decorator(func: T) -> T: - @functools.wraps(func) - async def wrapper(interaction: Interaction, *args, **kwargs): - missing = [ - perm.value - for perm in permissions - if not getattr(interaction.user.guild_permissions, perm.value, False) - ] - if missing: - missing_permissions = ", ".join(f"`{m}`" for m in missing) - await send_error( - interaction, - title="🚫 Action not allowed", - description=f"You are missing the following permissions: {missing_permissions}", - ) - return None - return await func(interaction, *args, **kwargs) - - return wrapper - - return decorator + warnings.warn( + "`ModuBotDiscord.commands.check_permission` is deprecated, use `ModuBotDiscord.checks.permissions.check_permission` instead", + DeprecationWarning, + stacklevel=2, + ) + + return _check_permission_new(*permissions) def check_bot_permission(*permissions: PermissionEnum) -> Callable[[T], T]: - def decorator(func: T) -> T: - @functools.wraps(func) - async def wrapper(interaction: Interaction, *args, **kwargs): - if not interaction.guild: - await send_error( - interaction, - title="🚫 Action not allowed", - description="This command can only be used in a server.", - ) - return None - - bot_permissions = interaction.guild.me.guild_permissions - missing = [ - perm.value - for perm in permissions - if not getattr(bot_permissions, perm.value, False) - ] - if missing: - missing_permissions = ", ".join(f"`{m}`" for m in missing) - await send_error( - interaction, - title="🚫 Action not allowed", - description=f"The bot is missing the following permissions: {missing_permissions}", - ) - return None - - return await func(interaction, *args, **kwargs) - - return wrapper - - return decorator + warnings.warn( + "`ModuBotDiscord.commands.check_bot_permission` is deprecated, use `ModuBotDiscord.checks.permissions.check_bot_permission` instead", + DeprecationWarning, + stacklevel=2, + ) + return _check_bot_permission_new(*permissions) -def check_bot_owner() -> Callable[[T], T]: - def decorator(func: T) -> T: - @functools.wraps(func) - async def wrapper(interaction: Interaction, *args, **kwargs): - if interaction.user.id != DiscordConfig.OWNER_ID: - await send_error( - interaction, - title="🚫 Action not allowed", - description="You must be the bot owner to use this command.", - ) - return None - return await func(interaction, *args, **kwargs) - return wrapper +def check_bot_owner() -> Callable[[T], T]: + warnings.warn( + "`ModuBotDiscord.commands.check_bot_owner` is deprecated, use `ModuBotDiscord.checks.owner.check_bot_owner` instead", + DeprecationWarning, + stacklevel=2, + ) - return decorator + return _check_bot_owner_new() def check_guild_owner() -> Callable[[T], T]: - def decorator(func: T) -> T: - @functools.wraps(func) - async def wrapper(interaction: Interaction, *args, **kwargs): - if ( - not interaction.guild - or interaction.user.id != interaction.guild.owner_id - ): - await send_error( - interaction, - title="🚫 Action not allowed", - description="You must be the server owner to use this command.", - ) - return None - return await func(interaction, *args, **kwargs) - - return wrapper - - return decorator + warnings.warn( + "`ModuBotDiscord.commands.check_guild_owner` is deprecated, use `ModuBotDiscord.checks.owner.check_guild_owner` instead", + DeprecationWarning, + stacklevel=2, + ) + + return _check_guild_owner_new() class BaseCommand(ABC): diff --git a/ModuBotDiscord/enums/permission.py b/ModuBotDiscord/enums/permission.py index 3012b30..e13a10d 100644 --- a/ModuBotDiscord/enums/permission.py +++ b/ModuBotDiscord/enums/permission.py @@ -1,3 +1,4 @@ +import warnings from enum import Enum @@ -59,3 +60,10 @@ class PermissionEnum(str, Enum): VIEW_CHANNEL = "view_channel" VIEW_CREATOR_MONETIZATION_ANALYTICS = "view_creator_monetization_analytics" VIEW_GUILD_INSIGHTS = "view_guild_insights" + + def __new__(cls, value): + warnings.warn( + "`ModuBotDiscord.enums.permission.PermissionEnum` is deprecated, use `ModuBotDiscord.checks.permissions.Permission` instead", + DeprecationWarning, + stacklevel=2, + ) diff --git a/ModuBotDiscord/utils/enums.py b/ModuBotDiscord/utils/enums.py new file mode 100644 index 0000000..d332c32 --- /dev/null +++ b/ModuBotDiscord/utils/enums.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ErrorType(Enum): + DEFAULT = "⚠️ An error occurred" + ACTION_NOT_ALLOWED = "🚫 Action not allowed" + VALIDATION = "❗ Invalid input" + TIMEOUT = "⌛ Timeout occurred" + NOT_FOUND = "🔍 Not found" diff --git a/ModuBotDiscord/utils/messages.py b/ModuBotDiscord/utils/messages.py new file mode 100644 index 0000000..fa0c566 --- /dev/null +++ b/ModuBotDiscord/utils/messages.py @@ -0,0 +1,81 @@ +import logging +from typing import List, Optional, Union + +from discord import AllowedMentions, Embed, File, Interaction, Poll +from discord.interactions import InteractionMessage +from discord.ui import View +from discord.utils import MISSING, _MissingSentinel +from discord.webhook.async_ import WebhookMessage + +from .enums import ErrorType + +logger = logging.getLogger(__name__) + + +async def send_message( + interaction: Interaction, + content: Optional[str] = None, + *, + embed: Union[Embed, _MissingSentinel] = MISSING, + embeds: Union[List[Embed], _MissingSentinel] = MISSING, + file: Union[File, _MissingSentinel] = MISSING, + files: Union[List[File], _MissingSentinel] = MISSING, + view: Union[View, _MissingSentinel] = MISSING, + tts: bool = False, + ephemeral: bool = False, + allowed_mentions: Union[AllowedMentions, _MissingSentinel] = MISSING, + suppress_embeds: bool = False, + silent: bool = False, + delete_after: Optional[float] = None, + poll: Union[Poll, _MissingSentinel] = MISSING, +) -> Optional[Union[InteractionMessage, WebhookMessage]]: + if interaction.is_expired(): + logger.warning("Interaction is expired. Skipping send_message().") + return None + + if interaction.response.is_done(): + return await interaction.followup.send( + content=content, + embed=embed, + embeds=embeds, + file=file, + files=files, + view=view, + tts=tts, + ephemeral=ephemeral, + allowed_mentions=allowed_mentions, + suppress_embeds=suppress_embeds, + silent=silent, + poll=poll, + ) + + await interaction.response.send_message( + content=content, + embed=embed, + embeds=embeds, + file=file, + files=files, + view=view, + tts=tts, + ephemeral=ephemeral, + allowed_mentions=allowed_mentions, + suppress_embeds=suppress_embeds, + silent=silent, + delete_after=delete_after, + poll=poll, + ) + + return await interaction.original_response() + + +async def send_error( + interaction: Interaction, + title: Union[str, ErrorType] = ErrorType.DEFAULT, + description: Optional[str] = None, +) -> None: + + if isinstance(title, ErrorType): + title = title.value + + embed: Embed = Embed(title=title, description=description, color=0xFF0000) + await send_message(interaction, embed=embed, ephemeral=True) diff --git a/pyproject.toml b/pyproject.toml index ee24c03..d4c5067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ModuBotDiscord" -version = "0.4.0" +version = "0.5.0" description = "Modular Discord bot framework built on top of ModuBotCore" authors = [{ name = "Endkind", email = "endkind.ender@endkind.net" }] readme = "README.md"