diff --git a/telegram/bot.py b/telegram/bot.py index 3ec6aca..c865884 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -484,6 +484,24 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE): # Send typing indicator await msg.chat.send_action("typing") + # Fetch any X/Twitter links in the message (tweet or article) + x_link_context = "" + x_urls = re.findall(r'https?://(?:twitter\.com|x\.com)/\w+/status/\d+', text) + if x_urls: + from x_search import fetch_tweet_by_url + for url in x_urls[:3]: # Cap at 3 links + try: + tweet_data = await fetch_tweet_by_url(url) + if tweet_data: + x_link_context += f"\n## Linked Tweet by @{tweet_data['author']}\n" + if tweet_data.get("title"): + x_link_context += f"Title: {tweet_data['title']}\n" + x_link_context += f"{tweet_data['text'][:500]}\n" + x_link_context += f"Engagement: {tweet_data.get('engagement', 0)} | URL: {url}\n" + logger.info("Fetched X link: @%s — %s", tweet_data['author'], tweet_data['text'][:60]) + except Exception as e: + logger.warning("Failed to fetch X link %s: %s", url, e) + # Haiku pre-pass: does this message need an X search? (Option A: two-pass) if not research_context: # Skip if /research already ran try: @@ -580,6 +598,8 @@ Write like a sharp analyst talking to peers, not like an AI. Specifically: {research_context} +{x_link_context} + ## Conversation History (NEVER ask a question your history already answers) {_format_conversation_history(msg.chat_id, user.id if user else 0)} diff --git a/telegram/x_search.py b/telegram/x_search.py index 2efa246..2aca9fa 100644 --- a/telegram/x_search.py +++ b/telegram/x_search.py @@ -157,3 +157,75 @@ tags: [x-research, telegram-research] Engagement: {tweet.get('likes', 0)} likes, {tweet.get('retweets', 0)} retweets, {tweet.get('replies', 0)} replies Author followers: {tweet.get('author_followers', 0)} """ + + +async def fetch_tweet_by_url(url: str) -> dict | None: + """Fetch a specific tweet/article by X URL. Extracts username and tweet ID, + searches via advanced_search (tweet/detail doesn't work with this API provider). + """ + import re as _re + + # Extract username and tweet ID from URL + match = _re.search(r'(?:twitter\.com|x\.com)/(\w+)/status/(\d+)', url) + if not match: + return None + + username = match.group(1) + tweet_id = match.group(2) + + key = _load_api_key() + if not key: + return None + + try: + async with aiohttp.ClientSession() as session: + # First try article endpoint + async with session.get( + "https://api.twitterapi.io/twitter/article", + params={"tweet_id": tweet_id}, + headers={"X-API-Key": key}, + timeout=aiohttp.ClientTimeout(total=10), + ) as resp: + if resp.status == 200: + data = await resp.json() + article = data.get("article") + if article: + return { + "text": article.get("text", article.get("content", "")), + "url": url, + "author": username, + "author_name": article.get("author", {}).get("name", ""), + "author_followers": article.get("author", {}).get("followers", 0), + "engagement": 0, + "tweet_date": article.get("createdAt", ""), + "is_article": True, + "title": article.get("title", ""), + } + + # Fallback: search from:username and match by ID + async with session.get( + API_URL, + params={"query": f"from:{username}", "queryType": "Latest"}, + headers={"X-API-Key": key}, + timeout=aiohttp.ClientTimeout(total=10), + ) as resp: + if resp.status >= 400: + return None + data = await resp.json() + for tweet in data.get("tweets", []): + if str(tweet.get("id")) == tweet_id: + author = tweet.get("author", {}) + return { + "text": tweet.get("text", ""), + "url": url, + "author": author.get("userName", username), + "author_name": author.get("name", ""), + "author_followers": author.get("followers", 0), + "engagement": (tweet.get("likeCount", 0) or 0) + (tweet.get("retweetCount", 0) or 0), + "tweet_date": tweet.get("createdAt", ""), + "is_article": False, + } + except Exception as e: + logger.warning("Tweet fetch error for %s: %s", url, e) + + return None