Compare commits

..

19 Commits

Author SHA1 Message Date
Chiggy-Playz
96153bb177
Fix slash command scope image in docs (#104)
This time its visible properly
2021-10-30 17:18:11 +01:00
Gnome
babbb22462
Fix small typing issue 2021-10-29 14:06:44 +01:00
Gnome
eef8c07379
Optimise _unwrap_slash_groups and similar 2021-10-29 13:33:15 +01:00
Chiggy-Playz
be9e693047
Fix Literal inside Optional not showing choices (#98) 2021-10-27 14:00:21 +01:00
Chiggy-Playz
351bc5bc19
Add Protocol Urls (#103)
Co-authored-by: Stocker <44980366+StockerMC@users.noreply.github.com>
Co-authored-by: Gnome! <45660393+Gnome-py@users.noreply.github.com>
2021-10-27 13:32:50 +01:00
Gnome
5bb88062fa
Basic interaction autocomplete support 2021-10-26 12:27:31 +01:00
Gnome
e99ee71233
Add ctx.defer to help with 3 second slash command response rule.
Acts as `ctx.interaction.response.defer` or loops `ctx.trigger_typing` depending on context.
2021-10-23 21:19:51 +01:00
iDutchy
63dbecf65d
Fix incorrect doc
I forgot about the decorator... min_values can also be 0, so this should prevent confusion
2021-10-20 02:04:28 +02:00
iDutchy
f46d3bfa28
fix incorrect doc
As it seems, this stated min_values must be between 1-25 even tho docs state it must be between 0-25. This changes that doc so that it might prevent confusion in the future
2021-10-20 01:54:11 +02:00
Stocker
983cbb3161
Add the ability to set the option name with commands.Option (#102)
* Add the ability to set the option name with commands.Option
* Document commands.Option.name
2021-10-16 15:00:56 +01:00
Soheab
838d9d8986
Add ability to set a flag description. (#99)
* Add ability to set a flag description.

This PR adds the ability to set a flag description that shows in the slash command options menu.
2021-10-16 13:27:02 +01:00
Chiggy-Playz
e0bf2f9121
Add Channel types support (#100) 2021-10-13 17:34:13 +01:00
Gnome
0abac8698d
Fix slash command flag parsing
Also removes the extra space at the end of fake message content
2021-10-08 20:06:05 +01:00
Gnome
d781af8be5
Remove maintainer list from README.rst
This list became outdated straight away, and is a bad idea in general.
2021-10-08 18:24:22 +01:00
Gnome
9e31aad96d Fix code style issues with Black 2021-10-07 17:34:29 +01:00
Chiggy-Playz
eca1d9a470
Sort events by categories (#88) 2021-10-07 16:48:38 +01:00
Duck
0bbcfd7f33
Update resource links (#65)
* Updated links

* Remove github discussions from getting help
2021-10-06 20:32:48 +01:00
Ian Webster
ec1e2add21
Update user-agent (#92) 2021-10-04 21:11:10 +01:00
Gnome
4277f65051 Implement _FakeSlashMessage.clean_content
Closes #83
2021-10-03 21:05:00 +01:00
22 changed files with 1146 additions and 760 deletions

View File

@ -17,18 +17,6 @@ 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>`_)
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>`_.
Key Features

View File

@ -1434,7 +1434,7 @@ class Messageable:
components=components,
)
ret = state.store_message(channel=channel, data=data)
ret = state.create_message(channel=channel, data=data)
if view:
state.store_view(view, ret.id)
@ -1501,7 +1501,7 @@ class Messageable:
channel = await self._get_channel()
data = await self._state.http.get_message(channel.id, id)
return self._state.store_message(channel=channel, data=data)
return self._state.create_message(channel=channel, data=data)
async def pins(self) -> List[Message]:
"""|coro|
@ -1528,7 +1528,7 @@ class Messageable:
channel = await self._get_channel()
state = self._state
data = await state.http.pins_from(channel.id)
return [state.store_message(channel=channel, data=m) for m in data]
return [state.create_message(channel=channel, data=m) for m in data]
def history(
self,

View File

@ -53,7 +53,7 @@ from .widget import Widget
from .guild import Guild
from .emoji import Emoji
from .channel import _threaded_channel_factory, PartialMessageable
from .enums import ChannelType, StickerType
from .enums import ChannelType
from .mentions import AllowedMentions
from .errors import *
from .enums import Status, VoiceRegion
@ -76,8 +76,7 @@ from .threads import Thread
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
if TYPE_CHECKING:
from .abc import SnowflakeTime, PrivateChannel, GuildChannel as GuildChannelABC, Snowflake
from .guild import GuildChannel
from .abc import SnowflakeTime, PrivateChannel, GuildChannel, Snowflake
from .channel import DMChannel
from .message import Message
from .member import Member
@ -781,7 +780,7 @@ class Client:
"""List[:class:`~discord.User`]: Returns a list of all the users the bot can see."""
return list(self._connection._users.values())
def get_channel(self, id: int, /) -> Optional[Union[GuildChannelABC, Thread, PrivateChannel]]:
def get_channel(self, id: int, /) -> Optional[Union[GuildChannel, Thread, PrivateChannel]]:
"""Returns a channel or thread with the given ID.
Parameters
@ -934,7 +933,7 @@ class Client:
"""
return self._connection.get_sticker(id)
def get_all_channels(self) -> Generator[GuildChannelABC, None, None]:
def get_all_channels(self) -> Generator[GuildChannel, None, None]:
"""A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'.
This is equivalent to: ::
@ -1489,7 +1488,6 @@ class Client:
"""|coro|
Retrieves the bot's application information.
This will fill up :attr:`application_id` and :attr:`application_flags`.
Raises
-------
@ -1504,8 +1502,6 @@ class Client:
data = await self.http.application_info()
if "rpc_origins" not in data:
data["rpc_origins"] = None
self._connection.store_appinfo(data)
return AppInfo(self._connection, data)
async def fetch_user(self, user_id: int, /) -> User:
@ -1539,11 +1535,10 @@ class Client:
data = await self.http.get_user(user_id)
return User(state=self._connection, data=data)
async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannelABC, PrivateChannel, Thread]:
async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannel, PrivateChannel, Thread]:
"""|coro|
Retrieves a :class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`, or :class:`.Thread` with the specified ID.
If found, will store the Channel in the internal cache, meaning :meth:``get_channel`` will succeed afterwards.
.. note::
@ -1575,18 +1570,14 @@ class Client:
if ch_type in (ChannelType.group, ChannelType.private):
# the factory will be a DMChannel or GroupChannel here
channel: PrivateChannel = factory(me=self.user, data=data, state=self._connection) # type: ignore
self._connection._add_private_channel(channel) # type: ignore
channel = factory(me=self.user, data=data, state=self._connection) # type: ignore
else:
# the factory can't be a DMChannel or GroupChannel here
guild_id = int(data["guild_id"]) # type: ignore
guild = self.get_guild(guild_id)
guild = self.get_guild(guild_id) or Object(id=guild_id)
# GuildChannels expect a Guild, we may be passing an Object
channel = factory(guild=guild, state=self._connection, data=data) # type: ignore
if guild is None:
return factory(guild=Object(guild_id), state=self._connection, data=data) # type: ignore
channel: GuildChannel = factory(guild=guild, state=self._connection, data=data) # type: ignore
guild._add_channel(channel)
return channel
async def fetch_webhook(self, webhook_id: int, /) -> Webhook:
@ -1615,7 +1606,6 @@ class Client:
"""|coro|
Retrieves a :class:`.Sticker` with the specified ID.
If found, will store the sticker in the internal cache, meaning :meth:``get_sticker`` will succeed afterwards.
.. versionadded:: 2.0
@ -1633,11 +1623,7 @@ class Client:
"""
data = await self.http.get_sticker(sticker_id)
cls, _ = _sticker_factory(data["type"]) # type: ignore
if data["type"] == StickerType.guild: # type: ignore
return self._connection.store_sticker(data) # type: ignore
else:
return cls(state=self._connection, data=data) # type: ignore
return cls(state=self._connection, data=data) # type: ignore
async def fetch_premium_sticker_packs(self) -> List[StickerPack]:
"""|coro|

View File

@ -55,6 +55,7 @@ __all__ = (
"InteractionType",
"InteractionResponseType",
"NSFWLevel",
"ProtocolURL",
)
@ -529,6 +530,7 @@ class InteractionType(Enum):
ping = 1
application_command = 2
component = 3
application_command_autocomplete = 4
class InteractionResponseType(Enum):
@ -539,6 +541,7 @@ class InteractionResponseType(Enum):
deferred_channel_message = 5 # (with source)
deferred_message_update = 6 # for components
message_update = 7 # for components
application_command_autocomplete_result = 8
class VideoQualityMode(Enum):
@ -590,6 +593,74 @@ class NSFWLevel(Enum, comparable=True):
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")

View File

@ -28,6 +28,7 @@ from __future__ import annotations
import asyncio
import collections
import collections.abc
from functools import cached_property
import inspect
import importlib.util
@ -72,7 +73,9 @@ from .cog import Cog
if TYPE_CHECKING:
import importlib.machinery
from discord.role import Role
from discord.message import Message
from discord.abc import PartialMessageableChannel
from ._types import (
Check,
CoroFunc,
@ -94,10 +97,17 @@ 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]
attachments = components = reactions = stickers = []
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]
@classmethod
def from_interaction(
cls, interaction: discord.Interaction, channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread]
@ -108,6 +118,22 @@ class _FakeSlashMessage(discord.PartialMessage):
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]:
"""A callable that implements a command prefix equivalent to being mentioned.
@ -162,16 +188,17 @@ def _is_submodule(parent: str, child: str) -> bool:
def _unwrap_slash_groups(
data: ApplicationCommandInteractionData,
) -> Tuple[str, List[ApplicationCommandInteractionDataOption]]:
) -> Tuple[str, Dict[str, ApplicationCommandInteractionDataOption]]:
command_name = data["name"]
command_options = data.get("options") or []
while any(o["type"] in {1, 2} for o in command_options): # type: ignore
for option in command_options: # type: ignore
if option["type"] in {1, 2}: # type: ignore
command_name += f' {option["name"]}' # type: ignore
command_options = option.get("options") or []
return command_name, command_options
command_options: Any = data.get("options") or []
while True:
try:
option = next(o for o in command_options if o["type"] in {1, 2})
except StopIteration:
return command_name, {o["name"]: o for o in command_options}
else:
command_name += f' {option["name"]}'
command_options = option.get("options") or []
def _quote_string_safe(string: str) -> str:
@ -1266,14 +1293,14 @@ class BotBase(GroupMixin):
# Make our fake message so we can pass it to ext.commands
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
ignore_params: List[inspect.Parameter] = []
for name, param in command.clean_params.items():
if inspect.isclass(param.annotation) and issubclass(param.annotation, FlagConverter):
for name, flag in param.annotation.get_flags().items():
option = next((o for o in command_options if o["name"] == name), None)
option = command_options.get(name)
if option is None:
if flag.required:
@ -1281,10 +1308,10 @@ class BotBase(GroupMixin):
else:
prefix = param.annotation.__commands_flag_prefix__
delimiter = param.annotation.__commands_flag_delimiter__
message.content += f"{prefix}{name} {option['value']}{delimiter}" # type: ignore
message.content += f" {prefix}{name}{delimiter}{option['value']}" # type: ignore
continue
option = next((o for o in command_options if o["name"] == name), None)
option = command_options.get(name)
if option is None:
if param.default is param.empty and not command._is_typing_optional(param.annotation):
raise errors.MissingRequiredArgument(param)
@ -1297,9 +1324,9 @@ class BotBase(GroupMixin):
):
# String with space in without "consume rest"
option = cast(_ApplicationCommandInteractionDataOptionString, option)
message.content += f"{_quote_string_safe(option['value'])} "
message.content += f" {_quote_string_safe(option['value'])}"
else:
message.content += f'{option.get("value", "")} '
message.content += f' {option.get("value", "")}'
ctx = await self.get_context(message)
ctx._ignored_params = ignore_params

View File

@ -22,11 +22,12 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import asyncio
import inspect
import re
from datetime import timedelta
from typing import Any, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload
from typing import Any, Dict, Generic, List, Literal, NoReturn, Optional, TYPE_CHECKING, TypeVar, Union, overload
import discord.abc
import discord.utils
@ -155,6 +156,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
self.command_failed: bool = command_failed
self.current_parameter: Optional[inspect.Parameter] = current_parameter
self._ignored_params: List[inspect.Parameter] = []
self._typing_task: Optional[asyncio.Task[NoReturn]] = None
self._state: ConnectionState = self.message._state
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
@ -455,6 +457,10 @@ class Context(discord.abc.Messageable, Generic[BotT]):
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 (
self.interaction.response.responded_at is not None
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
@ -500,3 +506,30 @@ class Context(discord.abc.Messageable, Generic[BotT]):
self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]:
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,6 +1032,8 @@ class Option(Generic[T, DT]): # type: ignore
The default for this option, overwrites Option during parsing.
description: :class:`str`
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
@ -1039,17 +1041,18 @@ class Option(Generic[T, DT]): # type: ignore
__slots__ = (
"default",
"description",
"name",
)
def __init__(self, default: T = inspect.Parameter.empty, *, description: DT) -> None:
def __init__(
self, default: T = inspect.Parameter.empty, *, description: DT, name: str = discord.utils.MISSING
) -> None:
self.description = description
self.default = default
self.name: str = name
if TYPE_CHECKING:
# Terrible workaround for type checking reasons
def Option(default: T = inspect.Parameter.empty, *, description: str) -> T:
...
Option: Any
def _convert_to_bool(argument: str) -> bool:

View File

@ -136,6 +136,14 @@ application_option_type_lookup = {
discord.Role: 8,
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:
P = ParamSpec("P")
@ -166,8 +174,12 @@ def get_signature_parameters(
annotation = parameter.annotation
if isinstance(parameter.default, Option): # type: ignore
option = parameter.default
descriptions[name] = option.description
parameter = parameter.replace(default=option.default)
if option.name is not MISSING:
name = option.name
parameter.replace(name=name)
descriptions[name] = option.description
if annotation is parameter.empty:
params[name] = parameter
@ -1226,15 +1238,25 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
ctx.command = original
def _param_to_options(
self, name: str, annotation: Any, required: bool, varadic: bool
self, name: str, annotation: Any, required: bool, varadic: bool, description: Optional[str] = None
) -> List[Optional[ApplicationCommandInteractionDataOption]]:
if description is not None:
self.option_descriptions[name] = description
description = self.option_descriptions[name]
origin = getattr(annotation, "__origin__", None)
if inspect.isclass(annotation) and issubclass(annotation, FlagConverter):
return [
param
for name, flag in annotation.get_flags().items()
for param in self._param_to_options(
name, flag.annotation, required=flag.required, varadic=flag.annotation is tuple
name,
flag.annotation,
required=flag.required,
varadic=flag.annotation is tuple,
description=flag.description if flag.description is not MISSING else None,
)
]
@ -1242,15 +1264,16 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
annotation = str
origin = None
if not required and origin is not None and len(annotation.__args__) == 2:
if not required and origin is Union and annotation.__args__[-1] is type(None):
# Unpack Optional[T] (Union[T, None]) into just T
annotation, origin = annotation.__args__[0], None
annotation = annotation.__args__[0]
origin = getattr(annotation, "__origin__", None)
option: Dict[str, Any] = {
"type": 3,
"name": name,
"required": required,
"description": self.option_descriptions[name],
"description": description,
}
if origin is None:
@ -1265,12 +1288,23 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
for python_type, discord_type in application_option_type_lookup.items():
if issubclass(annotation, python_type):
option["type"] = discord_type
# Set channel types
if discord_type == 7:
option["channel_types"] = application_option_channel_types[annotation]
break
elif origin is Union:
if annotation in {Union[discord.Member, discord.Role], Union[MemberConverter, RoleConverter]}:
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:
literal_values = annotation.__args__
python_type = type(literal_values[0])

View File

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

View File

@ -951,15 +951,6 @@ class MemberCacheFlags(BaseFlags):
"""
return 2
@flag_value
def fetched(self):
""":class:`bool`: Whether to cache members that are fetched via :meth:``Guild.fetch_member``
or :meth:``Guild.fetch_members``
Members that leave the guild are no longer cached.
"""
return 4
@classmethod
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
"""A factory method that creates a :class:`MemberCacheFlags` based on
@ -977,8 +968,6 @@ class MemberCacheFlags(BaseFlags):
"""
self = cls.none()
self.fetched = True
if intents.members:
self.joined = True
if intents.voice_states:

View File

@ -428,7 +428,7 @@ class Guild(Hashable):
self.mfa_level: MFALevel = guild.get("mfa_level")
self.emojis: Tuple[Emoji, ...] = tuple(map(lambda d: state.store_emoji(self, d), guild.get("emojis", [])))
self.stickers: Tuple[GuildSticker, ...] = tuple(
map(lambda d: state.store_sticker(d), guild.get("stickers", []))
map(lambda d: state.store_sticker(self, d), guild.get("stickers", []))
)
self.features: List[GuildFeature] = guild.get("features", [])
self._splash: Optional[str] = guild.get("splash")
@ -1594,7 +1594,6 @@ class Guild(Hashable):
"""|coro|
Retrieves all :class:`abc.GuildChannel` that the guild has.
Will store the Channels in the internal cache, meaning :meth:``get_channel`` will succeed afterwards.
.. note::
@ -1617,12 +1616,11 @@ class Guild(Hashable):
data = await self._state.http.get_all_guild_channels(self.id)
def convert(d):
factory, _ = _guild_channel_factory(d["type"])
factory, ch_type = _guild_channel_factory(d["type"])
if factory is None:
raise InvalidData("Unknown channel type {type} for channel ID {id}.".format_map(d))
channel = factory(guild=self, state=self._state, data=d)
self._add_channel(channel)
return channel
return [convert(d) for d in data]
@ -1714,8 +1712,6 @@ class Guild(Hashable):
"""|coro|
Retrieves a :class:`Member` from a guild ID, and a member ID.
If found, will store the Member in the internal cache, filling up :attr:`members`
and meaning :meth:``get_member`` will succeed afterwards.
.. note::
@ -1741,11 +1737,7 @@ class Guild(Hashable):
The member from the member ID.
"""
data = await self._state.http.get_member(self.id, member_id)
member = Member(data=data, state=self._state, guild=self)
if self._state.member_cache_flags.fetched:
self._add_member(member)
return member
return Member(data=data, state=self._state, guild=self)
async def try_member(self, member_id: int, /) -> Optional[Member]:
"""|coro|
@ -2265,7 +2257,7 @@ class Guild(Hashable):
payload["tags"] = emoji
data = await self._state.http.create_guild_sticker(self.id, payload, file, reason)
return self._state.store_sticker(data)
return self._state.store_sticker(self, data)
async def delete_sticker(self, sticker: Snowflake, *, reason: Optional[str] = None) -> None:
"""|coro|

View File

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

View File

@ -51,6 +51,7 @@ if TYPE_CHECKING:
from .types.interactions import (
Interaction as InteractionPayload,
ApplicationCommandOptionChoice,
InteractionData,
)
from .guild import Guild
@ -137,7 +138,7 @@ class Interaction:
self.message: Optional[Message]
try:
self.message = self._state.store_message(channel=self.channel, data=data["message"]) # type: ignore
self.message = Message(state=self._state, channel=self.channel, data=data["message"]) # type: ignore
except KeyError:
self.message = None
@ -632,6 +633,43 @@ class InteractionResponse:
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:
__slots__ = ("_parent", "_interaction")

View File

@ -42,12 +42,22 @@ __all__ = (
)
if TYPE_CHECKING:
from .types.member import MemberWithUser as MemberWithUserPayload
from .types.user import PartialUser as PartialUserPayload
from .types.audit_log import AuditLog as AuditLogPayload
from .types.message import Message as MessagePayload
from .types.threads import Thread as ThreadPayload
from .types.guild import Guild as GuildPayload
from .types.audit_log import (
AuditLog as AuditLogPayload,
)
from .types.guild import (
Guild as GuildPayload,
)
from .types.message import (
Message as MessagePayload,
)
from .types.user import (
PartialUser as PartialUserPayload,
)
from .types.threads import (
Thread as ThreadPayload,
)
from .member import Member
from .user import User
@ -344,7 +354,7 @@ class HistoryIterator(_AsyncIterator["Message"]):
channel = self.channel
for element in data:
await self.messages.put(self.state.store_message(channel=channel, data=element))
await self.messages.put(self.state.create_message(channel=channel, data=element))
async def _retrieve_messages(self, retrieve) -> List[Message]:
"""Retrieve messages and update next parameters."""
@ -605,18 +615,14 @@ class MemberIterator(_AsyncIterator["Member"]):
if isinstance(after, datetime.datetime):
after = Object(id=time_snowflake(after, high=True))
self.guild = guild
self.limit = limit
self.guild: Guild = guild
self.after = after or OLDEST_OBJECT
self.state = self.guild._state
self.get_members = self.state.http.get_members
self.members = asyncio.Queue()
self.create_member = (
self.create_member_cache if self.state.member_cache_flags.fetched else self.create_member_no_cache
)
async def next(self) -> Member:
if self.members.empty():
await self.fill_members()
@ -651,16 +657,11 @@ class MemberIterator(_AsyncIterator["Member"]):
for element in reversed(data):
await self.members.put(self.create_member(element))
def create_member_no_cache(self, data: MemberWithUserPayload) -> Member:
def create_member(self, data):
from .member import Member
return Member(data=data, guild=self.guild, state=self.state)
def create_member_cache(self, data: MemberWithUserPayload) -> Member:
member = self.create_member_no_cache(data)
self.guild._add_member(member)
return member
class ArchivedThreadIterator(_AsyncIterator["Thread"]):
def __init__(

View File

@ -1331,7 +1331,7 @@ class Message(Hashable):
payload["components"] = []
data = await self._state.http.edit_message(self.channel.id, self.id, **payload)
message = self._state.store_message(channel=self.channel, data=data)
message = Message(state=self._state, channel=self.channel, data=data)
if view and not view.is_finished():
self._state.store_view(view, self.id)
@ -1756,7 +1756,7 @@ class PartialMessage(Hashable):
"""
data = await self._state.http.get_message(self.channel.id, self.id)
return self._state.store_message(channel=self.channel, data=data)
return self._state.create_message(channel=self.channel, data=data)
async def edit(self, **fields: Any) -> Optional[Message]:
"""|coro|
@ -1873,7 +1873,7 @@ class PartialMessage(Hashable):
if fields:
# data isn't unbound
msg = self._state.store_message(channel=self.channel, data=data) # type: ignore
msg = self._state.create_message(channel=self.channel, data=data) # type: ignore
if view and not view.is_finished():
self._state.store_view(view, self.id)
return msg

View File

@ -27,6 +27,7 @@ from __future__ import annotations
import asyncio
from collections import deque, OrderedDict
import copy
import datetime
import itertools
import logging
from typing import Dict, Optional, TYPE_CHECKING, Union, Callable, Any, List, TypeVar, Coroutine, Sequence, Tuple, Deque
@ -74,7 +75,6 @@ if TYPE_CHECKING:
from .types.sticker import GuildSticker as GuildStickerPayload
from .types.guild import Guild as GuildPayload
from .types.message import Message as MessagePayload
from .types.appinfo import AppInfo as AppInfoPayload
T = TypeVar("T")
CS = TypeVar("CS", bound="ConnectionState")
@ -323,13 +323,6 @@ class ConnectionState:
for vc in self.voice_clients:
vc.main_ws = ws # type: ignore
def store_message(self, channel: MessageableChannel, data: MessagePayload) -> Message:
message = Message(state=self, channel=channel, data=data)
if self._messages is not None:
self._messages.append(message)
return message
def store_user(self, data: UserPayload) -> User:
user_id = int(data["id"])
try:
@ -360,20 +353,11 @@ class ConnectionState:
self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data)
return emoji
def store_sticker(self, data: GuildStickerPayload) -> GuildSticker:
def store_sticker(self, guild: Guild, data: GuildStickerPayload) -> GuildSticker:
sticker_id = int(data["id"])
self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data)
return sticker
def store_appinfo(self, data: AppInfoPayload):
self.application_id = utils._get_as_snowflake(data, "id")
flags = data.get("flags")
if flags is not None:
self.application_flags = ApplicationFlags._from_value(flags)
return data
def store_view(self, view: View, message_id: Optional[int] = None) -> None:
self._view_store.add_view(view, message_id)
@ -579,7 +563,9 @@ class ConnectionState:
except KeyError:
pass
else:
self.store_appinfo(application)
self.application_id = utils._get_as_snowflake(application, "id")
# flags will always be present here
self.application_flags = ApplicationFlags._from_value(application["flags"]) # type: ignore
for guild_data in data["guilds"]:
self._add_guild_from_data(guild_data)
@ -595,7 +581,8 @@ class ConnectionState:
# channel would be the correct type here
message = Message(channel=channel, data=data, state=self) # type: ignore
self.dispatch("message", message)
self.store_message(channel, data)
if self._messages is not None:
self._messages.append(message)
# we ensure that the channel is either a TextChannel or Thread
if channel and channel.__class__ in (TextChannel, Thread):
channel.last_message_id = message.id # type: ignore
@ -1045,7 +1032,7 @@ class ConnectionState:
for emoji in before_stickers:
self._stickers.pop(emoji.id, None)
# guild won't be None here
guild.stickers = tuple(map(lambda d: self.store_sticker(d), data["stickers"])) # type: ignore
guild.stickers = tuple(map(lambda d: self.store_sticker(guild, d), data["stickers"])) # type: ignore
self.dispatch("guild_stickers_update", guild, before_stickers, guild.stickers)
def _get_create_guild(self, data):
@ -1416,6 +1403,11 @@ class ConnectionState:
if channel is not None:
return channel
def create_message(
self, *, channel: Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable], data: MessagePayload
) -> Message:
return Message(state=self, channel=channel, data=data)
class AutoShardedConnectionState(ConnectionState):
def __init__(self, *args: Any, **kwargs: Any) -> None:

View File

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
from typing import Any, Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
import inspect
import os
@ -60,7 +60,7 @@ class Button(Item[V]):
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.
url: Optional[:class:`str`]
The URL this button sends you to.
The URL this button sends you to. This param is automatically casted to :class:`str`.
disabled: :class:`bool`
Whether the button is disabled or not.
label: Optional[:class:`str`]
@ -91,7 +91,7 @@ class Button(Item[V]):
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
url: Optional[Any] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
):
@ -117,7 +117,7 @@ class Button(Item[V]):
self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button,
custom_id=custom_id,
url=url,
url=str(url) if url else None,
disabled=disabled,
label=label,
style=style,

View File

@ -72,7 +72,7 @@ class Select(Item[V]):
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum 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 0 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
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).
min_values: :class:`int`
The minimum 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 0 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
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
extlinks = {
"issue": ("https://github.com/Rapptz/discord.py/issues/%s", "GH-"),
"issue": ("https://github.com/iDevision/enhanced-discord.py/issues/%s", "GH-"),
}
# Links used for cross-referencing stuff in other documentation
@ -168,9 +168,8 @@ html_context = {
resource_links = {
"discord": "https://discord.gg/TvqYBrGXEm",
"issues": "https://github.com/Rapptz/discord.py/issues",
"discussions": "https://github.com/Rapptz/discord.py/discussions",
"examples": f"https://github.com/Rapptz/discord.py/tree/{branch}/examples",
"issues": "https://github.com/iDevision/enhanced-discord.py/issues",
"examples": f"https://github.com/iDevision/enhanced-discord.py/tree/{branch}/examples",
}
# Theme options are theme-specific and customize the look and feel of a theme

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -38,7 +38,6 @@ If you're having trouble with something, these resources might help.
- 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>`.
- Report bugs in the :resource:`issue tracker <issues>`.
- Ask in our :resource:`GitHub discussions page <discussions>`.
Extensions
------------