"""Microsoft Teams invoke Activity 路由分发。 处理 adaptiveCard/action invoke 的投票、确认、输入等交互, 以及 messageReaction 事件的路由。 """ from __future__ import annotations from typing import Any from yuxi.channels.models import ChannelIdentity, ChannelMessage, ChannelType, EventType, MessageType def normalize_reaction(activity: dict[str, Any]) -> ChannelMessage: """标准化 messageReaction Activity → ChannelMessage。 处理 reactionsAdded 和 reactionsRemoved 两种方向。 """ from_info = activity.get("from", {}) or {} conversation = activity.get("conversation", {}) or {} channel_data = activity.get("channelData") or {} aad_object_id = from_info.get("aadObjectId", "") or from_info.get("id", "") conversation_id = conversation.get("id", "") added = activity.get("reactionsAdded", []) removed = activity.get("reactionsRemoved", []) reactions_data = { "added": [{"type": r.get("type", "")} for r in added], "removed": [{"type": r.get("type", "")} for r in removed], } direction = "added" if added else "removed" if removed else "unknown" identity = ChannelIdentity( channel_id="msteams", channel_type=ChannelType.MS_TEAMS, channel_user_id=aad_object_id, channel_chat_id=conversation_id, channel_message_id=activity.get("id"), ) return ChannelMessage( identity=identity, event_type=EventType.SYSTEM_EVENT, message_type=MessageType.TEXT, chat_type="direct", content=f"reaction_{direction}", reply_to_message_id=activity.get("replyToId"), metadata={ "reaction_direction": direction, "reactions": reactions_data, "raw_activity": activity, "tenant_id": (channel_data.get("tenant") or {}).get("id", ""), "from_name": from_info.get("name", ""), }, ) def normalize_card_action(activity: dict[str, Any]) -> ChannelMessage: """标准化 adaptiveCard/action invoke → ChannelMessage。 提取 action 类型和提交数据,支持投票/确认/输入/通用 Submit。 """ from_info = activity.get("from", {}) or {} conversation = activity.get("conversation", {}) or {} channel_data = activity.get("channelData") or {} aad_object_id = from_info.get("aadObjectId", "") or from_info.get("id", "") conversation_id = conversation.get("id", "") value = activity.get("value", {}) or {} invoke_name = activity.get("name", "") action_type = _classify_invoke_action(invoke_name, value) poll_id = _extract_poll_id(value) if action_type == "vote" else "" identity = ChannelIdentity( channel_id="msteams", channel_type=ChannelType.MS_TEAMS, channel_user_id=aad_object_id, channel_chat_id=conversation_id, channel_message_id=activity.get("id"), ) return ChannelMessage( identity=identity, event_type=EventType.CARD_ACTION, message_type=MessageType.TEXT, chat_type="direct", content=_format_action_content(action_type, value), metadata={ "invoke_name": invoke_name, "action_type": action_type, "invoke_value": value, "poll_id": poll_id, "tenant_id": (channel_data.get("tenant") or {}).get("id", ""), "from_name": from_info.get("name", ""), "raw_activity": activity, }, ) def _classify_invoke_action(invoke_name: str, value: dict[str, Any]) -> str: if invoke_name == "adaptiveCard/action": data = value.get("data", {}) or {} if isinstance(data, dict): if "voteOption" in value: return "vote" if data.get("action") == "confirm": return "confirm" if data.get("action") == "cancel": return "cancel" if data.get("action") == "submit": return "input" if "action" in data: return data["action"] return "card_action" if invoke_name == "message/submitAction": return "feedback" if invoke_name == "fileConsent/invoke": return "file_consent" return invoke_name def _extract_poll_id(value: dict[str, Any]) -> str: """7 路径 pollId 提取:覆盖 Teams 不同版本文档差异。""" data = value.get("data", {}) or {} sources = [ lambda: data.get("poll_id"), lambda: value.get("poll_id"), lambda: value.get("pollId"), lambda: data.get("pollId"), lambda: (data.get("data", {}) or {}).get("poll_id") if isinstance(data.get("data"), dict) else None, lambda: str(data.get("poll_id", "")) or str(value.get("poll_id", "")), lambda: ( (value.get("action", {}) or {}).get("data", {}).get("poll_id") if isinstance(value.get("action"), dict) else None ), ] for source in sources: try: result = source() if result: return str(result) except Exception: continue return value.get("poll_id", "") or str(data.get("poll_id", "")) def _format_action_content(action_type: str, value: dict[str, Any]) -> str: if action_type == "vote": return str(value.get("voteOption", "")) if action_type == "feedback": fb_value = value.get("feedbackValue", "") return str(fb_value) if action_type in ("confirm", "cancel", "input"): data = value.get("data", {}) or {} return str(data) return str(value) def is_signin_invoke(activity: dict[str, Any]) -> bool: name = activity.get("name", "") return name in ("signin/tokenExchange", "signin/verifyState")