Browse Source

Eliminate channel-less interaction case, clean up and fix interaction execution (fixes #380)

pull/10109/head
dolfies 3 years ago
parent
commit
b2c4b6f03d
  1. 30
      discord/commands.py
  2. 53
      discord/components.py
  3. 98
      discord/interactions.py
  4. 32
      discord/modal.py
  5. 8
      discord/state.py

30
discord/commands.py

@ -28,6 +28,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Tuple, Ty
from .enums import AppCommandOptionType, AppCommandType, ChannelType, InteractionType, try_enum from .enums import AppCommandOptionType, AppCommandType, ChannelType, InteractionType, try_enum
from .errors import InvalidData from .errors import InvalidData
from .interactions import _wrapped_interaction
from .mixins import Hashable from .mixins import Hashable
from .permissions import Permissions from .permissions import Permissions
from .utils import _generate_nonce, _get_as_snowflake from .utils import _generate_nonce, _get_as_snowflake
@ -117,24 +118,17 @@ class ApplicationCommand(Protocol):
channel = channel or self.target_channel channel = channel or self.target_channel
if channel is None: if channel is None:
raise TypeError('__call__() missing 1 required argument: \'channel\'') raise TypeError('__call__() missing 1 required argument: \'channel\'')
state = self._state
acc_channel = await channel._get_channel() return await _wrapped_interaction(
nonce = _generate_nonce() self._state,
type = InteractionType.application_command _generate_nonce(),
InteractionType.application_command,
state._interaction_cache[nonce] = (type.value, data['name'], acc_channel) data['name'],
try: await channel._get_channel(), # type: ignore # acc_channel is always correct here
await state.http.interact(type, data, acc_channel, files=files, nonce=nonce, application_id=self.application_id) data,
i = await state.client.wait_for( files=files,
'interaction_finish', application_id=self.application_id,
check=lambda d: d.nonce == nonce, )
timeout=6,
)
except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc
finally: # Cleanup even if we failed
state._interaction_cache.pop(nonce, None)
return i
@property @property
def guild(self) -> Optional[Guild]: def guild(self) -> Optional[Guild]:

53
discord/components.py

@ -24,11 +24,10 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from asyncio import TimeoutError
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Union from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Union
from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, InteractionType from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, InteractionType
from .errors import InvalidData from .interactions import _wrapped_interaction
from .utils import _generate_nonce, get_slots, MISSING from .utils import _generate_nonce, get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag from .partial_emoji import PartialEmoji, _EmojiTag
@ -218,23 +217,15 @@ class Button(Component):
return self.url return self.url
message = self.message message = self.message
state = message._state return await _wrapped_interaction(
nonce = _generate_nonce() message._state,
type = InteractionType.component _generate_nonce(),
InteractionType.component,
state._interaction_cache[nonce] = (int(type), None, message.channel) None,
try: message.channel, # type: ignore # acc_channel is always correct here
await state.http.interact(type, self.to_dict(), message.channel, message, nonce=nonce) self.to_dict(),
i = await state.client.wait_for( message=message,
'interaction_finish', )
check=lambda d: d.nonce == nonce,
timeout=6,
)
except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc
finally: # Cleanup even if we failed
state._interaction_cache.pop(nonce, None)
return i
class SelectMenu(Component): class SelectMenu(Component):
@ -314,21 +305,15 @@ class SelectMenu(Component):
The interaction that was created. The interaction that was created.
""" """
message = self.message message = self.message
state = message._state return await _wrapped_interaction(
nonce = _generate_nonce() message._state,
type = InteractionType.component _generate_nonce(),
InteractionType.component,
state._interaction_cache[nonce] = (int(type), None, message.channel) None,
await state.http.interact(type, self.to_dict(options), message.channel, message, nonce=nonce) message.channel, # type: ignore # acc_channel is always correct here
try: self.to_dict(options),
i = await state.client.wait_for( message=message,
'interaction_finish', )
check=lambda d: d.nonce == nonce,
timeout=6,
)
except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc
return i
class SelectOption: class SelectOption:

98
discord/interactions.py

@ -24,14 +24,16 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import Optional, TYPE_CHECKING, Union import asyncio
from typing import TYPE_CHECKING, Optional, Union
from .enums import InteractionType, try_enum from .enums import InteractionType, try_enum
from .errors import InvalidData
from .mixins import Hashable from .mixins import Hashable
from .utils import cached_slot_property, find, MISSING from .utils import MISSING, cached_slot_property, find
if TYPE_CHECKING: if TYPE_CHECKING:
from .channel import DMChannel, GroupChannel, TextChannel, VoiceChannel from .channel import DMChannel, TextChannel, VoiceChannel
from .guild import Guild from .guild import Guild
from .message import Message from .message import Message
from .modal import Modal from .modal import Modal
@ -41,7 +43,7 @@ if TYPE_CHECKING:
from .types.user import User as UserPayload from .types.user import User as UserPayload
from .user import BaseUser, ClientUser from .user import BaseUser, ClientUser
MessageableChannel = Union[TextChannel, Thread, DMChannel, GroupChannel, VoiceChannel] MessageableChannel = Union[TextChannel, Thread, DMChannel, VoiceChannel]
# fmt: off # fmt: off
__all__ = ( __all__ = (
@ -69,30 +71,36 @@ class Interaction(Hashable):
Return the interaction's hash. Return the interaction's hash.
.. describe:: str(x)
Returns a string representation of the interaction, if any.
Attributes Attributes
------------ ------------
id: :class:`int` id: :class:`int`
The interaction ID. The interaction ID.
type: :class:`InteractionType`
The type of interaction.
nonce: Optional[Union[:class:`int`, :class:`str`]] nonce: Optional[Union[:class:`int`, :class:`str`]]
The interaction's nonce. Not always present. The interaction's nonce. Not always present.
channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`Thread`, :class:`DMChannel`]
The channel this interaction originated from.
user: Union[:class:`Member`, :class:`abc.User`]
The :class:`Member` who initiated the interaction.
If :attr:`channel` is a private channel or the
user has the left the guild, then it is a :class:`User` instead.
name: Optional[:class:`str`] name: Optional[:class:`str`]
The name of the application command, if applicable. The name of the application command, if applicable.
type: :class:`InteractionType`
The type of interaction.
successful: :class:`bool` successful: :class:`bool`
Whether the interaction succeeded. Whether the interaction succeeded.
If this is your interaction, this is not immediately available. If this is your interaction, this is not immediately available.
It is filled when Discord notifies us about the outcome of the interaction. It is filled when Discord notifies us about the outcome of the interaction.
user: Union[:class:`Member`, :class:`abc.User`]
The :class:`Member` who initiated the interaction.
If :attr:`channel` is a private channel or the
user has the left the guild, then it is a :class:`User` instead.
modal: Optional[:class:`Modal`] modal: Optional[:class:`Modal`]
The modal that is in response to this interaction. The modal that is in response to this interaction.
This is not immediately available and is filled when the modal is dispatched. This is not immediately available and is filled when the modal is dispatched.
""" """
__slots__ = ('id', 'type', 'nonce', 'user', 'name', 'successful', 'modal', '_cs_message', '_cs_channel', '_state') __slots__ = ('id', 'type', 'nonce', 'channel', 'user', 'name', 'successful', 'modal', '_cs_message', '_state')
def __init__( def __init__(
self, self,
@ -100,15 +108,16 @@ class Interaction(Hashable):
type: int, type: int,
nonce: Optional[Snowflake] = None, nonce: Optional[Snowflake] = None,
*, *,
channel: MessageableChannel,
user: BaseUser, user: BaseUser,
state: ConnectionState, state: ConnectionState,
name: Optional[str] = None, name: Optional[str] = None,
message: Optional[Message] = None, message: Optional[Message] = None,
channel: Optional[MessageableChannel] = None,
) -> None: ) -> None:
self.id = id self.id = id
self.nonce = nonce
self.type = try_enum(InteractionType, type) self.type = try_enum(InteractionType, type)
self.nonce = nonce
self.channel = channel
self.user = user self.user = user
self.name = name self.name = name
self.successful: bool = MISSING self.successful: bool = MISSING
@ -116,8 +125,6 @@ class Interaction(Hashable):
self._state = state self._state = state
if message is not None: if message is not None:
self._cs_message = message self._cs_message = message
if channel is not None:
self._cs_channel = channel
@classmethod @classmethod
def _from_self( def _from_self(
@ -137,13 +144,28 @@ class Interaction(Hashable):
state = message._state state = message._state
name = data.get('name') name = data.get('name')
user_cls = state.store_user(user) user_cls = state.store_user(user)
self = cls(int(id), type, user=user_cls, name=name, message=message, state=state) self = cls(
int(id),
type,
channel=message.channel, # type: ignore # message.channel is always correct here
user=user_cls,
name=name,
message=message,
state=state,
)
self.successful = True self.successful = True
return self return self
def __repr__(self) -> str: def __repr__(self) -> str:
s = self.successful s = self.successful
return f'<Interaction id={self.id} type={self.type}{f" successful={s}" if s is not None else ""} user={self.user!r}>' return (
f'<Interaction id={self.id} type={self.type!r}{f" successful={s}" if s is not MISSING else ""} user={self.user!r}>'
)
def __str__(self) -> str:
if self.name:
return f'{self.user.name} used **{"/" if self.type == InteractionType.application_command else ""}{self.name}**'
return ''
def __bool__(self) -> bool: def __bool__(self) -> bool:
if self.successful is not MISSING: if self.successful is not MISSING:
@ -164,11 +186,37 @@ class Interaction(Hashable):
@property @property
def guild(self) -> Optional[Guild]: def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: Returns the guild the interaction originated from.""" """Optional[:class:`Guild`]: Returns the guild the interaction originated from."""
return getattr(self.channel, 'guild', getattr(self.message, 'guild', None)) return getattr(self.channel, 'guild', None)
@cached_slot_property('_cs_channel')
def channel(self) -> MessageableChannel: async def _wrapped_interaction(
"""Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`]: state: ConnectionState,
Returns the channel this interaction originated from. nonce: str,
""" type: InteractionType,
return getattr(self.message, 'channel', None) name: Optional[str],
channel: MessageableChannel,
data: dict,
**kwargs,
) -> Interaction:
state._interaction_cache[nonce] = (type.value, name, channel)
try:
await state.http.interact(type, data, channel, nonce=nonce, **kwargs)
# The maximum possible time a response can take is 3 seconds,
# +/- a few milliseconds for network latency
# However, people have been getting errors because their gateway
# disconnects while waiting for the interaction, causing the
# response to be delayed until the gateway is reconnected
# 12 seconds should be enough to account for this
i = await state.client.wait_for(
'interaction_finish',
check=lambda d: d.nonce == nonce,
timeout=12,
)
except (asyncio.TimeoutError, asyncio.CancelledError) as exc:
raise InvalidData('Did not receive a response from Discord') from exc
finally:
# Cleanup even if we failed
state._interaction_cache.pop(nonce, None)
return i

32
discord/modal.py

@ -23,11 +23,11 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import List, Optional, TYPE_CHECKING, Union from typing import TYPE_CHECKING, List, Optional, Union
from .components import _component_factory from .components import _component_factory
from .enums import InteractionType from .enums import InteractionType
from .errors import InvalidData from .interactions import _wrapped_interaction
from .mixins import Hashable from .mixins import Hashable
from .utils import _generate_nonce from .utils import _generate_nonce
@ -127,22 +127,12 @@ class Modal(Hashable):
The interaction that was created. The interaction that was created.
""" """
interaction = self.interaction interaction = self.interaction
state = self._state return await _wrapped_interaction(
nonce = _generate_nonce() self._state,
type = InteractionType.modal_submit _generate_nonce(),
InteractionType.modal_submit,
state._interaction_cache[nonce] = (int(type), None, interaction.channel) None,
try: interaction.channel,
await state.http.interact( self.to_dict(),
type, self.to_dict(), interaction.channel, nonce=nonce, application_id=self.application.id application_id=self.application.id,
) )
i = await state.client.wait_for(
'interaction_finish',
check=lambda d: d.nonce == nonce,
timeout=6,
)
except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc
finally: # Cleanup even if we failed
state._interaction_cache.pop(nonce, None)
return i

8
discord/state.py

@ -2232,7 +2232,9 @@ class ConnectionState:
id = int(data['id']) id = int(data['id'])
i = self._interactions.get(id, None) i = self._interactions.get(id, None)
if i is None: if i is None:
i = Interaction(id, nonce=data['nonce'], user=self.user) # type: ignore # self.user is always present here _log.warning('INTERACTION_SUCCESS referencing an unknown interaction ID: %s. Discarding.', id)
return
i.successful = True i.successful = True
self.dispatch('interaction_finish', i) self.dispatch('interaction_finish', i)
@ -2240,7 +2242,9 @@ class ConnectionState:
id = int(data['id']) id = int(data['id'])
i = self._interactions.pop(id, None) i = self._interactions.pop(id, None)
if i is None: if i is None:
i = Interaction(id, nonce=data['nonce'], user=self.user) # type: ignore # self.user is always present here _log.warning('INTERACTION_FAILED referencing an unknown interaction ID: %s. Discarding.', id)
return
i.successful = False i.successful = False
self.dispatch('interaction_finish', i) self.dispatch('interaction_finish', i)

Loading…
Cancel
Save