Fix weird git problems
This commit is contained in:
commit
0aa8c5bef3
@ -595,7 +595,7 @@ class Client:
|
|||||||
async def start(self, token: str, *, reconnect: bool = True) -> None:
|
async def start(self, token: str, *, reconnect: bool = True) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
A shorthand coroutine for :meth:`login` + :meth:`connect`.
|
A shorthand coroutine for :meth:`login` + :meth:`setup` + :meth:`connect`.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
-------
|
-------
|
||||||
@ -603,8 +603,19 @@ class Client:
|
|||||||
An unexpected keyword argument was received.
|
An unexpected keyword argument was received.
|
||||||
"""
|
"""
|
||||||
await self.login(token)
|
await self.login(token)
|
||||||
|
await self.setup()
|
||||||
await self.connect(reconnect=reconnect)
|
await self.connect(reconnect=reconnect)
|
||||||
|
|
||||||
|
async def setup(self) -> Any:
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
A coroutine to be called to setup the bot, by default this is blank.
|
||||||
|
|
||||||
|
To perform asynchronous setup after the bot is logged in but before
|
||||||
|
it has connected to the Websocket, overwrite this coroutine.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""A blocking call that abstracts away the event loop
|
"""A blocking call that abstracts away the event loop
|
||||||
initialisation from you.
|
initialisation from you.
|
||||||
|
@ -28,17 +28,24 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import collections
|
import collections
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
from discord.http import HTTPClient
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
from typing import Any, Callable, Mapping, List, Dict, TYPE_CHECKING, Optional, TypeVar, Type, Union
|
from typing import Any, Callable, cast, Mapping, List, Dict, TYPE_CHECKING, Optional, TypeVar, Type, Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
from discord.types.interactions import (
|
||||||
|
ApplicationCommandInteractionData,
|
||||||
|
_ApplicationCommandInteractionDataOptionString
|
||||||
|
)
|
||||||
|
|
||||||
from .core import GroupMixin
|
from .core import GroupMixin
|
||||||
from .view import StringView
|
from .converter import Greedy
|
||||||
|
from .view import StringView, supported_quotes
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from . import errors
|
from . import errors
|
||||||
from .help import HelpCommand, DefaultHelpCommand
|
from .help import HelpCommand, DefaultHelpCommand
|
||||||
@ -66,6 +73,13 @@ T = TypeVar('T')
|
|||||||
CFT = TypeVar('CFT', bound='CoroFunc')
|
CFT = TypeVar('CFT', bound='CoroFunc')
|
||||||
CXT = TypeVar('CXT', bound='Context')
|
CXT = TypeVar('CXT', bound='Context')
|
||||||
|
|
||||||
|
class _FakeSlashMessage(discord.PartialMessage):
|
||||||
|
activity = application = edited_at = reference = webhook_id = None
|
||||||
|
attachments = components = reactions = stickers = mentions = []
|
||||||
|
author: Union[discord.User, discord.Member]
|
||||||
|
tts = False
|
||||||
|
|
||||||
|
|
||||||
def when_mentioned(bot: Union[Bot, AutoShardedBot], msg: Message) -> List[str]:
|
def when_mentioned(bot: Union[Bot, AutoShardedBot], msg: Message) -> List[str]:
|
||||||
"""A callable that implements a command prefix equivalent to being mentioned.
|
"""A callable that implements a command prefix equivalent to being mentioned.
|
||||||
|
|
||||||
@ -120,9 +134,17 @@ class _DefaultRepr:
|
|||||||
_default = _DefaultRepr()
|
_default = _DefaultRepr()
|
||||||
|
|
||||||
class BotBase(GroupMixin):
|
class BotBase(GroupMixin):
|
||||||
def __init__(self, command_prefix, help_command=_default, description=None, **options):
|
def __init__(self,
|
||||||
|
command_prefix,
|
||||||
|
help_command=_default,
|
||||||
|
description=None,
|
||||||
|
message_commands: bool = True,
|
||||||
|
slash_commands: bool = False, **options
|
||||||
|
):
|
||||||
super().__init__(**options)
|
super().__init__(**options)
|
||||||
self.command_prefix = command_prefix
|
self.command_prefix = command_prefix
|
||||||
|
self.slash_commands = slash_commands
|
||||||
|
self.message_commands = message_commands
|
||||||
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
||||||
self.__cogs: Dict[str, Cog] = {}
|
self.__cogs: Dict[str, Cog] = {}
|
||||||
self.__extensions: Dict[str, types.ModuleType] = {}
|
self.__extensions: Dict[str, types.ModuleType] = {}
|
||||||
@ -142,11 +164,17 @@ class BotBase(GroupMixin):
|
|||||||
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
||||||
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
|
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
|
||||||
|
|
||||||
|
if not (message_commands or slash_commands):
|
||||||
|
raise TypeError("Both message_commands and slash_commands are disabled.")
|
||||||
|
elif slash_commands:
|
||||||
|
self.slash_command_guild = options.get('slash_command_guild', None)
|
||||||
|
|
||||||
if help_command is _default:
|
if help_command is _default:
|
||||||
self.help_command = DefaultHelpCommand()
|
self.help_command = DefaultHelpCommand()
|
||||||
else:
|
else:
|
||||||
self.help_command = help_command
|
self.help_command = help_command
|
||||||
|
|
||||||
|
|
||||||
# internal helpers
|
# internal helpers
|
||||||
|
|
||||||
def dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None:
|
def dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None:
|
||||||
@ -156,6 +184,15 @@ class BotBase(GroupMixin):
|
|||||||
for event in self.extra_events.get(ev, []):
|
for event in self.extra_events.get(ev, []):
|
||||||
self._schedule_event(event, ev, *args, **kwargs) # type: ignore
|
self._schedule_event(event, ev, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
async def _create_application_commands(self, application_id: int, http: HTTPClient):
|
||||||
|
commands = [scmd for cmd in self.commands if not cmd.hidden and (scmd := cmd.to_application_command()) is not None]
|
||||||
|
|
||||||
|
if self.slash_command_guild is None:
|
||||||
|
await http.bulk_upsert_global_commands(application_id, payload=commands)
|
||||||
|
else:
|
||||||
|
await http.bulk_upsert_guild_commands(application_id, self.slash_command_guild, payload=commands)
|
||||||
|
|
||||||
|
|
||||||
@discord.utils.copy_doc(discord.Client.close)
|
@discord.utils.copy_doc(discord.Client.close)
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
for extension in tuple(self.__extensions):
|
for extension in tuple(self.__extensions):
|
||||||
@ -1031,8 +1068,89 @@ class BotBase(GroupMixin):
|
|||||||
await self.invoke(ctx)
|
await self.invoke(ctx)
|
||||||
|
|
||||||
async def on_message(self, message):
|
async def on_message(self, message):
|
||||||
|
if self.message_commands:
|
||||||
await self.process_commands(message)
|
await self.process_commands(message)
|
||||||
|
|
||||||
|
async def on_interaction(self, interaction: discord.Interaction):
|
||||||
|
if not self.slash_commands or interaction.type != discord.InteractionType.application_command:
|
||||||
|
return
|
||||||
|
|
||||||
|
assert interaction.user is not None
|
||||||
|
interaction.data = cast(ApplicationCommandInteractionData, interaction.data)
|
||||||
|
|
||||||
|
# Ensure the interaction channel is usable
|
||||||
|
channel = interaction.channel
|
||||||
|
if channel is None or isinstance(channel, discord.PartialMessageable):
|
||||||
|
if interaction.guild is None:
|
||||||
|
channel = await interaction.user.create_dm()
|
||||||
|
elif interaction.channel_id is not None:
|
||||||
|
channel = await interaction.guild.fetch_channel(interaction.channel_id)
|
||||||
|
else:
|
||||||
|
return # cannot do anything without stable channel
|
||||||
|
|
||||||
|
# Fetch out subcommands from the options
|
||||||
|
command_name = interaction.data['name']
|
||||||
|
command_options = interaction.data.get('options') or []
|
||||||
|
while any(o["type"] in {1, 2} for o in command_options):
|
||||||
|
for option in command_options:
|
||||||
|
if option['type'] in {1, 2}:
|
||||||
|
command_name += f' {option["name"]}'
|
||||||
|
command_options = option.get('options') or []
|
||||||
|
|
||||||
|
command = self.get_command(command_name)
|
||||||
|
if command is None:
|
||||||
|
raise errors.CommandNotFound(f'Command "{command_name}" is not found')
|
||||||
|
|
||||||
|
message: discord.Message = _FakeSlashMessage(id=interaction.id, channel=channel) # type: ignore
|
||||||
|
message.author = interaction.user
|
||||||
|
|
||||||
|
# Fetch a valid prefix, so process_commands can function
|
||||||
|
prefix = await self.get_prefix(message)
|
||||||
|
if isinstance(prefix, list):
|
||||||
|
prefix = prefix[0]
|
||||||
|
|
||||||
|
# Add arguments to fake message content, in the right order
|
||||||
|
message.content = f'{prefix}{command_name} '
|
||||||
|
for name, param in command.clean_params.items():
|
||||||
|
option = next((o for o in command_options if o['name'] == name), None) # type: ignore
|
||||||
|
|
||||||
|
if option is None:
|
||||||
|
if param.default is param.empty and not command._is_typing_optional(param.annotation):
|
||||||
|
raise errors.MissingRequiredArgument(param)
|
||||||
|
elif (
|
||||||
|
option["type"] == 3
|
||||||
|
and " " in option["value"] # type: ignore
|
||||||
|
and param.kind != param.KEYWORD_ONLY
|
||||||
|
and not isinstance(param.annotation, Greedy)
|
||||||
|
):
|
||||||
|
# String with space in without "consume rest"
|
||||||
|
option = cast(_ApplicationCommandInteractionDataOptionString, option)
|
||||||
|
|
||||||
|
# we need to quote this string otherwise we may spill into
|
||||||
|
# other parameters and cause all kinds of trouble, as many
|
||||||
|
# quotes are supported and some may be in the option, we
|
||||||
|
# loop through all supported quotes and if neither open or
|
||||||
|
# close are in the string, we add them
|
||||||
|
quoted = False
|
||||||
|
string = option['value']
|
||||||
|
for open, close in supported_quotes.items():
|
||||||
|
if not (open in string or close in string):
|
||||||
|
message.content += f"{open}{string}{close} "
|
||||||
|
quoted = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# all supported quotes are in the message and we cannot add any
|
||||||
|
# safely, very unlikely but still got to be covered
|
||||||
|
if not quoted:
|
||||||
|
raise errors.UnexpectedQuoteError(string)
|
||||||
|
else:
|
||||||
|
message.content += f'{option.get("value", "")} '
|
||||||
|
|
||||||
|
ctx = await self.get_context(message)
|
||||||
|
ctx.interaction = interaction
|
||||||
|
await self.invoke(ctx)
|
||||||
|
|
||||||
|
|
||||||
class Bot(BotBase, discord.Client):
|
class Bot(BotBase, discord.Client):
|
||||||
"""Represents a discord bot.
|
"""Represents a discord bot.
|
||||||
|
|
||||||
@ -1103,10 +1221,20 @@ class Bot(BotBase, discord.Client):
|
|||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
"""
|
"""
|
||||||
pass
|
async def setup(self):
|
||||||
|
if not self.slash_commands:
|
||||||
|
return
|
||||||
|
|
||||||
|
application = self.application_id or (await self.application_info()).id
|
||||||
|
await self._create_application_commands(application, self.http)
|
||||||
|
|
||||||
class AutoShardedBot(BotBase, discord.AutoShardedClient):
|
class AutoShardedBot(BotBase, discord.AutoShardedClient):
|
||||||
"""This is similar to :class:`.Bot` except that it is inherited from
|
"""This is similar to :class:`.Bot` except that it is inherited from
|
||||||
:class:`discord.AutoShardedClient` instead.
|
:class:`discord.AutoShardedClient` instead.
|
||||||
"""
|
"""
|
||||||
pass
|
async def setup(self):
|
||||||
|
if not self.slash_commands:
|
||||||
|
return
|
||||||
|
|
||||||
|
application = self.application_id or (await self.application_info()).id
|
||||||
|
await self._create_application_commands(application, self.http)
|
||||||
|
@ -25,8 +25,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, Generic, List, Optional, TYPE_CHECKING, TypeVar, Union
|
from typing import Any, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload
|
||||||
|
|
||||||
import discord.abc
|
import discord.abc
|
||||||
import discord.utils
|
import discord.utils
|
||||||
@ -41,6 +41,8 @@ if TYPE_CHECKING:
|
|||||||
from discord.member import Member
|
from discord.member import Member
|
||||||
from discord.state import ConnectionState
|
from discord.state import ConnectionState
|
||||||
from discord.user import ClientUser, User
|
from discord.user import ClientUser, User
|
||||||
|
from discord.webhook import WebhookMessage
|
||||||
|
from discord.interactions import Interaction
|
||||||
from discord.voice_client import VoiceProtocol
|
from discord.voice_client import VoiceProtocol
|
||||||
|
|
||||||
from .bot import Bot, AutoShardedBot
|
from .bot import Bot, AutoShardedBot
|
||||||
@ -121,6 +123,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
A boolean that indicates if the command failed to be parsed, checked,
|
A boolean that indicates if the command failed to be parsed, checked,
|
||||||
or invoked.
|
or invoked.
|
||||||
"""
|
"""
|
||||||
|
interaction: Optional[Interaction] = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
*,
|
*,
|
||||||
@ -395,6 +398,81 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
except CommandError as e:
|
except CommandError as e:
|
||||||
await cmd.on_help_command_error(self, e)
|
await cmd.on_help_command_error(self, e)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def send(self,
|
||||||
|
content: Optional[str] = None,
|
||||||
|
return_message: Literal[False] = False,
|
||||||
|
ephemeral: bool = False, **kwargs: Any
|
||||||
|
) -> Optional[Union[Message, WebhookMessage]]: ...
|
||||||
|
@overload
|
||||||
|
async def send(self,
|
||||||
|
content: Optional[str] = None,
|
||||||
|
return_message: Literal[True] = True,
|
||||||
|
ephemeral: bool = False, **kwargs: Any
|
||||||
|
) -> Union[Message, WebhookMessage]: ...
|
||||||
|
|
||||||
|
async def send(self,
|
||||||
|
content: Optional[str] = None,
|
||||||
|
return_message: bool = True,
|
||||||
|
ephemeral: bool = False, **kwargs: Any
|
||||||
|
) -> Optional[Union[Message, WebhookMessage]]:
|
||||||
|
"""
|
||||||
|
|coro|
|
||||||
|
|
||||||
|
A shortcut method to :meth:`.abc.Messageable.send` with interaction helpers.
|
||||||
|
|
||||||
|
This function takes all the parameters of :meth:`.abc.Messageable.send` plus the following:
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
return_message: :class:`bool`
|
||||||
|
Ignored if not in a slash command context.
|
||||||
|
If this is set to False more native interaction methods will be used.
|
||||||
|
ephemeral: :class:`bool`
|
||||||
|
Ignored if not in a slash command context.
|
||||||
|
Indicates if the message should only be visible to the user who started the interaction.
|
||||||
|
If a view is sent with an ephemeral message and it has no timeout set then the timeout
|
||||||
|
is set to 15 minutes.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Optional[Union[:class:`.Message`, :class:`.WebhookMessage`]]
|
||||||
|
In a slash command context, the message that was sent if return_message is True.
|
||||||
|
|
||||||
|
In a normal context, it always returns a :class:`.Message`
|
||||||
|
"""
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.interaction is None
|
||||||
|
or (
|
||||||
|
self.interaction.response.responded_at is not None
|
||||||
|
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
|
||||||
|
)):
|
||||||
|
return await super().send(content, **kwargs)
|
||||||
|
|
||||||
|
# Remove unsupported arguments from kwargs
|
||||||
|
kwargs.pop("nonce", None)
|
||||||
|
kwargs.pop("stickers", None)
|
||||||
|
kwargs.pop("reference", None)
|
||||||
|
kwargs.pop("delete_after", None)
|
||||||
|
kwargs.pop("mention_author", None)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
return_message
|
||||||
|
or self.interaction.response.is_done()
|
||||||
|
or any(arg in kwargs for arg in ("file", "files", "allowed_mentions"))
|
||||||
|
):
|
||||||
|
send = self.interaction.response.send_message
|
||||||
|
else:
|
||||||
|
# We have to defer in order to use the followup webhook
|
||||||
|
if not self.interaction.response.is_done():
|
||||||
|
await self.interaction.response.defer(ephemeral=ephemeral)
|
||||||
|
|
||||||
|
send = self.interaction.followup.send
|
||||||
|
|
||||||
|
return await send(content, ephemeral=ephemeral, **kwargs) # type: ignore
|
||||||
|
|
||||||
@discord.utils.copy_doc(Message.reply)
|
@discord.utils.copy_doc(Message.reply)
|
||||||
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
|
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
|
||||||
return await self.message.reply(content, **kwargs)
|
return await self.message.reply(content, **kwargs)
|
||||||
|
@ -77,6 +77,7 @@ __all__ = (
|
|||||||
'GuildStickerConverter',
|
'GuildStickerConverter',
|
||||||
'clean_content',
|
'clean_content',
|
||||||
'Greedy',
|
'Greedy',
|
||||||
|
'Option',
|
||||||
'run_converters',
|
'run_converters',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,6 +97,9 @@ T_co = TypeVar('T_co', covariant=True)
|
|||||||
CT = TypeVar('CT', bound=discord.abc.GuildChannel)
|
CT = TypeVar('CT', bound=discord.abc.GuildChannel)
|
||||||
TT = TypeVar('TT', bound=discord.Thread)
|
TT = TypeVar('TT', bound=discord.Thread)
|
||||||
|
|
||||||
|
NT = TypeVar('NT', bound=str)
|
||||||
|
DT = TypeVar('DT', bound=str)
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class Converter(Protocol[T_co]):
|
class Converter(Protocol[T_co]):
|
||||||
@ -1004,6 +1008,20 @@ class Greedy(List[T]):
|
|||||||
|
|
||||||
return cls(converter=converter)
|
return cls(converter=converter)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
def Option(default: T = inspect.Parameter.empty, *, name: str = None, description: str) -> T: ...
|
||||||
|
else:
|
||||||
|
class Option(Generic[T, DT, NT]):
|
||||||
|
description: DT
|
||||||
|
name: Optional[NT]
|
||||||
|
default: Union[T, inspect.Parameter.empty]
|
||||||
|
__slots__ = ('name', 'default', 'description',)
|
||||||
|
|
||||||
|
def __init__(self, default: T = inspect.Parameter.empty, *, name: NT = None, description: DT) -> None:
|
||||||
|
self.description = description
|
||||||
|
self.default = default
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_bool(argument: str) -> bool:
|
def _convert_to_bool(argument: str) -> bool:
|
||||||
lowered = argument.lower()
|
lowered = argument.lower()
|
||||||
|
@ -23,6 +23,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
@ -38,18 +39,21 @@ from typing import (
|
|||||||
TypeVar,
|
TypeVar,
|
||||||
Type,
|
Type,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import datetime
|
import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from .errors import *
|
from .errors import *
|
||||||
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping
|
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping
|
||||||
from .converter import run_converters, get_converter, Greedy
|
from .converter import run_converters, get_converter, Greedy, Option
|
||||||
from ._types import _BaseCommand
|
from ._types import _BaseCommand
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
from .context import Context
|
from .context import Context
|
||||||
@ -59,6 +63,7 @@ if TYPE_CHECKING:
|
|||||||
from typing_extensions import Concatenate, ParamSpec, TypeGuard
|
from typing_extensions import Concatenate, ParamSpec, TypeGuard
|
||||||
|
|
||||||
from discord.message import Message
|
from discord.message import Message
|
||||||
|
from discord.types.interactions import EditApplicationCommand
|
||||||
|
|
||||||
from ._types import (
|
from ._types import (
|
||||||
Coro,
|
Coro,
|
||||||
@ -106,6 +111,16 @@ ContextT = TypeVar('ContextT', bound='Context')
|
|||||||
GroupT = TypeVar('GroupT', bound='Group')
|
GroupT = TypeVar('GroupT', bound='Group')
|
||||||
HookT = TypeVar('HookT', bound='Hook')
|
HookT = TypeVar('HookT', bound='Hook')
|
||||||
ErrorT = TypeVar('ErrorT', bound='Error')
|
ErrorT = TypeVar('ErrorT', bound='Error')
|
||||||
|
application_option_type_lookup = {
|
||||||
|
str: 3,
|
||||||
|
bool: 5,
|
||||||
|
int: 4,
|
||||||
|
(discord.Member, discord.User): 6, # Preferably discord.abc.User, but 'Protocols with non-method members don't support issubclass()'
|
||||||
|
(discord.abc.GuildChannel, discord.DMChannel): 7,
|
||||||
|
discord.Role: 8,
|
||||||
|
discord.Object: 9,
|
||||||
|
float: 10
|
||||||
|
}
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
P = ParamSpec('P')
|
P = ParamSpec('P')
|
||||||
@ -123,13 +138,19 @@ def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
return function
|
return function
|
||||||
|
|
||||||
|
|
||||||
def get_signature_parameters(function: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, inspect.Parameter]:
|
def get_signature_parameters(function: Callable[..., Any], globalns: Dict[str, Any]) -> Tuple[Dict[str, inspect.Parameter], Dict[str, str]]:
|
||||||
signature = inspect.signature(function)
|
signature = inspect.signature(function)
|
||||||
params = {}
|
params = {}
|
||||||
cache: Dict[str, Any] = {}
|
cache: Dict[str, Any] = {}
|
||||||
|
descriptions = defaultdict(lambda: 'no description')
|
||||||
eval_annotation = discord.utils.evaluate_annotation
|
eval_annotation = discord.utils.evaluate_annotation
|
||||||
for name, parameter in signature.parameters.items():
|
for name, parameter in signature.parameters.items():
|
||||||
annotation = parameter.annotation
|
annotation = parameter.annotation
|
||||||
|
if isinstance(parameter.default, Option): # type: ignore
|
||||||
|
option = parameter.default
|
||||||
|
descriptions[name] = option.description
|
||||||
|
parameter = parameter.replace(default=option.default)
|
||||||
|
|
||||||
if annotation is parameter.empty:
|
if annotation is parameter.empty:
|
||||||
params[name] = parameter
|
params[name] = parameter
|
||||||
continue
|
continue
|
||||||
@ -143,7 +164,7 @@ def get_signature_parameters(function: Callable[..., Any], globalns: Dict[str, A
|
|||||||
|
|
||||||
params[name] = parameter.replace(annotation=annotation)
|
params[name] = parameter.replace(annotation=annotation)
|
||||||
|
|
||||||
return params
|
return params, descriptions
|
||||||
|
|
||||||
|
|
||||||
def wrap_callback(coro):
|
def wrap_callback(coro):
|
||||||
@ -309,6 +330,8 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
|||||||
|
|
||||||
self.callback = func
|
self.callback = func
|
||||||
self.enabled: bool = kwargs.get('enabled', True)
|
self.enabled: bool = kwargs.get('enabled', True)
|
||||||
|
self.slash_command: Optional[bool] = kwargs.get("slash_command", None)
|
||||||
|
self.normal_command: Optional[bool] = kwargs.get("normal_command", None)
|
||||||
|
|
||||||
help_doc = kwargs.get('help')
|
help_doc = kwargs.get('help')
|
||||||
if help_doc is not None:
|
if help_doc is not None:
|
||||||
@ -406,7 +429,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
globalns = {}
|
globalns = {}
|
||||||
|
|
||||||
self.params = get_signature_parameters(function, globalns)
|
self.params, self.option_descriptions = get_signature_parameters(function, globalns)
|
||||||
|
|
||||||
def add_check(self, func: Check) -> None:
|
def add_check(self, func: Check) -> None:
|
||||||
"""Adds a check to the command.
|
"""Adds a check to the command.
|
||||||
@ -1098,7 +1121,13 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
|||||||
A boolean indicating if the command can be invoked.
|
A boolean indicating if the command can be invoked.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.enabled:
|
if not self.enabled or (
|
||||||
|
ctx.interaction is not None
|
||||||
|
and self.slash_command is False
|
||||||
|
) or (
|
||||||
|
ctx.interaction is None
|
||||||
|
and self.normal_command is False
|
||||||
|
):
|
||||||
raise DisabledCommand(f'{self.name} command is disabled')
|
raise DisabledCommand(f'{self.name} command is disabled')
|
||||||
|
|
||||||
original = ctx.command
|
original = ctx.command
|
||||||
@ -1125,6 +1154,58 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
|
|||||||
finally:
|
finally:
|
||||||
ctx.command = original
|
ctx.command = original
|
||||||
|
|
||||||
|
def to_application_command(self, nested: int = 0) -> Optional[EditApplicationCommand]:
|
||||||
|
if self.slash_command is False:
|
||||||
|
return
|
||||||
|
elif nested == 3:
|
||||||
|
raise ValueError(f"{self.qualified_name} is too deeply nested!")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.short_doc or "no description",
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
if nested != 0:
|
||||||
|
payload["type"] = 1
|
||||||
|
|
||||||
|
for name, param in self.clean_params.items():
|
||||||
|
annotation: Type[Any] = param.annotation if param.annotation is not param.empty else str
|
||||||
|
origin = getattr(param.annotation, "__origin__", None)
|
||||||
|
|
||||||
|
if origin is None and isinstance(annotation, Greedy):
|
||||||
|
annotation = annotation.converter
|
||||||
|
origin = Greedy
|
||||||
|
|
||||||
|
option: Dict[str, Any] = {
|
||||||
|
"name": name,
|
||||||
|
"description": self.option_descriptions[name],
|
||||||
|
"required": param.default is param.empty and not self._is_typing_optional(annotation),
|
||||||
|
}
|
||||||
|
|
||||||
|
annotation = cast(Any, annotation)
|
||||||
|
if not option["required"] and origin is not None and len(annotation.__args__) == 2:
|
||||||
|
# Unpack Optional[T] (Union[T, None]) into just T
|
||||||
|
annotation, origin = annotation.__args__[0], None
|
||||||
|
|
||||||
|
if origin is None:
|
||||||
|
option["type"] = next(
|
||||||
|
(num for t, num in application_option_type_lookup.items()
|
||||||
|
if issubclass(annotation, t)), 3
|
||||||
|
)
|
||||||
|
elif origin is Literal and len(origin.__args__) <= 25: # type: ignore
|
||||||
|
option["choices"] = [{
|
||||||
|
"name": literal_value,
|
||||||
|
"value": literal_value
|
||||||
|
} for literal_value in origin.__args__] # type: ignore
|
||||||
|
else:
|
||||||
|
option["type"] = 3 # STRING
|
||||||
|
|
||||||
|
payload["options"].append(option)
|
||||||
|
|
||||||
|
# Now we have all options, make sure required is before optional.
|
||||||
|
payload["options"] = sorted(payload["options"], key=itemgetter("required"), reverse=True)
|
||||||
|
return payload # type: ignore
|
||||||
|
|
||||||
class GroupMixin(Generic[CogT]):
|
class GroupMixin(Generic[CogT]):
|
||||||
"""A mixin that implements common functionality for classes that behave
|
"""A mixin that implements common functionality for classes that behave
|
||||||
similar to :class:`.Group` and are allowed to register commands.
|
similar to :class:`.Group` and are allowed to register commands.
|
||||||
@ -1492,6 +1573,22 @@ class Group(GroupMixin[CogT], Command[CogT, P, T]):
|
|||||||
view.previous = previous
|
view.previous = previous
|
||||||
await super().reinvoke(ctx, call_hooks=call_hooks)
|
await super().reinvoke(ctx, call_hooks=call_hooks)
|
||||||
|
|
||||||
|
def to_application_command(self, nested: int = 0) -> Optional[EditApplicationCommand]:
|
||||||
|
if self.slash_command is False:
|
||||||
|
return
|
||||||
|
elif nested == 2:
|
||||||
|
raise ValueError(f"{self.qualified_name} is too deeply nested for slash commands!")
|
||||||
|
|
||||||
|
return { # type: ignore
|
||||||
|
"name": self.name,
|
||||||
|
"type": int(not (nested - 1)) + 1,
|
||||||
|
"description": self.short_doc or 'no description',
|
||||||
|
"options": [
|
||||||
|
cmd.to_application_command(nested=nested+1)
|
||||||
|
for cmd in self.commands
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
# Decorators
|
# Decorators
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -615,7 +615,7 @@ class HelpCommand:
|
|||||||
:class:`.abc.Messageable`
|
:class:`.abc.Messageable`
|
||||||
The destination where the help command will be output.
|
The destination where the help command will be output.
|
||||||
"""
|
"""
|
||||||
return self.context.channel
|
return self.context
|
||||||
|
|
||||||
async def send_error_message(self, error):
|
async def send_error_message(self, error):
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@ -977,6 +977,14 @@ class DefaultHelpCommand(HelpCommand):
|
|||||||
for page in self.paginator.pages:
|
for page in self.paginator.pages:
|
||||||
await destination.send(page)
|
await destination.send(page)
|
||||||
|
|
||||||
|
interaction = self.context.interaction
|
||||||
|
if (
|
||||||
|
interaction is not None
|
||||||
|
and destination == self.context.author
|
||||||
|
and not interaction.response.is_done()
|
||||||
|
):
|
||||||
|
await interaction.response.send_message("Sent help to your DMs!", ephemeral=True)
|
||||||
|
|
||||||
def add_command_formatting(self, command):
|
def add_command_formatting(self, command):
|
||||||
"""A utility function to format the non-indented block of commands and groups.
|
"""A utility function to format the non-indented block of commands and groups.
|
||||||
|
|
||||||
@ -1007,7 +1015,7 @@ class DefaultHelpCommand(HelpCommand):
|
|||||||
elif self.dm_help is None and len(self.paginator) > self.dm_help_threshold:
|
elif self.dm_help is None and len(self.paginator) > self.dm_help_threshold:
|
||||||
return ctx.author
|
return ctx.author
|
||||||
else:
|
else:
|
||||||
return ctx.channel
|
return ctx
|
||||||
|
|
||||||
async def prepare_help_command(self, ctx, command):
|
async def prepare_help_command(self, ctx, command):
|
||||||
self.paginator.clear()
|
self.paginator.clear()
|
||||||
|
@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
from .errors import UnexpectedQuoteError, InvalidEndOfQuotedStringError, ExpectedClosingQuoteError
|
from .errors import UnexpectedQuoteError, InvalidEndOfQuotedStringError, ExpectedClosingQuoteError
|
||||||
|
|
||||||
# map from opening quotes to closing quotes
|
# map from opening quotes to closing quotes
|
||||||
_quotes = {
|
supported_quotes = {
|
||||||
'"': '"',
|
'"': '"',
|
||||||
"‘": "’",
|
"‘": "’",
|
||||||
"‚": "‛",
|
"‚": "‛",
|
||||||
@ -44,7 +44,7 @@ _quotes = {
|
|||||||
"《": "》",
|
"《": "》",
|
||||||
"〈": "〉",
|
"〈": "〉",
|
||||||
}
|
}
|
||||||
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
|
_all_quotes = set(supported_quotes.keys()) | set(supported_quotes.values())
|
||||||
|
|
||||||
class StringView:
|
class StringView:
|
||||||
def __init__(self, buffer):
|
def __init__(self, buffer):
|
||||||
@ -129,7 +129,7 @@ class StringView:
|
|||||||
if current is None:
|
if current is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
close_quote = _quotes.get(current)
|
close_quote = supported_quotes.get(current)
|
||||||
is_quoted = bool(close_quote)
|
is_quoted = bool(close_quote)
|
||||||
if is_quoted:
|
if is_quoted:
|
||||||
result = []
|
result = []
|
||||||
|
@ -47,6 +47,8 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .types.interactions import (
|
from .types.interactions import (
|
||||||
Interaction as InteractionPayload,
|
Interaction as InteractionPayload,
|
||||||
InteractionData,
|
InteractionData,
|
||||||
@ -58,11 +60,11 @@ if TYPE_CHECKING:
|
|||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from .embeds import Embed
|
from .embeds import Embed
|
||||||
from .ui.view import View
|
from .ui.view import View
|
||||||
from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, PartialMessageable
|
from .channel import TextChannel, CategoryChannel, StoreChannel, PartialMessageable
|
||||||
from .threads import Thread
|
from .threads import Thread
|
||||||
|
|
||||||
InteractionChannel = Union[
|
InteractionChannel = Union[
|
||||||
VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable
|
TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable
|
||||||
]
|
]
|
||||||
|
|
||||||
MISSING: Any = utils.MISSING
|
MISSING: Any = utils.MISSING
|
||||||
@ -179,7 +181,7 @@ class Interaction:
|
|||||||
type = ChannelType.text if self.guild_id is not None else ChannelType.private
|
type = ChannelType.text if self.guild_id is not None else ChannelType.private
|
||||||
return PartialMessageable(state=self._state, id=self.channel_id, type=type)
|
return PartialMessageable(state=self._state, id=self.channel_id, type=type)
|
||||||
return None
|
return None
|
||||||
return channel
|
return channel # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def permissions(self) -> Permissions:
|
def permissions(self) -> Permissions:
|
||||||
@ -369,20 +371,20 @@ class InteractionResponse:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__: Tuple[str, ...] = (
|
__slots__: Tuple[str, ...] = (
|
||||||
'_responded',
|
'responded_at',
|
||||||
'_parent',
|
'_parent',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, parent: Interaction):
|
def __init__(self, parent: Interaction):
|
||||||
|
self.responded_at: Optional[datetime] = None
|
||||||
self._parent: Interaction = parent
|
self._parent: Interaction = parent
|
||||||
self._responded: bool = False
|
|
||||||
|
|
||||||
def is_done(self) -> bool:
|
def is_done(self) -> bool:
|
||||||
""":class:`bool`: Indicates whether an interaction response has been done before.
|
""":class:`bool`: Indicates whether an interaction response has been done before.
|
||||||
|
|
||||||
An interaction can only be responded to once.
|
An interaction can only be responded to once.
|
||||||
"""
|
"""
|
||||||
return self._responded
|
return self.responded_at is not None
|
||||||
|
|
||||||
async def defer(self, *, ephemeral: bool = False) -> None:
|
async def defer(self, *, ephemeral: bool = False) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@ -405,7 +407,7 @@ class InteractionResponse:
|
|||||||
InteractionResponded
|
InteractionResponded
|
||||||
This interaction has already been responded to before.
|
This interaction has already been responded to before.
|
||||||
"""
|
"""
|
||||||
if self._responded:
|
if self.is_done():
|
||||||
raise InteractionResponded(self._parent)
|
raise InteractionResponded(self._parent)
|
||||||
|
|
||||||
defer_type: int = 0
|
defer_type: int = 0
|
||||||
@ -423,7 +425,8 @@ class InteractionResponse:
|
|||||||
await adapter.create_interaction_response(
|
await adapter.create_interaction_response(
|
||||||
parent.id, parent.token, session=parent._session, type=defer_type, data=data
|
parent.id, parent.token, session=parent._session, type=defer_type, data=data
|
||||||
)
|
)
|
||||||
self._responded = True
|
|
||||||
|
self.responded_at = utils.utcnow()
|
||||||
|
|
||||||
async def pong(self) -> None:
|
async def pong(self) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
@ -439,7 +442,7 @@ class InteractionResponse:
|
|||||||
InteractionResponded
|
InteractionResponded
|
||||||
This interaction has already been responded to before.
|
This interaction has already been responded to before.
|
||||||
"""
|
"""
|
||||||
if self._responded:
|
if self.is_done():
|
||||||
raise InteractionResponded(self._parent)
|
raise InteractionResponded(self._parent)
|
||||||
|
|
||||||
parent = self._parent
|
parent = self._parent
|
||||||
@ -448,7 +451,7 @@ class InteractionResponse:
|
|||||||
await adapter.create_interaction_response(
|
await adapter.create_interaction_response(
|
||||||
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value
|
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value
|
||||||
)
|
)
|
||||||
self._responded = True
|
self.responded_at = utils.utcnow()
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
self,
|
self,
|
||||||
@ -494,7 +497,7 @@ class InteractionResponse:
|
|||||||
InteractionResponded
|
InteractionResponded
|
||||||
This interaction has already been responded to before.
|
This interaction has already been responded to before.
|
||||||
"""
|
"""
|
||||||
if self._responded:
|
if self.is_done():
|
||||||
raise InteractionResponded(self._parent)
|
raise InteractionResponded(self._parent)
|
||||||
|
|
||||||
payload: Dict[str, Any] = {
|
payload: Dict[str, Any] = {
|
||||||
@ -537,7 +540,7 @@ class InteractionResponse:
|
|||||||
|
|
||||||
self._parent._state.store_view(view)
|
self._parent._state.store_view(view)
|
||||||
|
|
||||||
self._responded = True
|
self.responded_at = utils.utcnow()
|
||||||
|
|
||||||
async def edit_message(
|
async def edit_message(
|
||||||
self,
|
self,
|
||||||
@ -578,7 +581,7 @@ class InteractionResponse:
|
|||||||
InteractionResponded
|
InteractionResponded
|
||||||
This interaction has already been responded to before.
|
This interaction has already been responded to before.
|
||||||
"""
|
"""
|
||||||
if self._responded:
|
if self.is_done():
|
||||||
raise InteractionResponded(self._parent)
|
raise InteractionResponded(self._parent)
|
||||||
|
|
||||||
parent = self._parent
|
parent = self._parent
|
||||||
@ -629,7 +632,7 @@ class InteractionResponse:
|
|||||||
if view and not view.is_finished():
|
if view and not view.is_finished():
|
||||||
state.store_view(view, message_id)
|
state.store_view(view, message_id)
|
||||||
|
|
||||||
self._responded = True
|
self.responded_at = utils.utcnow()
|
||||||
|
|
||||||
|
|
||||||
class _InteractionMessageState:
|
class _InteractionMessageState:
|
||||||
|
@ -229,8 +229,7 @@ class _EditApplicationCommandOptional(TypedDict, total=False):
|
|||||||
description: str
|
description: str
|
||||||
options: Optional[List[ApplicationCommandOption]]
|
options: Optional[List[ApplicationCommandOption]]
|
||||||
type: ApplicationCommandType
|
type: ApplicationCommandType
|
||||||
|
default_permission: bool
|
||||||
|
|
||||||
class EditApplicationCommand(_EditApplicationCommandOptional):
|
class EditApplicationCommand(_EditApplicationCommandOptional):
|
||||||
name: str
|
name: str
|
||||||
default_permission: bool
|
|
||||||
|
@ -357,7 +357,7 @@ class View:
|
|||||||
return
|
return
|
||||||
|
|
||||||
await item.callback(interaction)
|
await item.callback(interaction)
|
||||||
if not interaction.response._responded:
|
if not interaction.response.is_done():
|
||||||
await interaction.response.defer()
|
await interaction.response.defer()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return await self.on_error(e, item, interaction)
|
return await self.on_error(e, item, interaction)
|
||||||
|
@ -52,4 +52,3 @@ def setup(app):
|
|||||||
app.add_node(details, html=(visit_details_node, depart_details_node))
|
app.add_node(details, html=(visit_details_node, depart_details_node))
|
||||||
app.add_node(summary, html=(visit_summary_node, depart_summary_node))
|
app.add_node(summary, html=(visit_summary_node, depart_summary_node))
|
||||||
app.add_directive('details', DetailsDirective)
|
app.add_directive('details', DetailsDirective)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user