Skip to content

Commit adf47ca

Browse files
Implement autocomplete
1 parent 9496c72 commit adf47ca

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-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: 42 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

@@ -707,7 +709,47 @@ async def edit_message(
707709

708710
self._responded = True
709711

712+
async def send_autocomplete_result(
713+
self,
714+
*,
715+
choices: List[OptionChoice],
716+
) -> None:
717+
"""|coro|
718+
Responds to this interaction by sending the autocomplete choices.
719+
Parameters
720+
-----------
721+
choices: List[:class:`OptionChoice`]
722+
A list of choices.
723+
Raises
724+
-------
725+
HTTPException
726+
Sending the result failed.
727+
InteractionResponded
728+
This interaction has already been responded to before.
729+
"""
730+
if self._responded:
731+
raise InteractionResponded(self._parent)
732+
733+
parent = self._parent
734+
735+
if parent.type is not InteractionType.auto_complete:
736+
return
737+
738+
payload = {
739+
"choices": [c.to_dict() for c in choices]
740+
}
741+
742+
adapter = async_context.get()
743+
await adapter.create_interaction_response(
744+
parent.id,
745+
parent.token,
746+
session=parent._session,
747+
type=InteractionResponseType.auto_complete_result.value,
748+
data=payload,
749+
)
710750

751+
self._responded = True
752+
711753
class _InteractionMessageState:
712754
__slots__ = ('_parent', '_interaction')
713755

0 commit comments

Comments
 (0)