import logging import re from datetime import datetime from yuxi.channel.message.models import MessageType, PeerInfo, UnifiedMessage from yuxi.channel.routing.models import PeerKind logger = logging.getLogger(__name__) class FarcasterMonitor: def __init__(self, bot_fid: int = 0, bot_fname: str = ""): self._bot_fid = bot_fid self._bot_fname = bot_fname def convert_cast_to_unified( self, cast: dict, *, account_id: str = "default", ) -> UnifiedMessage | None: text = cast.get("text", "") author = cast.get("author", {}) author_fid = author.get("fid") if author_fid is None: logger.warning("Farcaster cast missing author.fid, skipping") return None if author_fid == self._bot_fid: return None clean_text = _strip_bot_mentions(text, self._bot_fname, self._bot_fid) if self._bot_fname else text display_name = author.get("display_name") or author.get("username") or f"fid:{author_fid}" timestamp = None ts_str = cast.get("timestamp", "") if ts_str: try: timestamp = datetime.fromisoformat(ts_str.replace("Z", "+00:00")) except (ValueError, TypeError): pass embeds = cast.get("embeds", []) media_urls = [] for embed in embeds: if isinstance(embed, dict) and "url" in embed: media_urls.append(embed["url"]) return UnifiedMessage( msg_id=_make_msg_id("cast", cast.get("hash", "")), channel_type="farcaster", account_id=account_id, content=clean_text or text, sender=PeerInfo( kind=PeerKind.DIRECT, id=f"farcaster:{author_fid}", display_name=display_name, ), message_type=MessageType.TEXT, media_urls=media_urls if media_urls else None, timestamp=timestamp, reply_to_id=cast.get("parent_hash"), raw_payload=cast, metadata={ "cast_hash": cast.get("hash", ""), "thread_hash": cast.get("thread_hash"), "author_fid": author_fid, "author_fname": author.get("username", ""), "pfp_url": author.get("pfp_url"), "channel": cast.get("channel"), }, ) def convert_reaction_to_unified( self, reaction: dict, *, account_id: str = "default", ) -> UnifiedMessage | None: reaction_type = reaction.get("reaction_type", "") user = reaction.get("user", {}) cast = reaction.get("cast", {}) user_fid = user.get("fid") if user_fid is None: return None if user_fid == self._bot_fid: return None display_name = user.get("display_name") or user.get("username") or f"fid:{user_fid}" cast_hash = cast.get("hash", "") return UnifiedMessage( msg_id=_make_msg_id("reaction", f"{cast_hash}:{user_fid}:{reaction_type}"), channel_type="farcaster", account_id=account_id, content=f"[{reaction_type}] cast/{cast_hash}", sender=PeerInfo( kind=PeerKind.DIRECT, id=f"farcaster:{user_fid}", display_name=display_name, ), message_type=MessageType.EVENT, raw_payload=reaction, timestamp=datetime.now(datetime.UTC), metadata={ "reaction_type": reaction_type, "cast_hash": cast_hash, "cast_text": cast.get("text", ""), "author_fid": user_fid, "author_fname": user.get("username", ""), }, ) def extract_mentions(self, text: str) -> list[str]: return re.findall(r"@(\w+)", text) def strip_mentions(self, text: str) -> str: return re.sub(r"@\w+", "", text).strip() def _strip_bot_mentions(text: str, bot_fname: str, bot_fid: int) -> str: text = re.sub(rf"@?{re.escape(bot_fname)}\b", "", text) text = re.sub(rf"@fid:{bot_fid}", "", text) return text.strip() def _make_msg_id(prefix: str, hash_val: str) -> str: return f"{prefix}:{hash_val}"