Fix weird git problems

This commit is contained in:
Gnome 2021-08-31 19:22:37 +01:00
commit 0aa8c5bef3
23 changed files with 408 additions and 67 deletions

View File

@ -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.

View File

@ -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,7 +1068,88 @@ class BotBase(GroupMixin):
await self.invoke(ctx) await self.invoke(ctx)
async def on_message(self, message): async def on_message(self, message):
await self.process_commands(message) if self.message_commands:
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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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 = []

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -355,7 +355,7 @@ texinfo_documents = [
#texinfo_no_detailmenu = False #texinfo_no_detailmenu = False
def setup(app): def setup(app):
if app.config.language == 'ja': if app.config.language == 'ja':
app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None) app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None)
app.config.html_context['discord_invite'] = 'https://discord.gg/nXzj3dg' app.config.html_context['discord_invite'] = 'https://discord.gg/nXzj3dg'
app.config.resource_links['discord'] = 'https://discord.gg/nXzj3dg' app.config.resource_links['discord'] = 'https://discord.gg/nXzj3dg'

View File

@ -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)

View File

@ -3,7 +3,7 @@ import re
requirements = [] requirements = []
with open('requirements.txt') as f: with open('requirements.txt') as f:
requirements = f.read().splitlines() requirements = f.read().splitlines()
version = '' version = ''
with open('discord/__init__.py') as f: with open('discord/__init__.py') as f: