diff --git a/plugins/platforms/line/adapter.py b/plugins/platforms/line/adapter.py index 00663702e..130bb2e2c 100644 --- a/plugins/platforms/line/adapter.py +++ b/plugins/platforms/line/adapter.py @@ -133,6 +133,21 @@ MEDIA_TOKEN_TTL_SECONDS = 1800 # 30 minutes; LINE caches the URL aggressively LINE_IMAGE_MAX_BYTES = 10 * 1024 * 1024 # 10 MB per LINE docs LINE_AV_MAX_BYTES = 200 * 1024 * 1024 # 200 MB for voice/video +# Map LINE webhook message types to the normalized MessageType the gateway +# routes on. LINE has no separate "voice" type — audio messages are recorded +# voice clips, so they map to VOICE (which the gateway sends through STT), +# mirroring how Telegram/WhatsApp classify voice notes. Anything unknown +# falls back to TEXT. +_LINE_MESSAGE_TYPES = { + "text": MessageType.TEXT, + "image": MessageType.PHOTO, + "video": MessageType.VIDEO, + "audio": MessageType.VOICE, + "file": MessageType.DOCUMENT, + "location": MessageType.LOCATION, + "sticker": MessageType.STICKER, +} + # A 1×1 transparent PNG used as fallback video preview thumbnail when no # explicit preview is supplied — LINE requires ``previewImageUrl`` for # video messages. Sourced from the Python stdlib (no Pillow dependency). @@ -968,7 +983,7 @@ class LineAdapter(BasePlatformAdapter): event_obj = MessageEvent( text=text, - message_type=MessageType.TEXT if msg_type == "text" else MessageType.IMAGE, + message_type=_LINE_MESSAGE_TYPES.get(msg_type, MessageType.TEXT), source=source_obj, raw_message=event, message_id=message_id, diff --git a/tests/gateway/test_line_plugin.py b/tests/gateway/test_line_plugin.py index 4f42c0f08..9d897ce7a 100644 --- a/tests/gateway/test_line_plugin.py +++ b/tests/gateway/test_line_plugin.py @@ -641,3 +641,36 @@ class TestAdapterInit: assert asyncio.run(ad.get_chat_info("U123"))["type"] == "dm" assert asyncio.run(ad.get_chat_info("C123"))["type"] == "group" assert asyncio.run(ad.get_chat_info("R123"))["type"] == "channel" + + +# --------------------------------------------------------------------------- +# 9. Inbound message-type classification +# --------------------------------------------------------------------------- + +class TestMessageTypeMapping: + """LINE webhook message types must map to the right normalized + MessageType so the gateway routes media correctly (e.g. voice → STT, + files → document handling). Regression guard for the old code that + referenced the non-existent ``MessageType.IMAGE`` and collapsed every + non-text message onto a single type.""" + + def test_image_event_not_attributeerror_regression(self): + # The bug: MessageType.IMAGE doesn't exist on the enum. + MessageType = _line.MessageType + assert not hasattr(MessageType, "IMAGE") + + def test_every_line_type_maps_to_correct_enum(self): + MessageType = _line.MessageType + mapping = _line._LINE_MESSAGE_TYPES + assert mapping["text"] == MessageType.TEXT + assert mapping["image"] == MessageType.PHOTO + assert mapping["video"] == MessageType.VIDEO + # LINE has no separate voice type — audio clips are voice notes. + assert mapping["audio"] == MessageType.VOICE + assert mapping["file"] == MessageType.DOCUMENT + assert mapping["location"] == MessageType.LOCATION + assert mapping["sticker"] == MessageType.STICKER + + def test_unknown_type_falls_back_to_text(self): + MessageType = _line.MessageType + assert _line._LINE_MESSAGE_TYPES.get("flex", MessageType.TEXT) == MessageType.TEXT