fix(discord): preflight send_voice uploads too; move the size gate into adapter_media
send_voice built its own discord.File from the audio bytes and never ran the size preflight, so an oversized audio attachment still burned the doomed 413 round-trip that #50846 is about. Route it through the same _reject_oversized_upload helper as _send_file_attachment. The limit constant and _discord_upload_limit_bytes lived on the adapter facade while every consumer is in adapter_media.py; the facade+sibling layout puts topic code in the sibling, so they move there.
This commit is contained in:
@@ -170,13 +170,6 @@ _DISCORD_SELECT_MAX_ROWS = 5
|
||||
# Model-select capacity: keep 2 rows for Back/Cancel, fill the rest with selects.
|
||||
_DISCORD_MODEL_SELECT_CAPACITY = (_DISCORD_SELECT_MAX_ROWS - 2) * _DISCORD_SELECT_MAX_OPTIONS
|
||||
_DISCORD_BUTTON_LABEL_LIMIT = 80
|
||||
# Default Discord attachment cap for DMs / channels without a guild boost
|
||||
# context. 20 MiB since the Sep 3 2026 API change (10 MiB before); guild
|
||||
# channels expose a boost-raised limit via ``guild.filesize_limit``, but
|
||||
# discord.py's fallback constant can lag the platform default, so the
|
||||
# effective limit is never taken below this floor. See issue #50846 and
|
||||
# https://docs.discord.com/developers/change-log (Sep 3, 2026).
|
||||
_DISCORD_DEFAULT_UPLOAD_LIMIT_BYTES = 20 * 1024 * 1024
|
||||
_DISCORD_ELLIPSIS = "\u2026"
|
||||
_DISCORD_NONCONVERSATIONAL_METADATA_KEYS = frozenset({
|
||||
"non_conversational", "non_conversational_history",
|
||||
@@ -3164,24 +3157,6 @@ class DiscordAdapter(DiscordMediaMixin, BasePlatformAdapter):
|
||||
success=True, message_id=last_id, continuation_message_ids=tuple(continuation_ids),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _discord_upload_limit_bytes(channel: Any) -> int:
|
||||
"""Return the effective Discord attachment size limit for *channel*.
|
||||
|
||||
Prefer the guild's boost-aware ``filesize_limit`` when present; fall
|
||||
back to the platform default for DMs / group DMs without a guild.
|
||||
The guild value is floored at the platform default: discord.py's
|
||||
unboosted-tier constant can lag a platform-wide raise (10 MiB in
|
||||
2.7.1 vs the 20 MiB default since Sep 3 2026), and under-reporting
|
||||
makes the preflight reject files Discord would accept.
|
||||
"""
|
||||
guild = getattr(channel, "guild", None)
|
||||
if guild is not None:
|
||||
limit = getattr(guild, "filesize_limit", None)
|
||||
if isinstance(limit, int) and limit > 0:
|
||||
return max(limit, _DISCORD_DEFAULT_UPLOAD_LIMIT_BYTES)
|
||||
return _DISCORD_DEFAULT_UPLOAD_LIMIT_BYTES
|
||||
|
||||
async def play_tts(self, chat_id: str, audio_path: str, **kwargs) -> SendResult:
|
||||
"""Play auto-TTS audio: in the guild's VC if joined, else as a file attachment."""
|
||||
for gid, text_ch_id in self._voice_text_channels.items():
|
||||
|
||||
@@ -11,8 +11,60 @@ from gateway.platforms.base import SendResult
|
||||
|
||||
logger = logging.getLogger("plugins.platforms.discord.adapter")
|
||||
|
||||
# Default Discord attachment cap for DMs / channels without a guild boost
|
||||
# context. 20 MiB since the Sep 3 2026 API change (10 MiB before); guild
|
||||
# channels expose a boost-raised limit via ``guild.filesize_limit``, but
|
||||
# discord.py's fallback constant can lag the platform default, so the
|
||||
# effective limit is never taken below this floor. See issue #50846 and
|
||||
# https://docs.discord.com/developers/change-log (Sep 3, 2026).
|
||||
_DISCORD_DEFAULT_UPLOAD_LIMIT_BYTES = 20 * 1024 * 1024
|
||||
|
||||
|
||||
class DiscordMediaMixin:
|
||||
@staticmethod
|
||||
def _discord_upload_limit_bytes(channel: Any) -> int:
|
||||
"""Return the effective Discord attachment size limit for *channel*.
|
||||
|
||||
Prefer the guild's boost-aware ``filesize_limit`` when present; fall
|
||||
back to the platform default for DMs / group DMs without a guild.
|
||||
The guild value is floored at the platform default: discord.py's
|
||||
unboosted-tier constant can lag a platform-wide raise (10 MiB in
|
||||
2.7.1 vs the 20 MiB default since Sep 3 2026), and under-reporting
|
||||
makes the preflight reject files Discord would accept.
|
||||
"""
|
||||
guild = getattr(channel, "guild", None)
|
||||
if guild is not None:
|
||||
limit = getattr(guild, "filesize_limit", None)
|
||||
if isinstance(limit, int) and limit > 0:
|
||||
return max(limit, _DISCORD_DEFAULT_UPLOAD_LIMIT_BYTES)
|
||||
return _DISCORD_DEFAULT_UPLOAD_LIMIT_BYTES
|
||||
|
||||
async def _reject_oversized_upload(self, channel: Any, file_path: str, filename: str) -> Optional[SendResult]:
|
||||
"""Preflight ``file_path`` against the channel's upload cap (#50846): a doomed
|
||||
``413`` round-trip is skipped and the user gets a notice naming the size and the
|
||||
limit. Returns the failed result, or ``None`` when the file may be uploaded."""
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
except OSError as exc:
|
||||
return SendResult(success=False, error=f"Cannot stat file {filename}: {exc}")
|
||||
limit = self._discord_upload_limit_bytes(channel)
|
||||
if file_size <= limit:
|
||||
return None
|
||||
size_mb = file_size / (1024 * 1024)
|
||||
limit_mb = limit / (1024 * 1024)
|
||||
error = f"File too large for Discord upload: {filename} is {size_mb:.1f} MB (limit {limit_mb:.0f} MB)"
|
||||
logger.warning("[%s] %s", self.name, error)
|
||||
notice = (
|
||||
f"⚠️ Could not attach `{filename}` — {size_mb:.1f} MB exceeds Discord's "
|
||||
f"{limit_mb:.0f} MB upload limit for this channel. Compress the file or share a link instead."
|
||||
)
|
||||
try:
|
||||
if not self._is_forum_parent(channel):
|
||||
await channel.send(content=notice)
|
||||
except Exception:
|
||||
logger.debug("[%s] Failed to send oversized-file notice for %s", self.name, filename, exc_info=True)
|
||||
return SendResult(success=False, error=error)
|
||||
|
||||
async def _send_file_attachment(
|
||||
self, chat_id: str, file_path: str, caption: Optional[str] = None,
|
||||
file_name: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None,
|
||||
@@ -33,35 +85,9 @@ class DiscordMediaMixin:
|
||||
if not channel:
|
||||
return SendResult(success=False, error=f"Channel {chat_id} not found")
|
||||
filename = file_name or os.path.basename(file_path)
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
except OSError as exc:
|
||||
return SendResult(success=False, error=f"Cannot stat file {filename}: {exc}")
|
||||
# Reject oversized files before upload (#50846): no doomed 413 round-trip,
|
||||
# and the user gets an explicit notice instead of a silent failure.
|
||||
limit = self._discord_upload_limit_bytes(channel)
|
||||
if file_size > limit:
|
||||
size_mb = file_size / (1024 * 1024)
|
||||
limit_mb = limit / (1024 * 1024)
|
||||
error = (
|
||||
f"File too large for Discord upload: {filename} is "
|
||||
f"{size_mb:.1f} MB (limit {limit_mb:.0f} MB)"
|
||||
)
|
||||
logger.warning("[%s] %s", self.name, error)
|
||||
notice = (
|
||||
f"⚠️ Could not attach `{filename}` — {size_mb:.1f} MB exceeds "
|
||||
f"Discord's {limit_mb:.0f} MB upload limit for this channel. "
|
||||
f"Compress the file or share a link instead."
|
||||
)
|
||||
try:
|
||||
if not self._is_forum_parent(channel):
|
||||
await channel.send(content=notice)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"[%s] Failed to send oversized-file notice for %s",
|
||||
self.name, filename, exc_info=True,
|
||||
)
|
||||
return SendResult(success=False, error=error)
|
||||
rejected = await self._reject_oversized_upload(channel, file_path, filename)
|
||||
if rejected is not None:
|
||||
return rejected
|
||||
logger.info(
|
||||
"[%s] Sending file attachment %s (%s) to %s", self.name, filename,
|
||||
os.path.splitext(filename)[1].lower() or "no-ext", chat_id,
|
||||
@@ -246,6 +272,9 @@ class DiscordMediaMixin:
|
||||
if not os.path.exists(audio_path):
|
||||
return SendResult(success=False, error=f"Audio file not found: {audio_path}")
|
||||
filename = os.path.basename(audio_path)
|
||||
rejected = await self._reject_oversized_upload(channel, audio_path, filename)
|
||||
if rejected is not None:
|
||||
return rejected
|
||||
reference = self._reply_reference_for_send(reply_to, channel)
|
||||
with open(audio_path, "rb") as f:
|
||||
file_data = f.read()
|
||||
|
||||
Reference in New Issue
Block a user