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
30 changes: 23 additions & 7 deletions lark_oapi/channel/normalize/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,19 @@ def _safe_json(raw: Any) -> Dict[str, Any]:
def _flatten_post_text(post: Dict[str, Any]) -> Tuple[str, str]:
"""Return (title, plain_text) from a post AST.

Post content has locale keys (`zh_cn`, `en_us`). We pick the first locale.
Post content may be locale-keyed (``{"zh_cn": {...}, "en_us": {...}}``)
or carry ``content`` directly (the classic single-locale post shape).
For locale-keyed payloads the first locale is used.
"""
if not isinstance(post, dict):
return "", ""
first_key = next(iter(post), None)
if first_key is None:
return "", ""
locale_doc = post.get(first_key)
if "content" in post:
locale_doc = post
else:
first_key = next(iter(post), None)
if first_key is None:
return "", ""
locale_doc = post.get(first_key)
if not isinstance(locale_doc, dict):
return "", ""
title = locale_doc.get("title") or ""
Expand All @@ -82,8 +87,19 @@ def _flatten_post_text(post: Dict[str, Any]) -> Tuple[str, str]:
elif tag == "a":
chunk.append(el.get("text") or el.get("href") or "")
elif tag == "at":
nm = el.get("user_name") or el.get("user_id") or ""
chunk.append(f"@{nm}" if nm else "@")
el_id = el.get("id")
is_all = el.get("user_id") == "all" or (
isinstance(el_id, dict) and el_id.get("user_id") == "all"
)
if is_all:
# Mention-all node: render the ``@_all`` placeholder so the
# pipeline's mention-all probes (``text_has_mention_all`` /
# ``resolve_mentions``) fire for rich-text @all messages.
# See https://github.com/larksuite/oapi-sdk-python/issues/138
chunk.append("@_all")
else:
nm = el.get("user_name") or el.get("user_id") or ""
chunk.append(f"@{nm}" if nm else "@")
elif tag == "emotion":
chunk.append(f":{el.get('emoji_type') or ''}:")
elif tag == "img":
Expand Down
83 changes: 83 additions & 0 deletions lark_oapi/channel/tests/test_post_mention_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""mentioned_all for rich-text post @all messages (issue #138).

A post that @-mentions everyone carries an ``at`` AST node with
``user_id == "all"`` and Feishu does NOT populate ``mentions[]`` for @all —
so the only signal is the rendered text. The flatten renderer must emit the
``@_all`` placeholder so the pipeline's probes (``text_has_mention_all`` /
``resolve_mentions``) fire.
"""

import json

import pytest

from lark_oapi.channel.normalize.pipeline import InboundPipeline, PipelineConfig, PipelineDeps


def _msg(msg_type="post", content=None):
return {
"message_id": "om_1",
"create_time": 1000,
"chat_id": "oc_1",
"chat_type": "group",
"message_type": msg_type,
"content": json.dumps(content or {"text": "hi"}, ensure_ascii=False),
"mentions": [],
}


def _sender(open_id="ou_sender"):
return {"sender_id": {"open_id": open_id, "user_id": "u1"}, "sender_type": "user"}


@pytest.mark.asyncio
async def test_post_mention_all_sets_mentioned_all():
# Covers both wire shapes: the classic single-locale ``{"content": ...}``
# post and locale-keyed posts (``{"zh_cn": {...}}``).
for post_content in (
{
"content": [
[
{"tag": "at", "user_id": "all", "user_name": "Everyone"},
{"tag": "text", "text": " heads up everyone"},
]
]
},
{
"zh_cn": {
"content": [
[
{"tag": "at", "user_id": "all", "user_name": "所有人"},
{"tag": "text", "text": " 全体注意"},
]
]
}
},
):
p = InboundPipeline(PipelineConfig(), PipelineDeps())
inbound = await p.process(
event_id="e", message_event=_msg(content=post_content), sender=_sender()
)
assert inbound is not None
assert inbound.mentioned_all is True
# the @_all placeholder resolves to the human-visible form
assert "@all" in inbound.content.text


@pytest.mark.asyncio
async def test_post_regular_at_does_not_set_mentioned_all():
post_content = {
"content": [
[
{"tag": "at", "user_id": "ou_user", "user_name": "Alice"},
{"tag": "text", "text": " hi"},
]
]
}
p = InboundPipeline(PipelineConfig(), PipelineDeps())
inbound = await p.process(
event_id="e", message_event=_msg(content=post_content), sender=_sender()
)
assert inbound is not None
assert inbound.mentioned_all is False
assert "@Alice" in inbound.content.text