Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gateway/platforms/weixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,10 +1109,10 @@ async def send_document(
) -> SendResult:
return await self._send_file_result(chat_id, file_path, caption or "", "send_document")

async def send_video(self, chat_id: str, video_path: str, caption: Optional[str] = None, reply_to=None, metadata=None) -> SendResult:
async def send_video(self, chat_id: str, video_path: str, caption: Optional[str] = None, reply_to=None, metadata=None, **kwargs) -> SendResult:
return await self._send_file_result(chat_id, video_path, caption or "", "send_video")

async def send_voice(self, chat_id: str, audio_path: str, caption: Optional[str] = None, reply_to=None, metadata=None) -> SendResult:
async def send_voice(self, chat_id: str, audio_path: str, caption: Optional[str] = None, reply_to=None, metadata=None, **kwargs) -> SendResult:
# Native outbound voice bubbles are not proven-working upstream; a file attachment at least plays (even .silk).
return await self._send_file_result(chat_id, audio_path, caption or "[voice message as attachment]", "send_voice", force_file_attachment=True)

Expand Down
4 changes: 2 additions & 2 deletions plugins/platforms/line/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,8 +818,8 @@ async def send_image_file(
return await self._send_messages(chat_id, msgs + ([_text_message(caption)] if caption else []))

async def send_voice(
self, chat_id: str, audio_path: str, duration_ms: int = 1000, metadata: Optional[Dict[str, Any]] = None
) -> SendResult:
self, chat_id: str, audio_path: str, duration_ms: int = 1000, metadata: Optional[Dict[str, Any]] = None,
**kwargs) -> SendResult:
path, err = self._check_media_file("audio", audio_path)
if err:
return err
Expand Down
6 changes: 4 additions & 2 deletions plugins/platforms/matrix/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1595,9 +1595,11 @@ async def send_document(

async def send_voice(
self, chat_id: str, audio_path: str, caption: Optional[str] = None, reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None) -> SendResult:
metadata: Optional[Dict[str, Any]] = None, **kwargs) -> SendResult:
"""Upload audio as an MSC3245 voice message. Voice bubbles need Ogg/Opus but callers pass any
format (e.g. TTS output), so transcode here — best-effort: without ffmpeg the original is sent."""
format (e.g. TTS output), so transcode here — best-effort: without ffmpeg the original is sent.
The shared media dispatcher passes ``is_voice``; accept-and-ignore it here (routing is decided
by the caller), matching the base adapter's ``**kwargs`` tolerance."""
converted_path: Optional[str] = None
if not str(audio_path).lower().endswith((".ogg", ".oga", ".opus")):
# 48k (not the 32k default): Element renders voice bubbles at a higher quality tier.
Expand Down
2 changes: 1 addition & 1 deletion plugins/platforms/mattermost/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ async def send_document(
return await self._send_local_file(chat_id, file_path, caption, reply_to, file_name, metadata)

async def send_voice(self, chat_id: str, audio_path: str, caption: Optional[str] = None,
reply_to: Optional[str] = None, metadata: _Metadata = None) -> SendResult:
reply_to: Optional[str] = None, metadata: _Metadata = None, **kwargs) -> SendResult:
return await self._send_local_file(chat_id, audio_path, caption, reply_to, metadata=metadata)

async def send_video(self, chat_id: str, video_path: str, caption: Optional[str] = None,
Expand Down
47 changes: 47 additions & 0 deletions tests/gateway/test_media_send_voice_kwargs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""The shared media dispatcher passes ``is_voice`` to every adapter's ``send_voice``.

Adapter overrides that narrow the base signature without ``**kwargs`` raise
TypeError before any upload happens, so MEDIA: audio replies are silently
dropped on those platforms (run_turn.py wraps its call site in
``suppress(Exception)``). The invariant: dispatching media with
``is_voice=True`` reaches the adapter's send path regardless of how the
override narrows its signature.
"""
import inspect

import pytest

from gateway.platforms.base import BasePlatformAdapter

# Adapters whose send_voice override was missing **kwargs (regression guards).
_VOICE_OVERRIDES = [
("plugins.platforms.line.adapter", "LineAdapter"),
("plugins.platforms.matrix.adapter", "MatrixAdapter"),
("plugins.platforms.mattermost.adapter", "MattermostAdapter"),
]


def _adapter_class(module_name: str, class_name: str):
module = __import__(module_name, fromlist=[class_name])
return getattr(module, class_name)


@pytest.mark.parametrize("module_name,class_name", _VOICE_OVERRIDES)
def test_send_voice_override_accepts_is_voice_kwarg(module_name: str, class_name: str):
"""send_voice overrides must tolerate the dispatcher's is_voice kwarg."""
cls = _adapter_class(module_name, class_name)
sig = inspect.signature(cls.send_voice)
assert sig.parameters.get("kwargs").kind == inspect.Parameter.VAR_KEYWORD, (
f"{class_name}.send_voice does not accept **kwargs; the shared media "
"dispatcher passes is_voice= and the call would raise TypeError before upload"
)


def test_dispatch_send_voice_with_is_voice_reaches_base_sender():
"""The base adapter's send_voice/_send_media_file path tolerates is_voice end-to-end.

Asserts the dispatcher's exact call shape binds against the BASE class
signature — the contract every override must preserve.
"""
sig = inspect.signature(BasePlatformAdapter.send_voice)
sig.bind(None, "chat", "/tmp/a.wav", caption=None, reply_to=None, metadata=None, is_voice=True)