feat(userslash): context-aware command filtering + custom description
User-install contexts (DMs, group DMs, non-member guilds) now only show commands that can actually work outside a guild: - _command_needs_guild() heuristic checks @guild_only, Red privilege levels (MOD/ADMIN/GUILD_OWNER), and user permission requirements, walking the parent chain for subcommands. - Autocomplete pre-filters the candidate pool in user-install contexts and shows available DM-compatible commands when nothing is typed. - Command execution blocks guild-only commands early with a clear ephemeral error instead of letting them fail cryptically. Also: - Slash command description changed to 'Shine bright like a Ruby' - Added [p]userslash setdescription command to change it on the fly (persisted in Config, applied on cog load)
This commit is contained in:
parent
180121bbdc
commit
f75f51a6b4
2 changed files with 126 additions and 7 deletions
|
|
@ -45,6 +45,7 @@ class UserSlash(commands.Cog):
|
|||
self.config.register_global(
|
||||
whitelist_enabled=False,
|
||||
whitelisted_users=[],
|
||||
description="",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -74,6 +75,11 @@ class UserSlash(commands.Cog):
|
|||
await self.bot.wait_until_red_ready()
|
||||
assert self.bot.user
|
||||
|
||||
# Apply custom description if set
|
||||
custom_desc = await self.config.description()
|
||||
if custom_desc:
|
||||
user_slash_command.description = custom_desc
|
||||
|
||||
# Apply native user-install flags (discord.py 2.4+)
|
||||
apply_user_install_native(user_slash_command)
|
||||
|
||||
|
|
@ -243,6 +249,35 @@ class UserSlash(commands.Cog):
|
|||
if page:
|
||||
await ctx.send("\n".join(page))
|
||||
|
||||
@_userslash.command(name="setdescription", aliases=["setdesc"])
|
||||
async def _setdesc(self, ctx: commands.Context, *, description: str = "") -> None:
|
||||
"""Set a custom description for the slash command.
|
||||
|
||||
The description appears under the ``/botname`` command in the Discord UI.
|
||||
Pass no argument (or an empty string) to reset to the default.
|
||||
|
||||
Run ``[p]userslash sync`` afterwards to push the change to Discord.
|
||||
|
||||
Example: ``[p]userslash setdescription Shine bright like a Ruby``
|
||||
"""
|
||||
if not description.strip():
|
||||
await self.config.description.set("")
|
||||
user_slash_command.description = (
|
||||
"Shine bright like a Ruby"
|
||||
)
|
||||
await ctx.send(
|
||||
"✅ Description reset to default. "
|
||||
"Run `[p]userslash sync` to push the change."
|
||||
)
|
||||
else:
|
||||
description = description[:100] # Discord limit
|
||||
await self.config.description.set(description)
|
||||
user_slash_command.description = description
|
||||
await ctx.send(
|
||||
f"✅ Description set to: *{description}*\n"
|
||||
"Run `[p]userslash sync` to push the change."
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Whitelist commands
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -30,6 +30,57 @@ from .utils import walk_aliases
|
|||
LOG = logging.getLogger("red.evecogs.userslash.slash_bridge")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Context helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _is_user_install_context(interaction: discord.Interaction) -> bool:
|
||||
"""True when the interaction originates outside a guild the bot is a member of."""
|
||||
return not interaction.guild or not getattr(interaction.guild, "me", None)
|
||||
|
||||
|
||||
def _command_needs_guild(cmd: commands.Command) -> bool:
|
||||
"""Heuristic: return True if *cmd* almost certainly requires a guild context.
|
||||
|
||||
Walks the parent chain so subcommands inherit their parent's requirements.
|
||||
Checks:
|
||||
- ``@commands.guild_only()`` / ``@commands.no_pm()`` in discord.py checks
|
||||
- Red privilege levels (MOD, ADMIN, GUILD_OWNER)
|
||||
- Explicit ``user_perms`` on Red's ``Requires`` object
|
||||
"""
|
||||
current: Optional[commands.Command] = cmd
|
||||
while current is not None:
|
||||
# discord.py @guild_only() / @no_pm()
|
||||
for check in getattr(current, "checks", ()):
|
||||
qn = getattr(check, "__qualname__", "")
|
||||
if "guild_only" in qn or "no_pm" in qn:
|
||||
return True
|
||||
|
||||
# Red's Requires system
|
||||
requires = getattr(current, "requires", None)
|
||||
if requires is not None:
|
||||
# MOD=1, ADMIN=2, GUILD_OWNER=3 all require a guild
|
||||
priv = getattr(requires, "privilege_level", None)
|
||||
if priv is not None:
|
||||
try:
|
||||
if priv.value in (1, 2, 3):
|
||||
return True
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
|
||||
# Explicit user-permission requirements (manage_guild, etc.)
|
||||
user_perms = getattr(requires, "user_perms", None)
|
||||
if user_perms is not None:
|
||||
try:
|
||||
if isinstance(user_perms, discord.Permissions) and user_perms.value != 0:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
current = getattr(current, "parent", None)
|
||||
return False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Whitelist helper
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -69,7 +120,7 @@ async def user_slash_command(
|
|||
attachment: Optional[discord.Attachment] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Run any bot command from anywhere — servers, DMs, and group DMs.
|
||||
Shine bright like a Ruby
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
|
|
@ -93,6 +144,18 @@ async def user_slash_command(
|
|||
)
|
||||
return
|
||||
|
||||
# --- Block guild-only commands in user-install contexts ---
|
||||
if _is_user_install_context(interaction):
|
||||
probe = interaction.client.get_command(command)
|
||||
if probe and _command_needs_guild(probe):
|
||||
if not interaction.response.is_done():
|
||||
await interaction.response.send_message(
|
||||
f"❌ `{command}` requires a server context and can't be "
|
||||
"used in DMs, group DMs, or servers the bot isn't in.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
actual = interaction.client.get_command(command)
|
||||
ctx = await InterContext.from_interaction(interaction, recreate_message=True)
|
||||
error = None
|
||||
|
|
@ -202,26 +265,47 @@ async def _autocomplete(
|
|||
# Fallback settings when guild config is unavailable
|
||||
help_settings = await HelpSettings.from_context.__wrapped__(ctx) # type: ignore
|
||||
|
||||
user_install = _is_user_install_context(interaction)
|
||||
show_hidden = help_settings.show_hidden
|
||||
|
||||
# Build the candidate pool — in user-install contexts, pre-filter to
|
||||
# commands that can actually work outside a guild.
|
||||
all_names = list(
|
||||
walk_aliases(interaction.client, show_hidden=show_hidden)
|
||||
)
|
||||
|
||||
if user_install:
|
||||
pool: List[str] = []
|
||||
for n in all_names:
|
||||
cmd = interaction.client.get_command(n)
|
||||
if cmd and not _command_needs_guild(cmd):
|
||||
pool.append(n)
|
||||
else:
|
||||
pool = all_names
|
||||
|
||||
if current:
|
||||
extracted = cast(
|
||||
List[str],
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
heapq.nlargest,
|
||||
6,
|
||||
walk_aliases(
|
||||
interaction.client, show_hidden=help_settings.show_hidden
|
||||
),
|
||||
24 if user_install else 6,
|
||||
pool,
|
||||
functools.partial(fuzz.token_sort_ratio, current),
|
||||
),
|
||||
)
|
||||
extracted.append("help")
|
||||
else:
|
||||
extracted = ["help"]
|
||||
if user_install:
|
||||
# Show available DM-compatible commands when nothing is typed yet
|
||||
extracted = pool[:24]
|
||||
extracted.append("help")
|
||||
else:
|
||||
extracted = ["help"]
|
||||
|
||||
_filter: Callable[[commands.Command], Awaitable[bool]] = (
|
||||
operator.methodcaller(
|
||||
"can_run" if help_settings.show_hidden else "can_see", ctx
|
||||
"can_run" if show_hidden else "can_see", ctx
|
||||
)
|
||||
)
|
||||
matches: Dict[commands.Command, str] = {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue