All checks were successful
continuous-integration/drone/push Build is passing
1257 lines
46 KiB
Python
1257 lines
46 KiB
Python
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 <your_jwt_token>
|
||
```
|
||
""",
|
||
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 <your_jwt_token>
|
||
|
||
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.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) |