- Updated CLI to load configuration from user-specific and project-specific YAML files, prioritizing user settings. - Introduced a new command `/platforms` to display the status of connected messaging platforms (Telegram, Discord, WhatsApp). - Implemented a gateway system for handling messaging interactions, including session management and delivery routing for cron job outputs. - Added support for environment variable configuration and a dedicated gateway configuration file for advanced settings. - Enhanced documentation in README.md and added a new messaging.md file to guide users on platform integrations and setup. - Updated toolsets to include platform-specific capabilities for Telegram, Discord, and WhatsApp, ensuring secure and tailored interactions.
298 lines
10 KiB
Python
298 lines
10 KiB
Python
"""
|
|
Discord platform adapter.
|
|
|
|
Uses discord.py library for:
|
|
- Receiving messages from servers and DMs
|
|
- Sending responses back
|
|
- Handling threads and channels
|
|
"""
|
|
|
|
import asyncio
|
|
from typing import Dict, List, Optional, Any
|
|
|
|
try:
|
|
import discord
|
|
from discord import Message as DiscordMessage, Intents
|
|
from discord.ext import commands
|
|
DISCORD_AVAILABLE = True
|
|
except ImportError:
|
|
DISCORD_AVAILABLE = False
|
|
discord = None
|
|
DiscordMessage = Any
|
|
Intents = Any
|
|
commands = None
|
|
|
|
import sys
|
|
sys.path.insert(0, str(__file__).rsplit("/", 3)[0])
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
from gateway.platforms.base import (
|
|
BasePlatformAdapter,
|
|
MessageEvent,
|
|
MessageType,
|
|
SendResult,
|
|
)
|
|
|
|
|
|
def check_discord_requirements() -> bool:
|
|
"""Check if Discord dependencies are available."""
|
|
return DISCORD_AVAILABLE
|
|
|
|
|
|
class DiscordAdapter(BasePlatformAdapter):
|
|
"""
|
|
Discord bot adapter.
|
|
|
|
Handles:
|
|
- Receiving messages from servers and DMs
|
|
- Sending responses with Discord markdown
|
|
- Thread support
|
|
- Slash commands (future)
|
|
"""
|
|
|
|
# Discord message limits
|
|
MAX_MESSAGE_LENGTH = 2000
|
|
|
|
def __init__(self, config: PlatformConfig):
|
|
super().__init__(config, Platform.DISCORD)
|
|
self._client: Optional[commands.Bot] = None
|
|
self._ready_event = asyncio.Event()
|
|
|
|
async def connect(self) -> bool:
|
|
"""Connect to Discord and start receiving events."""
|
|
if not DISCORD_AVAILABLE:
|
|
print(f"[{self.name}] discord.py not installed. Run: pip install discord.py")
|
|
return False
|
|
|
|
if not self.config.token:
|
|
print(f"[{self.name}] No bot token configured")
|
|
return False
|
|
|
|
try:
|
|
# Set up intents
|
|
intents = Intents.default()
|
|
intents.message_content = True
|
|
intents.dm_messages = True
|
|
intents.guild_messages = True
|
|
|
|
# Create bot
|
|
self._client = commands.Bot(
|
|
command_prefix="!", # Not really used, we handle raw messages
|
|
intents=intents,
|
|
)
|
|
|
|
# Register event handlers
|
|
@self._client.event
|
|
async def on_ready():
|
|
print(f"[{self.name}] Connected as {self._client.user}")
|
|
self._ready_event.set()
|
|
|
|
@self._client.event
|
|
async def on_message(message: DiscordMessage):
|
|
# Ignore bot's own messages
|
|
if message.author == self._client.user:
|
|
return
|
|
await self._handle_message(message)
|
|
|
|
# Start the bot in background
|
|
asyncio.create_task(self._client.start(self.config.token))
|
|
|
|
# Wait for ready
|
|
await asyncio.wait_for(self._ready_event.wait(), timeout=30)
|
|
|
|
self._running = True
|
|
return True
|
|
|
|
except asyncio.TimeoutError:
|
|
print(f"[{self.name}] Timeout waiting for connection")
|
|
return False
|
|
except Exception as e:
|
|
print(f"[{self.name}] Failed to connect: {e}")
|
|
return False
|
|
|
|
async def disconnect(self) -> None:
|
|
"""Disconnect from Discord."""
|
|
if self._client:
|
|
try:
|
|
await self._client.close()
|
|
except Exception as e:
|
|
print(f"[{self.name}] Error during disconnect: {e}")
|
|
|
|
self._running = False
|
|
self._client = None
|
|
self._ready_event.clear()
|
|
print(f"[{self.name}] Disconnected")
|
|
|
|
async def send(
|
|
self,
|
|
chat_id: str,
|
|
content: str,
|
|
reply_to: Optional[str] = None,
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
) -> SendResult:
|
|
"""Send a message to a Discord channel."""
|
|
if not self._client:
|
|
return SendResult(success=False, error="Not connected")
|
|
|
|
try:
|
|
# Get the channel
|
|
channel = self._client.get_channel(int(chat_id))
|
|
if not channel:
|
|
channel = await self._client.fetch_channel(int(chat_id))
|
|
|
|
if not channel:
|
|
return SendResult(success=False, error=f"Channel {chat_id} not found")
|
|
|
|
# Format and split message if needed
|
|
formatted = self.format_message(content)
|
|
chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)
|
|
|
|
message_ids = []
|
|
reference = None
|
|
|
|
if reply_to:
|
|
try:
|
|
ref_msg = await channel.fetch_message(int(reply_to))
|
|
reference = ref_msg
|
|
except Exception:
|
|
pass # Ignore if we can't find the referenced message
|
|
|
|
for i, chunk in enumerate(chunks):
|
|
msg = await channel.send(
|
|
content=chunk,
|
|
reference=reference if i == 0 else None,
|
|
)
|
|
message_ids.append(str(msg.id))
|
|
|
|
return SendResult(
|
|
success=True,
|
|
message_id=message_ids[0] if message_ids else None,
|
|
raw_response={"message_ids": message_ids}
|
|
)
|
|
|
|
except Exception as e:
|
|
return SendResult(success=False, error=str(e))
|
|
|
|
async def send_typing(self, chat_id: str) -> None:
|
|
"""Send typing indicator."""
|
|
if self._client:
|
|
try:
|
|
channel = self._client.get_channel(int(chat_id))
|
|
if channel:
|
|
await channel.typing()
|
|
except Exception:
|
|
pass # Ignore typing indicator failures
|
|
|
|
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
|
"""Get information about a Discord channel."""
|
|
if not self._client:
|
|
return {"name": "Unknown", "type": "dm"}
|
|
|
|
try:
|
|
channel = self._client.get_channel(int(chat_id))
|
|
if not channel:
|
|
channel = await self._client.fetch_channel(int(chat_id))
|
|
|
|
if not channel:
|
|
return {"name": str(chat_id), "type": "dm"}
|
|
|
|
# Determine channel type
|
|
if isinstance(channel, discord.DMChannel):
|
|
chat_type = "dm"
|
|
name = channel.recipient.name if channel.recipient else str(chat_id)
|
|
elif isinstance(channel, discord.Thread):
|
|
chat_type = "thread"
|
|
name = channel.name
|
|
elif isinstance(channel, discord.TextChannel):
|
|
chat_type = "channel"
|
|
name = f"#{channel.name}"
|
|
if channel.guild:
|
|
name = f"{channel.guild.name} / {name}"
|
|
else:
|
|
chat_type = "channel"
|
|
name = getattr(channel, "name", str(chat_id))
|
|
|
|
return {
|
|
"name": name,
|
|
"type": chat_type,
|
|
"guild_id": str(channel.guild.id) if hasattr(channel, "guild") and channel.guild else None,
|
|
"guild_name": channel.guild.name if hasattr(channel, "guild") and channel.guild else None,
|
|
}
|
|
except Exception as e:
|
|
return {"name": str(chat_id), "type": "dm", "error": str(e)}
|
|
|
|
def format_message(self, content: str) -> str:
|
|
"""
|
|
Format message for Discord.
|
|
|
|
Discord uses its own markdown variant.
|
|
"""
|
|
# Discord markdown is fairly standard, no special escaping needed
|
|
return content
|
|
|
|
async def _handle_message(self, message: DiscordMessage) -> None:
|
|
"""Handle incoming Discord messages."""
|
|
# Determine message type
|
|
msg_type = MessageType.TEXT
|
|
if message.content.startswith("/"):
|
|
msg_type = MessageType.COMMAND
|
|
elif message.attachments:
|
|
# Check attachment types
|
|
for att in message.attachments:
|
|
if att.content_type:
|
|
if att.content_type.startswith("image/"):
|
|
msg_type = MessageType.PHOTO
|
|
elif att.content_type.startswith("video/"):
|
|
msg_type = MessageType.VIDEO
|
|
elif att.content_type.startswith("audio/"):
|
|
msg_type = MessageType.AUDIO
|
|
else:
|
|
msg_type = MessageType.DOCUMENT
|
|
break
|
|
|
|
# Determine chat type
|
|
if isinstance(message.channel, discord.DMChannel):
|
|
chat_type = "dm"
|
|
chat_name = message.author.name
|
|
elif isinstance(message.channel, discord.Thread):
|
|
chat_type = "thread"
|
|
chat_name = message.channel.name
|
|
else:
|
|
chat_type = "group" # Treat server channels as groups
|
|
chat_name = getattr(message.channel, "name", str(message.channel.id))
|
|
if hasattr(message.channel, "guild") and message.channel.guild:
|
|
chat_name = f"{message.channel.guild.name} / #{chat_name}"
|
|
|
|
# Get thread ID if in a thread
|
|
thread_id = None
|
|
if isinstance(message.channel, discord.Thread):
|
|
thread_id = str(message.channel.id)
|
|
|
|
# Build source
|
|
source = self.build_source(
|
|
chat_id=str(message.channel.id),
|
|
chat_name=chat_name,
|
|
chat_type=chat_type,
|
|
user_id=str(message.author.id),
|
|
user_name=message.author.display_name,
|
|
thread_id=thread_id,
|
|
)
|
|
|
|
# Build media URLs
|
|
media_urls = [att.url for att in message.attachments]
|
|
media_types = [att.content_type or "unknown" for att in message.attachments]
|
|
|
|
event = MessageEvent(
|
|
text=message.content,
|
|
message_type=msg_type,
|
|
source=source,
|
|
raw_message=message,
|
|
message_id=str(message.id),
|
|
media_urls=media_urls,
|
|
media_types=media_types,
|
|
reply_to_message_id=str(message.reference.message_id) if message.reference else None,
|
|
timestamp=message.created_at,
|
|
)
|
|
|
|
await self.handle_message(event)
|