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 .errors import InvalidData
from .interactions import _wrapped_interaction
from .mixins import Hashable
from .permissions import Permissions
from .utils import _generate_nonce, _get_as_snowflake
@ -117,24 +118,17 @@ class ApplicationCommand(Protocol):
channel = channel or self.target_channel
if channel is None:
raise TypeError('__call__() missing 1 required argument: \'channel\'')
state = self._state
acc_channel = await channel._get_channel()
nonce = _generate_nonce()
type = InteractionType.application_command
state._interaction_cache[nonce] = (type.value, data['name'], acc_channel)
try:
await state.http.interact(type, data, acc_channel, files=files, nonce=nonce, 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
return await _wrapped_interaction(
self._state,
_generate_nonce(),
InteractionType.application_command,
data['name'],
await channel._get_channel(), # type: ignore # acc_channel is always correct here
data,
files=files,
application_id=self.application_id,
)
@property
def guild(self) -> Optional[Guild]:

53
discord/components.py

@ -24,11 +24,10 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from asyncio import TimeoutError
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Union
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 .partial_emoji import PartialEmoji, _EmojiTag
@ -218,23 +217,15 @@ class Button(Component):
return self.url
message = self.message
state = message._state
nonce = _generate_nonce()
type = InteractionType.component
state._interaction_cache[nonce] = (int(type), None, message.channel)
try:
await state.http.interact(type, self.to_dict(), message.channel, message, nonce=nonce)
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
return await _wrapped_interaction(
message._state,
_generate_nonce(),
InteractionType.component,
None,
message.channel, # type: ignore # acc_channel is always correct here
self.to_dict(),
message=message,
)
class SelectMenu(Component):
@ -314,21 +305,15 @@ class SelectMenu(Component):
The interaction that was created.
"""
message = self.message
state = message._state
nonce = _generate_nonce()
type = InteractionType.component
state._interaction_cache[nonce] = (int(type), None, message.channel)
await state.http.interact(type, self.to_dict(options), message.channel, message, nonce=nonce)
try:
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
return i
return await _wrapped_interaction(
message._state,
_generate_nonce(),
InteractionType.component,
None,
message.channel, # type: ignore # acc_channel is always correct here
self.to_dict(options),
message=message,
)
class SelectOption:

98
discord/interactions.py

@ -24,14 +24,16 @@ DEALINGS IN THE SOFTWARE.
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 .errors import InvalidData
from .mixins import Hashable
from .utils import cached_slot_property, find, MISSING
from .utils import MISSING, cached_slot_property, find
if TYPE_CHECKING:
from .channel import DMChannel, GroupChannel, TextChannel, VoiceChannel
from .channel import DMChannel, TextChannel, VoiceChannel
from .guild import Guild
from .message import Message
from .modal import Modal
@ -41,7 +43,7 @@ if TYPE_CHECKING:
from .types.user import User as UserPayload
from .user import BaseUser, ClientUser
MessageableChannel = Union[TextChannel, Thread, DMChannel, GroupChannel, VoiceChannel]
MessageableChannel = Union[TextChannel, Thread, DMChannel, VoiceChannel]
# fmt: off
__all__ = (
@ -69,30 +71,36 @@ class Interaction(Hashable):
Return the interaction's hash.
.. describe:: str(x)
Returns a string representation of the interaction, if any.
Attributes
------------
id: :class:`int`
The interaction ID.
type: :class:`InteractionType`
The type of interaction.
nonce: Optional[Union[:class:`int`, :class:`str`]]
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`]
The name of the application command, if applicable.
type: :class:`InteractionType`
The type of interaction.
successful: :class:`bool`
Whether the interaction succeeded.
If this is your interaction, this is not immediately available.
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`]
The modal that is in response to this interaction.
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__(
self,
@ -100,15 +108,16 @@ class Interaction(Hashable):
type: int,
nonce: Optional[Snowflake] = None,
*,
channel: MessageableChannel,
user: BaseUser,
state: ConnectionState,
name: Optional[str] = None,
message: Optional[Message] = None,
channel: Optional[MessageableChannel] = None,
) -> None:
self.id = id
self.nonce = nonce
self.type = try_enum(InteractionType, type)
self.nonce = nonce
self.channel = channel
self.user = user
self.name = name
self.successful: bool = MISSING
@ -116,8 +125,6 @@ class Interaction(Hashable):
self._state = state
if message is not None:
self._cs_message = message
if channel is not None:
self._cs_channel = channel
@classmethod
def _from_self(
@ -137,13 +144,28 @@ class Interaction(Hashable):
state = message._state
name = data.get('name')
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
return self
def __repr__(self) -> str:
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:
if self.successful is not MISSING:
@ -164,11 +186,37 @@ class Interaction(Hashable):
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: Returns the guild the interaction originated from."""
return getattr(self.channel, 'guild', getattr(self.message, 'guild', None))
@cached_slot_property('_cs_channel')
def channel(self) -> MessageableChannel:
"""Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`, :class:`GroupChannel`]:
Returns the channel this interaction originated from.
"""
return getattr(self.message, 'channel', None)
return getattr(self.channel, 'guild', None)
async def _wrapped_interaction(
state: ConnectionState,
nonce: str,
type: InteractionType,
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 typing import List, Optional, TYPE_CHECKING, Union
from typing import TYPE_CHECKING, List, Optional, Union
from .components import _component_factory
from .enums import InteractionType
from .errors import InvalidData
from .interactions import _wrapped_interaction
from .mixins import Hashable
from .utils import _generate_nonce
@ -127,22 +127,12 @@ class Modal(Hashable):
The interaction that was created.
"""
interaction = self.interaction
state = self._state
nonce = _generate_nonce()
type = InteractionType.modal_submit
state._interaction_cache[nonce] = (int(type), None, interaction.channel)
try:
await state.http.interact(
type, self.to_dict(), interaction.channel, nonce=nonce, 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
return await _wrapped_interaction(
self._state,
_generate_nonce(),
InteractionType.modal_submit,
None,
interaction.channel,
self.to_dict(),
application_id=self.application.id,
)

8
discord/state.py

@ -2232,7 +2232,9 @@ class ConnectionState:
id = int(data['id'])
i = self._interactions.get(id, 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
self.dispatch('interaction_finish', i)
@ -2240,7 +2242,9 @@ class ConnectionState:
id = int(data['id'])
i = self._interactions.pop(id, 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
self.dispatch('interaction_finish', i)

Loading…
Cancel
Save