Skip to content

Commit a560796

Browse files
authored
Merge branch 'discord-py-interactions:master' into master
2 parents 859d061 + 5ddbd4f commit a560796

File tree

11 files changed

+495
-25
lines changed

11 files changed

+495
-25
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: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class SlashCommand:
2929
:type client: Union[discord.Client, discord.ext.commands.Bot]
3030
:param sync_commands: Whether to sync commands automatically. Default `False`.
3131
:type sync_commands: bool
32+
:param debug_guild: Guild ID of guild to use for testing commands. Prevents setting global commands in favor of guild commands, which update instantly
33+
:type debug_guild: int
3234
:param delete_from_unused_guilds: If the bot should make a request to set no commands for guilds that haven't got any commands registered in :class:``SlashCommand``. Default `False`.
3335
:type delete_from_unused_guilds: bool
3436
:param sync_on_cog_reload: Whether to sync commands on cog reload. Default `False`.
@@ -44,6 +46,7 @@ class SlashCommand:
4446
4547
:ivar _discord: Discord client of this client.
4648
: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.
4750
:ivar req: :class:`.http.SlashCommandRequest` of this client.
4851
:ivar logger: Logger of this client.
4952
:ivar sync_commands: Whether to sync commands automatically.
@@ -55,18 +58,20 @@ def __init__(
5558
self,
5659
client: typing.Union[discord.Client, commands.Bot],
5760
sync_commands: bool = False,
61+
debug_guild: typing.Optional[int] = None,
5862
delete_from_unused_guilds: bool = False,
5963
sync_on_cog_reload: bool = False,
6064
override_type: bool = False,
6165
application_id: typing.Optional[int] = None,
6266
):
6367
self._discord = client
64-
self.commands = {}
68+
self.commands = {"context": {}}
6569
self.subcommands = {}
6670
self.components = {}
6771
self.logger = logging.getLogger("discord_slash")
6872
self.req = http.SlashCommandRequest(self.logger, self._discord, application_id)
6973
self.sync_commands = sync_commands
74+
self.debug_guild = debug_guild
7075
self.sync_on_cog_reload = sync_on_cog_reload
7176

7277
if self.sync_commands:
@@ -266,12 +271,53 @@ async def to_dict(self):
266271
await self._discord.wait_until_ready() # In case commands are still not registered to SlashCommand.
267272
all_guild_ids = []
268273
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
269282
for i in self.commands[x].allowed_guild_ids:
270283
if i not in all_guild_ids:
271284
all_guild_ids.append(i)
272285
cmds = {"global": [], "guild": {x: [] for x in all_guild_ids}}
273286
wait = {} # Before merging to return dict, let's first put commands to temporary dict.
274287
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+
275321
selected = self.commands[x]
276322
if selected.allowed_guild_ids:
277323
for y in selected.allowed_guild_ids:
@@ -283,7 +329,10 @@ async def to_dict(self):
283329
"options": selected.options or [],
284330
"default_permission": selected.default_permission,
285331
"permissions": {},
332+
"type": selected._type,
286333
}
334+
if command_dict["type"] != 1:
335+
command_dict.pop("description")
287336
if y in selected.permissions:
288337
command_dict["permissions"][y] = selected.permissions[y]
289338
wait[y][x] = copy.deepcopy(command_dict)
@@ -296,14 +345,20 @@ async def to_dict(self):
296345
"options": selected.options or [],
297346
"default_permission": selected.default_permission,
298347
"permissions": selected.permissions or {},
348+
"type": selected._type,
299349
}
350+
if command_dict["type"] != 1:
351+
command_dict.pop("description")
300352
wait["global"][x] = copy.deepcopy(command_dict)
301353

302354
# Separated normal command add and subcommand add not to
303355
# merge subcommands to one. More info at Issue #88
304356
# https://github.com/eunwoo1104/discord-py-slash-command/issues/88
305357

306358
for x in self.commands:
359+
if x == "context":
360+
continue # no menus have subcommands.
361+
307362
if not self.commands[x].has_subcommands:
308363
continue
309364
tgt = self.subcommands[x]
@@ -373,7 +428,8 @@ async def sync_all_commands(
373428
permissions_map = {}
374429
cmds = await self.to_dict()
375430
self.logger.info("Syncing commands...")
376-
cmds_formatted = {None: cmds["global"]}
431+
# if debug_guild is set, global commands get re-routed to the guild to update quickly
432+
cmds_formatted = {self.debug_guild: cmds["global"]}
377433
for guild in cmds["guild"]:
378434
cmds_formatted[guild] = cmds["guild"][guild]
379435

@@ -419,7 +475,7 @@ async def sync_all_commands(
419475
if ex.status == 400:
420476
# catch bad requests
421477
cmd_nums = set(
422-
re.findall(r"In\s(\d).", ex.args[0])
478+
re.findall(r"^[\w-]{1,32}$", ex.args[0])
423479
) # find all discords references to commands
424480
error_string = ex.args[0]
425481

@@ -589,6 +645,66 @@ def add_slash_command(
589645
self.logger.debug(f"Added command `{name}`")
590646
return obj
591647

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+
592708
def add_subcommand(
593709
self,
594710
cmd,
@@ -911,6 +1027,34 @@ def wrapper(cmd):
9111027

9121028
return wrapper
9131029

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+
9141058
def add_component_callback(
9151059
self,
9161060
callback: typing.Coroutine,
@@ -1250,12 +1394,15 @@ async def on_socket_response(self, msg):
12501394

12511395
to_use = msg["d"]
12521396
interaction_type = to_use["type"]
1253-
if interaction_type in (1, 2):
1254-
return await self._on_slash(to_use)
1255-
if interaction_type == 3:
1256-
return await self._on_component(to_use)
1257-
1258-
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
12591406

12601407
async def _on_component(self, to_use):
12611408
ctx = context.ComponentContext(self.req, to_use, self._discord, self.logger)
@@ -1314,6 +1461,34 @@ async def _on_slash(self, to_use):
13141461

13151462
await self.invoke_command(selected_cmd, ctx, args)
13161463

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+
13171492
async def handle_subcommand(self, ctx: context.SlashContext, data: dict):
13181493
"""
13191494
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.

discord_slash/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Discord Slash Constants"""
22

3-
__version__ = "2.4.1"
3+
__version__ = "3.0.0"
44

55
BASE_API = "https://discord.com/api/v8"

0 commit comments

Comments
 (0)