import asyncio import json import logging from datetime import datetime, timedelta from typing import List, Optional, Dict, Set import math import httpx from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, status, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy import func, select, update, desc, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession from services.emergency_service.models import EmergencyAlert, EmergencyResponse, EmergencyReport, SafetyCheck from services.emergency_service.schemas import ( AlertStatus, AlertType, EmergencyAlertCreate, EmergencyAlertResponse, EmergencyAlertUpdate, EmergencyResponseCreate, EmergencyResponseResponse, EmergencyStatistics, EmergencyReportCreate, EmergencyReportResponse, NearbyAlertResponse, SafetyCheckCreate, SafetyCheckResponse, EmergencyEventDetails, UserInfo, ) # Import User model from user_service from services.user_service.models import User from shared.auth import get_current_user_from_token from shared.config import settings from shared.database import AsyncSessionLocal # Database dependency async def get_db(): async with AsyncSessionLocal() as session: try: yield session except Exception: await session.rollback() raise finally: await session.close() app = FastAPI( title="Emergency Service", version="1.0.0", description=""" Emergency Service API для системы безопасности женщин. ## Авторизация Все эндпоинты требуют Bearer токен в заголовке Authorization. Получить токен можно через User Service: ``` POST /api/v1/auth/login ``` Использование токена: ``` Authorization: Bearer ``` """, contact={ "name": "Women's Safety App Team", "url": "https://example.com/support", "email": "support@example.com", }, ) # Configure logger logger = logging.getLogger(__name__) # Security scheme for OpenAPI documentation security = HTTPBearer( scheme_name="JWT Bearer Token", description="JWT Bearer токен для авторизации. Получите токен через User Service /api/v1/auth/login" ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class WebSocketManager: """Manage WebSocket connections for emergency notifications""" def __init__(self): self.active_connections: Dict[int, WebSocket] = {} self.connection_info: Dict[int, dict] = {} # Дополнительная информация о подключениях async def connect(self, websocket: WebSocket, user_id: int): """Connect a WebSocket for a specific user""" await websocket.accept() self.active_connections[user_id] = websocket # Сохраняем информацию о подключении self.connection_info[user_id] = { "connected_at": datetime.now(), "client_host": websocket.client.host if websocket.client else "unknown", "client_port": websocket.client.port if websocket.client else 0, "last_ping": datetime.now(), "message_count": 0, "status": "connected" } print(f"WebSocket connected for user {user_id} from {websocket.client}") # Отправляем приветственное сообщение await self.send_personal_message(json.dumps({ "type": "connection_established", "message": "WebSocket connection established successfully", "user_id": user_id, "timestamp": datetime.now().isoformat() }), user_id) def disconnect(self, user_id: int): """Disconnect a WebSocket for a specific user""" if user_id in self.active_connections: del self.active_connections[user_id] if user_id in self.connection_info: self.connection_info[user_id]["status"] = "disconnected" self.connection_info[user_id]["disconnected_at"] = datetime.now() print(f"WebSocket disconnected for user {user_id}") async def send_personal_message(self, message: str, user_id: int): """Send a message to a specific user""" if user_id in self.active_connections: websocket = self.active_connections[user_id] try: await websocket.send_text(message) # Обновляем статистику if user_id in self.connection_info: self.connection_info[user_id]["message_count"] += 1 self.connection_info[user_id]["last_ping"] = datetime.now() except Exception as e: print(f"Error sending message to user {user_id}: {e}") self.disconnect(user_id) async def broadcast_alert(self, alert_data: dict, user_ids: Optional[List[int]] = None): """Broadcast alert to specific users or all connected users""" message = json.dumps({ "type": "emergency_alert", "data": alert_data }) target_users = user_ids if user_ids else list(self.active_connections.keys()) for user_id in target_users: await self.send_personal_message(message, user_id) async def send_alert_update(self, alert_id: int, alert_data: dict, user_ids: Optional[List[int]] = None): """Send alert update to specific users""" message = json.dumps({ "type": "alert_update", "alert_id": alert_id, "data": alert_data }) target_users = user_ids if user_ids else list(self.active_connections.keys()) for user_id in target_users: await self.send_personal_message(message, user_id) def get_connected_users_count(self) -> int: """Получить количество подключенных пользователей""" return len(self.active_connections) def get_connected_users_list(self) -> List[int]: """Получить список ID подключенных пользователей""" return list(self.active_connections.keys()) def get_connection_info(self, user_id: Optional[int] = None) -> dict: """Получить информацию о подключениях""" if user_id: return self.connection_info.get(user_id, {}) # Возвращаем общую статистику active_count = len(self.active_connections) total_messages = sum(info.get("message_count", 0) for info in self.connection_info.values()) connection_details = {} for user_id, info in self.connection_info.items(): if info.get("status") == "connected": connected_at = info.get("connected_at") last_ping = info.get("last_ping") connection_details[user_id] = { "connected_at": connected_at.isoformat() if connected_at else None, "client_host": info.get("client_host"), "client_port": info.get("client_port"), "last_ping": last_ping.isoformat() if last_ping else None, "message_count": info.get("message_count", 0), "status": info.get("status"), "duration_seconds": int((datetime.now() - connected_at).total_seconds()) if connected_at and info.get("status") == "connected" else None } return { "active_connections": active_count, "total_messages_sent": total_messages, "connected_users": list(self.active_connections.keys()), "connection_details": connection_details } async def ping_all_connections(self): """Проверить все WebSocket подключения""" disconnected_users = [] for user_id, websocket in list(self.active_connections.items()): try: ping_message = json.dumps({ "type": "ping", "timestamp": datetime.now().isoformat() }) await websocket.send_text(ping_message) # Обновляем время последнего пинга if user_id in self.connection_info: self.connection_info[user_id]["last_ping"] = datetime.now() except Exception as e: print(f"Connection lost for user {user_id}: {e}") disconnected_users.append(user_id) # Удаляем неактивные подключения for user_id in disconnected_users: self.disconnect(user_id) return { "active_connections": len(self.active_connections), "disconnected_users": disconnected_users, "ping_time": datetime.now().isoformat() } # Global WebSocket manager instance ws_manager = WebSocketManager() async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db), ): """ Get current user from JWT Bearer token for OpenAPI documentation. Требует Bearer токен в заголовке Authorization: Authorization: Bearer Returns simplified User object to avoid SQLAlchemy issues. """ try: # Получаем данные пользователя из токена напрямую from shared.auth import verify_token user_data = verify_token(credentials.credentials) if user_data is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) # Возвращаем упрощенный объект пользователя return type('User', (), { 'id': user_data["user_id"], 'email': user_data.get("email", "unknown@example.com"), 'username': user_data.get("username", f"user_{user_data['user_id']}") })() except HTTPException: raise except Exception as e: logger.error(f"Authentication failed: {str(e)}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) async def get_current_user_websocket(token: str): """Get current user from WebSocket token - PRODUCTION READY""" try: from shared.auth import verify_token import logging # Логируем попытку аутентификации (без токена в логах!) print(f"🔐 WebSocket auth: Attempting authentication for token length={len(token)}") # ВАЖНО: Никаких заглушек! Только настоящие JWT токены if token.startswith("temp_token") or token.startswith("test_token"): print(f"❌ WebSocket auth: REJECTED - Temporary tokens not allowed in production!") print(f"❌ Token prefix: {token[:20]}...") return None # Проверяем JWT токен user_data = verify_token(token) if not user_data: print(f"❌ WebSocket auth: Invalid or expired JWT token") return None print(f"✅ WebSocket auth: JWT token valid for user_id={user_data['user_id']}, email={user_data.get('email', 'N/A')}") # Создаем объект пользователя из токена class AuthenticatedUser: def __init__(self, user_id, email): self.id = user_id self.email = email return AuthenticatedUser(user_data['user_id'], user_data.get('email', f'user_{user_data["user_id"]}')) except Exception as e: print(f"❌ WebSocket auth error: {e}") return None def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """Calculate distance between two points in kilometers using Haversine formula.""" # Convert latitude and longitude from degrees to radians lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) # Haversine formula dlat = lat2 - lat1 dlon = lon2 - lon1 a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 c = 2 * math.asin(math.sqrt(a)) # Radius of earth in kilometers r = 6371 return c * r @app.get("/health") async def health_check(): """Health check endpoint""" return {"status": "healthy", "service": "emergency_service"} @app.get("/alerts") async def get_alerts_public(db: AsyncSession = Depends(get_db)): """Get all emergency alerts (public endpoint for testing)""" result = await db.execute( select(EmergencyAlert) .order_by(EmergencyAlert.created_at.desc()) .limit(50) ) alerts = result.scalars().all() return [EmergencyAlertResponse.model_validate(alert) for alert in alerts] @app.post("/alerts") async def create_alert_public( alert_data: dict, db: AsyncSession = Depends(get_db) ): """Create emergency alert (public endpoint for testing)""" try: new_alert = EmergencyAlert( user_id=alert_data.get("user_id", 1), alert_type=alert_data.get("alert_type", "medical"), latitude=alert_data.get("latitude", 0), longitude=alert_data.get("longitude", 0), title=alert_data.get("title", "Emergency Alert"), description=alert_data.get("description", ""), is_resolved=False ) db.add(new_alert) await db.commit() await db.refresh(new_alert) return {"status": "success", "alert_id": new_alert.id} except Exception as e: await db.rollback() return {"status": "error", "detail": str(e)} @app.websocket("/api/v1/emergency/ws/{user_id}") async def websocket_endpoint(websocket: WebSocket, user_id: str): """WebSocket endpoint for emergency notifications""" print(f"🔌 WebSocket connection attempt from {websocket.client}") print(f"📝 user_id: {user_id}") print(f"🔗 Query params: {dict(websocket.query_params)}") # Get token from query parameter token = websocket.query_params.get("token") print(f"🎫 Token received: {token}") if not token: print("❌ No token provided, closing connection") await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return # Authenticate user authenticated_user = await get_current_user_websocket(token) if not authenticated_user: print("❌ Authentication failed, closing connection") await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return # Get user ID as integer - authenticated_user is an instance with id attribute auth_user_id = authenticated_user.id print(f"✅ User authenticated: {authenticated_user.email} (ID: {auth_user_id})") # Verify user_id matches authenticated user try: if int(user_id) != auth_user_id: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return except ValueError: if user_id != "current_user_id": await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return # Handle special case where client uses 'current_user_id' as placeholder user_id = str(auth_user_id) # Connect WebSocket await ws_manager.connect(websocket, auth_user_id) try: # Send initial connection message await ws_manager.send_personal_message( json.dumps({ "type": "connection_established", "message": "Connected to emergency notifications", "user_id": auth_user_id }), auth_user_id ) # Keep connection alive and listen for messages while True: try: # Wait for messages (ping/pong, etc.) data = await websocket.receive_text() message = json.loads(data) # Handle different message types if message.get("type") == "ping": await ws_manager.send_personal_message( json.dumps({"type": "pong"}), auth_user_id ) except WebSocketDisconnect: break except Exception as e: print(f"WebSocket error: {e}") break except WebSocketDisconnect: pass finally: ws_manager.disconnect(auth_user_id) async def get_nearby_users( latitude: float, longitude: float, radius_km: float = 1.0 ) -> List[dict]: """Get users within radius using Location Service""" async with httpx.AsyncClient() as client: try: response = await client.get( f"http://localhost:8003/api/v1/nearby-users", params={ "latitude": latitude, "longitude": longitude, "radius_km": radius_km, }, timeout=5.0, ) if response.status_code == 200: return response.json() return [] except Exception: return [] async def send_websocket_notifications_to_nearby_users(alert, nearby_users: List[dict]) -> int: """Send real-time WebSocket notifications to nearby users who are online""" online_count = 0 # Create notification message notification = { "type": "emergency_alert", "alert_id": alert.id, "alert_type": alert.alert_type, "latitude": alert.latitude, "longitude": alert.longitude, "address": alert.address, "message": alert.message or "Экстренная ситуация рядом с вами!", "created_at": alert.created_at.isoformat(), "distance_km": None # Will be calculated per user } print(f"🔔 Sending WebSocket notifications to {len(nearby_users)} nearby users") for user in nearby_users: user_id = user.get("user_id") distance = user.get("distance_km", 0) # Update distance in notification notification["distance_km"] = round(distance, 2) # Check if user has active WebSocket connection if user_id in ws_manager.active_connections: try: # Send notification via WebSocket await ws_manager.send_personal_message( json.dumps(notification, ensure_ascii=False, default=str), user_id ) online_count += 1 print(f"📡 Sent WebSocket notification to user {user_id} ({distance:.1f}km away)") except Exception as e: print(f"❌ Failed to send WebSocket to user {user_id}: {e}") else: print(f"💤 User {user_id} is offline - will receive push notification only") print(f"✅ WebSocket notifications sent to {online_count}/{len(nearby_users)} online users") return online_count async def send_emergency_notifications(alert_id: int, nearby_users: List[dict]): """Send push notifications to nearby users""" if not nearby_users: return print(f"📱 Sending push notifications to {len(nearby_users)} users via Notification Service") async with httpx.AsyncClient() as client: try: response = await client.post( "http://localhost:8005/api/v1/send-emergency-notifications", json={ "alert_id": alert_id, "user_ids": [user["user_id"] for user in nearby_users], "message": "🚨 Экстренная ситуация рядом с вами! Проверьте приложение.", "title": "Экстренное уведомление" }, timeout=10.0, ) if response.status_code == 200: print(f"✅ Push notifications sent successfully") else: print(f"⚠️ Push notification service responded with {response.status_code}") except Exception as e: print(f"❌ Failed to send push notifications: {e}") @app.post("/api/v1/alert", response_model=EmergencyAlertResponse) async def create_emergency_alert( alert_data: EmergencyAlertCreate, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Create new emergency alert""" # Create alert db_alert = EmergencyAlert( user_id=current_user.id, latitude=alert_data.latitude, longitude=alert_data.longitude, address=alert_data.address, alert_type=alert_data.alert_type, message=alert_data.message, ) db.add(db_alert) await db.commit() await db.refresh(db_alert) # Process alert in background background_tasks.add_task( process_emergency_alert_in_background, int(db_alert.id), # Convert to int explicitly alert_data.latitude, alert_data.longitude ) return EmergencyAlertResponse.model_validate(db_alert) async def process_emergency_alert_in_background(alert_id: int, latitude: float, longitude: float): """Process emergency alert - notify nearby users via WebSocket and Push notifications""" try: print(f"🚨 Processing emergency alert {alert_id} at coordinates ({latitude}, {longitude})") # Get nearby users within 5km radius nearby_users = await get_nearby_users(latitude, longitude, radius_km=5.0) print(f"📍 Found {len(nearby_users)} nearby users within 5km radius") if nearby_users: # Create new database session for background task from shared.database import AsyncSessionLocal async with AsyncSessionLocal() as db: try: # Get full alert details for notifications result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) alert = result.scalars().first() if not alert: print(f"❌ Alert {alert_id} not found in database") return # Update alert with notification count await db.execute( update(EmergencyAlert) .where(EmergencyAlert.id == alert_id) .values(notified_users_count=len(nearby_users)) ) await db.commit() print(f"✅ Updated alert {alert_id} with {len(nearby_users)} notified users") # Send real-time WebSocket notifications to online users online_notifications_sent = await send_websocket_notifications_to_nearby_users(alert, nearby_users) # Send push notifications via notification service await send_emergency_notifications(alert_id, nearby_users) print(f"📱 Sent notifications: {online_notifications_sent} WebSocket + {len(nearby_users)} Push") except Exception as e: print(f"❌ Error processing emergency alert: {e}") await db.rollback() else: print(f"ℹ️ No nearby users found for alert {alert_id}") except Exception as e: print(f"❌ Error in process_emergency_alert_in_background: {e}") @app.post("/api/v1/alert/{alert_id}/respond", response_model=EmergencyResponseResponse) async def respond_to_alert( alert_id: int, response_data: EmergencyResponseCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Respond to emergency alert""" # Check if alert exists result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) alert = result.scalars().first() if not alert: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found" ) # Check if already responded existing_response = await db.execute( select(EmergencyResponse).filter( EmergencyResponse.alert_id == alert_id, EmergencyResponse.responder_id == current_user.id ) ) if existing_response.scalars().first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="You have already responded to this alert" ) # Create response db_response = EmergencyResponse( alert_id=alert_id, responder_id=current_user.id, response_type=response_data.response_type, message=response_data.message, eta_minutes=response_data.eta_minutes, ) db.add(db_response) # Update responded users count await db.execute( update(EmergencyAlert) .where(EmergencyAlert.id == alert_id) .values(responded_users_count=EmergencyAlert.responded_users_count + 1) ) await db.commit() await db.refresh(db_response) # Create response with responder info response_dict = db_response.__dict__.copy() response_dict['responder_name'] = current_user.username response_dict['responder_phone'] = getattr(current_user, 'phone_number', None) return EmergencyResponseResponse.model_validate(response_dict) @app.put("/api/v1/alert/{alert_id}/resolve") async def resolve_alert( alert_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Resolve emergency alert""" result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) alert = result.scalars().first() if not alert: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found" ) # Only alert creator can resolve if alert.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only alert creator can resolve the alert" ) # Update alert await db.execute( update(EmergencyAlert) .where(EmergencyAlert.id == alert_id) .values( is_resolved=True, resolved_at=datetime.utcnow(), resolved_by=current_user.id ) ) await db.commit() return {"message": "Alert resolved successfully"} @app.get("/api/v1/alerts/my", response_model=List[EmergencyAlertResponse]) async def get_my_alerts( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get current user's emergency alerts""" result = await db.execute( select(EmergencyAlert) .filter(EmergencyAlert.user_id == current_user.id) .order_by(EmergencyAlert.created_at.desc()) ) alerts = result.scalars().all() return [EmergencyAlertResponse.model_validate(alert) for alert in alerts] @app.get("/api/v1/alerts/active", response_model=List[EmergencyAlertResponse]) async def get_active_alerts( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get active emergency alerts""" result = await db.execute( select(EmergencyAlert) .filter(EmergencyAlert.is_resolved == False) .order_by(EmergencyAlert.created_at.desc()) .limit(50) ) alerts = result.scalars().all() return [EmergencyAlertResponse.model_validate(alert) for alert in alerts] @app.get("/api/v1/emergency/reports", response_model=List[EmergencyAlertResponse]) async def get_emergency_reports( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get emergency reports/alerts for admin users or general statistics""" # For now, return all active alerts (in production, add admin check) result = await db.execute( select(EmergencyAlert) .filter(EmergencyAlert.is_resolved == False) .order_by(EmergencyAlert.created_at.desc()) .limit(50) ) alerts = result.scalars().all() return [EmergencyAlertResponse.model_validate(alert) for alert in alerts] @app.get("/api/v1/stats", response_model=EmergencyStatistics) async def get_emergency_stats( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get emergency statistics""" # Get total alerts count total_alerts_result = await db.execute(select(func.count(EmergencyAlert.id))) total_alerts = total_alerts_result.scalar() or 0 # Get active alerts count active_alerts_result = await db.execute( select(func.count(EmergencyAlert.id)).filter(EmergencyAlert.is_resolved == False) ) active_alerts = active_alerts_result.scalar() or 0 # Calculate resolved alerts resolved_alerts = total_alerts - active_alerts # Get total responders count total_responders_result = await db.execute(select(func.count(EmergencyResponse.id))) total_responders = total_responders_result.scalar() or 0 return EmergencyStatistics( total_alerts=total_alerts, active_alerts=active_alerts, resolved_alerts=resolved_alerts, avg_response_time_minutes=0, # TODO: Calculate this total_responders=total_responders, ) @app.get("/api/v1/alerts/nearby", response_model=List[NearbyAlertResponse]) async def get_nearby_alerts( latitude: float = Query(..., ge=-90, le=90), longitude: float = Query(..., ge=-180, le=180), radius_km: float = Query(default=10.0, ge=0.1, le=100), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get nearby emergency alerts within specified radius""" # For now, return all active alerts (in production, add distance filtering) result = await db.execute( select(EmergencyAlert) .filter(EmergencyAlert.is_resolved == False) .order_by(EmergencyAlert.created_at.desc()) .limit(20) ) alerts = result.scalars().all() nearby_alerts = [] for alert in alerts: distance = calculate_distance(latitude, longitude, alert.latitude, alert.longitude) if distance <= radius_km: nearby_alerts.append(NearbyAlertResponse( id=alert.id, alert_type=alert.alert_type, latitude=alert.latitude, longitude=alert.longitude, address=alert.address, distance_km=round(distance, 2), created_at=alert.created_at, responded_users_count=alert.responded_users_count or 0 )) return sorted(nearby_alerts, key=lambda x: x.distance_km) @app.post("/api/v1/report", response_model=EmergencyReportResponse) async def create_emergency_report( report_data: EmergencyReportCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Create emergency report""" db_report = EmergencyReport( user_id=current_user.id if not report_data.is_anonymous else None, latitude=report_data.latitude, longitude=report_data.longitude, address=report_data.address, report_type=report_data.report_type, description=report_data.description, is_anonymous=report_data.is_anonymous, severity=report_data.severity ) db.add(db_report) await db.commit() await db.refresh(db_report) return EmergencyReportResponse.model_validate(db_report) @app.get("/api/v1/reports", response_model=List[EmergencyReportResponse]) async def get_emergency_reports( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get emergency reports""" result = await db.execute( select(EmergencyReport) .order_by(EmergencyReport.created_at.desc()) .limit(50) ) reports = result.scalars().all() return [EmergencyReportResponse.model_validate(report) for report in reports] @app.post("/api/v1/safety-check", response_model=SafetyCheckResponse) async def create_safety_check( check_data: SafetyCheckCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Create safety check-in""" db_check = SafetyCheck( user_id=current_user.id, message=check_data.message, location_latitude=check_data.location_latitude, location_longitude=check_data.location_longitude ) db.add(db_check) await db.commit() await db.refresh(db_check) return SafetyCheckResponse.model_validate(db_check) @app.get("/api/v1/safety-checks", response_model=List[SafetyCheckResponse]) async def get_safety_checks( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get user's safety check-ins""" result = await db.execute( select(SafetyCheck) .filter(SafetyCheck.user_id == current_user.id) .order_by(SafetyCheck.created_at.desc()) .limit(50) ) checks = result.scalars().all() return [SafetyCheckResponse.model_validate(check) for check in checks] @app.put("/api/v1/alert/{alert_id}", response_model=EmergencyAlertResponse) async def update_emergency_alert( alert_id: int, update_data: EmergencyAlertUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Update emergency alert""" result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) alert = result.scalars().first() if not alert: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found" ) # Only alert creator can update if alert.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only alert creator can update the alert" ) # Update fields update_dict = {} if update_data.is_resolved is not None: update_dict['is_resolved'] = update_data.is_resolved if update_data.is_resolved: update_dict['resolved_at'] = datetime.utcnow() update_dict['resolved_by'] = current_user.id if update_data.message is not None: update_dict['message'] = update_data.message if update_dict: await db.execute( update(EmergencyAlert) .where(EmergencyAlert.id == alert_id) .values(**update_dict) ) await db.commit() await db.refresh(alert) return EmergencyAlertResponse.model_validate(alert) @app.get("/api/v1/alert/{alert_id}/responses", response_model=List[EmergencyResponseResponse]) async def get_alert_responses( alert_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get responses for specific alert""" # Check if alert exists result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) alert = result.scalars().first() if not alert: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found" ) # Get responses responses_result = await db.execute( select(EmergencyResponse) .filter(EmergencyResponse.alert_id == alert_id) .order_by(EmergencyResponse.created_at.desc()) ) responses = responses_result.scalars().all() return [EmergencyResponseResponse.model_validate(response) for response in responses] @app.get("/api/v1/websocket/connections") async def get_websocket_connections( current_user: User = Depends(get_current_user) ): """Получить информацию о WebSocket подключениях""" return ws_manager.get_connection_info() @app.get("/api/v1/websocket/connections/{user_id}") async def get_user_websocket_info( user_id: int, current_user: User = Depends(get_current_user) ): """Получить информацию о подключении конкретного пользователя""" connection_info = ws_manager.get_connection_info(user_id) if not connection_info: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User connection not found" ) return connection_info @app.post("/api/v1/websocket/ping") async def ping_websocket_connections( current_user: User = Depends(get_current_user) ): """Проверить все WebSocket подключения (пинг)""" result = await ws_manager.ping_all_connections() return result @app.get("/api/v1/websocket/stats") async def get_websocket_stats( current_user: User = Depends(get_current_user) ): """Получить общую статистику WebSocket подключений""" info = ws_manager.get_connection_info() return { "total_connections": info["active_connections"], "connected_users": info["connected_users"], "total_messages_sent": info["total_messages_sent"], "connection_count": len(info["connected_users"]), "timestamp": datetime.now().isoformat() } @app.post("/api/v1/websocket/broadcast") async def broadcast_test_message( message: str, current_user: User = Depends(get_current_user) ): """Отправить тестовое сообщение всем подключенным пользователям""" test_data = { "type": "test_broadcast", "message": message, "from_user": current_user.id, "timestamp": datetime.now().isoformat() } await ws_manager.broadcast_alert(test_data) return { "message": "Test broadcast sent", "recipients": ws_manager.get_connected_users_list(), "data": test_data } # MOBILE APP COMPATIBILITY ENDPOINTS # Мобильное приложение ожидает endpoints с /api/v1/emergency/events @app.post("/api/v1/emergency/events", response_model=EmergencyAlertResponse) async def create_emergency_event( alert_data: EmergencyAlertCreate, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Create emergency event (alias for create_alert for mobile compatibility)""" # Используем существующую логику создания alert return await create_emergency_alert(alert_data, background_tasks, current_user, db) @app.get("/api/v1/emergency/events/nearby", response_model=List[NearbyAlertResponse]) async def get_nearby_emergency_events( latitude: float = Query(..., description="User latitude"), longitude: float = Query(..., description="User longitude"), radius: float = Query(5.0, description="Search radius in km"), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get nearby emergency events (alias for nearby alerts for mobile compatibility)""" # Используем существующую логику поиска nearby alerts return await get_nearby_alerts(latitude, longitude, radius, current_user, db) @app.get("/api/v1/emergency/events", response_model=List[EmergencyAlertResponse]) async def get_emergency_events( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get all emergency events (alias for active alerts for mobile compatibility)""" # Используем существующую логику получения активных alerts return await get_active_alerts(current_user, db) @app.get("/api/v1/emergency/events/my", response_model=List[EmergencyAlertResponse]) async def get_my_emergency_events( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get my emergency events (alias for my alerts for mobile compatibility)""" # Используем существующую логику получения моих alerts return await get_my_alerts(current_user, db) @app.get("/api/v1/emergency/events/{event_id}", response_model=EmergencyEventDetails) async def get_emergency_event( event_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get full detailed information about emergency event by ID""" try: # Получаем alert с информацией о пользователе alert_result = await db.execute( select(EmergencyAlert, User) .join(User, EmergencyAlert.user_id == User.id) .filter(EmergencyAlert.id == event_id) ) alert_data = alert_result.first() if not alert_data: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Emergency event not found" ) alert, user = alert_data # Получаем все ответы на это событие с информацией о респондентах responses_result = await db.execute( select(EmergencyResponse, User) .join(User, EmergencyResponse.responder_id == User.id) .filter(EmergencyResponse.alert_id == event_id) .order_by(EmergencyResponse.created_at.desc()) ) # Формируем список ответов responses = [] for response_data in responses_result: emergency_response, responder = response_data # Формируем полное имя респондента responder_name = responder.username if responder.first_name and responder.last_name: responder_name = f"{responder.first_name} {responder.last_name}" elif responder.first_name: responder_name = responder.first_name elif responder.last_name: responder_name = responder.last_name response_dict = { "id": emergency_response.id, "alert_id": emergency_response.alert_id, "responder_id": emergency_response.responder_id, "response_type": emergency_response.response_type, "message": emergency_response.message, "eta_minutes": emergency_response.eta_minutes, "created_at": emergency_response.created_at, "responder_name": responder_name, "responder_phone": responder.phone } responses.append(EmergencyResponseResponse(**response_dict)) # Создаем объект с информацией о пользователе full_name = None if user.first_name and user.last_name: full_name = f"{user.first_name} {user.last_name}" elif user.first_name: full_name = user.first_name elif user.last_name: full_name = user.last_name user_info = UserInfo( id=user.id, username=user.username, full_name=full_name, phone=user.phone ) # Определяем статус события на основе is_resolved from services.emergency_service.schemas import AlertStatus event_status = AlertStatus.RESOLVED if alert.is_resolved else AlertStatus.ACTIVE # Формируем полный ответ event_details = EmergencyEventDetails( id=alert.id, uuid=alert.uuid, user_id=alert.user_id, latitude=alert.latitude, longitude=alert.longitude, address=alert.address, alert_type=alert.alert_type, message=alert.message, status=event_status, created_at=alert.created_at, updated_at=alert.updated_at, resolved_at=alert.resolved_at, user=user_info, responses=responses, notifications_sent=len(responses), # Примерная статистика websocket_notifications_sent=alert.notified_users_count or 0, push_notifications_sent=alert.responded_users_count or 0, contact_emergency_services=True, # Значение по умолчанию notify_emergency_contacts=True # Значение по умолчанию ) logger.info(f"Retrieved detailed event info for event_id={event_id}, responses_count={len(responses)}") return event_details except HTTPException: raise except Exception as e: logger.error(f"Error retrieving emergency event {event_id}: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve emergency event details" ) @app.get("/api/v1/emergency/events/{event_id}/brief", response_model=EmergencyAlertResponse) async def get_emergency_event_brief( event_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Get brief information about emergency event by ID (for mobile apps)""" # Получаем конкретный alert result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == event_id)) alert = result.scalars().first() if not alert: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Emergency event not found" ) logger.info(f"Retrieved brief event info for event_id={event_id}") return EmergencyAlertResponse.model_validate(alert) @app.put("/api/v1/emergency/events/{event_id}/resolve") async def resolve_emergency_event( event_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Resolve emergency event (alias for resolve alert)""" # Используем существующую логику resolve alert return await resolve_alert(event_id, current_user, db) @app.post("/api/v1/emergency/events/{event_id}/respond", response_model=EmergencyResponseResponse) async def respond_to_emergency_event( event_id: int, response: EmergencyResponseCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Respond to emergency event (alias for respond to alert)""" # Используем существующую логику respond to alert return await respond_to_alert(event_id, response, current_user, db) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8002)