Skip to content

Commit 505a908

Browse files
committed
Add working context menus.
1 parent 3f23e5e commit 505a908

File tree

6 files changed

+193
-42
lines changed

6 files changed

+193
-42
lines changed

bot.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from discord_slash import SlashCommand, SlashContext
1+
from discord_slash import SlashCommand, SlashContext, MenuContext
22
from discord_slash.model import ContextMenuType
33
from discord import Intents
44
from discord.ext.commands import Bot
@@ -23,8 +23,7 @@ async def testcmd(ctx: SlashContext):
2323
await ctx.send("test!")
2424

2525
@slash.context_menu(ContextMenuType.MESSAGE, name="testname", guild_ids=[852402668294766612])
26-
async def testname(ctx: SlashContext):
27-
print(ctx.message_menus)
28-
await ctx.send("test!")
26+
async def testname(ctx: MenuContext):
27+
await ctx.send("test!", hidden=True)
2928

3029
bot.run(open(".TOKEN", "r").read(), reconnect=True, bot=True)

discord_slash/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from .client import SlashCommand # noqa: F401
1212
from .const import __version__ # noqa: F401
13-
from .context import ComponentContext, SlashContext # noqa: F401
13+
from .context import ComponentContext, SlashContext, MenuContext # noqa: F401
1414
from .dpy_overrides import ComponentMessage # noqa: F401
1515
from .model import ButtonStyle, ComponentType, SlashCommandOptionType # noqa: F401
1616
from .utils import manage_commands # noqa: F401

discord_slash/client.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,10 +1023,6 @@ def context_menu(self, target: int, name: str, guild_ids: list = None):
10231023
"""
10241024

10251025
def wrapper(cmd):
1026-
decorator_name = getattr(cmd, "__name__", None)
1027-
decorator_type = getattr(cmd, "__target__", None)
1028-
decorator_guilds = guild_ids or []
1029-
10301026
# _obj = self.add_slash_command(
10311027
# cmd,
10321028
# name,

discord_slash/cog_ext.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,48 @@ def wrapper(cmd):
188188

189189
return wrapper
190190

191+
def cog_context_menu(self, target: int, name: str, guild_ids: list = None):
192+
"""
193+
Decorator that adds context menu commands.
194+
195+
:param target: The type of menu.
196+
:type target: int
197+
:param name: A name to register as the command in the menu.
198+
:type name: str
199+
:param guild_ids: A list of guild IDs to register the command under. Defaults to ``None``.
200+
:type guild_ids: list
201+
"""
202+
203+
def wrapper(cmd):
204+
decorator_name = getattr(cmd, "__name__", None)
205+
decorator_type = getattr(cmd, "__target__", None)
206+
decorator_guilds = guild_ids or []
207+
208+
# _obj = self.add_slash_command(
209+
# cmd,
210+
# name,
211+
# "",
212+
# guild_ids
213+
# )
214+
215+
# This has to call both, as its a arg-less menu.
216+
217+
_cmd = {
218+
"default_permission": None,
219+
"has_permissions": None,
220+
"name": name,
221+
"type": target,
222+
"func": cmd,
223+
"description": "",
224+
"guild_ids": guild_ids,
225+
"api_options": [],
226+
"connector": {},
227+
"has_subcommands": False,
228+
"api_permissions": {},
229+
}
230+
return CogBaseCommandObject(name or cmd.__name__, _cmd, type=target)
231+
232+
return wrapper
191233

192234
def permission(guild_id: int, permissions: list):
193235
"""

discord_slash/context.py

Lines changed: 139 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,9 @@ def __init__(
4747
logger,
4848
):
4949
self._token = _json["token"]
50-
self._type = _json["type"] # Factor to check if its a slash command vs menus
5150
self.message = None
5251
self.data = _json["data"]
53-
try:
54-
self._message_menu_id = self.data["resolved"]["messages"] if "resolved" in self.data.keys() else None # Should be set later.
55-
except:
56-
self._message_menu_id = []
57-
try:
58-
self._author_menus_id = self.data["resolved"]["members"] if "resolved" in self.data.keys() else None
59-
except:
60-
self._author_menus_id = []
61-
self.interaction_id = self.data["id"] if "resolved" in self.data.keys() else _json["id"]
52+
self.interaction_id = _json["id"]
6253
self._http = _http
6354
self.bot = _discord
6455
self._logger = logger
@@ -67,32 +58,10 @@ def __init__(
6758
self.values = _json["data"]["values"] if "values" in _json["data"] else None
6859
self._deferred_hidden = False # To check if the patch to the deferred response matches
6960
self.guild_id = int(_json["guild_id"]) if "guild_id" in _json.keys() else None
70-
if self.guild and self._author_menus_id:
71-
self.author_menus = discord.Member(
72-
data=self._author_menus_id[[id for id in self._author_menus_id][0]],
73-
state=self.bot._connection
74-
)
75-
else:
76-
self.author_menus = None
7761
self.author_id = int(
7862
_json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"]
7963
)
8064
self.channel_id = int(_json["channel_id"])
81-
self.message_menus = None
82-
try:
83-
if self._message_menu_id != None:
84-
_data = self._message_menu_id[[id for id in self._message_menu_id][0]]
85-
self.message_menus = model.SlashMessage(
86-
state=self.bot._connection,
87-
channel=_discord.get_channel(self.channel_id),
88-
data=_data,
89-
_http=_http,
90-
interaction_token=self._token,
91-
)
92-
else:
93-
raise KeyError
94-
except KeyError as err:
95-
return err
9665
if self.guild:
9766
self.author = discord.Member(
9867
data=_json["member"], state=self.bot._connection, guild=self.guild
@@ -665,3 +634,141 @@ async def edit_origin(self, **fields):
665634
# Commented out for now as sometimes (or at least, when not deferred) _json is an empty string?
666635
# self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel,
667636
# data=_json)
637+
638+
class MenuContext(InteractionContext):
639+
"""
640+
Context of a context menu interaction. Has all attributes from :class:`InteractionContext`, plus the context-specific ones below.
641+
642+
:ivar target_id: The target ID of the context menu command.
643+
:ivar context_type: The type of context menu command.
644+
:ivar menu_messages: Dictionary of messages collected from the context menu command. Defaults to ``None``.
645+
:ivar menu_authors: Dictionary of users collected from the context menu command. Defaults to ``None``.
646+
:ivar context_message: The message of the context menu command if present. Defaults to ``None``.
647+
:ivar context_author: The author of the context menu command if present. Defaults to ``None``.
648+
:ivar _resolved: The data set for the context menu.
649+
"""
650+
651+
def __init__(
652+
self,
653+
_http: http.SlashCommandRequest,
654+
_json: dict,
655+
_discord: typing.Union[discord.Client, commands.Bot],
656+
logger,
657+
):
658+
super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger)
659+
self.target_id = super().data["target_id"]
660+
self.context_type = super()._json["type"]
661+
662+
try:
663+
self.menu_messages = self.data["resolved"]["messages"] if "resolved" in self.data.keys() else None # Should be set later.
664+
except:
665+
self.menu_messages = None
666+
try:
667+
self.menu_authors = self.data["resolved"]["members"] if "resolved" in self.data.keys() else None
668+
except:
669+
self.menu_authors = None
670+
671+
self.context_message = [msg for msg in self.menu_messages][0] if self.menu_messages != None else []
672+
self.context_author = [user for user in self.menu_authors][0] if self.menu_authors != None else []
673+
674+
if super().guild and self.author:
675+
self.context_author = discord.Member(
676+
data=self.author,
677+
state=self.bot._connection
678+
)
679+
680+
try:
681+
if self._message_menu_id != None:
682+
self.message_menus = model.SlashMessage(
683+
state=self.bot._connection,
684+
channel=_discord.get_channel(self.channel_id),
685+
data=self.context_message,
686+
_http=_http,
687+
interaction_token=self._token,
688+
)
689+
else:
690+
raise KeyError
691+
except:
692+
return
693+
694+
@property
695+
def cog(self) -> typing.Optional[commands.Cog]:
696+
"""
697+
Returns the cog associated with the command invoked, if any.
698+
699+
:return: Optional[commands.Cog]
700+
"""
701+
702+
cmd_obj = self.slash.commands[self.command]
703+
704+
if isinstance(cmd_obj, (model.CogBaseCommandObject, model.CogSubcommandObject)):
705+
return cmd_obj.cog
706+
else:
707+
return None
708+
709+
async def defer(self, hidden: bool = False, edit_origin: bool = False, ignore: bool = False):
710+
"""
711+
'Defers' the response, showing a loading state to the user
712+
713+
:param hidden: Whether the deferred response should be ephemeral. Default ``False``.
714+
:param edit_origin: Whether the type is editing the origin message. If ``False``, the deferred response will be for a follow up message. Defaults ``False``.
715+
:param ignore: Whether to just ignore and not edit or send response. Using this can avoid showing interaction loading state. Default ``False``.
716+
"""
717+
if self.deferred or self.responded:
718+
raise error.AlreadyResponded("You have already responded to this command!")
719+
720+
base = {"type": 6 if edit_origin or ignore else 5}
721+
722+
if edit_origin and ignore:
723+
raise error.IncorrectFormat("'edit_origin' and 'ignore' are mutually exclusive")
724+
725+
if hidden:
726+
if edit_origin:
727+
raise error.IncorrectFormat(
728+
"'hidden' and 'edit_origin' flags are mutually exclusive"
729+
)
730+
elif ignore:
731+
self._deferred_hidden = True
732+
else:
733+
base["data"] = {"flags": 64}
734+
self._deferred_hidden = True
735+
736+
self._deferred_edit_origin = edit_origin
737+
738+
await self._http.post_initial_response(base, self.interaction_id, self._token)
739+
self.deferred = not ignore
740+
741+
if ignore:
742+
self.responded = True
743+
744+
async def send(
745+
self,
746+
content: str = "",
747+
*,
748+
embed: discord.Embed = None,
749+
embeds: typing.List[discord.Embed] = None,
750+
tts: bool = False,
751+
file: discord.File = None,
752+
files: typing.List[discord.File] = None,
753+
allowed_mentions: discord.AllowedMentions = None,
754+
hidden: bool = False,
755+
delete_after: float = None,
756+
components: typing.List[dict] = None,
757+
) -> model.SlashMessage:
758+
if self.deferred and self._deferred_edit_origin:
759+
self._logger.warning(
760+
"Deferred response might not be what you set it to! (edit origin / send response message) "
761+
"This is because it was deferred with different response type."
762+
)
763+
return await super().send(
764+
content,
765+
embed=embed,
766+
embeds=embeds,
767+
tts=tts,
768+
file=file,
769+
files=files,
770+
allowed_mentions=allowed_mentions,
771+
hidden=hidden,
772+
delete_after=delete_after,
773+
components=components,
774+
)

discord_slash/model.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ class CogBaseCommandObject(BaseCommandObject):
374374
"""
375375

376376
def __init__(self, *args):
377+
# this is a really bad way to add context menu support
378+
# but i cannot be bothered anymore to make it better until
379+
# v4.0 is out for rewrite. sorry!
380+
args[1] = 1 if not args[1] else args[1]
377381
super().__init__(*args)
378382
self.cog = None # Manually set this later.
379383

@@ -707,7 +711,10 @@ class ContextMenuType(IntEnum):
707711

708712
@classmethod
709713
def from_type(cls, t: type):
710-
if issubclass(t, discord.abc.User):
714+
if (
715+
isinstance(t, discord.Member) or
716+
issubclass(t, discord.abc.User)
717+
):
711718
return cls.USER
712719
if issubclass(t, discord.abc.Messageable):
713720
return cls.MESSAGE

0 commit comments

Comments
 (0)