mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 07:21:37 +08:00
Compare commits
1 Commits
skill/gith
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d60316c99 |
@@ -408,7 +408,7 @@ class VoiceReceiver:
|
|||||||
class DiscordAdapter(BasePlatformAdapter):
|
class DiscordAdapter(BasePlatformAdapter):
|
||||||
"""
|
"""
|
||||||
Discord bot adapter.
|
Discord bot adapter.
|
||||||
|
|
||||||
Handles:
|
Handles:
|
||||||
- Receiving messages from servers and DMs
|
- Receiving messages from servers and DMs
|
||||||
- Sending responses with Discord markdown
|
- Sending responses with Discord markdown
|
||||||
@@ -418,10 +418,10 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
- Auto-threading for long conversations
|
- Auto-threading for long conversations
|
||||||
- Reaction-based feedback
|
- Reaction-based feedback
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Discord message limits
|
# Discord message limits
|
||||||
MAX_MESSAGE_LENGTH = 2000
|
MAX_MESSAGE_LENGTH = 2000
|
||||||
|
|
||||||
# Auto-disconnect from voice channel after this many seconds of inactivity
|
# Auto-disconnect from voice channel after this many seconds of inactivity
|
||||||
VOICE_TIMEOUT = 300
|
VOICE_TIMEOUT = 300
|
||||||
|
|
||||||
@@ -449,7 +449,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
self._bot_task: Optional[asyncio.Task] = None
|
self._bot_task: Optional[asyncio.Task] = None
|
||||||
# Cap to prevent unbounded growth (Discord threads get archived).
|
# Cap to prevent unbounded growth (Discord threads get archived).
|
||||||
self._MAX_TRACKED_THREADS = 500
|
self._MAX_TRACKED_THREADS = 500
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
"""Connect to Discord and start receiving events."""
|
"""Connect to Discord and start receiving events."""
|
||||||
if not DISCORD_AVAILABLE:
|
if not DISCORD_AVAILABLE:
|
||||||
@@ -480,11 +480,11 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
logger.warning("Opus codec found at %s but failed to load", opus_path)
|
logger.warning("Opus codec found at %s but failed to load", opus_path)
|
||||||
if not discord.opus.is_loaded():
|
if not discord.opus.is_loaded():
|
||||||
logger.warning("Opus codec not found — voice channel playback disabled")
|
logger.warning("Opus codec not found — voice channel playback disabled")
|
||||||
|
|
||||||
if not self.config.token:
|
if not self.config.token:
|
||||||
logger.error("[%s] No bot token configured", self.name)
|
logger.error("[%s] No bot token configured", self.name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Acquire scoped lock to prevent duplicate bot token usage
|
# Acquire scoped lock to prevent duplicate bot token usage
|
||||||
from gateway.status import acquire_scoped_lock
|
from gateway.status import acquire_scoped_lock
|
||||||
@@ -504,13 +504,13 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
intents.guild_messages = True
|
intents.guild_messages = True
|
||||||
intents.members = True
|
intents.members = True
|
||||||
intents.voice_states = True
|
intents.voice_states = True
|
||||||
|
|
||||||
# Create bot
|
# Create bot
|
||||||
self._client = commands.Bot(
|
self._client = commands.Bot(
|
||||||
command_prefix="!", # Not really used, we handle raw messages
|
command_prefix="!", # Not really used, we handle raw messages
|
||||||
intents=intents,
|
intents=intents,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse allowed user entries (may contain usernames or IDs)
|
# Parse allowed user entries (may contain usernames or IDs)
|
||||||
allowed_env = os.getenv("DISCORD_ALLOWED_USERS", "")
|
allowed_env = os.getenv("DISCORD_ALLOWED_USERS", "")
|
||||||
if allowed_env:
|
if allowed_env:
|
||||||
@@ -518,17 +518,17 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
_clean_discord_id(uid) for uid in allowed_env.split(",")
|
_clean_discord_id(uid) for uid in allowed_env.split(",")
|
||||||
if uid.strip()
|
if uid.strip()
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter_self = self # capture for closure
|
adapter_self = self # capture for closure
|
||||||
|
|
||||||
# Register event handlers
|
# Register event handlers
|
||||||
@self._client.event
|
@self._client.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
logger.info("[%s] Connected as %s", adapter_self.name, adapter_self._client.user)
|
logger.info("[%s] Connected as %s", adapter_self.name, adapter_self._client.user)
|
||||||
|
|
||||||
# Resolve any usernames in the allowed list to numeric IDs
|
# Resolve any usernames in the allowed list to numeric IDs
|
||||||
await adapter_self._resolve_allowed_usernames()
|
await adapter_self._resolve_allowed_usernames()
|
||||||
|
|
||||||
# Sync slash commands with Discord
|
# Sync slash commands with Discord
|
||||||
try:
|
try:
|
||||||
synced = await adapter_self._client.tree.sync()
|
synced = await adapter_self._client.tree.sync()
|
||||||
@@ -536,18 +536,22 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
except Exception as e: # pragma: no cover - defensive logging
|
except Exception as e: # pragma: no cover - defensive logging
|
||||||
logger.warning("[%s] Slash command sync failed: %s", adapter_self.name, e, exc_info=True)
|
logger.warning("[%s] Slash command sync failed: %s", adapter_self.name, e, exc_info=True)
|
||||||
adapter_self._ready_event.set()
|
adapter_self._ready_event.set()
|
||||||
|
|
||||||
@self._client.event
|
@self._client.event
|
||||||
async def on_message(message: DiscordMessage):
|
async def on_message(message: DiscordMessage):
|
||||||
# Always ignore our own messages
|
# Always ignore our own messages
|
||||||
if message.author == self._client.user:
|
if message.author == self._client.user:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Ignore Discord system messages (thread renames, pins, member joins, etc.)
|
# Ignore Discord system messages (thread renames, pins, member joins, etc.)
|
||||||
# Allow both default and reply types — replies have a distinct MessageType.
|
# Allow both default and reply types — replies have a distinct MessageType.
|
||||||
if message.type not in (discord.MessageType.default, discord.MessageType.reply):
|
if message.type not in (discord.MessageType.default, discord.MessageType.reply):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if the message author is in the allowed user list
|
||||||
|
if not self._is_allowed_user(str(message.author.id)):
|
||||||
|
return
|
||||||
|
|
||||||
# Bot message filtering (DISCORD_ALLOW_BOTS):
|
# Bot message filtering (DISCORD_ALLOW_BOTS):
|
||||||
# "none" — ignore all other bots (default)
|
# "none" — ignore all other bots (default)
|
||||||
# "mentions" — accept bot messages only when they @mention us
|
# "mentions" — accept bot messages only when they @mention us
|
||||||
@@ -560,7 +564,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
if not self._client.user or self._client.user not in message.mentions:
|
if not self._client.user or self._client.user not in message.mentions:
|
||||||
return
|
return
|
||||||
# "all" falls through to handle_message
|
# "all" falls through to handle_message
|
||||||
|
|
||||||
# If the message @mentions other users but NOT the bot, the
|
# If the message @mentions other users but NOT the bot, the
|
||||||
# sender is talking to someone else — stay silent. Only
|
# sender is talking to someone else — stay silent. Only
|
||||||
# applies in server channels; in DMs the user is always
|
# applies in server channels; in DMs the user is always
|
||||||
@@ -614,23 +618,23 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
# Register slash commands
|
# Register slash commands
|
||||||
self._register_slash_commands()
|
self._register_slash_commands()
|
||||||
|
|
||||||
# Start the bot in background
|
# Start the bot in background
|
||||||
self._bot_task = asyncio.create_task(self._client.start(self.config.token))
|
self._bot_task = asyncio.create_task(self._client.start(self.config.token))
|
||||||
|
|
||||||
# Wait for ready
|
# Wait for ready
|
||||||
await asyncio.wait_for(self._ready_event.wait(), timeout=30)
|
await asyncio.wait_for(self._ready_event.wait(), timeout=30)
|
||||||
|
|
||||||
self._running = True
|
self._running = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.error("[%s] Timeout waiting for connection to Discord", self.name, exc_info=True)
|
logger.error("[%s] Timeout waiting for connection to Discord", self.name, exc_info=True)
|
||||||
return False
|
return False
|
||||||
except Exception as e: # pragma: no cover - defensive logging
|
except Exception as e: # pragma: no cover - defensive logging
|
||||||
logger.error("[%s] Failed to connect to Discord: %s", self.name, e, exc_info=True)
|
logger.error("[%s] Failed to connect to Discord: %s", self.name, e, exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def disconnect(self) -> None:
|
async def disconnect(self) -> None:
|
||||||
"""Disconnect from Discord."""
|
"""Disconnect from Discord."""
|
||||||
# Clean up all active voice connections before closing the client
|
# Clean up all active voice connections before closing the client
|
||||||
@@ -703,7 +707,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
if hasattr(message, "add_reaction"):
|
if hasattr(message, "add_reaction"):
|
||||||
await self._remove_reaction(message, "👀")
|
await self._remove_reaction(message, "👀")
|
||||||
await self._add_reaction(message, "✅" if success else "❌")
|
await self._add_reaction(message, "✅" if success else "❌")
|
||||||
|
|
||||||
async def send(
|
async def send(
|
||||||
self,
|
self,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
@@ -720,24 +724,24 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
channel = self._client.get_channel(int(chat_id))
|
channel = self._client.get_channel(int(chat_id))
|
||||||
if not channel:
|
if not channel:
|
||||||
channel = await self._client.fetch_channel(int(chat_id))
|
channel = await self._client.fetch_channel(int(chat_id))
|
||||||
|
|
||||||
if not channel:
|
if not channel:
|
||||||
return SendResult(success=False, error=f"Channel {chat_id} not found")
|
return SendResult(success=False, error=f"Channel {chat_id} not found")
|
||||||
|
|
||||||
# Format and split message if needed
|
# Format and split message if needed
|
||||||
formatted = self.format_message(content)
|
formatted = self.format_message(content)
|
||||||
chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)
|
chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)
|
||||||
|
|
||||||
message_ids = []
|
message_ids = []
|
||||||
reference = None
|
reference = None
|
||||||
|
|
||||||
if reply_to:
|
if reply_to:
|
||||||
try:
|
try:
|
||||||
ref_msg = await channel.fetch_message(int(reply_to))
|
ref_msg = await channel.fetch_message(int(reply_to))
|
||||||
reference = ref_msg
|
reference = ref_msg
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Could not fetch reply-to message: %s", e)
|
logger.debug("Could not fetch reply-to message: %s", e)
|
||||||
|
|
||||||
for i, chunk in enumerate(chunks):
|
for i, chunk in enumerate(chunks):
|
||||||
chunk_reference = reference if i == 0 else None
|
chunk_reference = reference if i == 0 else None
|
||||||
try:
|
try:
|
||||||
@@ -764,13 +768,13 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
message_ids.append(str(msg.id))
|
message_ids.append(str(msg.id))
|
||||||
|
|
||||||
return SendResult(
|
return SendResult(
|
||||||
success=True,
|
success=True,
|
||||||
message_id=message_ids[0] if message_ids else None,
|
message_id=message_ids[0] if message_ids else None,
|
||||||
raw_response={"message_ids": message_ids}
|
raw_response={"message_ids": message_ids}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e: # pragma: no cover - defensive logging
|
except Exception as e: # pragma: no cover - defensive logging
|
||||||
logger.error("[%s] Failed to send Discord message: %s", self.name, e, exc_info=True)
|
logger.error("[%s] Failed to send Discord message: %s", self.name, e, exc_info=True)
|
||||||
return SendResult(success=False, error=str(e))
|
return SendResult(success=False, error=str(e))
|
||||||
@@ -1242,25 +1246,25 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
"""Send an image natively as a Discord file attachment."""
|
"""Send an image natively as a Discord file attachment."""
|
||||||
if not self._client:
|
if not self._client:
|
||||||
return SendResult(success=False, error="Not connected")
|
return SendResult(success=False, error="Not connected")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
channel = self._client.get_channel(int(chat_id))
|
channel = self._client.get_channel(int(chat_id))
|
||||||
if not channel:
|
if not channel:
|
||||||
channel = await self._client.fetch_channel(int(chat_id))
|
channel = await self._client.fetch_channel(int(chat_id))
|
||||||
if not channel:
|
if not channel:
|
||||||
return SendResult(success=False, error=f"Channel {chat_id} not found")
|
return SendResult(success=False, error=f"Channel {chat_id} not found")
|
||||||
|
|
||||||
# Download the image and send as a Discord file attachment
|
# Download the image and send as a Discord file attachment
|
||||||
# (Discord renders attachments inline, unlike plain URLs)
|
# (Discord renders attachments inline, unlike plain URLs)
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(image_url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
async with session.get(image_url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
raise Exception(f"Failed to download image: HTTP {resp.status}")
|
raise Exception(f"Failed to download image: HTTP {resp.status}")
|
||||||
|
|
||||||
image_data = await resp.read()
|
image_data = await resp.read()
|
||||||
|
|
||||||
# Determine filename from URL or content type
|
# Determine filename from URL or content type
|
||||||
content_type = resp.headers.get("content-type", "image/png")
|
content_type = resp.headers.get("content-type", "image/png")
|
||||||
ext = "png"
|
ext = "png"
|
||||||
@@ -1270,16 +1274,16 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
ext = "gif"
|
ext = "gif"
|
||||||
elif "webp" in content_type:
|
elif "webp" in content_type:
|
||||||
ext = "webp"
|
ext = "webp"
|
||||||
|
|
||||||
import io
|
import io
|
||||||
file = discord.File(io.BytesIO(image_data), filename=f"image.{ext}")
|
file = discord.File(io.BytesIO(image_data), filename=f"image.{ext}")
|
||||||
|
|
||||||
msg = await channel.send(
|
msg = await channel.send(
|
||||||
content=caption if caption else None,
|
content=caption if caption else None,
|
||||||
file=file,
|
file=file,
|
||||||
)
|
)
|
||||||
return SendResult(success=True, message_id=str(msg.id))
|
return SendResult(success=True, message_id=str(msg.id))
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"[%s] aiohttp not installed, falling back to URL. Run: pip install aiohttp",
|
"[%s] aiohttp not installed, falling back to URL. Run: pip install aiohttp",
|
||||||
@@ -1330,7 +1334,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
except Exception as e: # pragma: no cover - defensive logging
|
except Exception as e: # pragma: no cover - defensive logging
|
||||||
logger.error("[%s] Failed to send document, falling back to base adapter: %s", self.name, e, exc_info=True)
|
logger.error("[%s] Failed to send document, falling back to base adapter: %s", self.name, e, exc_info=True)
|
||||||
return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata)
|
return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata)
|
||||||
|
|
||||||
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
||||||
"""Start a persistent typing indicator for a channel.
|
"""Start a persistent typing indicator for a channel.
|
||||||
|
|
||||||
@@ -1374,20 +1378,20 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
await task
|
await task
|
||||||
except (asyncio.CancelledError, Exception):
|
except (asyncio.CancelledError, Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
||||||
"""Get information about a Discord channel."""
|
"""Get information about a Discord channel."""
|
||||||
if not self._client:
|
if not self._client:
|
||||||
return {"name": "Unknown", "type": "dm"}
|
return {"name": "Unknown", "type": "dm"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
channel = self._client.get_channel(int(chat_id))
|
channel = self._client.get_channel(int(chat_id))
|
||||||
if not channel:
|
if not channel:
|
||||||
channel = await self._client.fetch_channel(int(chat_id))
|
channel = await self._client.fetch_channel(int(chat_id))
|
||||||
|
|
||||||
if not channel:
|
if not channel:
|
||||||
return {"name": str(chat_id), "type": "dm"}
|
return {"name": str(chat_id), "type": "dm"}
|
||||||
|
|
||||||
# Determine channel type
|
# Determine channel type
|
||||||
if isinstance(channel, discord.DMChannel):
|
if isinstance(channel, discord.DMChannel):
|
||||||
chat_type = "dm"
|
chat_type = "dm"
|
||||||
@@ -1403,7 +1407,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
else:
|
else:
|
||||||
chat_type = "channel"
|
chat_type = "channel"
|
||||||
name = getattr(channel, "name", str(chat_id))
|
name = getattr(channel, "name", str(chat_id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": chat_type,
|
"type": chat_type,
|
||||||
@@ -1413,7 +1417,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
except Exception as e: # pragma: no cover - defensive logging
|
except Exception as e: # pragma: no cover - defensive logging
|
||||||
logger.error("[%s] Failed to get chat info for %s: %s", self.name, chat_id, e, exc_info=True)
|
logger.error("[%s] Failed to get chat info for %s: %s", self.name, chat_id, e, exc_info=True)
|
||||||
return {"name": str(chat_id), "type": "dm", "error": str(e)}
|
return {"name": str(chat_id), "type": "dm", "error": str(e)}
|
||||||
|
|
||||||
async def _resolve_allowed_usernames(self) -> None:
|
async def _resolve_allowed_usernames(self) -> None:
|
||||||
"""
|
"""
|
||||||
Resolve non-numeric entries in DISCORD_ALLOWED_USERS to Discord user IDs.
|
Resolve non-numeric entries in DISCORD_ALLOWED_USERS to Discord user IDs.
|
||||||
@@ -1481,7 +1485,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
def format_message(self, content: str) -> str:
|
def format_message(self, content: str) -> str:
|
||||||
"""
|
"""
|
||||||
Format message for Discord.
|
Format message for Discord.
|
||||||
|
|
||||||
Discord uses its own markdown variant.
|
Discord uses its own markdown variant.
|
||||||
"""
|
"""
|
||||||
# Discord markdown is fairly standard, no special escaping needed
|
# Discord markdown is fairly standard, no special escaping needed
|
||||||
@@ -1647,7 +1651,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
chat_name = interaction.channel.name
|
chat_name = interaction.channel.name
|
||||||
if hasattr(interaction.channel, "guild") and interaction.channel.guild:
|
if hasattr(interaction.channel, "guild") and interaction.channel.guild:
|
||||||
chat_name = f"{interaction.channel.guild.name} / #{chat_name}"
|
chat_name = f"{interaction.channel.guild.name} / #{chat_name}"
|
||||||
|
|
||||||
# Get channel topic (if available)
|
# Get channel topic (if available)
|
||||||
chat_topic = getattr(interaction.channel, "topic", None)
|
chat_topic = getattr(interaction.channel, "topic", None)
|
||||||
|
|
||||||
@@ -2051,7 +2055,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
if doc_ext in SUPPORTED_DOCUMENT_TYPES:
|
if doc_ext in SUPPORTED_DOCUMENT_TYPES:
|
||||||
msg_type = MessageType.DOCUMENT
|
msg_type = MessageType.DOCUMENT
|
||||||
break
|
break
|
||||||
|
|
||||||
# When auto-threading kicked in, route responses to the new thread
|
# When auto-threading kicked in, route responses to the new thread
|
||||||
effective_channel = auto_threaded_channel or message.channel
|
effective_channel = auto_threaded_channel or message.channel
|
||||||
|
|
||||||
@@ -2070,7 +2074,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
# Get channel topic (if available - TextChannels have topics, DMs/threads don't)
|
# Get channel topic (if available - TextChannels have topics, DMs/threads don't)
|
||||||
chat_topic = getattr(message.channel, "topic", None)
|
chat_topic = getattr(message.channel, "topic", None)
|
||||||
|
|
||||||
# Build source
|
# Build source
|
||||||
source = self.build_source(
|
source = self.build_source(
|
||||||
chat_id=str(effective_channel.id),
|
chat_id=str(effective_channel.id),
|
||||||
@@ -2081,7 +2085,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
chat_topic=chat_topic,
|
chat_topic=chat_topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build media URLs -- download image attachments to local cache so the
|
# Build media URLs -- download image attachments to local cache so the
|
||||||
# vision tool can access them reliably (Discord CDN URLs can expire).
|
# vision tool can access them reliably (Discord CDN URLs can expire).
|
||||||
media_urls = []
|
media_urls = []
|
||||||
@@ -2175,7 +2179,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
"[Discord] Failed to cache document %s: %s",
|
"[Discord] Failed to cache document %s: %s",
|
||||||
att.filename, e, exc_info=True,
|
att.filename, e, exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
event_text = message.content
|
event_text = message.content
|
||||||
if pending_text_injection:
|
if pending_text_injection:
|
||||||
event_text = f"{pending_text_injection}\n\n{event_text}" if event_text else pending_text_injection
|
event_text = f"{pending_text_injection}\n\n{event_text}" if event_text else pending_text_injection
|
||||||
|
|||||||
Reference in New Issue
Block a user