"""Telegram search service for crypto and geopolitical channel monitoring.""" import asyncio import logging from datetime import datetime, timedelta from typing import Optional, List try: import nest_asyncio nest_asyncio.apply() except ImportError: pass logger = logging.getLogger(__name__) # Default public channels: crypto + geopolitics/politics # Verified 2026-03-31: all channels return text messages with meaningful content DEFAULT_CHANNELS = [ # Crypto / macro "CryptoVIPSignalTA", # crypto signals + macro news "whale_alert_io", # on-chain whale transfers "WatcherGuru", # crypto/macro breaking news # Geopolitics & politics "DDGeopolitics", # geopolitical analysis (views ~20k) "disclosetv", # US politics, breaking news (views ~50-70k) "realDonaldTrump", # Trump's own posts "intelslava", # Russia/Ukraine, geopolitics (views ~50k) "TheScrollOfBenjamin", # Middle East geopolitics "WarMonitors", # conflict breaking news (views ~14k, active) ] class TelegramSearchService: """ Searches crypto-relevant public Telegram channels for messages. Uses Telethon (MTProto API) to search public channels by keyword. Requires a one-time auth to generate a session string. """ def __init__( self, api_id: str = "", api_hash: str = "", session_string: str = "", channels: Optional[List[str]] = None, ): self.api_id = api_id self.api_hash = api_hash self.session_string = session_string self.channels = channels or DEFAULT_CHANNELS self._client = None def is_available(self) -> bool: """Check if Telegram search is available.""" if not (self.api_id and self.api_hash and self.session_string): return False try: import telethon # noqa: F401 return True except ImportError: logger.warning("telethon not installed. Telegram search disabled.") return False def _get_client(self): """Get or create the Telethon client.""" if self._client is None: from telethon import TelegramClient from telethon.sessions import StringSession self._client = TelegramClient( StringSession(self.session_string), int(self.api_id), self.api_hash, ) return self._client async def _search_channel( self, channel: str, query: str, limit: int = 5, ) -> List[dict]: """Search a single channel for messages matching a query.""" from telethon.errors import ( FloodWaitError, ChannelPrivateError, UsernameNotOccupiedError, UsernameInvalidError, ) results = [] try: client = self._get_client() async for message in client.iter_messages( channel, search=query, limit=limit, ): if not message.text: continue results.append({ "channel": channel, "text": message.text, "date": message.date.strftime("%Y-%m-%d %H:%M UTC") if message.date else "", "views": message.views or 0, "forwards": message.forwards or 0, }) except FloodWaitError as e: logger.warning(f"Telegram flood wait: {e.seconds}s for channel {channel}") except (ChannelPrivateError, UsernameNotOccupiedError, UsernameInvalidError): logger.debug(f"Channel {channel} not accessible, skipping") except Exception as e: logger.error(f"Error searching Telegram channel {channel}: {e}") return results async def _search_all_channels(self, query: str, limit_per_channel: int = 5) -> List[dict]: """Search all configured channels.""" client = self._get_client() async with client: all_results = [] for channel in self.channels: results = await self._search_channel(channel, query, limit_per_channel) all_results.extend(results) # Sort by views descending all_results.sort(key=lambda x: x["views"], reverse=True) return all_results def _format_report(self, query: str, messages: List[dict]) -> str: """Format search results as a report string.""" if not messages: return f"No Telegram messages found for '{query}'." total_views = sum(m["views"] for m in messages) lines = [ f"--- Telegram Search Results for '{query}' ---", f"Total Views in Sample: {total_views:,}", f"Channels Searched: {', '.join(self.channels)}", "Messages:", ] for idx, msg in enumerate(messages): text_preview = msg["text"][:200] if len(msg["text"]) > 200: text_preview += "..." lines.append( f'{idx + 1}. [{msg["channel"]}] ({msg["date"]}, ' f'👁 {msg["views"]:,}): "{text_preview}"' ) lines.append("-------------------------------------------") return "\n".join(lines) def search_for_market(self, query: str, limit: int = 10) -> str: """ Search Telegram channels for messages relevant to a market. Args: query: Search query limit: Max total messages to return Returns: Formatted report string """ if not self.is_available(): return "Telegram search unavailable: credentials not configured or telethon not installed." try: # Calculate per-channel limit limit_per_channel = max(3, limit // len(self.channels)) # Run async search — handle both sync and async calling contexts coro = self._search_all_channels(query, limit_per_channel) try: loop = asyncio.get_running_loop() # Already inside an async event loop — use a new thread import concurrent.futures with concurrent.futures.ThreadPoolExecutor() as pool: messages = pool.submit( asyncio.run, coro ).result(timeout=30) except RuntimeError: # No running loop — safe to use run_until_complete loop = asyncio.get_event_loop() messages = loop.run_until_complete(coro) # Trim to total limit messages = messages[:limit] return self._format_report(query, messages) except Exception as e: logger.error(f"Telegram search failed: {e}") return f"Telegram search failed: {str(e)}"