Replaces OneTrueSlash with full user-install support: - Single proxy slash command bridges all text commands - Works in servers, DMs, and group DMs - REST API patcher sets integration_types & contexts on all global commands - Owner commands: [p]userslash sync/patch/status - Fuzzy autocomplete (rapidfuzz) - Enhanced InterContext/InterMessage for non-guild contexts
195 lines
6.3 KiB
Python
195 lines
6.3 KiB
Python
"""
|
|
InterMessage — lightweight ``discord.Message`` stand-in built from
|
|
a slash-command ``Interaction`` so Red's text-command pipeline can process it.
|
|
|
|
Enhanced from OneTrueSlash to handle user-install contexts (DMs, group DMs,
|
|
and guilds where the bot is not a member).
|
|
"""
|
|
|
|
import asyncio
|
|
from copy import copy
|
|
from typing import TypeVar
|
|
|
|
import discord
|
|
|
|
from .channel import InterChannel
|
|
|
|
_TT = TypeVar("_TT", bound=type)
|
|
|
|
|
|
def _noop_step(*_args, **_kwargs):
|
|
"""Coroutine that just yields to the event loop once."""
|
|
return asyncio.sleep(0)
|
|
|
|
|
|
def neuter_coros(cls: _TT) -> _TT:
|
|
"""Replace every inherited coroutine with a harmless no-op."""
|
|
for name in dir(cls):
|
|
if name in cls.__dict__:
|
|
continue
|
|
if (attr := getattr(cls, name, None)) is None:
|
|
continue
|
|
if asyncio.iscoroutinefunction(attr):
|
|
setattr(cls, name, property(lambda self: _noop_step))
|
|
return cls
|
|
|
|
|
|
@neuter_coros
|
|
class InterMessage(discord.Message):
|
|
__slots__ = ()
|
|
|
|
def __init__(self, **kwargs) -> None:
|
|
raise RuntimeError("InterMessage must be created via _from_interaction")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Factory
|
|
# ------------------------------------------------------------------
|
|
|
|
@classmethod
|
|
def _from_interaction(
|
|
cls, interaction: discord.Interaction, prefix: str
|
|
) -> "InterMessage":
|
|
assert interaction.data
|
|
assert interaction.client.user
|
|
|
|
self = cls.__new__(cls)
|
|
self._state = interaction._state
|
|
self._edited_timestamp = None
|
|
|
|
# Constant / default fields
|
|
self.tts = False
|
|
self.webhook_id = None
|
|
self.mention_everyone = False
|
|
self.embeds = []
|
|
self.role_mentions = []
|
|
self.id = interaction.id
|
|
self.nonce = None
|
|
self.pinned = False
|
|
self.type = discord.MessageType.default
|
|
self.flags = discord.MessageFlags()
|
|
self.reactions = []
|
|
self.reference = None
|
|
self.application = None
|
|
self.activity = None
|
|
self.stickers = []
|
|
self.components = []
|
|
self.role_subscription = None
|
|
self.application_id = None
|
|
self.position = None
|
|
|
|
channel = interaction.channel
|
|
self.guild = interaction.guild
|
|
|
|
# --- Determine author & channel based on context ---
|
|
|
|
if interaction.guild_id and not interaction.guild:
|
|
# Guild ID present but guild not in cache (e.g. user-install in a
|
|
# server the bot hasn't joined). Treat as DM so Red doesn't crash.
|
|
if isinstance(interaction.user, discord.Member):
|
|
self.author = interaction.user._user
|
|
else:
|
|
self.author = interaction.user
|
|
channel = cls._make_dm_channel(interaction)
|
|
|
|
elif not interaction.guild:
|
|
# True DM or group DM from a user-install context.
|
|
self.author = interaction.user
|
|
if channel is None:
|
|
channel = cls._make_dm_channel(interaction)
|
|
else:
|
|
channel = copy(channel)
|
|
|
|
else:
|
|
# Normal guild context — bot is a member.
|
|
self.author = interaction.user
|
|
if channel is not None:
|
|
channel = copy(channel)
|
|
else:
|
|
channel = cls._make_dm_channel(interaction)
|
|
|
|
# Mix InterChannel into the channel so send/typing route correctly
|
|
if channel is not None:
|
|
channel.__class__ = type(
|
|
InterChannel.__name__,
|
|
(InterChannel, channel.__class__),
|
|
{"__slots__": ()},
|
|
)
|
|
self.channel = channel # type: ignore
|
|
|
|
self._recreate_from_interaction(interaction, prefix)
|
|
return self
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def _make_dm_channel(interaction: discord.Interaction) -> discord.DMChannel:
|
|
"""Fabricate a minimal DMChannel when the real one is unavailable."""
|
|
assert interaction.client.user
|
|
user = interaction.user
|
|
user_json = (
|
|
user._to_minimal_user_json()
|
|
if hasattr(user, "_to_minimal_user_json")
|
|
else {
|
|
"id": str(user.id),
|
|
"username": str(user),
|
|
"discriminator": "0",
|
|
"avatar": None,
|
|
}
|
|
)
|
|
return discord.DMChannel(
|
|
me=interaction.client.user,
|
|
state=interaction._state,
|
|
data={
|
|
"id": interaction.channel_id or interaction.id,
|
|
"name": f"DM with {user}",
|
|
"type": 1,
|
|
"last_message_id": None,
|
|
"recipients": [
|
|
user_json,
|
|
interaction.client.user._to_minimal_user_json(),
|
|
],
|
|
},
|
|
)
|
|
|
|
def _recreate_from_interaction(
|
|
self, interaction: discord.Interaction, prefix: str
|
|
) -> None:
|
|
assert interaction.data and interaction.client.user
|
|
|
|
self.content = f"{prefix}{interaction.namespace.command}"
|
|
if interaction.namespace.arguments:
|
|
self.content = f"{self.content} {interaction.namespace.arguments}"
|
|
if interaction.namespace.attachment:
|
|
self.attachments = [interaction.namespace.attachment]
|
|
else:
|
|
self.attachments = []
|
|
|
|
resolved = interaction.data.get("resolved", {})
|
|
if self.guild:
|
|
self.mentions = [
|
|
discord.Member(data=d, guild=self.guild, state=self._state)
|
|
for d in resolved.get("members", {}).values()
|
|
]
|
|
else:
|
|
self.mentions = [
|
|
discord.User(data=d, state=self._state)
|
|
for d in resolved.get("users", {}).values()
|
|
]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Overrides
|
|
# ------------------------------------------------------------------
|
|
|
|
def to_reference(self, *, fail_if_not_exists: bool = True):
|
|
return None
|
|
|
|
def to_message_reference_dict(self):
|
|
return discord.utils.MISSING
|
|
|
|
async def reply(self, *args, **kwargs):
|
|
return await self.channel.send(*args, **kwargs)
|
|
|
|
def edit(self, *args, **kwargs):
|
|
return asyncio.sleep(0, self)
|