EveCogs/userslash/message.py

196 lines
6.3 KiB
Python
Raw Normal View History

"""
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)