Dmitry Beresnev
		
	commited on
		
		
					Commit 
							
							·
						
						0543a64
	
1
								Parent(s):
							
							93393a6
								
tg bot refactoring
Browse files- .env.example +1 -1
- main.py +0 -20
- src/api/finnhub/financial_news_requester.py +3 -4
- src/telegram_bot/__init__.py +0 -0
- src/telegram_bot/config.py +27 -0
- src/telegram_bot/logger.py +43 -0
- src/{telegram_bot.py → telegram_bot/telegram_bot.py} +4 -0
- src/telegram_bot/telegram_bot_service.py +190 -0
- src/telegram_bot/tg_models.py +16 -0
- src/tg_bot.py +5 -190
    	
        .env.example
    CHANGED
    
    | @@ -5,4 +5,4 @@ GEMINI_API_TOKEN= | |
| 5 | 
             
            OPENROUTER_API_TOKEN=
         | 
| 6 | 
             
            GOOGLE_APPS_SCRIPT_URL=
         | 
| 7 | 
             
            WEBHOOK_SECRET=
         | 
| 8 | 
            -
             | 
|  | |
| 5 | 
             
            OPENROUTER_API_TOKEN=
         | 
| 6 | 
             
            GOOGLE_APPS_SCRIPT_URL=
         | 
| 7 | 
             
            WEBHOOK_SECRET=
         | 
| 8 | 
            +
            SPACE_URL=
         | 
    	
        main.py
    CHANGED
    
    | @@ -1,23 +1,3 @@ | |
| 1 | 
            -
            '''
         | 
| 2 | 
            -
            from src.telegram_bot import main as telegram_bot
         | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
            if __name__ == "__main__":
         | 
| 6 | 
            -
                telegram_bot()
         | 
| 7 | 
            -
            '''
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            '''
         | 
| 10 | 
            -
            from fastapi import FastAPI
         | 
| 11 | 
            -
            import uvicorn
         | 
| 12 | 
            -
             | 
| 13 | 
            -
            app = FastAPI()
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            @app.get("/")
         | 
| 16 | 
            -
            def read_root():
         | 
| 17 | 
            -
                return {"Hello": "World"}
         | 
| 18 | 
            -
            '''
         | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
             
            from src.tg_bot import main as tgbot
         | 
| 22 |  | 
| 23 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1 | 
             
            from src.tg_bot import main as tgbot
         | 
| 2 |  | 
| 3 |  | 
    	
        src/api/finnhub/financial_news_requester.py
    CHANGED
    
    | @@ -1,14 +1,13 @@ | |
| 1 | 
            -
            import os
         | 
| 2 | 
             
            import logging
         | 
| 3 | 
             
            from typing import Any
         | 
| 4 |  | 
| 5 | 
             
            from dotenv import load_dotenv
         | 
| 6 | 
             
            import finnhub
         | 
| 7 |  | 
|  | |
| 8 |  | 
| 9 | 
            -
            load_dotenv()
         | 
| 10 |  | 
| 11 | 
            -
             | 
| 12 |  | 
| 13 |  | 
| 14 | 
             
            def fetch_comp_financial_news(ticker: str = 'NVDA', date_from = "2025-07-31", date_to = "2025-08-01") -> list[dict[str, Any]] | None:
         | 
| @@ -16,7 +15,7 @@ def fetch_comp_financial_news(ticker: str = 'NVDA', date_from = "2025-07-31", da | |
| 16 | 
             
                Fetch financial news using Finnhub API.
         | 
| 17 | 
             
                """
         | 
| 18 | 
             
                try:
         | 
| 19 | 
            -
                    finnhub_client = finnhub.Client(api_key= | 
| 20 | 
             
                    news_feed = finnhub_client.company_news(ticker, _from=date_from, to=date_to)
         | 
| 21 | 
             
                    if not news_feed:
         | 
| 22 | 
             
                        logging.warning(f"No news found for ticker {ticker}")
         | 
|  | |
|  | |
| 1 | 
             
            import logging
         | 
| 2 | 
             
            from typing import Any
         | 
| 3 |  | 
| 4 | 
             
            from dotenv import load_dotenv
         | 
| 5 | 
             
            import finnhub
         | 
| 6 |  | 
| 7 | 
            +
            from src.telegram_bot.config import Config
         | 
| 8 |  | 
|  | |
| 9 |  | 
| 10 | 
            +
            load_dotenv()
         | 
| 11 |  | 
| 12 |  | 
| 13 | 
             
            def fetch_comp_financial_news(ticker: str = 'NVDA', date_from = "2025-07-31", date_to = "2025-08-01") -> list[dict[str, Any]] | None:
         | 
|  | |
| 15 | 
             
                Fetch financial news using Finnhub API.
         | 
| 16 | 
             
                """
         | 
| 17 | 
             
                try:
         | 
| 18 | 
            +
                    finnhub_client = finnhub.Client(api_key=Config.FINNHUB_API_KEY)
         | 
| 19 | 
             
                    news_feed = finnhub_client.company_news(ticker, _from=date_from, to=date_to)
         | 
| 20 | 
             
                    if not news_feed:
         | 
| 21 | 
             
                        logging.warning(f"No news found for ticker {ticker}")
         | 
    	
        src/telegram_bot/__init__.py
    ADDED
    
    | 
            File without changes
         | 
    	
        src/telegram_bot/config.py
    ADDED
    
    | @@ -0,0 +1,27 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import os
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from src.telegram_bot.logger import main_logger
         | 
| 4 | 
            +
             | 
| 5 | 
            +
             | 
| 6 | 
            +
            class Config:
         | 
| 7 | 
            +
                BOT_TOKEN: str = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_TOKEN")
         | 
| 8 | 
            +
                GOOGLE_APPS_SCRIPT_URL: str = os.getenv("GOOGLE_APPS_SCRIPT_URL", "")
         | 
| 9 | 
            +
                WEBHOOK_SECRET: str = os.getenv("WEBHOOK_SECRET", "secret-key")
         | 
| 10 | 
            +
                SPACE_ID = os.environ.get('SPACE_ID', '')
         | 
| 11 | 
            +
                SPACE_URL = os.environ.get('SPACE_URL', '')
         | 
| 12 | 
            +
                PORT: int = int(os.getenv("PORT", 7860))
         | 
| 13 | 
            +
                FINNHUB_API_KEY = os.getenv('FINNHUB_API_TOKEN')
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                @classmethod
         | 
| 16 | 
            +
                def validate(cls) -> bool:
         | 
| 17 | 
            +
                    """Validate required configuration"""
         | 
| 18 | 
            +
                    missing = []
         | 
| 19 | 
            +
                    for var in dir(cls):
         | 
| 20 | 
            +
                        if var.isupper():
         | 
| 21 | 
            +
                            value = getattr(cls, var)
         | 
| 22 | 
            +
                            if value in (None, "", []):
         | 
| 23 | 
            +
                                missing.append(var)
         | 
| 24 | 
            +
                    if missing:
         | 
| 25 | 
            +
                        main_logger.error(f"Missing required environment variables: {missing}")
         | 
| 26 | 
            +
                        return False
         | 
| 27 | 
            +
                    return True
         | 
    	
        src/telegram_bot/logger.py
    ADDED
    
    | @@ -0,0 +1,43 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import logging
         | 
| 2 | 
            +
            import sys
         | 
| 3 | 
            +
            from pathlib import Path
         | 
| 4 | 
            +
             | 
| 5 | 
            +
             | 
| 6 | 
            +
            def setup_logger(name: str = None, level: str = "INFO", log_file: str = None) -> logging.Logger:
         | 
| 7 | 
            +
                """
         | 
| 8 | 
            +
                Set up logger with consistent configuration across the application.
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                Args:
         | 
| 11 | 
            +
                    name: Logger name (defaults to root logger)
         | 
| 12 | 
            +
                    level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
         | 
| 13 | 
            +
                    log_file: Optional file path for logging to file
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                Returns:
         | 
| 16 | 
            +
                    Configured logger instance
         | 
| 17 | 
            +
                """
         | 
| 18 | 
            +
                logger = logging.getLogger(name)
         | 
| 19 | 
            +
                # Prevent adding multiple handlers if logger already configured
         | 
| 20 | 
            +
                if logger.handlers:
         | 
| 21 | 
            +
                    return logger
         | 
| 22 | 
            +
                logger.setLevel(getattr(logging, level.upper()))
         | 
| 23 | 
            +
                # Create formatter
         | 
| 24 | 
            +
                formatter = logging.Formatter(
         | 
| 25 | 
            +
                    '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
         | 
| 26 | 
            +
                    datefmt='%Y-%m-%d %H:%M:%S'
         | 
| 27 | 
            +
                )
         | 
| 28 | 
            +
                # Console handler
         | 
| 29 | 
            +
                console_handler = logging.StreamHandler(sys.stdout)
         | 
| 30 | 
            +
                console_handler.setFormatter(formatter)
         | 
| 31 | 
            +
                logger.addHandler(console_handler)
         | 
| 32 | 
            +
                # File handler (optional)
         | 
| 33 | 
            +
                if log_file:
         | 
| 34 | 
            +
                    # Create log directory if it doesn't exist
         | 
| 35 | 
            +
                    Path(log_file).parent.mkdir(parents=True, exist_ok=True)
         | 
| 36 | 
            +
                    file_handler = logging.FileHandler(log_file)
         | 
| 37 | 
            +
                    file_handler.setFormatter(formatter)
         | 
| 38 | 
            +
                    logger.addHandler(file_handler)
         | 
| 39 | 
            +
                return logger
         | 
| 40 | 
            +
             | 
| 41 | 
            +
             | 
| 42 | 
            +
            # Initialize the main application logger
         | 
| 43 | 
            +
            main_logger = setup_logger("telegram-bot", level="INFO", log_file=None)
         | 
    	
        src/{telegram_bot.py → telegram_bot/telegram_bot.py}
    RENAMED
    
    | @@ -1,3 +1,7 @@ | |
|  | |
|  | |
|  | |
|  | |
| 1 | 
             
            import logging
         | 
| 2 | 
             
            import os
         | 
| 3 | 
             
            from typing import Any
         | 
|  | |
| 1 | 
            +
            '''
         | 
| 2 | 
            +
            this module should be deleted in the future
         | 
| 3 | 
            +
            '''
         | 
| 4 | 
            +
             | 
| 5 | 
             
            import logging
         | 
| 6 | 
             
            import os
         | 
| 7 | 
             
            from typing import Any
         | 
    	
        src/telegram_bot/telegram_bot_service.py
    ADDED
    
    | @@ -0,0 +1,190 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            from typing import Any
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            import httpx
         | 
| 4 | 
            +
            from fastapi import HTTPException
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            from src.telegram_bot.logger import main_logger
         | 
| 7 | 
            +
            from src.telegram_bot.config import Config
         | 
| 8 | 
            +
            from src.telegram_bot.tg_models import TelegramUpdate
         | 
| 9 | 
            +
            from src.api.finnhub.financial_news_requester import fetch_comp_financial_news
         | 
| 10 | 
            +
             | 
| 11 | 
            +
             | 
| 12 | 
            +
            class TelegramBotService:
         | 
| 13 | 
            +
                def __init__(self):
         | 
| 14 | 
            +
                    self.config = Config()
         | 
| 15 | 
            +
                    self.http_client: httpx.AsyncClient | None = None
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                async def initialize(self):
         | 
| 18 | 
            +
                    """Initialize HTTP client"""
         | 
| 19 | 
            +
                    self.http_client = httpx.AsyncClient(
         | 
| 20 | 
            +
                        timeout=httpx.Timeout(30.0),
         | 
| 21 | 
            +
                        limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
         | 
| 22 | 
            +
                    )
         | 
| 23 | 
            +
                    main_logger.info("TelegramBotService initialized")
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                async def cleanup(self):
         | 
| 26 | 
            +
                    """Cleanup resources"""
         | 
| 27 | 
            +
                    if self.http_client:
         | 
| 28 | 
            +
                        await self.http_client.aclose()
         | 
| 29 | 
            +
                    main_logger.info("TelegramBotService cleaned up")
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                async def send_message_via_proxy(
         | 
| 32 | 
            +
                        self,
         | 
| 33 | 
            +
                        chat_id: int,
         | 
| 34 | 
            +
                        text: str,
         | 
| 35 | 
            +
                        parse_mode: str = "HTML"
         | 
| 36 | 
            +
                ) -> dict[str, Any]:
         | 
| 37 | 
            +
                    """
         | 
| 38 | 
            +
                    Send message to Telegram via Google Apps Script proxy
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    Args:
         | 
| 41 | 
            +
                        chat_id: Telegram chat ID
         | 
| 42 | 
            +
                        text: Message text
         | 
| 43 | 
            +
                        parse_mode: Message parse mode (HTML, Markdown, etc.)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    Returns:
         | 
| 46 | 
            +
                        Response from Telegram API
         | 
| 47 | 
            +
                    """
         | 
| 48 | 
            +
                    if not self.http_client:
         | 
| 49 | 
            +
                        raise RuntimeError("HTTP client not initialized")
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    payload = {
         | 
| 52 | 
            +
                        "method": "sendMessage",
         | 
| 53 | 
            +
                        "bot_token": self.config.BOT_TOKEN,
         | 
| 54 | 
            +
                        "chat_id": chat_id,
         | 
| 55 | 
            +
                        "text": text,
         | 
| 56 | 
            +
                        "parse_mode": parse_mode
         | 
| 57 | 
            +
                    }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    try:
         | 
| 60 | 
            +
                        main_logger.info(f"Sending message to chat {chat_id} via proxy")
         | 
| 61 | 
            +
                        response = await self.http_client.post(
         | 
| 62 | 
            +
                            self.config.GOOGLE_APPS_SCRIPT_URL,
         | 
| 63 | 
            +
                            json=payload,
         | 
| 64 | 
            +
                            headers={"Content-Type": "application/json"}
         | 
| 65 | 
            +
                        )
         | 
| 66 | 
            +
                        response.raise_for_status()
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                        result = response.json()
         | 
| 69 | 
            +
                        main_logger.info(f"Message sent successfully: {result.get('ok', False)}")
         | 
| 70 | 
            +
                        return result
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    except httpx.RequestError as e:
         | 
| 73 | 
            +
                        main_logger.error(f"HTTP request failed: {e}")
         | 
| 74 | 
            +
                        raise HTTPException(status_code=500, detail="Failed to send message")
         | 
| 75 | 
            +
                    except Exception as e:
         | 
| 76 | 
            +
                        main_logger.error(f"Unexpected error: {e}")
         | 
| 77 | 
            +
                        raise HTTPException(status_code=500, detail="Internal server error")
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                async def process_update(self, update: TelegramUpdate) -> None:
         | 
| 80 | 
            +
                    """Process incoming Telegram update"""
         | 
| 81 | 
            +
                    try:
         | 
| 82 | 
            +
                        if not update.message:
         | 
| 83 | 
            +
                            main_logger.debug("No message in update, skipping")
         | 
| 84 | 
            +
                            return
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                        message = update.message
         | 
| 87 | 
            +
                        chat_id = message.get("chat", {}).get("id")
         | 
| 88 | 
            +
                        text = message.get("text", "").strip()
         | 
| 89 | 
            +
                        user_name = message.get("from", {}).get("first_name", "User")
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                        if not chat_id:
         | 
| 92 | 
            +
                            main_logger.warning("No chat ID found in message")
         | 
| 93 | 
            +
                            return
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                        main_logger.info(f"Processing message: '{text}' from user: {user_name}")
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                        # Command handling
         | 
| 98 | 
            +
                        if text.startswith("/"):
         | 
| 99 | 
            +
                            await self._handle_command(chat_id, text, user_name)
         | 
| 100 | 
            +
                        else:
         | 
| 101 | 
            +
                            await self._handle_regular_message(chat_id, text, user_name)
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    except Exception as e:
         | 
| 104 | 
            +
                        main_logger.error(f"Error processing update: {e}")
         | 
| 105 | 
            +
                        if update.message and update.message.get("chat", {}).get("id"):
         | 
| 106 | 
            +
                            await self.send_message_via_proxy(
         | 
| 107 | 
            +
                                update.message["chat"]["id"],
         | 
| 108 | 
            +
                                "Sorry, something went wrong. Please try again later."
         | 
| 109 | 
            +
                            )
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                async def _handle_command(self, chat_id: int, command: str, user_name: str) -> None:
         | 
| 112 | 
            +
                    """Handle bot commands"""
         | 
| 113 | 
            +
                    command = command.lower().strip()
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                    if command in ["/start", "/hello"]:
         | 
| 116 | 
            +
                        response = f"👋 Hello, {user_name}! Welcome to the Financial News Bot!\n\n"
         | 
| 117 | 
            +
                        response += "Available commands:\n"
         | 
| 118 | 
            +
                        response += "/hello - Say hello\n"
         | 
| 119 | 
            +
                        response += "/help - Show help\n"
         | 
| 120 | 
            +
                        response += "/status - Check bot status"
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    elif command == "/help":
         | 
| 123 | 
            +
                        response = "🤖 <b>Financial News Bot Help</b>\n\n"
         | 
| 124 | 
            +
                        response += "<b>Commands:</b>\n"
         | 
| 125 | 
            +
                        response += "/start or /hello - Get started\n"
         | 
| 126 | 
            +
                        response += "/help - Show this help message\n"
         | 
| 127 | 
            +
                        response += "/status - Check bot status\n\n"
         | 
| 128 | 
            +
                        response += "<b>About:</b>\n"
         | 
| 129 | 
            +
                        response += "This bot provides financial news and sentiment analysis."
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    elif command == "/status":
         | 
| 132 | 
            +
                        response = "✅ <b>Bot Status: Online</b>\n\n"
         | 
| 133 | 
            +
                        response += "🔧 System: Running on HuggingFace Spaces\n"
         | 
| 134 | 
            +
                        response += "🌐 Proxy: Google Apps Script\n"
         | 
| 135 | 
            +
                        response += "📊 Status: All systems operational"
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                    elif command == "/run":
         | 
| 138 | 
            +
                        await self.news_feed_analysing(chat_id, command, user_name)
         | 
| 139 | 
            +
                        return
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                    else:
         | 
| 142 | 
            +
                        response = f"❓ Unknown command: {command}\n\n"
         | 
| 143 | 
            +
                        response += "Type /help to see available commands."
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                    await self.send_message_via_proxy(chat_id, response)
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                async def _handle_regular_message(self, chat_id: int, text: str, user_name: str) -> None:
         | 
| 148 | 
            +
                    """Handle regular (non-command) messages"""
         | 
| 149 | 
            +
                    response = f"Hello {user_name}! 👋\n\n"
         | 
| 150 | 
            +
                    response += f"You said: <i>\"{text}\"</i>\n\n"
         | 
| 151 | 
            +
                    response += "I'm a financial news bot. Try these commands:\n"
         | 
| 152 | 
            +
                    response += "/hello - Get started\n"
         | 
| 153 | 
            +
                    response += "/help - Show help\n"
         | 
| 154 | 
            +
                    response += "/status - Check status"
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                    await self.send_message_via_proxy(chat_id, response)
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                def _format_news_for_telegram(self, news_json: list[dict[str, Any]]) -> str:
         | 
| 159 | 
            +
                    message = ""
         | 
| 160 | 
            +
                    for item in news_json:
         | 
| 161 | 
            +
                        message += (
         | 
| 162 | 
            +
                            f"📰 <b>{item.get('headline', 'No headline')}</b>\n"
         | 
| 163 | 
            +
                            f"📝 {item.get('summary', 'No summary')}\n"
         | 
| 164 | 
            +
                            f"🏷️ Source: {item.get('source', 'Unknown')}\n"
         | 
| 165 | 
            +
                            f"🔗 <a href=\"{item.get('url', '#')}\">Read more</a>\n\n"
         | 
| 166 | 
            +
                        )
         | 
| 167 | 
            +
                    return message if message else "No news available."
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                async def news_feed_analysing(self, chat_id: int, text: str, user_name: str) -> None:
         | 
| 170 | 
            +
                    await self.send_message_via_proxy(chat_id, "Fetching latest financial news...")
         | 
| 171 | 
            +
                    try:
         | 
| 172 | 
            +
                        feed = fetch_comp_financial_news()
         | 
| 173 | 
            +
                        main_logger.info(f"Processed: {len(feed)} news items")
         | 
| 174 | 
            +
                        formatted_news = self._format_news_for_telegram(feed)
         | 
| 175 | 
            +
                        # Split message if too long (Telegram limit is 4096 characters)
         | 
| 176 | 
            +
                        if len(formatted_news) > 4000:
         | 
| 177 | 
            +
                            items = formatted_news.split('\n\n')
         | 
| 178 | 
            +
                            chunk = ""
         | 
| 179 | 
            +
                            for item in items:
         | 
| 180 | 
            +
                                if len(chunk) + len(item) + 2 > 4000:
         | 
| 181 | 
            +
                                    await self.send_message_via_proxy(chat_id, chunk)
         | 
| 182 | 
            +
                                    chunk = ""
         | 
| 183 | 
            +
                                chunk += item + "\n\n"
         | 
| 184 | 
            +
                            if chunk:
         | 
| 185 | 
            +
                                await self.send_message_via_proxy(chat_id, chunk)
         | 
| 186 | 
            +
                        else:
         | 
| 187 | 
            +
                            await self.send_message_via_proxy(chat_id, formatted_news)
         | 
| 188 | 
            +
                    except Exception as e:
         | 
| 189 | 
            +
                        main_logger.error(f"Error in run_crew: {e}")
         | 
| 190 | 
            +
                        await self.send_message_via_proxy(chat_id, f"Sorry, there was an error fetching news: {str(e)}")
         | 
    	
        src/telegram_bot/tg_models.py
    ADDED
    
    | @@ -0,0 +1,16 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            from typing import Any
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from pydantic import BaseModel
         | 
| 4 | 
            +
             | 
| 5 | 
            +
             | 
| 6 | 
            +
            class TelegramUpdate(BaseModel):
         | 
| 7 | 
            +
                update_id: int
         | 
| 8 | 
            +
                message: dict[str, Any] | None = None
         | 
| 9 | 
            +
                callback_query: dict[str, Any] | None = None
         | 
| 10 | 
            +
             | 
| 11 | 
            +
             | 
| 12 | 
            +
            class TelegramMessage(BaseModel):
         | 
| 13 | 
            +
                message_id: int
         | 
| 14 | 
            +
                from_user: dict[str, Any] = {}
         | 
| 15 | 
            +
                chat: dict[str, Any]
         | 
| 16 | 
            +
                text: str | None = None
         | 
    	
        src/tg_bot.py
    CHANGED
    
    | @@ -5,9 +5,6 @@ This implementation uses Google Apps Script as a proxy to bypass | |
| 5 | 
             
            HuggingFace's restrictions on Telegram API calls.
         | 
| 6 | 
             
            """
         | 
| 7 |  | 
| 8 | 
            -
            import logging
         | 
| 9 | 
            -
            import os
         | 
| 10 | 
            -
            from typing import Optional, Dict, Any
         | 
| 11 | 
             
            import httpx
         | 
| 12 | 
             
            from fastapi import FastAPI, Request, HTTPException
         | 
| 13 | 
             
            from pydantic import BaseModel
         | 
| @@ -15,192 +12,10 @@ import uvicorn | |
| 15 | 
             
            import asyncio
         | 
| 16 | 
             
            from contextlib import asynccontextmanager
         | 
| 17 |  | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
            )
         | 
| 23 | 
            -
            logger = logging.getLogger(__name__)
         | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
            # Configuration
         | 
| 27 | 
            -
            class Config:
         | 
| 28 | 
            -
                BOT_TOKEN: str = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_TOKEN")
         | 
| 29 | 
            -
                GOOGLE_APPS_SCRIPT_URL: str = os.getenv("GOOGLE_APPS_SCRIPT_URL", "")
         | 
| 30 | 
            -
                WEBHOOK_SECRET: str = os.getenv("WEBHOOK_SECRET", "your-secret-key")
         | 
| 31 | 
            -
                PORT: int = int(os.getenv("PORT", 7860))
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                @classmethod
         | 
| 34 | 
            -
                def validate(cls) -> bool:
         | 
| 35 | 
            -
                    """Validate required configuration"""
         | 
| 36 | 
            -
                    missing = []
         | 
| 37 | 
            -
                    if not cls.BOT_TOKEN:
         | 
| 38 | 
            -
                        missing.append("BOT_TOKEN")
         | 
| 39 | 
            -
                    if not cls.GOOGLE_APPS_SCRIPT_URL:
         | 
| 40 | 
            -
                        missing.append("GOOGLE_APPS_SCRIPT_URL")
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                    if missing:
         | 
| 43 | 
            -
                        logger.error(f"Missing required environment variables: {missing}")
         | 
| 44 | 
            -
                        return False
         | 
| 45 | 
            -
                    return True
         | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
            # Pydantic models for request/response validation
         | 
| 49 | 
            -
            class TelegramUpdate(BaseModel):
         | 
| 50 | 
            -
                update_id: int
         | 
| 51 | 
            -
                message: Optional[Dict[str, Any]] = None
         | 
| 52 | 
            -
                callback_query: Optional[Dict[str, Any]] = None
         | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
            class TelegramMessage(BaseModel):
         | 
| 56 | 
            -
                message_id: int
         | 
| 57 | 
            -
                from_user: Dict[str, Any] = {}
         | 
| 58 | 
            -
                chat: Dict[str, Any]
         | 
| 59 | 
            -
                text: Optional[str] = None
         | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
            # Telegram Bot Service
         | 
| 63 | 
            -
            class TelegramBotService:
         | 
| 64 | 
            -
                def __init__(self):
         | 
| 65 | 
            -
                    self.config = Config()
         | 
| 66 | 
            -
                    self.http_client: Optional[httpx.AsyncClient] = None
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                async def initialize(self):
         | 
| 69 | 
            -
                    """Initialize HTTP client"""
         | 
| 70 | 
            -
                    self.http_client = httpx.AsyncClient(
         | 
| 71 | 
            -
                        timeout=httpx.Timeout(30.0),
         | 
| 72 | 
            -
                        limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
         | 
| 73 | 
            -
                    )
         | 
| 74 | 
            -
                    logger.info("TelegramBotService initialized")
         | 
| 75 | 
            -
             | 
| 76 | 
            -
                async def cleanup(self):
         | 
| 77 | 
            -
                    """Cleanup resources"""
         | 
| 78 | 
            -
                    if self.http_client:
         | 
| 79 | 
            -
                        await self.http_client.aclose()
         | 
| 80 | 
            -
                    logger.info("TelegramBotService cleaned up")
         | 
| 81 | 
            -
             | 
| 82 | 
            -
                async def send_message_via_proxy(
         | 
| 83 | 
            -
                        self,
         | 
| 84 | 
            -
                        chat_id: int,
         | 
| 85 | 
            -
                        text: str,
         | 
| 86 | 
            -
                        parse_mode: str = "HTML"
         | 
| 87 | 
            -
                ) -> Dict[str, Any]:
         | 
| 88 | 
            -
                    """
         | 
| 89 | 
            -
                    Send message to Telegram via Google Apps Script proxy
         | 
| 90 | 
            -
             | 
| 91 | 
            -
                    Args:
         | 
| 92 | 
            -
                        chat_id: Telegram chat ID
         | 
| 93 | 
            -
                        text: Message text
         | 
| 94 | 
            -
                        parse_mode: Message parse mode (HTML, Markdown, etc.)
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                    Returns:
         | 
| 97 | 
            -
                        Response from Telegram API
         | 
| 98 | 
            -
                    """
         | 
| 99 | 
            -
                    if not self.http_client:
         | 
| 100 | 
            -
                        raise RuntimeError("HTTP client not initialized")
         | 
| 101 | 
            -
             | 
| 102 | 
            -
                    payload = {
         | 
| 103 | 
            -
                        "method": "sendMessage",
         | 
| 104 | 
            -
                        "bot_token": self.config.BOT_TOKEN,
         | 
| 105 | 
            -
                        "chat_id": chat_id,
         | 
| 106 | 
            -
                        "text": text,
         | 
| 107 | 
            -
                        "parse_mode": parse_mode
         | 
| 108 | 
            -
                    }
         | 
| 109 | 
            -
             | 
| 110 | 
            -
                    try:
         | 
| 111 | 
            -
                        logger.info(f"Sending message to chat {chat_id} via proxy")
         | 
| 112 | 
            -
                        response = await self.http_client.post(
         | 
| 113 | 
            -
                            self.config.GOOGLE_APPS_SCRIPT_URL,
         | 
| 114 | 
            -
                            json=payload,
         | 
| 115 | 
            -
                            headers={"Content-Type": "application/json"}
         | 
| 116 | 
            -
                        )
         | 
| 117 | 
            -
                        response.raise_for_status()
         | 
| 118 | 
            -
             | 
| 119 | 
            -
                        result = response.json()
         | 
| 120 | 
            -
                        logger.info(f"Message sent successfully: {result.get('ok', False)}")
         | 
| 121 | 
            -
                        return result
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                    except httpx.RequestError as e:
         | 
| 124 | 
            -
                        logger.error(f"HTTP request failed: {e}")
         | 
| 125 | 
            -
                        raise HTTPException(status_code=500, detail="Failed to send message")
         | 
| 126 | 
            -
                    except Exception as e:
         | 
| 127 | 
            -
                        logger.error(f"Unexpected error: {e}")
         | 
| 128 | 
            -
                        raise HTTPException(status_code=500, detail="Internal server error")
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                async def process_update(self, update: TelegramUpdate) -> None:
         | 
| 131 | 
            -
                    """Process incoming Telegram update"""
         | 
| 132 | 
            -
                    try:
         | 
| 133 | 
            -
                        if not update.message:
         | 
| 134 | 
            -
                            logger.debug("No message in update, skipping")
         | 
| 135 | 
            -
                            return
         | 
| 136 | 
            -
             | 
| 137 | 
            -
                        message = update.message
         | 
| 138 | 
            -
                        chat_id = message.get("chat", {}).get("id")
         | 
| 139 | 
            -
                        text = message.get("text", "").strip()
         | 
| 140 | 
            -
                        user_name = message.get("from", {}).get("first_name", "User")
         | 
| 141 | 
            -
             | 
| 142 | 
            -
                        if not chat_id:
         | 
| 143 | 
            -
                            logger.warning("No chat ID found in message")
         | 
| 144 | 
            -
                            return
         | 
| 145 | 
            -
             | 
| 146 | 
            -
                        logger.info(f"Processing message: '{text}' from user: {user_name}")
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                        # Command handling
         | 
| 149 | 
            -
                        if text.startswith("/"):
         | 
| 150 | 
            -
                            await self._handle_command(chat_id, text, user_name)
         | 
| 151 | 
            -
                        else:
         | 
| 152 | 
            -
                            await self._handle_regular_message(chat_id, text, user_name)
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                    except Exception as e:
         | 
| 155 | 
            -
                        logger.error(f"Error processing update: {e}")
         | 
| 156 | 
            -
                        if update.message and update.message.get("chat", {}).get("id"):
         | 
| 157 | 
            -
                            await self.send_message_via_proxy(
         | 
| 158 | 
            -
                                update.message["chat"]["id"],
         | 
| 159 | 
            -
                                "Sorry, something went wrong. Please try again later."
         | 
| 160 | 
            -
                            )
         | 
| 161 | 
            -
             | 
| 162 | 
            -
                async def _handle_command(self, chat_id: int, command: str, user_name: str) -> None:
         | 
| 163 | 
            -
                    """Handle bot commands"""
         | 
| 164 | 
            -
                    command = command.lower().strip()
         | 
| 165 | 
            -
             | 
| 166 | 
            -
                    if command in ["/start", "/hello"]:
         | 
| 167 | 
            -
                        response = f"👋 Hello, {user_name}! Welcome to the Financial News Bot!\n\n"
         | 
| 168 | 
            -
                        response += "Available commands:\n"
         | 
| 169 | 
            -
                        response += "/hello - Say hello\n"
         | 
| 170 | 
            -
                        response += "/help - Show help\n"
         | 
| 171 | 
            -
                        response += "/status - Check bot status"
         | 
| 172 | 
            -
             | 
| 173 | 
            -
                    elif command == "/help":
         | 
| 174 | 
            -
                        response = "🤖 <b>Financial News Bot Help</b>\n\n"
         | 
| 175 | 
            -
                        response += "<b>Commands:</b>\n"
         | 
| 176 | 
            -
                        response += "/start or /hello - Get started\n"
         | 
| 177 | 
            -
                        response += "/help - Show this help message\n"
         | 
| 178 | 
            -
                        response += "/status - Check bot status\n\n"
         | 
| 179 | 
            -
                        response += "<b>About:</b>\n"
         | 
| 180 | 
            -
                        response += "This bot provides financial news and sentiment analysis."
         | 
| 181 | 
            -
             | 
| 182 | 
            -
                    elif command == "/status":
         | 
| 183 | 
            -
                        response = "✅ <b>Bot Status: Online</b>\n\n"
         | 
| 184 | 
            -
                        response += "🔧 System: Running on HuggingFace Spaces\n"
         | 
| 185 | 
            -
                        response += "🌐 Proxy: Google Apps Script\n"
         | 
| 186 | 
            -
                        response += "📊 Status: All systems operational"
         | 
| 187 | 
            -
             | 
| 188 | 
            -
                    else:
         | 
| 189 | 
            -
                        response = f"❓ Unknown command: {command}\n\n"
         | 
| 190 | 
            -
                        response += "Type /help to see available commands."
         | 
| 191 | 
            -
             | 
| 192 | 
            -
                    await self.send_message_via_proxy(chat_id, response)
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                async def _handle_regular_message(self, chat_id: int, text: str, user_name: str) -> None:
         | 
| 195 | 
            -
                    """Handle regular (non-command) messages"""
         | 
| 196 | 
            -
                    response = f"Hello {user_name}! 👋\n\n"
         | 
| 197 | 
            -
                    response += f"You said: <i>\"{text}\"</i>\n\n"
         | 
| 198 | 
            -
                    response += "I'm a financial news bot. Try these commands:\n"
         | 
| 199 | 
            -
                    response += "/hello - Get started\n"
         | 
| 200 | 
            -
                    response += "/help - Show help\n"
         | 
| 201 | 
            -
                    response += "/status - Check status"
         | 
| 202 | 
            -
             | 
| 203 | 
            -
                    await self.send_message_via_proxy(chat_id, response)
         | 
| 204 |  | 
| 205 |  | 
| 206 | 
             
            # Global bot service instance
         | 
| @@ -333,4 +148,4 @@ if __name__ == "__main__": | |
| 333 | 
             
                    host="0.0.0.0",
         | 
| 334 | 
             
                    port=Config.PORT,
         | 
| 335 | 
             
                    log_level="info"
         | 
| 336 | 
            -
                )
         | 
|  | |
| 5 | 
             
            HuggingFace's restrictions on Telegram API calls.
         | 
| 6 | 
             
            """
         | 
| 7 |  | 
|  | |
|  | |
|  | |
| 8 | 
             
            import httpx
         | 
| 9 | 
             
            from fastapi import FastAPI, Request, HTTPException
         | 
| 10 | 
             
            from pydantic import BaseModel
         | 
|  | |
| 12 | 
             
            import asyncio
         | 
| 13 | 
             
            from contextlib import asynccontextmanager
         | 
| 14 |  | 
| 15 | 
            +
            from src.telegram_bot.config import Config
         | 
| 16 | 
            +
            from src.telegram_bot.logger import main_logger as logger
         | 
| 17 | 
            +
            from src.telegram_bot.tg_models import TelegramUpdate
         | 
| 18 | 
            +
            from src.telegram_bot.telegram_bot_service import TelegramBotService
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 19 |  | 
| 20 |  | 
| 21 | 
             
            # Global bot service instance
         | 
|  | |
| 148 | 
             
                    host="0.0.0.0",
         | 
| 149 | 
             
                    port=Config.PORT,
         | 
| 150 | 
             
                    log_level="info"
         | 
| 151 | 
            +
                )
         | 
