Skip to content

Commit bd917bd

Browse files
authored
Merge pull request #285 from goverfl0w/master
Implement context menus.
2 parents 78fd739 + 394b53d commit bd917bd

File tree

8 files changed

+482
-23
lines changed

8 files changed

+482
-23
lines changed

discord_slash/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"""
22
discord-py-slash-command
33
~~~~~~~~~~~~~~~~~~~~~~~~
4-
54
Simple Discord Slash Command extension for discord.py
6-
75
:copyright: (c) 2020-2021 eunwoo1104
86
:license: MIT
97
"""
108

119
from .client import SlashCommand # noqa: F401
1210
from .const import __version__ # noqa: F401
13-
from .context import ComponentContext, SlashContext # noqa: F401
11+
from .context import ComponentContext, MenuContext, SlashContext # noqa: F401
1412
from .dpy_overrides import ComponentMessage # noqa: F401
15-
from .model import ButtonStyle, ComponentType, SlashCommandOptionType # noqa: F401
13+
from .model import ButtonStyle, ComponentType, ContextMenuType, SlashCommandOptionType # noqa: F401
1614
from .utils import manage_commands # noqa: F401
1715
from .utils import manage_components # noqa: F401

discord_slash/client.py

Lines changed: 178 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class SlashCommand:
4646
4747
:ivar _discord: Discord client of this client.
4848
:ivar commands: Dictionary of the registered commands via :func:`.slash` decorator.
49+
:ivar menu_commands: Dictionary of the registered context menus via the :func:`.context_menu` decorator.
4950
:ivar req: :class:`.http.SlashCommandRequest` of this client.
5051
:ivar logger: Logger of this client.
5152
:ivar sync_commands: Whether to sync commands automatically.
@@ -64,7 +65,7 @@ def __init__(
6465
application_id: typing.Optional[int] = None,
6566
):
6667
self._discord = client
67-
self.commands = {}
68+
self.commands = {"context": {}}
6869
self.subcommands = {}
6970
self.components = {}
7071
self.logger = logging.getLogger("discord_slash")
@@ -270,12 +271,53 @@ async def to_dict(self):
270271
await self._discord.wait_until_ready() # In case commands are still not registered to SlashCommand.
271272
all_guild_ids = []
272273
for x in self.commands:
274+
if x == "context":
275+
# handle context menu separately.
276+
for _x in self.commands["context"]:
277+
_selected = self.commands["context"][_x]
278+
for i in _selected.allowed_guild_ids:
279+
if i not in all_guild_ids:
280+
all_guild_ids.append(i)
281+
continue
273282
for i in self.commands[x].allowed_guild_ids:
274283
if i not in all_guild_ids:
275284
all_guild_ids.append(i)
276285
cmds = {"global": [], "guild": {x: [] for x in all_guild_ids}}
277286
wait = {} # Before merging to return dict, let's first put commands to temporary dict.
278287
for x in self.commands:
288+
if x == "context":
289+
# handle context menu separately.
290+
for _x in self.commands["context"]: # x is the new reference dict
291+
selected = self.commands["context"][_x]
292+
293+
if selected.allowed_guild_ids:
294+
for y in selected.allowed_guild_ids:
295+
if y not in wait:
296+
wait[y] = {}
297+
command_dict = {
298+
"name": _x,
299+
"options": selected.options or [],
300+
"default_permission": selected.default_permission,
301+
"permissions": {},
302+
"type": selected._type,
303+
}
304+
if y in selected.permissions:
305+
command_dict["permissions"][y] = selected.permissions[y]
306+
wait[y][x] = copy.deepcopy(command_dict)
307+
else:
308+
if "global" not in wait:
309+
wait["global"] = {}
310+
command_dict = {
311+
"name": _x,
312+
"options": selected.options or [],
313+
"default_permission": selected.default_permission,
314+
"permissions": selected.permissions or {},
315+
"type": selected._type,
316+
}
317+
wait["global"][x] = copy.deepcopy(command_dict)
318+
319+
continue
320+
279321
selected = self.commands[x]
280322
if selected.allowed_guild_ids:
281323
for y in selected.allowed_guild_ids:
@@ -287,7 +329,10 @@ async def to_dict(self):
287329
"options": selected.options or [],
288330
"default_permission": selected.default_permission,
289331
"permissions": {},
332+
"type": selected._type,
290333
}
334+
if command_dict["type"] != 1:
335+
command_dict.pop("description")
291336
if y in selected.permissions:
292337
command_dict["permissions"][y] = selected.permissions[y]
293338
wait[y][x] = copy.deepcopy(command_dict)
@@ -300,14 +345,20 @@ async def to_dict(self):
300345
"options": selected.options or [],
301346
"default_permission": selected.default_permission,
302347
"permissions": selected.permissions or {},
348+
"type": selected._type,
303349
}
350+
if command_dict["type"] != 1:
351+
command_dict.pop("description")
304352
wait["global"][x] = copy.deepcopy(command_dict)
305353

306354
# Separated normal command add and subcommand add not to
307355
# merge subcommands to one. More info at Issue #88
308356
# https://github.com/eunwoo1104/discord-py-slash-command/issues/88
309357

310358
for x in self.commands:
359+
if x == "context":
360+
continue # no menus have subcommands.
361+
311362
if not self.commands[x].has_subcommands:
312363
continue
313364
tgt = self.subcommands[x]
@@ -424,7 +475,7 @@ async def sync_all_commands(
424475
if ex.status == 400:
425476
# catch bad requests
426477
cmd_nums = set(
427-
re.findall(r"In\s(\d).", ex.args[0])
478+
re.findall(r"^[\w-]{1,32}$", ex.args[0])
428479
) # find all discords references to commands
429480
error_string = ex.args[0]
430481

@@ -594,6 +645,66 @@ def add_slash_command(
594645
self.logger.debug(f"Added command `{name}`")
595646
return obj
596647

648+
def _cog_ext_add_context_menu(self, target: int, name: str, guild_ids: list = None):
649+
"""
650+
Creates a new cog_based context menu command.
651+
652+
:param cmd: Command Coroutine.
653+
:type cmd: Coroutine
654+
:param name: The name of the command
655+
:type name: str
656+
:param _type: The context menu type.
657+
:type _type: int
658+
"""
659+
660+
def add_context_menu(self, cmd, name: str, _type: int, guild_ids: list = None):
661+
"""
662+
Creates a new context menu command.
663+
664+
:param cmd: Command Coroutine.
665+
:type cmd: Coroutine
666+
:param name: The name of the command
667+
:type name: str
668+
:param _type: The context menu type.
669+
:type _type: int
670+
"""
671+
672+
name = [name or cmd.__name__][0]
673+
guild_ids = guild_ids or []
674+
675+
if not all(isinstance(item, int) for item in guild_ids):
676+
raise error.IncorrectGuildIDType(
677+
f"The snowflake IDs {guild_ids} given are not a list of integers. Because of discord.py convention, please use integer IDs instead. Furthermore, the command '{name}' will be deactivated and broken until fixed."
678+
)
679+
680+
if name in self.commands["context"]:
681+
tgt = self.commands["context"][name]
682+
if not tgt.has_subcommands:
683+
raise error.DuplicateCommand(name)
684+
has_subcommands = tgt.has_subcommands # noqa
685+
for x in tgt.allowed_guild_ids:
686+
if x not in guild_ids:
687+
guild_ids.append(x)
688+
689+
_cmd = {
690+
"default_permission": None,
691+
"has_permissions": None,
692+
"name": name,
693+
"type": _type,
694+
"func": cmd,
695+
"description": "",
696+
"guild_ids": guild_ids,
697+
"api_options": [],
698+
"connector": {},
699+
"has_subcommands": False,
700+
"api_permissions": {},
701+
}
702+
703+
obj = model.BaseCommandObject(name, cmd=_cmd, _type=_type)
704+
self.commands["context"][name] = obj
705+
self.logger.debug(f"Added context command `{name}`")
706+
return obj
707+
597708
def add_subcommand(
598709
self,
599710
cmd,
@@ -916,6 +1027,34 @@ def wrapper(cmd):
9161027

9171028
return wrapper
9181029

1030+
def context_menu(self, *, target: int, name: str, guild_ids: list = None):
1031+
"""
1032+
Decorator that adds context menu commands.
1033+
1034+
:param target: The type of menu.
1035+
:type target: int
1036+
:param name: A name to register as the command in the menu.
1037+
:type name: str
1038+
:param guild_ids: A list of guild IDs to register the command under. Defaults to ``None``.
1039+
:type guild_ids: list
1040+
"""
1041+
1042+
def wrapper(cmd):
1043+
# _obj = self.add_slash_command(
1044+
# cmd,
1045+
# name,
1046+
# "",
1047+
# guild_ids
1048+
# )
1049+
1050+
# This has to call both, as its a arg-less menu.
1051+
1052+
obj = self.add_context_menu(cmd, name, target, guild_ids)
1053+
1054+
return obj
1055+
1056+
return wrapper
1057+
9191058
def add_component_callback(
9201059
self,
9211060
callback: typing.Coroutine,
@@ -1255,12 +1394,15 @@ async def on_socket_response(self, msg):
12551394

12561395
to_use = msg["d"]
12571396
interaction_type = to_use["type"]
1258-
if interaction_type in (1, 2):
1259-
return await self._on_slash(to_use)
1260-
if interaction_type == 3:
1261-
return await self._on_component(to_use)
1262-
1263-
raise NotImplementedError
1397+
if interaction_type in (1, 2, 3) or msg["s"] == 5:
1398+
await self._on_slash(to_use)
1399+
await self._on_context_menu(to_use)
1400+
try:
1401+
await self._on_component(to_use) # noqa
1402+
except KeyError:
1403+
pass # for some reason it complains about custom_id being an optional arg when it's fine?
1404+
return
1405+
# raise NotImplementedError
12641406

12651407
async def _on_component(self, to_use):
12661408
ctx = context.ComponentContext(self.req, to_use, self._discord, self.logger)
@@ -1319,6 +1461,34 @@ async def _on_slash(self, to_use):
13191461

13201462
await self.invoke_command(selected_cmd, ctx, args)
13211463

1464+
async def _on_context_menu(self, to_use):
1465+
if to_use["data"]["name"] in self.commands["context"]:
1466+
ctx = context.MenuContext(self.req, to_use, self._discord, self.logger)
1467+
cmd_name = to_use["data"]["name"]
1468+
1469+
if cmd_name not in self.commands["context"] and cmd_name in self.subcommands:
1470+
return # menus don't have subcommands you smooth brain
1471+
1472+
selected_cmd = self.commands["context"][cmd_name]
1473+
1474+
if (
1475+
selected_cmd.allowed_guild_ids
1476+
and ctx.guild_id not in selected_cmd.allowed_guild_ids
1477+
):
1478+
return
1479+
1480+
if selected_cmd.has_subcommands and not selected_cmd.func:
1481+
return await self.handle_subcommand(ctx, to_use)
1482+
1483+
if "options" in to_use["data"]:
1484+
for x in to_use["data"]["options"]:
1485+
if "value" not in x:
1486+
return await self.handle_subcommand(ctx, to_use)
1487+
1488+
self._discord.dispatch("context_menu", ctx)
1489+
1490+
await self.invoke_command(selected_cmd, ctx, args={})
1491+
13221492
async def handle_subcommand(self, ctx: context.SlashContext, data: dict):
13231493
"""
13241494
Coroutine for handling subcommand.

discord_slash/cog_ext.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,49 @@ def wrapper(cmd):
189189
return wrapper
190190

191191

192+
# I don't feel comfortable with having these right now, they're too buggy even when they were working.
193+
194+
195+
def cog_context_menu(*, name: str, guild_ids: list = None, target: int = 1):
196+
"""
197+
Decorator that adds context menu commands.
198+
199+
:param target: The type of menu.
200+
:type target: int
201+
:param name: A name to register as the command in the menu.
202+
:type name: str
203+
:param guild_ids: A list of guild IDs to register the command under. Defaults to ``None``.
204+
:type guild_ids: list
205+
"""
206+
207+
def wrapper(cmd):
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, target)
231+
232+
return wrapper
233+
234+
192235
def permission(guild_id: int, permissions: list):
193236
"""
194237
Decorator that add permissions. This will set the permissions for a single guild, you can use it more than once for each command.

0 commit comments

Comments
 (0)