from __future__ import annotations import logging from datetime import datetime, timezone from yuxi.channel.extensions.googlechat.formatting import sanitize_text from yuxi.channel.extensions.googlechat.media import build_attachment_placeholder from yuxi.channel.extensions.googlechat.types import GoogleChatEvent, ResolvedGoogleChatAccount from yuxi.channel.message.models import ( GroupContext, MessageType, PeerInfo, UnifiedMessage, ) from yuxi.channel.routing.models import PeerKind logger = logging.getLogger(__name__) class GoogleChatMonitor: def __init__(self): self._last_event_time: float = 0 def event_to_unified_message( self, raw_payload: dict, event: GoogleChatEvent, text: str | None = None, account: ResolvedGoogleChatAccount | None = None, ) -> UnifiedMessage | None: if not event.message: return None msg = event.message sender = msg.sender space_name = event.effective_space_name space_type = event.space_type safe_text = text if text is not None else sanitize_text(msg.argument_text or msg.text) if not safe_text: return None is_dm = space_type == "DM" or event.effective_space.single_user_bot_dm if event.effective_space else False is_group = space_type in ("GROUP_CHAT", "SPACE") and not is_dm sender_kind = PeerKind.DIRECT if is_dm else PeerKind.GROUP sender_info = PeerInfo( kind=sender_kind, id=sender.name or "unknown", display_name=sender.display_name, username=sender.email, is_bot=sender.type == "BOT", ) group = None if is_group: group = GroupContext( id=space_name, name=event.effective_space.display_name if event.effective_space else None, kind="group", ) thread_id = None if msg.thread: thread_id = msg.thread.get("name") timestamp = None if event.event_time: try: from datetime import datetime as dt timestamp = dt.fromisoformat(event.event_time.replace("Z", "+00:00")) except (ValueError, TypeError): pass media_urls = [] media_types = [] for att in msg.attachment: dr = att.get("driveDataRef") or att.get("attachmentDataRef") if dr: media_urls.append(dr.get("resourceName", "")) media_types.append(att.get("contentType", "")) if msg.attachment: placeholder = build_attachment_placeholder(msg.attachment) if placeholder: safe_text = f"{placeholder}\n{safe_text}" was_mentioned = _detect_mention(msg.annotations) slash_cmd = _detect_slash_command(msg.annotations) metadata = { "ChatType": "direct" if is_dm else "group", "WasMentioned": was_mentioned, "CommandAuthorized": True, "space_type": space_type, "space_name": space_name, } if slash_cmd: metadata["SlashCommand"] = slash_cmd if is_group and event.effective_space: metadata["GroupSpace"] = event.effective_space.display_name return UnifiedMessage( msg_id=msg.name or f"gc:{int(datetime.now(tz=timezone.utc).timestamp())}", channel_type="googlechat", account_id=account.account_id if account else "default", content=safe_text, sender=sender_info, message_type=MessageType.TEXT, media_urls=media_urls, media_types=media_types, group=group, timestamp=timestamp, raw_payload=raw_payload, message_thread_id=thread_id, metadata=metadata, was_mentioned=was_mentioned, ) async def inject_thread_context( self, account: ResolvedGoogleChatAccount, space_name: str, thread_name: str, ) -> str | None: try: from yuxi.channel.extensions.googlechat.api import list_thread_messages from yuxi.channel.extensions.googlechat.formatting import format_thread_context messages = await list_thread_messages(account, space_name, thread_name, page_size=20) if not messages: return None history = [] for msg in messages: sender = msg.get("sender", {}) history.append({ "author": sender.get("displayName", sender.get("name", "Unknown")), "content": msg.get("text", msg.get("argumentText", "")), }) return format_thread_context(history) except Exception: logger.debug("Failed to fetch thread history for %s in %s", thread_name, space_name) return None def _detect_mention(annotations: list[dict]) -> bool: if not annotations: return False for ann in annotations: if ann.get("type") == "USER_MENTION": um = ann.get("userMention", {}) mention_user = um.get("user", {}) if mention_user.get("name") == "users/app": return True return False def _detect_slash_command(annotations: list[dict]) -> str | None: if not annotations: return None for ann in annotations: if ann.get("type") == "SLASH_COMMAND": sc = ann.get("slashCommand", {}) return sc.get("commandId") return None