feat: enhance message sending to include file attachments in Discord API
This commit is contained in:
@@ -75,7 +75,7 @@ class DiscordChannel(BaseChannel):
|
|||||||
self._http = None
|
self._http = None
|
||||||
|
|
||||||
async def send(self, msg: OutboundMessage) -> None:
|
async def send(self, msg: OutboundMessage) -> None:
|
||||||
"""Send a message through Discord REST API."""
|
"""Send a message through Discord REST API, including file attachments."""
|
||||||
if not self._http:
|
if not self._http:
|
||||||
logger.warning("Discord HTTP client not initialized")
|
logger.warning("Discord HTTP client not initialized")
|
||||||
return
|
return
|
||||||
@@ -84,6 +84,11 @@ class DiscordChannel(BaseChannel):
|
|||||||
headers = {"Authorization": f"Bot {self.config.token}"}
|
headers = {"Authorization": f"Bot {self.config.token}"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Send file attachments first
|
||||||
|
for media_path in msg.media or []:
|
||||||
|
await self._send_file(url, headers, media_path, reply_to=msg.reply_to)
|
||||||
|
|
||||||
|
# Send text content
|
||||||
chunks = split_message(msg.content or "", MAX_MESSAGE_LEN)
|
chunks = split_message(msg.content or "", MAX_MESSAGE_LEN)
|
||||||
if not chunks:
|
if not chunks:
|
||||||
return
|
return
|
||||||
@@ -91,8 +96,8 @@ class DiscordChannel(BaseChannel):
|
|||||||
for i, chunk in enumerate(chunks):
|
for i, chunk in enumerate(chunks):
|
||||||
payload: dict[str, Any] = {"content": chunk}
|
payload: dict[str, Any] = {"content": chunk}
|
||||||
|
|
||||||
# Only set reply reference on the first chunk
|
# Only set reply reference on the first chunk (if no media was sent)
|
||||||
if i == 0 and msg.reply_to:
|
if i == 0 and msg.reply_to and not msg.media:
|
||||||
payload["message_reference"] = {"message_id": msg.reply_to}
|
payload["message_reference"] = {"message_id": msg.reply_to}
|
||||||
payload["allowed_mentions"] = {"replied_user": False}
|
payload["allowed_mentions"] = {"replied_user": False}
|
||||||
|
|
||||||
@@ -123,6 +128,54 @@ class DiscordChannel(BaseChannel):
|
|||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _send_file(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
headers: dict[str, str],
|
||||||
|
file_path: str,
|
||||||
|
reply_to: str | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Send a file attachment via Discord REST API using multipart/form-data."""
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.is_file():
|
||||||
|
logger.warning("Discord file not found, skipping: {}", file_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if path.stat().st_size > MAX_ATTACHMENT_BYTES:
|
||||||
|
logger.warning("Discord file too large (>20MB), skipping: {}", path.name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload_json: dict[str, Any] = {}
|
||||||
|
if reply_to:
|
||||||
|
payload_json["message_reference"] = {"message_id": reply_to}
|
||||||
|
payload_json["allowed_mentions"] = {"replied_user": False}
|
||||||
|
|
||||||
|
for attempt in range(3):
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
files = {"files[0]": (path.name, f, "application/octet-stream")}
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
if payload_json:
|
||||||
|
data["payload_json"] = json.dumps(payload_json)
|
||||||
|
response = await self._http.post(
|
||||||
|
url, headers=headers, files=files, data=data
|
||||||
|
)
|
||||||
|
if response.status_code == 429:
|
||||||
|
resp_data = response.json()
|
||||||
|
retry_after = float(resp_data.get("retry_after", 1.0))
|
||||||
|
logger.warning("Discord rate limited, retrying in {}s", retry_after)
|
||||||
|
await asyncio.sleep(retry_after)
|
||||||
|
continue
|
||||||
|
response.raise_for_status()
|
||||||
|
logger.info("Discord file sent: {}", path.name)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == 2:
|
||||||
|
logger.error("Error sending Discord file {}: {}", path.name, e)
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
async def _gateway_loop(self) -> None:
|
async def _gateway_loop(self) -> None:
|
||||||
"""Main gateway loop: identify, heartbeat, dispatch events."""
|
"""Main gateway loop: identify, heartbeat, dispatch events."""
|
||||||
if not self._ws:
|
if not self._ws:
|
||||||
|
|||||||
Reference in New Issue
Block a user