Skip to content

Commit 1e77d6e

Browse files
authored
Merge pull request #346 from Pycord-Development/autocomplete
Implement autocomplete
2 parents 4fb38da + b850b09 commit 1e77d6e

File tree

3 files changed

+84
-4
lines changed

3 files changed

+84
-4
lines changed

discord/bot.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959

6060
from .errors import Forbidden, DiscordException
6161
from .interactions import Interaction
62+
from .enums import InteractionType
6263

6364
CoroFunc = Callable[..., Coroutine[Any, Any, Any]]
6465
CFT = TypeVar('CFT', bound=CoroFunc)
@@ -86,7 +87,7 @@ def pending_application_commands(self):
8687
return self._pending_application_commands
8788

8889
@property
89-
def commands(self) -> List[Union[ApplicationCommand, ...]]:
90+
def commands(self) -> List[Union[ApplicationCommand, Any]]:
9091
commands = list(self.application_commands.values())
9192
if self._supports_prefixed_commands:
9293
commands += self.prefixed_commands
@@ -364,14 +365,20 @@ async def process_application_commands(self, interaction: Interaction) -> None:
364365
interaction: :class:`discord.Interaction`
365366
The interaction to process
366367
"""
367-
if not interaction.is_command():
368+
if interaction.type not in (
369+
InteractionType.application_command,
370+
InteractionType.auto_complete
371+
):
368372
return
369373

370374
try:
371375
command = self.application_commands[interaction.data["id"]]
372376
except KeyError:
373377
self.dispatch("unknown_command", interaction)
374378
else:
379+
if interaction.type is InteractionType.auto_complete:
380+
return await command.invoke_autocomplete_callback(interaction)
381+
375382
ctx = await self.get_application_context(interaction)
376383
ctx.command = command
377384
self.dispatch("application_command", ctx)

discord/commands/commands.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import functools
3131
import inspect
3232
from collections import OrderedDict
33-
from typing import Any, Callable, Dict, List, Optional, Union
33+
from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
3434

3535
from ..enums import SlashCommandOptionType, ChannelType
3636
from ..member import Member
@@ -60,6 +60,9 @@
6060
"MessageCommand",
6161
)
6262

63+
if TYPE_CHECKING:
64+
from ..interactions import Interaction
65+
6366
def wrap_callback(coro):
6467
@functools.wraps(coro)
6568
async def wrapped(*args, **kwargs):
@@ -351,7 +354,7 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
351354
self.cog = None
352355

353356
params = self._get_signature_parameters()
354-
self.options = self._parse_options(params)
357+
self.options: List[Option] = self._parse_options(params)
355358

356359
try:
357360
checks = func.__commands_checks__
@@ -487,6 +490,17 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
487490
else:
488491
await self.callback(ctx, **kwargs)
489492

493+
async def invoke_autocomplete_callback(self, interaction: Interaction):
494+
for op in interaction.data.get("options", []):
495+
if op.get("focused", False):
496+
option = find(lambda o: o.name == op["name"], self.options)
497+
result = await option.autocomplete(interaction, op.get("value", None))
498+
choices = [
499+
o if isinstance(o, OptionChoice) else OptionChoice(o)
500+
for o in result
501+
]
502+
await interaction.response.send_autocomplete_result(choices=choices)
503+
490504
def qualified_name(self):
491505
return self.name
492506

@@ -581,13 +595,21 @@ def __init__(
581595
if not (isinstance(self.max_value, minmax_types) or self.min_value is None):
582596
raise TypeError(f"Expected {minmax_typehint} for max_value, got \"{type(self.max_value).__name__}\"")
583597

598+
self.autocomplete = kwargs.pop("autocomplete", None)
599+
if (
600+
self.autocomplete and
601+
not asyncio.iscoroutinefunction(self.autocomplete)
602+
):
603+
raise TypeError("Autocomplete callback must be a coroutine.")
604+
584605
def to_dict(self) -> Dict:
585606
as_dict = {
586607
"name": self.name,
587608
"description": self.description,
588609
"type": self.input_type.value,
589610
"required": self.required,
590611
"choices": [c.to_dict() for c in self.choices],
612+
"autocomplete": bool(self.autocomplete)
591613
}
592614
if self.channel_types:
593615
as_dict["channel_types"] = [t.value for t in self.channel_types]
@@ -722,6 +744,13 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
722744
ctx.interaction.data = option
723745
await command.invoke(ctx)
724746

747+
async def invoke_autocomplete_callback(self, interaction: Interaction) -> None:
748+
option = interaction.data["options"][0]
749+
command = find(lambda x: x.name == option["name"], self.subcommands)
750+
interaction.data = option
751+
await command.invoke_autocomplete_callback(interaction)
752+
753+
725754
class ContextMenuCommand(ApplicationCommand):
726755
r"""A class that implements the protocol for context menu commands.
727756

discord/interactions.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@
6161
from .ui.view import View
6262
from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, PartialMessageable
6363
from .threads import Thread
64+
from .commands import OptionChoice
6465

6566
InteractionChannel = Union[
6667
VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable
6768
]
6869

70+
6971
MISSING: Any = utils.MISSING
7072

7173

@@ -692,7 +694,49 @@ async def edit_message(
692694

693695
self._responded = True
694696

697+
async def send_autocomplete_result(
698+
self,
699+
*,
700+
choices: List[OptionChoice],
701+
) -> None:
702+
"""|coro|
703+
Responds to this interaction by sending the autocomplete choices.
704+
705+
Parameters
706+
-----------
707+
choices: List[:class:`OptionChoice`]
708+
A list of choices.
709+
710+
Raises
711+
-------
712+
HTTPException
713+
Sending the result failed.
714+
InteractionResponded
715+
This interaction has already been responded to before.
716+
"""
717+
if self._responded:
718+
raise InteractionResponded(self._parent)
719+
720+
parent = self._parent
695721

722+
if parent.type is not InteractionType.auto_complete:
723+
return
724+
725+
payload = {
726+
"choices": [c.to_dict() for c in choices]
727+
}
728+
729+
adapter = async_context.get()
730+
await adapter.create_interaction_response(
731+
parent.id,
732+
parent.token,
733+
session=parent._session,
734+
type=InteractionResponseType.auto_complete_result.value,
735+
data=payload,
736+
)
737+
738+
self._responded = True
739+
696740
class _InteractionMessageState:
697741
__slots__ = ('_parent', '_interaction')
698742

0 commit comments

Comments
 (0)