187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
# pyright: reportIncompatibleMethodOverride=false
|
|
|
|
from typing import TYPE_CHECKING, Any, Awaitable, Callable, TypeVar, Generic
|
|
from abc import ABC, abstractmethod
|
|
import re
|
|
import inspect
|
|
|
|
from .errors import BadArgument
|
|
from disagreement.models import Member, Guild, Role
|
|
|
|
if TYPE_CHECKING:
|
|
from .core import CommandContext
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class Converter(ABC, Generic[T]):
|
|
"""
|
|
Base class for custom command argument converters.
|
|
Subclasses must implement the `convert` method.
|
|
"""
|
|
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> T:
|
|
"""
|
|
Converts the argument to the desired type.
|
|
|
|
Args:
|
|
ctx: The invocation context.
|
|
argument: The string argument to convert.
|
|
|
|
Returns:
|
|
The converted argument.
|
|
|
|
Raises:
|
|
BadArgument: If the conversion fails.
|
|
"""
|
|
raise NotImplementedError("Converter subclass must implement convert method.")
|
|
|
|
|
|
class Greedy(list):
|
|
"""Type hint helper to greedily consume arguments."""
|
|
|
|
converter: Any = None
|
|
|
|
def __class_getitem__(cls, param: Any) -> type: # pyright: ignore[override]
|
|
if isinstance(param, tuple):
|
|
if len(param) != 1:
|
|
raise TypeError("Greedy[...] expects a single parameter")
|
|
param = param[0]
|
|
name = f"Greedy[{getattr(param, '__name__', str(param))}]"
|
|
return type(name, (Greedy,), {"converter": param})
|
|
|
|
|
|
# --- Built-in Type Converters ---
|
|
|
|
|
|
class IntConverter(Converter[int]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> int:
|
|
try:
|
|
return int(argument)
|
|
except ValueError:
|
|
raise BadArgument(f"'{argument}' is not a valid integer.")
|
|
|
|
|
|
class FloatConverter(Converter[float]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> float:
|
|
try:
|
|
return float(argument)
|
|
except ValueError:
|
|
raise BadArgument(f"'{argument}' is not a valid number.")
|
|
|
|
|
|
class BoolConverter(Converter[bool]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> bool:
|
|
lowered = argument.lower()
|
|
if lowered in ("yes", "y", "true", "t", "1", "on", "enable", "enabled"):
|
|
return True
|
|
elif lowered in ("no", "n", "false", "f", "0", "off", "disable", "disabled"):
|
|
return False
|
|
raise BadArgument(f"'{argument}' is not a valid boolean-like value.")
|
|
|
|
|
|
class StringConverter(Converter[str]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> str:
|
|
# For basic string, no conversion is needed, but this provides a consistent interface
|
|
return argument
|
|
|
|
|
|
# --- Discord Model Converters ---
|
|
|
|
|
|
class MemberConverter(Converter["Member"]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> "Member":
|
|
if not ctx.message.guild_id:
|
|
raise BadArgument("Member converter requires guild context.")
|
|
|
|
match = re.match(r"<@!?(\d+)>$", argument)
|
|
member_id = match.group(1) if match else argument
|
|
|
|
guild = ctx.bot.get_guild(ctx.message.guild_id)
|
|
if guild:
|
|
member = guild.get_member(member_id)
|
|
if member:
|
|
return member
|
|
|
|
member = await ctx.bot.fetch_member(ctx.message.guild_id, member_id)
|
|
if member:
|
|
return member
|
|
raise BadArgument(f"Member '{argument}' not found.")
|
|
|
|
|
|
class RoleConverter(Converter["Role"]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> "Role":
|
|
if not ctx.message.guild_id:
|
|
raise BadArgument("Role converter requires guild context.")
|
|
|
|
match = re.match(r"<@&(?P<id>\d+)>$", argument)
|
|
role_id = match.group("id") if match else argument
|
|
|
|
guild = ctx.bot.get_guild(ctx.message.guild_id)
|
|
if guild:
|
|
role = guild.get_role(role_id)
|
|
if role:
|
|
return role
|
|
|
|
role = await ctx.bot.fetch_role(ctx.message.guild_id, role_id)
|
|
if role:
|
|
return role
|
|
raise BadArgument(f"Role '{argument}' not found.")
|
|
|
|
|
|
class GuildConverter(Converter["Guild"]):
|
|
async def convert(self, ctx: "CommandContext", argument: str) -> "Guild":
|
|
guild_id = argument.strip("<>") # allow <id> style
|
|
|
|
guild = ctx.bot.get_guild(guild_id)
|
|
if guild:
|
|
return guild
|
|
|
|
guild = await ctx.bot.fetch_guild(guild_id)
|
|
if guild:
|
|
return guild
|
|
raise BadArgument(f"Guild '{argument}' not found.")
|
|
|
|
|
|
# Default converters mapping
|
|
DEFAULT_CONVERTERS: dict[type, Converter[Any]] = {
|
|
int: IntConverter(),
|
|
float: FloatConverter(),
|
|
bool: BoolConverter(),
|
|
str: StringConverter(),
|
|
Member: MemberConverter(),
|
|
Guild: GuildConverter(),
|
|
Role: RoleConverter(),
|
|
# User: UserConverter(), # Add when User model and converter are ready
|
|
}
|
|
|
|
|
|
async def run_converters(ctx: "CommandContext", annotation: Any, argument: str) -> Any:
|
|
"""
|
|
Attempts to run a converter for the given annotation and argument.
|
|
"""
|
|
converter = DEFAULT_CONVERTERS.get(annotation)
|
|
if converter:
|
|
return await converter.convert(ctx, argument)
|
|
|
|
# If no direct converter, check if annotation itself is a Converter subclass
|
|
if inspect.isclass(annotation) and issubclass(annotation, Converter):
|
|
try:
|
|
instance = annotation() # type: ignore
|
|
return await instance.convert(ctx, argument)
|
|
except Exception as e: # Catch instantiation errors or other issues
|
|
raise BadArgument(
|
|
f"Failed to use custom converter {annotation.__name__}: {e}"
|
|
)
|
|
|
|
# If it's a custom class that's not a Converter, we can't handle it by default
|
|
# Or if it's a complex type hint like Union, Optional, Literal etc.
|
|
# This part needs more advanced logic for those.
|
|
|
|
# For now, if no specific converter, and it's not 'str', raise error or return as str?
|
|
# Let's be strict for now if an annotation is given but no converter found.
|
|
if annotation is not str and annotation is not inspect.Parameter.empty:
|
|
raise BadArgument(f"No converter found for type annotation '{annotation}'.")
|
|
|
|
return argument # Default to string if no annotation or annotation is str
|