Compare commits

..

1 Commits

Author SHA1 Message Date
Gnome
e649f356be Turn KeepAliveHandler into an asyncio Task 2021-10-07 17:22:32 +01:00
17 changed files with 695 additions and 1125 deletions

View File

@ -17,6 +17,18 @@ The Future of enhanced-discord.py
-------------------------- --------------------------
Enhanced discord.py is a fork of Rapptz's discord.py, that went unmaintained (`gist <https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1>`_) Enhanced discord.py is a fork of Rapptz's discord.py, that went unmaintained (`gist <https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1>`_)
It is currently maintained by (in alphabetical order)
- Chillymosh#8175
- Daggy#9889
- dank Had0cK#6081
- Dutchy#6127
- Eyesofcreeper#0001
- Gnome!#6669
- IAmTomahawkx#1000
- Jadon#2494
An overview of added features is available on the `custom features page <https://enhanced-dpy.readthedocs.io/en/latest/index.html#custom-features>`_. An overview of added features is available on the `custom features page <https://enhanced-dpy.readthedocs.io/en/latest/index.html#custom-features>`_.
Key Features Key Features

View File

@ -55,7 +55,6 @@ __all__ = (
"InteractionType", "InteractionType",
"InteractionResponseType", "InteractionResponseType",
"NSFWLevel", "NSFWLevel",
"ProtocolURL",
) )
@ -530,7 +529,6 @@ class InteractionType(Enum):
ping = 1 ping = 1
application_command = 2 application_command = 2
component = 3 component = 3
application_command_autocomplete = 4
class InteractionResponseType(Enum): class InteractionResponseType(Enum):
@ -541,7 +539,6 @@ class InteractionResponseType(Enum):
deferred_channel_message = 5 # (with source) deferred_channel_message = 5 # (with source)
deferred_message_update = 6 # for components deferred_message_update = 6 # for components
message_update = 7 # for components message_update = 7 # for components
application_command_autocomplete_result = 8
class VideoQualityMode(Enum): class VideoQualityMode(Enum):
@ -593,74 +590,6 @@ class NSFWLevel(Enum, comparable=True):
age_restricted = 3 age_restricted = 3
class ProtocolURL(Enum):
# General
home = "discord://-/channels/@me/"
nitro = "discord://-/store"
apps = "discord://-/apps" # Breaks the client on windows (Shows download links for different OS)
guild_discovery = "discord://-/guild-discovery"
guild_create = "discord://-/guilds/create"
guild_invite = "discord://-/invite/{invite_code}"
# Settings
account_settings = "discord://-/settings/account"
profile_settings = "discord://-/settings/profile-customization"
privacy_settings = "discord://-/settings/privacy-and-safety"
safety_settings = "discord://-/settings/privacy-and-safety" # Alias
authorized_apps_settings = "discord://-/settings/authorized-apps"
connections_settings = "discord://-/settings/connections"
nitro_settings = "discord://-/settings/premium" # Same as store, but inside of settings
guild_premium_subscription = "discord://-/settings/premium-guild-subscription"
subscription_settings = "discord://-/settings/subscriptions"
gift_inventory_settings = "discord://-/settings/inventory"
billing_settings = "discord://-/settings/billing"
appearance_settings = "discord://-/settings/appearance"
accessibility_settings = "discord://-/settings/accessibility"
voice_video_settings = "discord://-/settings/voice"
text_images_settings = "discord://-/settings/text"
notifications_settings = "discord://-/settings/notifications"
keybinds_settings = "discord://-/settings/keybinds"
language_settings = "discord://-/settings/locale"
windows_settings = "discord://-/settings/windows" # Doesnt work if used on wrong platform
linux_settings = "discord://-/settings/linux" # Doesnt work if used on wrong platform
streamer_mode_settings = "discord://-/settings/streamer-mode"
advanced_settings = "discord://-/settings/advanced"
activity_status_settings = "discord://-/settings/activity-status"
game_overlay_settings = "discord://-/settings/overlay"
hypesquad_settings = "discord://-/settings/hypesquad-online"
changelogs = "discord://-/settings/changelogs"
# Doesn't work if you don't have it actually activated. Just blank screen.
experiments = "discord://-/settings/experiments"
developer_options = "discord://-/settings/developer-options" # Same as experiments
hotspot_options = "discord://-/settings/hotspot-options" # Same as experiments
# Users, Guilds, and DMs
user_profile = "discord://-/users/{user_id}"
dm_channel = "discord://-/channels/@me/{channel_id}"
dm_message = "discord://-/channels/@me/{channel_id}/{message_id}"
guild_channel = "discord://-/channels/{guild_id}/{channel_id}"
guild_message = "discord://-/channels/{guild_id}/{channel_id}/{message_id}"
guild_membership_screening = "discord://-/member-verification/{guild_id}"
# Library
games_library = "discord://-/library"
library_settings = "discord://-/library/settings"
def __str__(self) -> str:
return self.value
def format(self, **kwargs: Any) -> str:
return self.value.format(**kwargs)
T = TypeVar("T") T = TypeVar("T")

View File

@ -28,7 +28,6 @@ from __future__ import annotations
import asyncio import asyncio
import collections import collections
import collections.abc import collections.abc
from functools import cached_property
import inspect import inspect
import importlib.util import importlib.util
@ -73,9 +72,7 @@ from .cog import Cog
if TYPE_CHECKING: if TYPE_CHECKING:
import importlib.machinery import importlib.machinery
from discord.role import Role
from discord.message import Message from discord.message import Message
from discord.abc import PartialMessageableChannel
from ._types import ( from ._types import (
Check, Check,
CoroFunc, CoroFunc,
@ -97,16 +94,9 @@ CXT = TypeVar("CXT", bound="Context")
class _FakeSlashMessage(discord.PartialMessage): class _FakeSlashMessage(discord.PartialMessage):
activity = application = edited_at = reference = webhook_id = None activity = application = edited_at = reference = webhook_id = None
attachments = components = reactions = stickers = [] attachments = components = reactions = stickers = mentions = []
tts = False
raw_mentions = discord.Message.raw_mentions
clean_content = discord.Message.clean_content
channel_mentions = discord.Message.channel_mentions
raw_role_mentions = discord.Message.raw_role_mentions
raw_channel_mentions = discord.Message.raw_channel_mentions
author: Union[discord.User, discord.Member] author: Union[discord.User, discord.Member]
tts = False
@classmethod @classmethod
def from_interaction( def from_interaction(
@ -118,22 +108,6 @@ class _FakeSlashMessage(discord.PartialMessage):
return self return self
@cached_property
def mentions(self) -> List[Union[discord.Member, discord.User]]:
client = self._state._get_client()
if self.guild:
ensure_user = lambda id: self.guild.get_member(id) or client.get_user(id) # type: ignore
else:
ensure_user = client.get_user
return discord.utils._unique(filter(None, map(ensure_user, self.raw_mentions)))
@cached_property
def role_mentions(self) -> List[Role]:
if self.guild is None:
return []
return discord.utils._unique(filter(None, map(self.guild.get_role, self.raw_role_mentions)))
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.
@ -188,17 +162,16 @@ def _is_submodule(parent: str, child: str) -> bool:
def _unwrap_slash_groups( def _unwrap_slash_groups(
data: ApplicationCommandInteractionData, data: ApplicationCommandInteractionData,
) -> Tuple[str, Dict[str, ApplicationCommandInteractionDataOption]]: ) -> Tuple[str, List[ApplicationCommandInteractionDataOption]]:
command_name = data["name"] command_name = data["name"]
command_options: Any = data.get("options") or [] command_options = data.get("options") or []
while True: while any(o["type"] in {1, 2} for o in command_options): # type: ignore
try: for option in command_options: # type: ignore
option = next(o for o in command_options if o["type"] in {1, 2}) if option["type"] in {1, 2}: # type: ignore
except StopIteration: command_name += f' {option["name"]}' # type: ignore
return command_name, {o["name"]: o for o in command_options} command_options = option.get("options") or []
else:
command_name += f' {option["name"]}' return command_name, command_options
command_options = option.get("options") or []
def _quote_string_safe(string: str) -> str: def _quote_string_safe(string: str) -> str:
@ -1293,14 +1266,14 @@ class BotBase(GroupMixin):
# Make our fake message so we can pass it to ext.commands # Make our fake message so we can pass it to ext.commands
message: discord.Message = _FakeSlashMessage.from_interaction(interaction, channel) # type: ignore message: discord.Message = _FakeSlashMessage.from_interaction(interaction, channel) # type: ignore
message.content = f"/{command_name}" message.content = f"/{command_name} "
# Add arguments to fake message content, in the right order # Add arguments to fake message content, in the right order
ignore_params: List[inspect.Parameter] = [] ignore_params: List[inspect.Parameter] = []
for name, param in command.clean_params.items(): for name, param in command.clean_params.items():
if inspect.isclass(param.annotation) and issubclass(param.annotation, FlagConverter): if inspect.isclass(param.annotation) and issubclass(param.annotation, FlagConverter):
for name, flag in param.annotation.get_flags().items(): for name, flag in param.annotation.get_flags().items():
option = command_options.get(name) option = next((o for o in command_options if o["name"] == name), None)
if option is None: if option is None:
if flag.required: if flag.required:
@ -1308,10 +1281,10 @@ class BotBase(GroupMixin):
else: else:
prefix = param.annotation.__commands_flag_prefix__ prefix = param.annotation.__commands_flag_prefix__
delimiter = param.annotation.__commands_flag_delimiter__ delimiter = param.annotation.__commands_flag_delimiter__
message.content += f" {prefix}{name}{delimiter}{option['value']}" # type: ignore message.content += f"{prefix}{name} {option['value']}{delimiter}" # type: ignore
continue continue
option = command_options.get(name) option = next((o for o in command_options if o["name"] == name), None)
if option is None: if option is None:
if param.default is param.empty and not command._is_typing_optional(param.annotation): if param.default is param.empty and not command._is_typing_optional(param.annotation):
raise errors.MissingRequiredArgument(param) raise errors.MissingRequiredArgument(param)
@ -1324,9 +1297,9 @@ class BotBase(GroupMixin):
): ):
# String with space in without "consume rest" # String with space in without "consume rest"
option = cast(_ApplicationCommandInteractionDataOptionString, option) option = cast(_ApplicationCommandInteractionDataOptionString, option)
message.content += f" {_quote_string_safe(option['value'])}" message.content += f"{_quote_string_safe(option['value'])} "
else: else:
message.content += f' {option.get("value", "")}' message.content += f'{option.get("value", "")} '
ctx = await self.get_context(message) ctx = await self.get_context(message)
ctx._ignored_params = ignore_params ctx._ignored_params = ignore_params

View File

@ -22,12 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
import asyncio
import inspect import inspect
import re import re
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, Generic, List, Literal, NoReturn, Optional, TYPE_CHECKING, TypeVar, Union, overload 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
@ -156,7 +155,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
self.command_failed: bool = command_failed self.command_failed: bool = command_failed
self.current_parameter: Optional[inspect.Parameter] = current_parameter self.current_parameter: Optional[inspect.Parameter] = current_parameter
self._ignored_params: List[inspect.Parameter] = [] self._ignored_params: List[inspect.Parameter] = []
self._typing_task: Optional[asyncio.Task[NoReturn]] = None
self._state: ConnectionState = self.message._state self._state: ConnectionState = self.message._state
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T: async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
@ -457,10 +455,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
In a normal context, it always returns a :class:`.Message` In a normal context, it always returns a :class:`.Message`
""" """
if self._typing_task is not None:
self._typing_task.cancel()
self._typing_task = None
if self.interaction is None or ( if self.interaction is None or (
self.interaction.response.responded_at is not None self.interaction.response.responded_at is not None
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15) and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
@ -506,30 +500,3 @@ class Context(discord.abc.Messageable, Generic[BotT]):
self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]: ) -> Optional[Union[Message, WebhookMessage]]:
return await self.send(content, return_message=return_message, reference=self.message, **kwargs) # type: ignore return await self.send(content, return_message=return_message, reference=self.message, **kwargs) # type: ignore
async def defer(self, *, ephemeral: bool = False, trigger_typing: bool = True) -> None:
"""|coro|
Defers the Slash Command interaction if ran in a slash command **or**
Loops triggering ``Bot is typing`` in the channel if run in a message command.
Parameters
------------
trigger_typing: :class:`bool`
Indicates whether to trigger typing in a message command.
ephemeral: :class:`bool`
Indicates whether the deferred message will eventually be ephemeral in a slash command.
"""
if self.interaction is None:
if self._typing_task is None and trigger_typing:
async def typing_task():
while True:
await self.trigger_typing()
await asyncio.sleep(10)
self._typing_task = self.bot.loop.create_task(typing_task())
else:
await self.interaction.response.defer(ephemeral=ephemeral)

View File

@ -1032,8 +1032,6 @@ class Option(Generic[T, DT]): # type: ignore
The default for this option, overwrites Option during parsing. The default for this option, overwrites Option during parsing.
description: :class:`str` description: :class:`str`
The description for this option, is unpacked to :attr:`.Command.option_descriptions` The description for this option, is unpacked to :attr:`.Command.option_descriptions`
name: :class:`str`
The name of the option. This defaults to the parameter name.
""" """
description: DT description: DT
@ -1041,18 +1039,17 @@ class Option(Generic[T, DT]): # type: ignore
__slots__ = ( __slots__ = (
"default", "default",
"description", "description",
"name",
) )
def __init__( def __init__(self, default: T = inspect.Parameter.empty, *, description: DT) -> None:
self, default: T = inspect.Parameter.empty, *, description: DT, name: str = discord.utils.MISSING
) -> None:
self.description = description self.description = description
self.default = default self.default = default
self.name: str = name
Option: Any if TYPE_CHECKING:
# Terrible workaround for type checking reasons
def Option(default: T = inspect.Parameter.empty, *, description: str) -> T:
...
def _convert_to_bool(argument: str) -> bool: def _convert_to_bool(argument: str) -> bool:

View File

@ -136,14 +136,6 @@ application_option_type_lookup = {
discord.Role: 8, discord.Role: 8,
float: 10, float: 10,
} }
application_option_channel_types = {
discord.VoiceChannel: [2],
discord.TextChannel: [0, 5, 6],
discord.CategoryChannel: [4],
discord.Thread: [10, 11, 12],
discord.StageChannel: [13],
}
if TYPE_CHECKING: if TYPE_CHECKING:
P = ParamSpec("P") P = ParamSpec("P")
@ -174,12 +166,8 @@ def get_signature_parameters(
annotation = parameter.annotation annotation = parameter.annotation
if isinstance(parameter.default, Option): # type: ignore if isinstance(parameter.default, Option): # type: ignore
option = parameter.default option = parameter.default
parameter = parameter.replace(default=option.default)
if option.name is not MISSING:
name = option.name
parameter.replace(name=name)
descriptions[name] = option.description 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
@ -1238,25 +1226,15 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
ctx.command = original ctx.command = original
def _param_to_options( def _param_to_options(
self, name: str, annotation: Any, required: bool, varadic: bool, description: Optional[str] = None self, name: str, annotation: Any, required: bool, varadic: bool
) -> List[Optional[ApplicationCommandInteractionDataOption]]: ) -> List[Optional[ApplicationCommandInteractionDataOption]]:
if description is not None:
self.option_descriptions[name] = description
description = self.option_descriptions[name]
origin = getattr(annotation, "__origin__", None) origin = getattr(annotation, "__origin__", None)
if inspect.isclass(annotation) and issubclass(annotation, FlagConverter): if inspect.isclass(annotation) and issubclass(annotation, FlagConverter):
return [ return [
param param
for name, flag in annotation.get_flags().items() for name, flag in annotation.get_flags().items()
for param in self._param_to_options( for param in self._param_to_options(
name, name, flag.annotation, required=flag.required, varadic=flag.annotation is tuple
flag.annotation,
required=flag.required,
varadic=flag.annotation is tuple,
description=flag.description if flag.description is not MISSING else None,
) )
] ]
@ -1264,16 +1242,15 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
annotation = str annotation = str
origin = None origin = None
if not required and origin is Union and annotation.__args__[-1] is type(None): if not required and origin is not None and len(annotation.__args__) == 2:
# Unpack Optional[T] (Union[T, None]) into just T # Unpack Optional[T] (Union[T, None]) into just T
annotation = annotation.__args__[0] annotation, origin = annotation.__args__[0], None
origin = getattr(annotation, "__origin__", None)
option: Dict[str, Any] = { option: Dict[str, Any] = {
"type": 3, "type": 3,
"name": name, "name": name,
"required": required, "required": required,
"description": description, "description": self.option_descriptions[name],
} }
if origin is None: if origin is None:
@ -1288,23 +1265,12 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
for python_type, discord_type in application_option_type_lookup.items(): for python_type, discord_type in application_option_type_lookup.items():
if issubclass(annotation, python_type): if issubclass(annotation, python_type):
option["type"] = discord_type option["type"] = discord_type
# Set channel types
if discord_type == 7:
option["channel_types"] = application_option_channel_types[annotation]
break break
elif origin is Union: elif origin is Union:
if annotation in {Union[discord.Member, discord.Role], Union[MemberConverter, RoleConverter]}: if annotation in {Union[discord.Member, discord.Role], Union[MemberConverter, RoleConverter]}:
option["type"] = 9 option["type"] = 9
elif all([arg in application_option_channel_types for arg in annotation.__args__]):
option["type"] = 7
option["channel_types"] = [
discord_value
for arg in annotation.__args__
for discord_value in application_option_channel_types[arg]
]
elif origin is Literal: elif origin is Literal:
literal_values = annotation.__args__ literal_values = annotation.__args__
python_type = type(literal_values[0]) python_type = type(literal_values[0])
@ -1728,9 +1694,7 @@ class Group(GroupMixin[CogT], Command[CogT, P, T]):
"name": self.name, "name": self.name,
"type": int(not (nested - 1)) + 1, "type": int(not (nested - 1)) + 1,
"description": self.short_doc or "no description", "description": self.short_doc or "no description",
"options": [ "options": [cmd.to_application_command(nested=nested + 1) for cmd in sorted(self.commands, key=lambda x: x.name)],
cmd.to_application_command(nested=nested + 1) for cmd in sorted(self.commands, key=lambda x: x.name)
],
} }

View File

@ -81,8 +81,6 @@ class Flag:
------------ ------------
name: :class:`str` name: :class:`str`
The name of the flag. The name of the flag.
description: :class:`str`
The description of the flag.
aliases: List[:class:`str`] aliases: List[:class:`str`]
The aliases of the flag name. The aliases of the flag name.
attribute: :class:`str` attribute: :class:`str`
@ -99,7 +97,6 @@ class Flag:
""" """
name: str = MISSING name: str = MISSING
description: str = MISSING
aliases: List[str] = field(default_factory=list) aliases: List[str] = field(default_factory=list)
attribute: str = MISSING attribute: str = MISSING
annotation: Any = MISSING annotation: Any = MISSING
@ -120,7 +117,6 @@ class Flag:
def flag( def flag(
*, *,
name: str = MISSING, name: str = MISSING,
description: str = MISSING,
aliases: List[str] = MISSING, aliases: List[str] = MISSING,
default: Any = MISSING, default: Any = MISSING,
max_args: int = MISSING, max_args: int = MISSING,
@ -133,8 +129,6 @@ def flag(
------------ ------------
name: :class:`str` name: :class:`str`
The flag name. If not given, defaults to the attribute name. The flag name. If not given, defaults to the attribute name.
description: :class:`str`
Description of the flag for the slash commands options. The default value is `'no description'`.
aliases: List[:class:`str`] aliases: List[:class:`str`]
Aliases to the flag name. If not given no aliases are set. Aliases to the flag name. If not given no aliases are set.
default: Any default: Any
@ -149,9 +143,7 @@ def flag(
Whether multiple given values overrides the previous value. The default Whether multiple given values overrides the previous value. The default
value depends on the annotation given. value depends on the annotation given.
""" """
return Flag( return Flag(name=name, aliases=aliases, default=default, max_args=max_args, override=override)
name=name, description=description, aliases=aliases, default=default, max_args=max_args, override=override
)
def validate_flag_name(name: str, forbidden: Set[str]): def validate_flag_name(name: str, forbidden: Set[str]):

View File

@ -153,38 +153,38 @@ class GatewayRatelimiter:
await asyncio.sleep(delta) await asyncio.sleep(delta)
class KeepAliveHandler(threading.Thread): class KeepAliveHandler:
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *, ws: DiscordWebSocket, shard_id: int = None, interval: float = None) -> None:
ws = kwargs.pop("ws")
interval = kwargs.pop("interval", None)
shard_id = kwargs.pop("shard_id", None)
threading.Thread.__init__(self, *args, **kwargs)
self.ws: DiscordWebSocket = ws self.ws: DiscordWebSocket = ws
self._main_thread_id: int = ws.thread_id
self.interval: Optional[float] = interval
self.daemon: bool = True
self.shard_id: Optional[int] = shard_id self.shard_id: Optional[int] = shard_id
self.interval: Optional[float] = interval
self.heartbeat_timeout: float = self.ws._max_heartbeat_timeout
self.msg: str = "Keeping shard ID %s websocket alive with sequence %s." self.msg: str = "Keeping shard ID %s websocket alive with sequence %s."
self.block_msg: str = "Shard ID %s heartbeat blocked for more than %s seconds." self.block_msg: str = "Shard ID %s heartbeat blocked for more than %s seconds."
self.behind_msg: str = "Can't keep up, shard ID %s websocket is %.1fs behind." self.behind_msg: str = "Can't keep up, shard ID %s websocket is %.1fs behind."
self._stop_ev: threading.Event = threading.Event() self._stop_ev: asyncio.Event = asyncio.Event()
self._last_ack: float = time.perf_counter()
self._last_send: float = time.perf_counter() self._last_send: float = time.perf_counter()
self._last_recv: float = time.perf_counter() self._last_recv: float = time.perf_counter()
self._last_ack: float = time.perf_counter()
self.latency: float = float("inf") self.latency: float = float("inf")
self.heartbeat_timeout: float = ws._max_heartbeat_timeout
def run(self) -> None: async def run(self) -> None:
while not self._stop_ev.wait(self.interval): while True:
try:
await asyncio.wait_for(self._stop_ev.wait(), timeout=self.interval)
except asyncio.TimeoutError:
pass
else:
return
if self._last_recv + self.heartbeat_timeout < time.perf_counter(): if self._last_recv + self.heartbeat_timeout < time.perf_counter():
_log.warning( _log.warning(
"Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id "Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id
) )
coro = self.ws.close(4000)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try: try:
f.result() await self.ws.close(4000)
except Exception: except Exception:
_log.exception("An error occurred while stopping the gateway. Ignoring.") _log.exception("An error occurred while stopping the gateway. Ignoring.")
finally: finally:
@ -193,24 +193,18 @@ class KeepAliveHandler(threading.Thread):
data = self.get_payload() data = self.get_payload()
_log.debug(self.msg, self.shard_id, data["d"]) _log.debug(self.msg, self.shard_id, data["d"])
coro = self.ws.send_heartbeat(data)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try: try:
# block until sending is complete # block until sending is complete
total = 0 total = 0
while True: while True:
try: try:
f.result(10) await asyncio.wait_for(self.ws.send_heartbeat(data), timeout=10)
break break
except concurrent.futures.TimeoutError: except asyncio.TimeoutError:
total += 10 total += 10
try:
frame = sys._current_frames()[self._main_thread_id] stack = "".join(traceback.format_stack())
except KeyError: msg = f"{self.block_msg}\nLoop traceback (most recent call last):\n{stack}"
msg = self.block_msg
else:
stack = "".join(traceback.format_stack(frame))
msg = f"{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}"
_log.warning(msg, self.shard_id, total) _log.warning(msg, self.shard_id, total)
except Exception: except Exception:
@ -225,6 +219,10 @@ class KeepAliveHandler(threading.Thread):
"d": self.ws.sequence, # type: ignore "d": self.ws.sequence, # type: ignore
} }
def start(self) -> None:
self.ws.loop.create_task(self.run())
def stop(self) -> None: def stop(self) -> None:
self._stop_ev.set() self._stop_ev.set()

View File

@ -188,8 +188,8 @@ class HTTPClient:
self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth
self.use_clock: bool = not unsync_clock self.use_clock: bool = not unsync_clock
u_agent = "DiscordBot (https://github.com/iDevision/enhanced-discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}" user_agent = "DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}"
self.user_agent: str = u_agent.format(__version__, sys.version_info, aiohttp.__version__) self.user_agent: str = user_agent.format(__version__, sys.version_info, aiohttp.__version__)
def recreate(self) -> None: def recreate(self) -> None:
if self.__session.closed: if self.__session.closed:

View File

@ -51,7 +51,6 @@ if TYPE_CHECKING:
from .types.interactions import ( from .types.interactions import (
Interaction as InteractionPayload, Interaction as InteractionPayload,
ApplicationCommandOptionChoice,
InteractionData, InteractionData,
) )
from .guild import Guild from .guild import Guild
@ -633,43 +632,6 @@ class InteractionResponse:
self.responded_at = utils.utcnow() self.responded_at = utils.utcnow()
async def autocomplete_result(self, choices: List[ApplicationCommandOptionChoice]):
"""|coro|
Responds to this autocomplete interaction with the resulting choices.
This should rarely be used.
Parameters
-----------
choices: List[Dict[:class:`str`, :class:`str`]]
The choices to be shown in the autocomplete UI of the user.
Must be a list of dictionaries with the ``name`` and ``value`` keys.
Raises
-------
HTTPException
Responding to the interaction failed.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
parent = self._parent
if parent.type is not InteractionType.application_command_autocomplete:
return
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
type=InteractionResponseType.application_command_autocomplete_result.value,
data={"choices": choices},
)
self.responded_at = utils.utcnow()
class _InteractionMessageState: class _InteractionMessageState:
__slots__ = ("_parent", "_interaction") __slots__ = ("_parent", "_interaction")

View File

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union from typing import Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
import inspect import inspect
import os import os
@ -60,7 +60,7 @@ class Button(Item[V]):
The ID of the button that gets received during an interaction. The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID. If this button is for a URL, it does not have a custom ID.
url: Optional[:class:`str`] url: Optional[:class:`str`]
The URL this button sends you to. This param is automatically casted to :class:`str`. The URL this button sends you to.
disabled: :class:`bool` disabled: :class:`bool`
Whether the button is disabled or not. Whether the button is disabled or not.
label: Optional[:class:`str`] label: Optional[:class:`str`]
@ -91,7 +91,7 @@ class Button(Item[V]):
label: Optional[str] = None, label: Optional[str] = None,
disabled: bool = False, disabled: bool = False,
custom_id: Optional[str] = None, custom_id: Optional[str] = None,
url: Optional[Any] = None, url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None, row: Optional[int] = None,
): ):
@ -117,7 +117,7 @@ class Button(Item[V]):
self._underlying = ButtonComponent._raw_construct( self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button, type=ComponentType.button,
custom_id=custom_id, custom_id=custom_id,
url=str(url) if url else None, url=url,
disabled=disabled, disabled=disabled,
label=label, label=label,
style=style, style=style,

View File

@ -72,7 +72,7 @@ class Select(Item[V]):
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 1 and 25.
max_values: :class:`int` max_values: :class:`int`
The maximum number of items that must be chosen for this select menu. The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25. Defaults to 1 and must be between 1 and 25.
@ -327,7 +327,7 @@ def select(
ordering. The row number must be between 0 and 4 (i.e. zero indexed). ordering. The row number must be between 0 and 4 (i.e. zero indexed).
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 1 and 25.
max_values: :class:`int` max_values: :class:`int`
The maximum number of items that must be chosen for this select menu. The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25. Defaults to 1 and must be between 1 and 25.

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ autodoc_typehints = "none"
# napoleon_attr_annotations = False # napoleon_attr_annotations = False
extlinks = { extlinks = {
"issue": ("https://github.com/iDevision/enhanced-discord.py/issues/%s", "GH-"), "issue": ("https://github.com/Rapptz/discord.py/issues/%s", "GH-"),
} }
# Links used for cross-referencing stuff in other documentation # Links used for cross-referencing stuff in other documentation
@ -168,8 +168,9 @@ html_context = {
resource_links = { resource_links = {
"discord": "https://discord.gg/TvqYBrGXEm", "discord": "https://discord.gg/TvqYBrGXEm",
"issues": "https://github.com/iDevision/enhanced-discord.py/issues", "issues": "https://github.com/Rapptz/discord.py/issues",
"examples": f"https://github.com/iDevision/enhanced-discord.py/tree/{branch}/examples", "discussions": "https://github.com/Rapptz/discord.py/discussions",
"examples": f"https://github.com/Rapptz/discord.py/tree/{branch}/examples",
} }
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@ -38,6 +38,7 @@ If you're having trouble with something, these resources might help.
- Ask us and hang out with us in our :resource:`Discord <discord>` server. - Ask us and hang out with us in our :resource:`Discord <discord>` server.
- If you're looking for something specific, try the :ref:`index <genindex>` or :ref:`searching <search>`. - If you're looking for something specific, try the :ref:`index <genindex>` or :ref:`searching <search>`.
- Report bugs in the :resource:`issue tracker <issues>`. - Report bugs in the :resource:`issue tracker <issues>`.
- Ask in our :resource:`GitHub discussions page <discussions>`.
Extensions Extensions
------------ ------------