from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from shared.config import settings from shared.database import get_db from services.user_service.main import get_current_user from services.user_service.models import User from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any from datetime import datetime import httpx import asyncio import json app = FastAPI(title="Notification Service", version="1.0.0") # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class NotificationRequest(BaseModel): title: str = Field(..., max_length=100) body: str = Field(..., max_length=500) data: Optional[Dict[str, Any]] = None priority: str = Field("normal", pattern="^(low|normal|high)$") class EmergencyNotificationRequest(BaseModel): alert_id: int user_ids: List[int] alert_type: Optional[str] = "general" location: Optional[str] = None class DeviceToken(BaseModel): token: str = Field(..., min_length=10) platform: str = Field(..., pattern="^(ios|android|web)$") class NotificationStats(BaseModel): total_sent: int successful_deliveries: int failed_deliveries: int emergency_notifications: int # Mock FCM client for demonstration class FCMClient: def __init__(self, server_key: str): self.server_key = server_key self.fcm_url = "https://fcm.googleapis.com/fcm/send" async def send_notification(self, tokens: List[str], notification_data: dict) -> dict: """Send push notification via FCM""" if not self.server_key: print("FCM Server Key not configured - notification would be sent") return {"success_count": len(tokens), "failure_count": 0} headers = { "Authorization": f"key={self.server_key}", "Content-Type": "application/json" } payload = { "registration_ids": tokens, "notification": { "title": notification_data.get("title"), "body": notification_data.get("body"), "sound": "default" }, "data": notification_data.get("data", {}), "priority": "high" if notification_data.get("priority") == "high" else "normal" } try: async with httpx.AsyncClient() as client: response = await client.post( self.fcm_url, headers=headers, json=payload, timeout=10.0 ) result = response.json() return { "success_count": result.get("success", 0), "failure_count": result.get("failure", 0), "results": result.get("results", []) } except Exception as e: print(f"FCM Error: {e}") return {"success_count": 0, "failure_count": len(tokens)} # Initialize FCM client fcm_client = FCMClient(settings.FCM_SERVER_KEY) # In-memory storage for demo (use Redis or database in production) user_device_tokens: Dict[int, List[str]] = {} notification_stats = { "total_sent": 0, "successful_deliveries": 0, "failed_deliveries": 0, "emergency_notifications": 0 } @app.post("/api/v1/register-device") async def register_device_token( device_data: DeviceToken, current_user: User = Depends(get_current_user) ): """Register device token for push notifications""" if current_user.id not in user_device_tokens: user_device_tokens[current_user.id] = [] # Remove existing token if present if device_data.token in user_device_tokens[current_user.id]: user_device_tokens[current_user.id].remove(device_data.token) # Add new token user_device_tokens[current_user.id].append(device_data.token) # Keep only last 3 tokens per user user_device_tokens[current_user.id] = user_device_tokens[current_user.id][-3:] return {"message": "Device token registered successfully"} @app.post("/api/v1/send-notification") async def send_notification( notification: NotificationRequest, target_user_id: int, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Send notification to specific user""" # Check if target user exists and accepts notifications result = await db.execute(select(User).filter(User.id == target_user_id)) target_user = result.scalars().first() if not target_user: raise HTTPException(status_code=404, detail="Target user not found") if not target_user.push_notifications_enabled: raise HTTPException(status_code=403, detail="User has disabled push notifications") # Get user's device tokens tokens = user_device_tokens.get(target_user_id, []) if not tokens: raise HTTPException(status_code=400, detail="No device tokens found for user") # Send notification in background background_tasks.add_task( send_push_notification, tokens, { "title": notification.title, "body": notification.body, "data": notification.data or {}, "priority": notification.priority } ) return {"message": "Notification queued for delivery"} @app.post("/api/v1/send-emergency-notifications") async def send_emergency_notifications( emergency_data: EmergencyNotificationRequest, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db) ): """Send emergency notifications to nearby users""" if not emergency_data.user_ids: return {"message": "No users to notify"} # Get users who have emergency notifications enabled result = await db.execute( select(User).filter( User.id.in_(emergency_data.user_ids), User.emergency_notifications_enabled == True, User.is_active == True ) ) users = result.scalars().all() # Collect all device tokens all_tokens = [] for user in users: tokens = user_device_tokens.get(user.id, []) all_tokens.extend(tokens) if not all_tokens: return {"message": "No device tokens found for target users"} # Prepare emergency notification emergency_title = "🚨 Emergency Alert Nearby" emergency_body = f"Someone needs help in your area. Alert type: {emergency_data.alert_type}" if emergency_data.location: emergency_body += f" Location: {emergency_data.location}" notification_data = { "title": emergency_title, "body": emergency_body, "data": { "type": "emergency", "alert_id": str(emergency_data.alert_id), "alert_type": emergency_data.alert_type }, "priority": "high" } # Send notifications in background background_tasks.add_task( send_emergency_push_notification, all_tokens, notification_data ) return {"message": f"Emergency notifications queued for {len(users)} users"} async def send_push_notification(tokens: List[str], notification_data: dict): """Send push notification using FCM""" try: result = await fcm_client.send_notification(tokens, notification_data) # Update stats notification_stats["total_sent"] += len(tokens) notification_stats["successful_deliveries"] += result["success_count"] notification_stats["failed_deliveries"] += result["failure_count"] print(f"Notification sent: {result['success_count']} successful, {result['failure_count']} failed") except Exception as e: print(f"Failed to send notification: {e}") notification_stats["failed_deliveries"] += len(tokens) async def send_emergency_push_notification(tokens: List[str], notification_data: dict): """Send emergency push notification with special handling""" try: # Emergency notifications are sent immediately with high priority result = await fcm_client.send_notification(tokens, notification_data) # Update stats notification_stats["total_sent"] += len(tokens) notification_stats["successful_deliveries"] += result["success_count"] notification_stats["failed_deliveries"] += result["failure_count"] notification_stats["emergency_notifications"] += len(tokens) print(f"Emergency notification sent: {result['success_count']} successful, {result['failure_count']} failed") except Exception as e: print(f"Failed to send emergency notification: {e}") notification_stats["emergency_notifications"] += len(tokens) notification_stats["failed_deliveries"] += len(tokens) @app.post("/api/v1/send-calendar-reminder") async def send_calendar_reminder( title: str, message: str, user_ids: List[int], background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db) ): """Send calendar reminder notifications""" # Get users who have notifications enabled result = await db.execute( select(User).filter( User.id.in_(user_ids), User.push_notifications_enabled == True, User.is_active == True ) ) users = result.scalars().all() # Send notifications to each user for user in users: tokens = user_device_tokens.get(user.id, []) if tokens: background_tasks.add_task( send_push_notification, tokens, { "title": title, "body": message, "data": {"type": "calendar_reminder"}, "priority": "normal" } ) return {"message": f"Calendar reminders queued for {len(users)} users"} @app.delete("/api/v1/device-token") async def unregister_device_token( token: str, current_user: User = Depends(get_current_user) ): """Unregister device token""" if current_user.id in user_device_tokens: tokens = user_device_tokens[current_user.id] if token in tokens: tokens.remove(token) if not tokens: del user_device_tokens[current_user.id] return {"message": "Device token unregistered successfully"} @app.get("/api/v1/my-devices") async def get_my_device_tokens( current_user: User = Depends(get_current_user) ): """Get user's registered device tokens (masked for security)""" tokens = user_device_tokens.get(current_user.id, []) masked_tokens = [f"{token[:8]}...{token[-8:]}" for token in tokens] return { "device_count": len(tokens), "tokens": masked_tokens } @app.get("/api/v1/stats", response_model=NotificationStats) async def get_notification_stats(current_user: User = Depends(get_current_user)): """Get notification service statistics""" return NotificationStats(**notification_stats) @app.get("/api/v1/health") async def health_check(): """Health check endpoint""" return { "status": "healthy", "service": "notification-service", "fcm_configured": bool(settings.FCM_SERVER_KEY) } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8005)