Skip to content

Commit 22bc506

Browse files
committed
Merge branch 'master' into v2.x
2 parents 1d21f2d + f38896a commit 22bc506

33 files changed

+685
-397
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# Warning: We have a feature freeze till release
2-
That means we won't accept new features for now. Only bug fixes.
1+
<!-- Warning: No new features will be merged until the next stable release. -->
32

43
## Summary
54

discord/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
__author__ = 'Pycord Development'
1414
__license__ = 'MIT'
1515
__copyright__ = 'Copyright 2015-2021 Rapptz & Copyright 2021-present Pycord Development'
16-
__version__ = '2.0.0b3'
16+
__version__ = '2.0.0b4'
1717

1818
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
1919

@@ -74,6 +74,6 @@ class VersionInfo(NamedTuple):
7474
serial: int
7575

7676

77-
version_info: VersionInfo = VersionInfo(major=2, minor=0, micro=0, releaselevel='beta', serial=3)
77+
version_info: VersionInfo = VersionInfo(major=2, minor=0, micro=0, releaselevel='beta', serial=4)
7878

7979
logging.getLogger(__name__).addHandler(logging.NullHandler())

discord/bot.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ async def get_desynced_commands(self, guild_id: Optional[int] = None) -> List[Di
243243
# We can suggest the user to upsert, edit, delete, or bulk upsert the commands
244244

245245
return_value = []
246-
cmds = copy.deepcopy(self.pending_application_commands)
246+
cmds = self.pending_application_commands.copy()
247247

248248
if guild_id is None:
249249
registered_commands = await self.http.get_global_commands(self.user.id)
@@ -282,7 +282,7 @@ async def get_desynced_commands(self, guild_id: Optional[int] = None) -> List[Di
282282
if type(to_check[check]) == list:
283283
for opt in to_check[check]:
284284

285-
cmd_vals = [val.get(opt, MISSING) for val in as_dict[check]]
285+
cmd_vals = [val.get(opt, MISSING) for val in as_dict[check]] if check in as_dict else []
286286
for i, val in enumerate(cmd_vals):
287287
# We need to do some falsy conversion here
288288
# The API considers False (autocomplete) and [] (choices) to be falsy values
@@ -379,11 +379,12 @@ async def register_commands(
379379
if commands is None:
380380
commands = self.pending_application_commands
381381

382-
commands = copy.deepcopy(commands)
382+
commands = [copy.copy(cmd) for cmd in commands]
383383

384-
for cmd in commands:
385-
to_rep_with = [guild_id] if guild_id is not None else guild_id
386-
cmd.guild_ids = to_rep_with
384+
if guild_id is not None:
385+
for cmd in commands:
386+
to_rep_with = [guild_id]
387+
cmd.guild_ids = to_rep_with
387388

388389
is_global = guild_id is None
389390

@@ -537,7 +538,8 @@ async def sync_commands(
537538
for cmd in commands:
538539
cmd.guild_ids = guild_ids
539540

540-
registered_commands = await self.register_commands(commands, force=force)
541+
global_commands = [cmd for cmd in commands if cmd.guild_ids is None]
542+
registered_commands = await self.register_commands(global_commands, force=force)
541543

542544
cmd_guild_ids = []
543545
registered_guild_commands = {}
@@ -549,10 +551,12 @@ async def sync_commands(
549551
if unregister_guilds is not None:
550552
cmd_guild_ids.extend(unregister_guilds)
551553
for guild_id in set(cmd_guild_ids):
554+
guild_commands = [cmd for cmd in commands if cmd.guild_ids is not None and guild_id in cmd.guild_ids]
552555
registered_guild_commands[guild_id] = await self.register_commands(
553-
commands,
556+
guild_commands,
554557
guild_id=guild_id,
555-
force=force)
558+
force=force
559+
)
556560

557561
# TODO: 2.1: Remove this and favor permissions v2
558562
# Global Command Permissions
@@ -572,13 +576,15 @@ async def sync_commands(
572576
# Permissions (Roles will be converted to IDs just before Upsert for Global Commands)
573577
global_permissions.append({"id": i["id"], "permissions": cmd.permissions})
574578

575-
for guild_id, guild_data in registered_guild_commands.items():
576-
commands = registered_guild_commands[guild_id]
579+
for guild_id, commands in registered_guild_commands.items():
577580
guild_permissions: List = []
578581

579582
for i in commands:
580-
cmd = find(lambda cmd: cmd.name == i["name"] and cmd.type == i["type"] and int(i["guild_id"]) in
581-
cmd.guild_ids, self.pending_application_commands)
583+
cmd = find(lambda cmd: cmd.name == i["name"] and cmd.type == i["type"] and cmd.guild_ids is not None
584+
and int(i["guild_id"]) in cmd.guild_ids, self.pending_application_commands)
585+
if not cmd:
586+
# command has not been added yet
587+
continue
582588
cmd.id = i["id"]
583589
self._application_commands[cmd.id] = cmd
584590

@@ -588,7 +594,8 @@ async def sync_commands(
588594
for perm in cmd.permissions
589595
if perm.guild_id is None
590596
or (
591-
perm.guild_id == guild_id and perm.guild_id in cmd.guild_ids
597+
perm.guild_id == guild_id and cmd.guild_ids is not None and perm.guild_id in
598+
cmd.guild_ids
592599
)
593600
]
594601
guild_permissions.append(
@@ -601,7 +608,8 @@ async def sync_commands(
601608
for perm in global_command["permissions"]
602609
if perm.guild_id is None
603610
or (
604-
perm.guild_id == guild_id and perm.guild_id in cmd.guild_ids
611+
perm.guild_id == guild_id and cmd.guild_ids is not None and perm.guild_id in
612+
cmd.guild_ids
605613
)
606614
]
607615
guild_permissions.append(
@@ -636,7 +644,7 @@ async def sync_commands(
636644
}
637645
)
638646
else:
639-
print(
647+
raise RuntimeError(
640648
"No Role ID found in Guild ({guild_id}) for Role ({role})".format(
641649
guild_id=guild_id, role=permission["id"]
642650
)
@@ -669,9 +677,8 @@ async def sync_commands(
669677

670678
# Make sure we don't have over 10 overwrites
671679
if len(new_cmd_perm["permissions"]) > 10:
672-
print(
673-
"Command '{name}' has more than 10 permission overrides in guild ({guild_id}).\nwill only use "
674-
"the first 10 permission overrides.".format(
680+
raise RuntimeError(
681+
"Command '{name}' has more than 10 permission overrides in guild ({guild_id}).".format(
675682
name=self._application_commands[new_cmd_perm["id"]].name,
676683
guild_id=guild_id,
677684
)
@@ -687,7 +694,7 @@ async def sync_commands(
687694
self.user.id, guild_id, guild_cmd_perms
688695
)
689696
except Forbidden:
690-
print(
697+
raise RuntimeError(
691698
f"Failed to add command permissions to guild {guild_id}",
692699
file=sys.stderr,
693700
)
@@ -722,8 +729,8 @@ async def process_application_commands(self, interaction: Interaction, auto_sync
722729
if auto_sync is None:
723730
auto_sync = self.auto_sync_commands
724731
if interaction.type not in (
725-
InteractionType.application_command,
726-
InteractionType.auto_complete
732+
InteractionType.application_command,
733+
InteractionType.auto_complete,
727734
):
728735
return
729736

@@ -1399,15 +1406,15 @@ class Bot(BotBase, Client):
13991406
14001407
.. versionadded:: 1.3
14011408
debug_guilds: Optional[List[:class:`int`]]
1402-
Guild IDs of guilds to use for testing commands. This is similar to debug_guild.
1403-
The bot will not create any global commands if a debug_guilds is passed.
1409+
Guild IDs of guilds to use for testing commands.
1410+
The bot will not create any global commands if debug guild IDs are passed.
14041411
1405-
..versionadded:: 2.0
1412+
.. versionadded:: 2.0
14061413
auto_sync_commands: :class:`bool`
14071414
Whether or not to automatically sync slash commands. This will call sync_commands in on_connect, and in
14081415
:attr:`.process_application_commands` if the command is not found. Defaults to ``True``.
14091416
1410-
..versionadded:: 2.0
1417+
.. versionadded:: 2.0
14111418
"""
14121419

14131420
pass

discord/cog.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
import discord.utils
3434
from . import errors
35-
from .commands import _BaseCommand, ApplicationCommand, ApplicationContext
35+
from .commands import _BaseCommand, ApplicationCommand, ApplicationContext, SlashCommandGroup
3636

3737
__all__ = (
3838
'CogMeta',
@@ -275,8 +275,8 @@ def walk_commands(self) -> Generator[ApplicationCommand, None, None]:
275275
A command or group from the cog.
276276
"""
277277
for command in self.__cog_commands__:
278-
if command.parent is None:
279-
yield command
278+
if isinstance(command, SlashCommandGroup):
279+
yield from command.walk_commands()
280280

281281
def get_listeners(self) -> List[Tuple[str, Callable[..., Any]]]:
282282
"""Returns a :class:`list` of (name, function) listener pairs that are defined in this cog.

discord/commands/context.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"""
2525
from __future__ import annotations
2626

27-
from typing import TYPE_CHECKING, Optional, TypeVar, Union
27+
from typing import TYPE_CHECKING, Optional, TypeVar, Union, Dict, List
2828

2929
import discord.abc
3030
from discord.interactions import InteractionMessage
@@ -99,15 +99,19 @@ async def _get_channel(self) -> Optional[InteractionChannel]:
9999

100100
async def invoke(self, command: ApplicationCommand[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
101101
r"""|coro|
102+
102103
Calls a command with the arguments given.
103104
This is useful if you want to just call the callback that a
104105
:class:`.ApplicationCommand` holds internally.
106+
105107
.. note::
108+
106109
This does not handle converters, checks, cooldowns, pre-invoke,
107110
or after-invoke hooks in any matter. It calls the internal callback
108111
directly as-if it was a regular function.
109112
You must take care in passing the proper arguments when
110113
using this function.
114+
111115
Parameters
112116
-----------
113117
command: :class:`.ApplicationCommand`
@@ -159,7 +163,7 @@ def message(self) -> Optional[Message]:
159163
def user(self) -> Optional[Union[Member, User]]:
160164
return self.interaction.user
161165

162-
author = user
166+
author: Optional[Union[Member, User]] = user
163167

164168
@property
165169
def voice_client(self) -> Optional[VoiceProtocol]:
@@ -172,6 +176,39 @@ def voice_client(self) -> Optional[VoiceProtocol]:
172176
def response(self) -> InteractionResponse:
173177
return self.interaction.response
174178

179+
@property
180+
def selected_options(self) -> Optional[List[Dict]]:
181+
"""The options and values that were selected by the user when sending the command.
182+
183+
Returns
184+
-------
185+
Optional[List[Dict]]
186+
A dictionary containing the options and values that were selected by the user when the command was processed, if applicable.
187+
Returns ``None`` if the command has not yet been invoked, or if there are no options defined for that command.
188+
"""
189+
return self.interaction.data.get("options", None)
190+
191+
@property
192+
def unselected_options(self) -> Optional[List[Option]]:
193+
"""The options that were not provided by the user when sending the command.
194+
195+
Returns
196+
-------
197+
Optional[List[:class:`.Option`]]
198+
A list of Option objects (if any) that were not selected by the user when the command was processed.
199+
Returns ``None`` if there are no options defined for that command.
200+
"""
201+
if self.command.options is not None: # type: ignore
202+
if self.selected_options:
203+
return [
204+
option
205+
for option in self.command.options # type: ignore
206+
if option.to_dict()["name"] not in [opt["name"] for opt in self.selected_options]
207+
]
208+
else:
209+
return self.command.options # type: ignore
210+
return None
211+
175212
@property
176213
def respond(self) -> Callable[..., Awaitable[Union[Interaction, WebhookMessage]]]:
177214
"""Callable[..., Union[:class:`~.Interaction`, :class:`~.Webhook`]]: Sends either a response

discord/commands/core.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from ..enums import SlashCommandOptionType, ChannelType
5757
from ..errors import ValidationError, ClientException
5858
from ..member import Member
59-
from ..message import Message
59+
from ..message import Attachment, Message
6060
from ..user import User
6161
from ..utils import find, get_or_fetch, async_all, utcnow
6262

@@ -495,6 +495,9 @@ def qualified_name(self) -> str:
495495
else:
496496
return self.name
497497

498+
def __str__(self) -> str:
499+
return self.qualified_name
500+
498501
def _set_cog(self, cog):
499502
self.cog = cog
500503

@@ -752,6 +755,11 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
752755
elif op.input_type == SlashCommandOptionType.string and (converter := op.converter) is not None:
753756
arg = await converter.convert(converter, ctx, arg)
754757

758+
elif op.input_type == SlashCommandOptionType.attachment:
759+
_data = ctx.interaction.data["resolved"]["attachments"][arg]
760+
_data["id"] = int(arg)
761+
arg = Attachment(state=ctx.interaction._state, data=_data)
762+
755763
kwargs[op._parameter_name] = arg
756764

757765
for o in self.options:

discord/commands/options.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from ..enums import ChannelType, SlashCommandOptionType
2828

29+
2930
__all__ = (
3031
"ThreadOption",
3132
"Option",
@@ -70,20 +71,30 @@ def __init__(self, input_type: Any, /, description: str = None, **kwargs) -> Non
7071
self.converter = input_type
7172
input_type = SlashCommandOptionType.string
7273
else:
73-
_type = SlashCommandOptionType.from_datatype(input_type)
74-
if _type == SlashCommandOptionType.channel:
75-
if not isinstance(input_type, tuple):
76-
input_type = (input_type,)
77-
for i in input_type:
78-
if i.__name__ == "GuildChannel":
79-
continue
80-
if isinstance(i, ThreadOption):
81-
self.channel_types.append(i._type)
82-
continue
83-
84-
channel_type = channel_type_map[i.__name__]
85-
self.channel_types.append(channel_type)
86-
input_type = _type
74+
try:
75+
_type = SlashCommandOptionType.from_datatype(input_type)
76+
except TypeError as exc:
77+
from ..ext.commands.converter import CONVERTER_MAPPING
78+
79+
if input_type in CONVERTER_MAPPING:
80+
self.converter = CONVERTER_MAPPING[input_type]
81+
input_type = SlashCommandOptionType.string
82+
else:
83+
raise exc
84+
else:
85+
if _type == SlashCommandOptionType.channel:
86+
if not isinstance(input_type, tuple):
87+
input_type = (input_type,)
88+
for i in input_type:
89+
if i.__name__ == "GuildChannel":
90+
continue
91+
if isinstance(i, ThreadOption):
92+
self.channel_types.append(i._type)
93+
continue
94+
95+
channel_type = channel_type_map[i.__name__]
96+
self.channel_types.append(channel_type)
97+
input_type = _type
8798
self.input_type = input_type
8899
self.required: bool = (
89100
kwargs.pop("required", True) if "default" not in kwargs else False

0 commit comments

Comments
 (0)