diff --git a/TELEGRAM_INTEGRATION_GUIDE.md b/TELEGRAM_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..1a1fbd0 --- /dev/null +++ b/TELEGRAM_INTEGRATION_GUIDE.md @@ -0,0 +1,394 @@ +# Telegram Notification Integration Guide + +This guide explains how to manually integrate the Telegram notification system into your existing `server.py` file. + +## Quick Setup + +If you want to quickly get started: + +1. **Install dependencies:** + ```bash + pip install -r requirements.txt + ``` + +2. **Set up your Telegram bot** (see [setup_telegram.md](setup_telegram.md)) + +3. **Set environment variables:** + ```bash + export TELEGRAM_BOT_TOKEN="your_bot_token_here" + export TELEGRAM_CHAT_ID="your_chat_id_here" + ``` + +4. **Apply the integration** (see below) + +--- + +## Manual Integration Steps + +### Step 1: Add Imports to server.py + +At the top of `server.py`, after the existing imports, add: + +```python +# Telegram Notification System +import asyncio +from notification_system import init_notifier, send_notification_sync + +# Global notifier instance +notifier = None +``` + +### Step 2: Modify classify_image() Function + +Find the `classify_image()` function. After this line: +```python +prediction_history.appendleft(result) +``` + +Add the following code: + +```python + # Send Telegram notification for concerning stages + if notifier and notifier.enabled: + stage = result.get('stage', 0) + if stage in getattr(config, 'TELEGRAM_ALERT_ON_STAGES', [2, 3]): + try: + send_notification_sync( + notifier, + notifier.send_freshness_alert( + stage=stage, + stage_name=result['stage_name'], + confidence=result['confidence'], + filename=result['filename'], + image_path=image_path if getattr(config, 'TELEGRAM_SEND_PHOTO', True) else None, + stage_probabilities=result.get('stage_probabilities'), + hex_colors=result.get('hex_colors') + ) + ) + print(f\" Telegram notification sent for {result['stage_name']}\") + except Exception as e: + print(f\" Telegram notification error: {e}\") +``` + +### Step 3: Add API Endpoints + +Add these new API endpoints before the `main()` function: + +```python +@app.route("/api/test-notification", methods=["POST"]) +def test_notification(): + """Test Telegram notification system.""" + if not notifier or not notifier.enabled: + return jsonify({ + "error": "Telegram notifications not configured", + "message": "Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables" + }), 503 + + try: + success = send_notification_sync(notifier, notifier.send_test_notification()) + if success: + return jsonify({ + "success": True, + "message": "Test notification sent successfully!" + }) + else: + return jsonify({ + "success": False, + "message": "Failed to send test notification" + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": str(e) + }), 500 + + +@app.route("/api/notification-status", methods=["GET"]) +def notification_status(): + """Get Telegram notification system status.""" + if not notifier: + return jsonify({ + "enabled": False, + "configured": False, + "message": "Notifier not initialized" + }) + + return jsonify({ + "enabled": notifier.enabled, + "configured": bool(notifier.bot_token and notifier.chat_id), + "bot_token_set": bool(notifier.bot_token), + "chat_id_set": bool(notifier.chat_id), + "alert_stages": getattr(config, 'TELEGRAM_ALERT_ON_STAGES', [2, 3]), + "send_photos": getattr(config, 'TELEGRAM_SEND_PHOTO', True) + }) +``` + +### Step 4: Initialize Notifier in main() + +Find the `main()` function. After the `load_model()` call, add: + +```python + # Initialize Telegram notifier + global notifier + if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() + if notifier and notifier.enabled: + print("✅ Telegram notifications: ENABLED") + print(f" Chat ID: {notifier.chat_id}") + + # Send startup notification if configured + if getattr(config, 'TELEGRAM_NOTIFY_ON_STARTUP', True): + try: + send_notification_sync( + notifier, + notifier.send_system_health( + status="starting", + uptime_seconds=0, + total_predictions=0, + model_loaded=True + ) + ) + print(" Startup notification sent") + except Exception as e: + print(f" ⚠️ Failed to send startup notification: {e}") + else: + print("⚠️ Telegram notifications: DISABLED (check configuration)") + else: + print("Telegram notifications: DISABLED (via config)") +``` + +### Step 5: Initialize for Production (Gunicorn) + +Find the module-level initialization section (at the bottom of server.py, before `if __name__ == "__main__":`). + +After these lines: +```python +server_start_time = datetime.now() +``` + +Add: + +```python +# Initialize Telegram notifier for gunicorn/production +if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() +``` + +--- + +## Testing the Integration + +### 1. Test Notification Status + +```bash +curl http://localhost:5000/api/notification-status +``` + +Expected response: +```json +{ + "enabled": true, + "configured": true, + "bot_token_set": true, + "chat_id_set": true, + "alert_stages": [2, 3], + "send_photos": true +} +``` + +### 2. Send Test Notification + +```bash +curl -X POST http://localhost:5000/api/test-notification +``` + +You should receive a test message in Telegram! + +### 3. Test with Real Image + +Upload an image that triggers Stage 3 or 4: + +```bash +curl -X POST -F "image=@spoiled_sample.jpg" http://localhost:5000/barcode +``` + +Check your Telegram for the freshness alert! + +--- + +## Troubleshooting + +### "Telegram notifications: DISABLED" + +**Problem:** Notifier is disabled even though configuration is set. + +**Solutions:** +- Check environment variables are set: `echo $TELEGRAM_BOT_TOKEN` +- Verify bot token format (should be like `1234567890:ABC...`) +- Ensure `python-telegram-bot` is installed: `pip install python-telegram-bot` +- Check config.py: `TELEGRAM_ENABLED` should be `True` + +### No Notifications Received + +**Problem:** Server runs but no Telegram messages arrive. + +**Solutions:** +- Test with `/api/test-notification` endpoint first +- Verify Chat ID is correct (use `@userinfobot` to get it) +- Check server logs for error messages +- Ensure bot is not blocked in Telegram +- Verify stage threshold: only Stage 2 and 3 trigger notifications by default + +### Import Error + +**Problem:** `ModuleNotFoundError: No module named 'telegram'` + +**Solution:** +```bash +pip install python-telegram-bot==20.7 +``` + +### Async Warnings + +**Problem:** Warnings about event loops in console. + +**Solution:** This is normal and can be ignored. Notifications will still work. To suppress warnings, upgrade to Python 3.9+. + +--- + +## Configuration Options + +All settings are in `config.py`: + +```python +# Basic Configuration +TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", None) +TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", None) +TELEGRAM_ENABLED = True + +# Notification Triggers +TELEGRAM_ALERT_ON_STAGES = [2, 3] # Which stages trigger notifications +TELEGRAM_PHOTO_MIN_STAGE = 2 # Minimum stage to send photos +TELEGRAM_SEND_PHOTO = True # Include photos in notifications + +# Advanced +TELEGRAM_NOTIFY_ON_STARTUP = True # Send notification on server start +TELEGRAM_DAILY_SUMMARY_ENABLED = True # Daily summary reports +TELEGRAM_DAILY_SUMMARY_TIME = "18:00" # Time for daily summary +TELEGRAM_HEALTH_CHECK_INTERVAL = 3600 # Health check interval (seconds) +``` + +--- + +## Advanced Features + +### Daily Summaries + +To enable daily summaries, you can add a scheduled task. Install the `schedule` library: + +```bash +pip install schedule +``` + +Then add this code to `server.py`: + +```python +import schedule + +def send_daily_summary(): + """Send daily summary of predictions.""" + if notifier and notifier.enabled: + predictions = list(prediction_history) + send_notification_sync(notifier, notifier.send_daily_summary(predictions)) + +# Schedule daily summary +if getattr(config, 'TELEGRAM_DAILY_SUMMARY_ENABLED', True): + summary_time = getattr(config, 'TELEGRAM_DAILY_SUMMARY_TIME', "18:00") + schedule.every().day.at(summary_time).do(send_daily_summary) + + # Run scheduler in background thread + def run_scheduler(): + while True: + schedule.run_pending() + time.sleep(60) + + scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) + scheduler_thread.start() +``` + +### Custom Alert Rules + +Add custom logic in `classify_image()`: + +```python +# Alert only during business hours +import datetime +current_hour = datetime.datetime.now().hour +if 9 <= current_hour <= 17: + # Send notification + send_notification_sync(...) + +# Alert on specific confidence thresholds +if result['confidence'] > 0.9 and result['stage'] >= 2: + # High confidence spoilage detected + send_notification_sync(...) +``` + +--- + +## Security Best Practices + +1. **Never commit tokens:** Always use environment variables +2. **Use `.env` files:** Add `.env` to `.gitignore` +3. **Rotate tokens regularly:** Generate new tokens monthly via BotFather +4. **Restrict bot permissions:** Your bot only needs to send messages +5. **Monitor usage:** Check for unauthorized access in BotFather + +--- + +## Complete Example + +Here's a minimal working example combining all steps: + +```python +# At top of server.py +import asyncio +from notification_system import init_notifier, send_notification_sync +notifier = None + +# In classify_image(), after prediction_history.appendleft(result) +if notifier and notifier.enabled and result.get('stage', 0) in [2, 3]: + try: + send_notification_sync(notifier, notifier.send_freshness_alert( + stage=result['stage'], + stage_name=result['stage_name'], + confidence=result['confidence'], + filename=result['filename'], + image_path=image_path, + stage_probabilities=result.get('stage_probabilities'), + hex_colors=result.get('hex_colors') + )) + except Exception as e: + print(f\"Notification error: {e}\") + +# In main(), after load_model() +global notifier +notifier = init_notifier() +if notifier and notifier.enabled: + print("✅ Telegram notifications enabled") + +# At module level +if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() +``` + +--- + +## Support + +For more information: +- [Setup Guide](setup_telegram.md) - Bot creation instructions +- [Architecture Documentation](ARCHITECTURE.md) - System design +- [Telegram Bot API](https://core.telegram.org/bots/api) - Official API docs + +If you encounter issues, check the server logs and verify your configuration with the `/api/notification-status` endpoint. diff --git a/config.py b/config.py index ed01ebb..3a257a3 100644 --- a/config.py +++ b/config.py @@ -114,3 +114,24 @@ def hour_to_stage(hours: int) -> int: HSV_BINS = (8, 8, 8) # histogram bins for H, S, V channels RGB_BINS = (8, 8, 8) # histogram bins for R, G, B channels N_DOMINANT_COLORS = 3 # k-means clusters for dominant color extraction + +# ─── Telegram Notification Settings ───────────────────────────────── +# Telegram Bot Configuration +# Get these values from BotFather (see setup_telegram.md for instructions) +TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", None) # Your bot token from BotFather +TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", None) # Your chat ID (can be user or group) + +# Notification Preferences +TELEGRAM_ENABLED = True # Enable/disable Telegram notifications globally +TELEGRAM_ALERT_ON_STAGES = [2, 3] # Send alerts for these stages (Early Spoilage, Spoiled) +TELEGRAM_PHOTO_MIN_STAGE = 2 # Minimum stage to send photos (saves bandwidth) +TELEGRAM_SEND_PHOTO = True # Include photo in notifications +TELEGRAM_DAILY_SUMMARY_ENABLED = True # Send daily summary reports +TELEGRAM_DAILY_SUMMARY_TIME = "18:00" # Time for daily summary (HH:MM format) +TELEGRAM_HEALTH_CHECK_INTERVAL = 3600 # Health check interval in seconds (3600 = 1 hour) +TELEGRAM_NOTIFY_ON_STARTUP = True # Send notification when server starts + +# Advanced Settings +TELEGRAM_MAX_RETRIES = 3 # Maximum retry attempts for failed notifications +TELEGRAM_RETRY_DELAY = 2 # Initial retry delay in seconds (exponential backoff) +TELEGRAM_RATE_LIMIT_BUFFER = 1 # Buffer time between messages in seconds diff --git a/notification_system.py b/notification_system.py new file mode 100644 index 0000000..798c989 --- /dev/null +++ b/notification_system.py @@ -0,0 +1,463 @@ +""" +Telegram Notification System for Freshness Monitor +=================================================== +Provides real-time alerts for freshness stage changes, daily summaries, +and system health monitoring via Telegram bot. + +Features: + - Async notification dispatch (non-blocking) + - Retry logic with exponential backoff + - Multiple notification types (alerts, summaries, health checks) + - Rich formatting with emojis and stage-colored indicators + - Error handling and logging + +Usage: + from notification_system import TelegramNotifier + + notifier = TelegramNotifier(bot_token, chat_id) + await notifier.send_freshness_alert(stage, confidence, image_path) +""" + +import asyncio +import logging +import os +import time +from datetime import datetime +from typing import Optional, Dict, List +from io import BytesIO + +try: + from telegram import Bot, InputFile + from telegram.error import TelegramError, RetryAfter, TimedOut + TELEGRAM_AVAILABLE = True +except ImportError: + TELEGRAM_AVAILABLE = False + logging.warning("python-telegram-bot not installed. Telegram notifications disabled.") + +import config + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class TelegramNotifier: + """ + Handles all Telegram notifications for the Freshness Monitor system. + Supports async operations to prevent blocking the main server. + """ + + # Stage emojis for visual appeal + STAGE_EMOJIS = { + 0: "🟢", # Very Fresh + 1: "🟡", # Fresh + 2: "🟠", # Early Spoilage + 3: "🔴", # Spoiled + } + + def __init__(self, bot_token: Optional[str] = None, chat_id: Optional[str] = None): + """ + Initialize the Telegram notifier. + + Args: + bot_token: Telegram bot token (optional, falls back to config) + chat_id: Telegram chat ID (optional, falls back to config) + """ + if not TELEGRAM_AVAILABLE: + logger.error("Telegram bot library not available. Install with: pip install python-telegram-bot") + self.enabled = False + return + + self.bot_token = bot_token or getattr(config, 'TELEGRAM_BOT_TOKEN', None) + self.chat_id = chat_id or getattr(config, 'TELEGRAM_CHAT_ID', None) + + # Validate configuration + if not self.bot_token or not self.chat_id: + logger.warning("Telegram bot token or chat ID not configured. Notifications disabled.") + self.enabled = False + return + + # Initialize bot + try: + self.bot = Bot(token=self.bot_token) + self.enabled = True + logger.info("Telegram notifier initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize Telegram bot: {e}") + self.enabled = False + + async def _send_message_with_retry( + self, + text: str, + max_retries: int = 3, + parse_mode: str = "HTML" + ) -> bool: + """ + Send a message with retry logic and exponential backoff. + + Args: + text: Message text to send + max_retries: Maximum number of retry attempts + parse_mode: Message formatting (HTML or Markdown) + + Returns: + True if message sent successfully, False otherwise + """ + if not self.enabled: + logger.debug("Telegram notifications disabled, skipping message") + return False + + for attempt in range(max_retries): + try: + await self.bot.send_message( + chat_id=self.chat_id, + text=text, + parse_mode=parse_mode + ) + logger.info("Telegram message sent successfully") + return True + + except RetryAfter as e: + wait_time = e.retry_after + logger.warning(f"Rate limited. Retrying after {wait_time} seconds...") + await asyncio.sleep(wait_time) + + except TimedOut: + wait_time = 2 ** attempt # Exponential backoff + logger.warning(f"Request timed out. Retrying in {wait_time} seconds...") + await asyncio.sleep(wait_time) + + except TelegramError as e: + logger.error(f"Telegram API error: {e}") + if attempt < max_retries - 1: + await asyncio.sleep(2 ** attempt) + else: + return False + + except Exception as e: + logger.error(f"Unexpected error sending Telegram message: {e}") + return False + + return False + + async def _send_photo_with_retry( + self, + photo_path: str, + caption: str, + max_retries: int = 3, + parse_mode: str = "HTML" + ) -> bool: + """ + Send a photo with caption with retry logic. + + Args: + photo_path: Path to the image file + caption: Photo caption text + max_retries: Maximum number of retry attempts + parse_mode: Caption formatting + + Returns: + True if photo sent successfully, False otherwise + """ + if not self.enabled: + return False + + if not os.path.exists(photo_path): + logger.error(f"Image file not found: {photo_path}") + return False + + for attempt in range(max_retries): + try: + with open(photo_path, 'rb') as photo_file: + await self.bot.send_photo( + chat_id=self.chat_id, + photo=InputFile(photo_file), + caption=caption, + parse_mode=parse_mode + ) + logger.info("Telegram photo sent successfully") + return True + + except RetryAfter as e: + wait_time = e.retry_after + logger.warning(f"Rate limited. Retrying after {wait_time} seconds...") + await asyncio.sleep(wait_time) + + except TimedOut: + wait_time = 2 ** attempt + logger.warning(f"Request timed out. Retrying in {wait_time} seconds...") + await asyncio.sleep(wait_time) + + except TelegramError as e: + logger.error(f"Telegram API error: {e}") + if attempt < max_retries - 1: + await asyncio.sleep(2 ** attempt) + else: + return False + + except Exception as e: + logger.error(f"Unexpected error sending Telegram photo: {e}") + return False + + return False + + async def send_freshness_alert( + self, + stage: int, + stage_name: str, + confidence: float, + filename: str, + image_path: Optional[str] = None, + stage_probabilities: Optional[Dict] = None, + hex_colors: Optional[Dict] = None + ) -> bool: + """ + Send a freshness stage alert with detailed information. + + Args: + stage: Freshness stage number (0-3) + stage_name: Human-readable stage name + confidence: Prediction confidence (0-1) + filename: Name of the analyzed file + image_path: Optional path to the image file + stage_probabilities: Optional dict of stage probabilities + hex_colors: Optional dict of dominant hex colors + + Returns: + True if alert sent successfully + """ + if not self.enabled: + return False + + emoji = self.STAGE_EMOJIS.get(stage, "⚪") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Build message with rich formatting + message = f"{emoji} Freshness Alert {emoji}\n\n" + message += f"Stage: {stage_name}\n" + message += f"Confidence: {confidence * 100:.1f}%\n" + message += f"File: {filename}\n" + message += f"Time: {timestamp}\n" + + # Add stage probabilities if available + if stage_probabilities: + message += "\nStage Probabilities:\n" + for stage_label, prob in stage_probabilities.items(): + emoji_icon = self.STAGE_EMOJIS.get( + list(config.LABEL_NAMES.values()).index(stage_label) + if stage_label in config.LABEL_NAMES.values() else 0, + "⚪" + ) + message += f"{emoji_icon} {stage_label}: {prob * 100:.1f}%\n" + + # Add dominant colors if available + if hex_colors: + message += f"\nDominant Colors: " + message += ", ".join(hex_colors.values()) + + # Add action recommendations based on stage + if stage >= 2: # Early Spoilage or worse + message += f"\n\n⚠️ Action Required: " + if stage == 2: + message += "Product is entering early spoilage stage. Consider consumption soon." + elif stage == 3: + message += "Product has spoiled. Do not consume!" + + # Send photo if available and stage is concerning + if image_path and stage >= getattr(config, 'TELEGRAM_PHOTO_MIN_STAGE', 2): + caption = f"{emoji} {stage_name} ({confidence * 100:.1f}% confidence)" + success = await self._send_photo_with_retry(image_path, caption) + if success: + # Send detailed message separately + return await self._send_message_with_retry(message) + else: + # Fallback to text-only if photo fails + return await self._send_message_with_retry(message) + else: + # Send text-only message + return await self._send_message_with_retry(message) + + async def send_daily_summary(self, predictions: List[Dict]) -> bool: + """ + Send a daily summary of all predictions. + + Args: + predictions: List of prediction dictionaries + + Returns: + True if summary sent successfully + """ + if not self.enabled or not predictions: + return False + + timestamp = datetime.now().strftime("%Y-%m-%d") + + # Count predictions by stage + stage_counts = {0: 0, 1: 0, 2: 0, 3: 0} + for pred in predictions: + stage = pred.get('stage', 0) + stage_counts[stage] = stage_counts.get(stage, 0) + 1 + + # Build summary message + message = f"📊 Daily Summary - {timestamp}\n\n" + message += f"Total Predictions: {len(predictions)}\n\n" + + message += "Breakdown by Stage:\n" + for stage, count in sorted(stage_counts.items()): + if count > 0: + emoji = self.STAGE_EMOJIS.get(stage, "⚪") + stage_name = config.LABEL_NAMES.get(stage, f"Stage {stage}") + percentage = (count / len(predictions)) * 100 + message += f"{emoji} {stage_name}: {count} ({percentage:.1f}%)\n" + + # Add warnings if needed + warning_count = stage_counts.get(2, 0) + stage_counts.get(3, 0) + if warning_count > 0: + message += f"\n⚠️ {warning_count} items require attention!" + else: + message += f"\n✅ All items are fresh!" + + return await self._send_message_with_retry(message) + + async def send_system_health( + self, + status: str, + uptime_seconds: int, + total_predictions: int, + model_loaded: bool, + last_prediction: Optional[Dict] = None + ) -> bool: + """ + Send system health status notification. + + Args: + status: System status (running, warning, error) + uptime_seconds: Server uptime in seconds + total_predictions: Total number of predictions made + model_loaded: Whether ML model is loaded + last_prediction: Last prediction data (optional) + + Returns: + True if health report sent successfully + """ + if not self.enabled: + return False + + # Calculate uptime + uptime_hours = uptime_seconds // 3600 + uptime_mins = (uptime_seconds % 3600) // 60 + + # Status emoji + status_emoji = { + 'running': '✅', + 'warning': '⚠️', + 'error': '❌', + 'starting': '🔄' + }.get(status.lower(), '❓') + + # Build health message + message = f"{status_emoji} System Health Report\n\n" + message += f"Status: {status.upper()}\n" + message += f"Uptime: {uptime_hours}h {uptime_mins}m\n" + message += f"Total Predictions: {total_predictions}\n" + message += f"Model Status: {'✅ Loaded' if model_loaded else '❌ Not Loaded'}\n" + + if last_prediction: + stage_name = last_prediction.get('stage_name', 'Unknown') + timestamp = last_prediction.get('timestamp', 'N/A') + emoji = self.STAGE_EMOJIS.get(last_prediction.get('stage', 0), "⚪") + message += f"\nLast Prediction:\n" + message += f"{emoji} {stage_name}\n" + message += f"Time: {timestamp}\n" + + return await self._send_message_with_retry(message) + + async def send_test_notification(self) -> bool: + """ + Send a test notification to verify configuration. + + Returns: + True if test message sent successfully + """ + if not self.enabled: + return False + + message = "🧪 Test Notification\n\n" + message += "Freshness Monitor Telegram notifications are working correctly!\n\n" + message += f"Bot Token: {self.bot_token[:10]}...\n" + message += f"Chat ID: {self.chat_id}\n" + message += f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + return await self._send_message_with_retry(message) + + async def send_error_alert(self, error_type: str, error_message: str) -> bool: + """ + Send an error alert notification. + + Args: + error_type: Type of error (e.g., "Model Loading", "Prediction") + error_message: Detailed error message + + Returns: + True if error alert sent successfully + """ + if not self.enabled: + return False + + message = f"❌ System Error Alert\n\n" + message += f"Error Type: {error_type}\n" + message += f"Message: {error_message}\n" + message += f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" + message += "⚠️ Please check the system logs for more details." + + return await self._send_message_with_retry(message) + + +# Convenience function for sync contexts +def send_notification_sync(notifier: TelegramNotifier, coro): + """ + Helper function to send notifications from synchronous code. + Creates a new event loop if needed. + + Args: + notifier: TelegramNotifier instance + coro: Coroutine to execute + + Returns: + Result of the coroutine + """ + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + # If loop is already running, schedule the task + asyncio.create_task(coro) + return True + else: + return loop.run_until_complete(coro) + except RuntimeError: + # No event loop exists, create a new one + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + +# Global notifier instance (initialized in server.py) +_global_notifier: Optional[TelegramNotifier] = None + + +def get_notifier() -> Optional[TelegramNotifier]: + """Get the global notifier instance.""" + return _global_notifier + + +def init_notifier(bot_token: Optional[str] = None, chat_id: Optional[str] = None): + """Initialize the global notifier instance.""" + global _global_notifier + _global_notifier = TelegramNotifier(bot_token, chat_id) + return _global_notifier diff --git a/requirements.txt b/requirements.txt index 2ec3cf5..a748e6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ Pillow>=10.0 joblib>=1.3 qrcode>=7.4 gunicorn>=21.2 +python-telegram-bot>=20.7 diff --git a/server_telegram_patch.py b/server_telegram_patch.py new file mode 100644 index 0000000..31d91b1 --- /dev/null +++ b/server_telegram_patch.py @@ -0,0 +1,190 @@ +""" +Telegram Integration Patch for server.py +========================================== + +Add these code blocks to your server.py file: + +1. At the top, after other imports: +""" + +# ─── Telegram Notification Import ─────────────────────────────────── +import asyncio +from notification_system import init_notifier, send_notification_sync + +# Initialize global notifier +notifier = None + +""" +2. Modify the classify_image function to add notification trigger. + Find the classify_image function and add this code after prediction_history.appendleft(result): +""" + + # Telegram notification for concerning stages + if notifier and notifier.enabled: + stage = result.get('stage', 0) + if stage in getattr(config, 'TELEGRAM_ALERT_ON_STAGES', [2, 3]): + # Send notification asynchronously (non-blocking) + try: + send_notification_sync( + notifier, + notifier.send_freshness_alert( + stage=stage, + stage_name=result['stage_name'], + confidence=result['confidence'], + filename=result['filename'], + image_path=image_path if getattr(config, 'TELEGRAM_SEND_PHOTO', True) else None, + stage_probabilities=result.get('stage_probabilities'), + hex_colors=result.get('hex_colors') + ) + ) + except Exception as e: + logger.warning(f"Failed to send Telegram notification: {e}") + +""" +3. Add new API endpoint for testing notifications. + Add this before the main() function: +""" + +@app.route("/api/test-notification", methods=["POST"]) +def test_notification(): + """Test Telegram notification system.""" + if not notifier or not notifier.enabled: + return jsonify({ + "error": "Telegram notifications not configured", + "message": "Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables" + }), 503 + + try: + success = send_notification_sync(notifier, notifier.send_test_notification()) + if success: + return jsonify({ + "success": True, + "message": "Test notification sent successfully!" + }) + else: + return jsonify({ + "success": False, + "message": "Failed to send test notification" + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": str(e) + }), 500 + + +@app.route("/api/notification-status", methods=["GET"]) +def notification_status(): + """Get Telegram notification system status.""" + if not notifier: + return jsonify({ + "enabled": False, + "configured": False, + "message": "Notifier not initialized" + }) + + return jsonify({ + "enabled": notifier.enabled, + "configured": bool(notifier.bot_token and notifier.chat_id), + "bot_token_set": bool(notifier.bot_token), + "chat_id_set": bool(notifier.chat_id), + "alert_stages": getattr(config, 'TELEGRAM_ALERT_ON_STAGES', [2, 3]), + "send_photos": getattr(config, 'TELEGRAM_SEND_PHOTO', True) + }) + + +""" +4. Modify the main() function to initialize the notifier. + Add this code at the beginning of main(), right after load_model(): +""" + + # Initialize Telegram notifier + global notifier + if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() + if notifier and notifier.enabled: + print("Telegram notifications: ENABLED") + # Send startup notification if configured + if getattr(config, 'TELEGRAM_NOTIFY_ON_STARTUP', True): + try: + send_notification_sync( + notifier, + notifier.send_system_health( + status="starting", + uptime_seconds=0, + total_predictions=0, + model_loaded=True + ) + ) + except Exception as e: + print(f"Failed to send startup notification: {e}") + else: + print("Telegram notifications: DISABLED (check configuration)") + else: + print("Telegram notifications: DISABLED (via config)") + + +""" +5. Also initialize notifier at module level (for gunicorn). + Add this code after the module-level initialization section: +""" + +# Initialize notifier for gunicorn/production +if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() + +""" + +=========================================== +COMPLETE INTEGRATION INSTRUCTIONS: +=========================================== + +To integrate Telegram notifications into server.py: + +1. Import the notification system at the top: + ```python + import asyncio + from notification_system import init_notifier, send_notification_sync + notifier = None + ``` + +2. In classify_image(), after `prediction_history.appendleft(result)`, add: + ```python + # Telegram notification + if notifier and notifier.enabled and result.get('stage', 0) in getattr(config, 'TELEGRAM_ALERT_ON_STAGES', [2, 3]): + try: + send_notification_sync(notifier, notifier.send_freshness_alert( + stage=result['stage'], + stage_name=result['stage_name'], + confidence=result['confidence'], + filename=result['filename'], + image_path=image_path, + stage_probabilities=result.get('stage_probabilities'), + hex_colors=result.get('hex_colors') + )) + except Exception as e: + print(f"Telegram notification error: {e}") + ``` + +3. Add the test endpoint before main(): + Copy the /api/test-notification and /api/notification-status endpoints from above. + +4. In main(), after load_model(), add: + ```python + global notifier + if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() + if notifier and notifier.enabled: + print("Telegram notifications: ENABLED") + else: + print("Telegram notifications: DISABLED") + ``` + +5. At module level, after server_start_time initialization: + ```python + if getattr(config, 'TELEGRAM_ENABLED', True): + notifier = init_notifier() + ``` + +That's it! The system will now send notifications automatically. +""" diff --git a/setup_telegram.md b/setup_telegram.md new file mode 100644 index 0000000..92b27fe --- /dev/null +++ b/setup_telegram.md @@ -0,0 +1,425 @@ +# Telegram Notification Setup Guide + +This guide walks you through setting up Telegram notifications for the Freshness Monitor system. + +## 📱 Overview + +The Telegram notification system provides real-time alerts for: +- **Freshness Warnings**: Notifications when products reach Stage 3 (Early Spoilage) or Stage 4 (Spoiled) +- **Daily Summaries**: End-of-day reports with statistics +- **System Health**: Periodic health checks and startup notifications +- **Error Alerts**: Critical system errors + +--- + +## 🤖 Step 1: Create a Telegram Bot + +1. **Open Telegram** and search for `@BotFather` (the official bot creation tool) + +2. **Start a conversation** with BotFather by clicking "Start" or sending `/start` + +3. **Create a new bot** by sending the command: + ``` + /newbot + ``` + +4. **Choose a name** for your bot (this is the display name users will see): + ``` + Example: Freshness Monitor Bot + ``` + +5. **Choose a username** for your bot (must end in 'bot'): + ``` + Example: freshness_monitor_bot + ``` + +6. **Save your bot token** - BotFather will send you a message like: + ``` + Done! Congratulations on your new bot. You will find it at t.me/freshness_monitor_bot. + You can now add a description... + + Use this token to access the HTTP API: + 1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890 + + Keep your token secure and store it safely, it can be used by anyone to control your bot. + ``` + + ⚠️ **Keep this token secure!** Anyone with this token can control your bot. + +--- + +## 👤 Step 2: Get Your Chat ID + +You need your Chat ID to receive messages from the bot. + +### Option A: Using a Bot (Easiest) + +1. **Search for** `@userinfobot` or `@get_id_bot` in Telegram + +2. **Start the bot** and it will immediately send you your Chat ID: + ``` + Your Chat ID: 123456789 + ``` + +### Option B: Manual Method + +1. **Send a message** to your newly created bot (search for it using the username from Step 1) + +2. **Visit this URL** in your browser (replace `YOUR_BOT_TOKEN` with your actual token): + ``` + https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates + ``` + +3. **Look for the Chat ID** in the JSON response: + ```json + { + "ok": true, + "result": [{ + "message": { + "chat": { + "id": 123456789, + ... + } + } + }] + } + ``` + +### For Group Notifications + +If you want notifications in a Telegram group: + +1. **Create a group** in Telegram +2. **Add your bot** to the group as a member +3. **Send a message** in the group (mention the bot with `@your_bot_username`) +4. **Get the group Chat ID** using Option B above (group IDs are negative numbers like `-123456789`) + +--- + +## ⚙️ Step 3: Configure the Freshness Monitor + +### Method 1: Environment Variables (Recommended for Production) + +Set environment variables on your system: + +**Linux/Mac:** +```bash +export TELEGRAM_BOT_TOKEN="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890" +export TELEGRAM_CHAT_ID="123456789" +``` + +**Windows (Command Prompt):** +```cmd +set TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890 +set TELEGRAM_CHAT_ID=123456789 +``` + +**Windows (PowerShell):** +```powershell +$env:TELEGRAM_BOT_TOKEN="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890" +$env:TELEGRAM_CHAT_ID="123456789" +``` + +**For Render.com or other cloud platforms:** +- Go to your service's environment variables settings +- Add `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` as environment variables + +### Method 2: Direct Configuration (Development Only) + +⚠️ **Not recommended for production** - tokens will be visible in your code! + +Edit `config.py` and replace the values: +```python +TELEGRAM_BOT_TOKEN = "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890" +TELEGRAM_CHAT_ID = "123456789" +``` + +--- + +## 📦 Step 4: Install Dependencies + +Install the required Python package: + +```bash +pip install python-telegram-bot==20.7 +``` + +Or install all dependencies: +```bash +pip install -r requirements.txt +``` + +--- + +## 🧪 Step 5: Test Your Configuration + +### Using the API Endpoint + +Start your server: +```bash +python server.py +``` + +Then send a test request: +```bash +curl -X POST http://localhost:5000/api/test-notification +``` + +You should receive a test message in Telegram! + +### Using Python Directly + +Create a test script `test_telegram.py`: +```python +import asyncio +from notification_system import TelegramNotifier + +async def test(): + notifier = TelegramNotifier() + success = await notifier.send_test_notification() + print(f"Test notification sent: {success}") + +asyncio.run(test()) +``` + +Run it: +```bash +python test_telegram.py +``` + +--- + +## 🎛️ Step 6: Customize Notification Preferences + +Edit `config.py` to adjust notification settings: + +```python +# Which stages trigger alerts? (0=Very Fresh, 1=Fresh, 2=Early Spoilage, 3=Spoiled) +TELEGRAM_ALERT_ON_STAGES = [2, 3] # Only alert on concerning stages + +# Send photos with notifications? (uses more bandwidth) +TELEGRAM_SEND_PHOTO = True + +# Minimum stage to include photos (saves bandwidth for fresh products) +TELEGRAM_PHOTO_MIN_STAGE = 2 + +# Daily summary report +TELEGRAM_DAILY_SUMMARY_ENABLED = True +TELEGRAM_DAILY_SUMMARY_TIME = "18:00" # 6 PM daily summary + +# Health checks every hour +TELEGRAM_HEALTH_CHECK_INTERVAL = 3600 # seconds + +# Notify when server starts +TELEGRAM_NOTIFY_ON_STARTUP = True +``` + +--- + +## 📱 Step 7: Bot Commands (Optional) + +You can add custom commands to your bot via BotFather: + +1. Send `/setcommands` to BotFather +2. Select your bot +3. Add these commands: + ``` + start - Start receiving notifications + status - Get system status + summary - Get today's summary + help - Show help message + ``` + +Then implement these commands in your bot (see Advanced Features below). + +--- + +## 🔒 Security Best Practices + +1. **Never commit tokens to Git** + - Always use environment variables + - Add `.env` files to `.gitignore` + +2. **Restrict bot permissions** + - Your bot only needs to send messages + - Disable unused privacy settings in BotFather + +3. **Use group IDs carefully** + - Only add your bot to trusted groups + - Monitor group members to prevent unauthorized access + +4. **Rotate tokens periodically** + - Use `/token` command in BotFather to generate new tokens + - Update your configuration when rotating + +--- + +## 🐛 Troubleshooting + +### "Bot token not configured" Error + +**Problem:** Server starts but notifications don't work. + +**Solution:** +- Check that environment variables are set correctly +- Verify the token format (should be like `1234567890:ABC...`) +- Make sure you're using the correct token from BotFather + +### "Chat not found" Error + +**Problem:** Bot sends messages but you don't receive them. + +**Solution:** +- Verify your Chat ID is correct +- Make sure you've started a conversation with the bot +- For groups, ensure the bot is a member and has permission to send messages + +### Messages Not Appearing + +**Problem:** No errors but messages don't arrive. + +**Solution:** +- Check if bot is blocked in Telegram +- Verify internet connectivity on the server +- Check server logs for rate limiting issues +- Try the test notification endpoint + +### Import Error: "No module named 'telegram'" + +**Problem:** Python can't find the telegram library. + +**Solution:** +```bash +pip install python-telegram-bot==20.7 +``` + +### Async Warnings in Logs + +**Problem:** Warnings about event loops. + +**Solution:** This is normal in some environments. Notifications will still work, but you can suppress warnings by upgrading to Python 3.9+. + +--- + +## 🚀 Advanced Features + +### Multiple Chat IDs + +To send notifications to multiple users/groups, modify `config.py`: + +```python +TELEGRAM_CHAT_ID = "123456789,987654321,-111222333" # Comma-separated +``` + +Then update `notification_system.py` to loop through all IDs. + +### Custom Alert Rules + +Create custom notification rules in `server.py`: + +```python +# Alert only during business hours +import datetime +current_hour = datetime.datetime.now().hour +if 9 <= current_hour <= 17 and stage >= 2: + send_notification_sync(notifier, notifier.send_freshness_alert(...)) +``` + +### Scheduled Reports + +Use `schedule` library for timed reports: + +```bash +pip install schedule +``` + +```python +import schedule +import time + +def send_daily_report(): + # Send summary notification + pass + +schedule.every().day.at("18:00").do(send_daily_report) + +while True: + schedule.run_pending() + time.sleep(60) +``` + +--- + +## 📊 Notification Examples + +### Freshness Alert + +``` +🟠 Freshness Alert 🟠 + +Stage: Stage 3 - Early Spoilage +Confidence: 87.3% +File: sample_12h.jpg +Time: 2026-03-04 16:45:22 + +Stage Probabilities: +🟢 Stage 1 - Very Fresh: 2.1% +🟡 Stage 2 - Fresh: 8.4% +🟠 Stage 3 - Early Spoilage: 87.3% +🔴 Stage 4 - Spoiled: 2.2% + +Dominant Colors: #c5c7c1, #7d7764, #a4a598 + +⚠️ Action Required: Product is entering early spoilage stage. Consider consumption soon. +``` + +### Daily Summary + +``` +📊 Daily Summary - 2026-03-04 + +Total Predictions: 24 + +Breakdown by Stage: +🟢 Stage 1 - Very Fresh: 8 (33.3%) +🟡 Stage 2 - Fresh: 10 (41.7%) +🟠 Stage 3 - Early Spoilage: 5 (20.8%) +🔴 Stage 4 - Spoiled: 1 (4.2%) + +⚠️ 6 items require attention! +``` + +### System Health + +``` +✅ System Health Report + +Status: RUNNING +Uptime: 12h 34m +Total Predictions: 156 +Model Status: ✅ Loaded + +Last Prediction: +🟢 Stage 1 - Very Fresh +Time: 2026-03-04T16:45:22 +``` + +--- + +## 📚 Additional Resources + +- [Telegram Bot API Documentation](https://core.telegram.org/bots/api) +- [python-telegram-bot Library](https://github.com/python-telegram-bot/python-telegram-bot) +- [BotFather Commands Reference](https://core.telegram.org/bots/features#botfather) + +--- + +## 💬 Support + +If you encounter issues: +1. Check the troubleshooting section above +2. Review server logs for error messages +3. Test with the `/api/test-notification` endpoint +4. Verify your bot token and chat ID are correct + +For questions about the Freshness Monitor system, see the main [README.md](README.md).