Files
AstrBot/tests/test_discord_adapter.py
Weilong Liao 7c366a708b fix: unify media reference handling (#8764)
* fix: unify media reference handling

* fix: accept bare base64 record media refs

* chore: update agents.md

* fix: unify file URI handling across media components and utilities

* fix: unify media reference type handling with MediaRefStr alias

* Potential fix for pull request finding 'CodeQL / Incomplete URL substring sanitization'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Update astrbot/core/platform/sources/discord/discord_platform_adapter.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: unify media handling and improve base64 decoding across components

* fix: simplify client_kwargs type definition and enhance media message handling in platform adapter

* fix: unify media utility documentation and enhance function descriptions

* perf: drop "pilk" requirement, improve audio outbound for tencent-related IM apps which using silk

* fix: unify Tencent Silk audio handling and enhance media resolver functionality

---

- Centralize media reference materialization and base64 resolution for local paths, http(s), base64://, data URIs, and legacy bare base64 payloads.
- Normalize incoming Record audio to wav and Image media to temporary jpg during preprocess, with event-scoped cleanup.
- Reuse the shared media resolver across OpenAI, Gemini, Anthropic, MiMo, DeerFlow, STT, and platform media paths while sanitizing logs and cleaning temporary conversion outputs.
- Ensure generated TTS audio is tracked for cleanup after the event finishes.

fix #8676
fix #8543
fix #7588
fix #7580
fix #8030
fix #8034
fix #7461
fix #7565
fix #6509
fix #7144
fix #7795



---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-06-14 10:37:16 +08:00

133 lines
4.0 KiB
Python

import base64
from io import BytesIO
from types import SimpleNamespace
import pytest
from astrbot.api.message_components import Image, Record
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.platform.sources.discord import (
discord_platform_adapter,
discord_platform_event,
)
from astrbot.core.platform.sources.discord.discord_platform_adapter import (
DiscordPlatformAdapter,
)
from astrbot.core.platform.sources.discord.discord_platform_event import (
DiscordPlatformEvent,
)
_PNG_BYTES = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
)
_WAV_BYTES = b"RIFF\x24\x00\x00\x00WAVEfmt " + b"\x00" * 16
_WAV_PATH = "/tmp/discord_voice.wav"
@pytest.mark.asyncio
async def test_discord_audio_attachment_resolves_to_wav_record(monkeypatch):
class FakeMediaResolver:
def __init__(self, media_ref: str, **kwargs) -> None:
assert media_ref == "https://cdn.example/voice.ogg"
assert kwargs["media_type"] == "audio"
async def to_path(self, **kwargs) -> str:
assert kwargs["target_format"] == "wav"
return _WAV_PATH
monkeypatch.setattr(
discord_platform_adapter,
"MediaResolver",
FakeMediaResolver,
)
adapter = DiscordPlatformAdapter.__new__(DiscordPlatformAdapter)
adapter.bot_self_id = "1"
adapter.client = SimpleNamespace(user=SimpleNamespace(id=1))
message = SimpleNamespace(
id=42,
content="",
channel=SimpleNamespace(id=123, guild=None),
author=SimpleNamespace(id=2, display_name="tester"),
attachments=[
SimpleNamespace(
content_type="audio/ogg",
filename="voice.ogg",
url="https://cdn.example/voice.ogg",
)
],
guild=None,
role_mentions=[],
)
abm = await adapter.convert_message({"message": message})
assert len(abm.message) == 1
assert isinstance(abm.message[0], Record)
assert abm.message[0].file == _WAV_PATH
assert abm.message[0].url == _WAV_PATH
assert abm.message[0].path == _WAV_PATH
@pytest.mark.asyncio
async def test_discord_send_image_resolves_data_uri_with_media_resolver(monkeypatch):
captured = {}
class FakeDiscordFile:
def __init__(self, fp: BytesIO, filename: str) -> None:
captured["bytes"] = fp.read()
captured["filename"] = filename
monkeypatch.setattr(discord_platform_event.discord, "File", FakeDiscordFile)
event = DiscordPlatformEvent.__new__(DiscordPlatformEvent)
image_base64 = base64.b64encode(_PNG_BYTES).decode("ascii")
content, files, view, embeds, reference_message_id = await event._parse_to_discord(
MessageChain(
chain=[
Image(file=f"data:image/png;base64,{image_base64}"),
]
)
)
assert content == ""
assert len(files) == 1
assert captured["bytes"] == _PNG_BYTES
assert captured["filename"] == "image.png"
assert view is None
assert embeds == []
assert reference_message_id is None
@pytest.mark.asyncio
async def test_discord_send_record_resolves_audio_with_media_resolver(monkeypatch):
captured = {}
class FakeDiscordFile:
def __init__(self, fp: BytesIO, filename: str) -> None:
captured["bytes"] = fp.read()
captured["filename"] = filename
monkeypatch.setattr(discord_platform_event.discord, "File", FakeDiscordFile)
event = DiscordPlatformEvent.__new__(DiscordPlatformEvent)
audio_base64 = base64.b64encode(_WAV_BYTES).decode("ascii")
content, files, view, embeds, reference_message_id = await event._parse_to_discord(
MessageChain(
chain=[
Record.fromBase64(audio_base64),
]
)
)
assert content == ""
assert len(files) == 1
assert captured["bytes"] == _WAV_BYTES
assert captured["filename"] == "audio.wav"
assert view is None
assert embeds == []
assert reference_message_id is None