This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict, Set
|
||||
import math
|
||||
|
||||
import httpx
|
||||
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, status
|
||||
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
|
||||
|
||||
@@ -24,18 +27,11 @@ from services.emergency_service.schemas import (
|
||||
NearbyAlertResponse,
|
||||
SafetyCheckCreate,
|
||||
SafetyCheckResponse,
|
||||
EmergencyEventDetails,
|
||||
UserInfo,
|
||||
)
|
||||
# Упрощенная модель User для Emergency Service
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
from shared.database import BaseModel
|
||||
|
||||
class User(BaseModel):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
# 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
|
||||
@@ -52,7 +48,40 @@ async def get_db():
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
app = FastAPI(title="Emergency Service", version="1.0.0")
|
||||
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(
|
||||
@@ -64,19 +93,239 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
user_data: dict = Depends(get_current_user_from_token),
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get current user from token via auth dependency."""
|
||||
# Get full user object from database
|
||||
result = await db.execute(select(User).filter(User.id == user_data["user_id"]))
|
||||
user = result.scalars().first()
|
||||
if user is None:
|
||||
"""
|
||||
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_404_NOT_FOUND, detail="User not found"
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
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:
|
||||
@@ -100,6 +349,86 @@ async def health_check():
|
||||
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]:
|
||||
@@ -122,20 +451,76 @@ async def get_nearby_users(
|
||||
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:
|
||||
await client.post(
|
||||
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 notifications: {e}")
|
||||
print(f"❌ Failed to send push notifications: {e}")
|
||||
|
||||
|
||||
@app.post("/api/v1/alert", response_model=EmergencyAlertResponse)
|
||||
@@ -172,16 +557,27 @@ async def create_emergency_alert(
|
||||
|
||||
|
||||
async def process_emergency_alert_in_background(alert_id: int, latitude: float, longitude: float):
|
||||
"""Process emergency alert - notify nearby users"""
|
||||
"""Process emergency alert - notify nearby users via WebSocket and Push notifications"""
|
||||
try:
|
||||
# Get nearby users
|
||||
nearby_users = await get_nearby_users(latitude, longitude)
|
||||
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)
|
||||
@@ -189,16 +585,25 @@ async def process_emergency_alert_in_background(alert_id: int, latitude: float,
|
||||
.values(notified_users_count=len(nearby_users))
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
print(f"✅ Updated alert {alert_id} with {len(nearby_users)} notified users")
|
||||
|
||||
# Send notifications
|
||||
# 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}")
|
||||
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}")
|
||||
print(f"❌ Error in process_emergency_alert_in_background: {e}")
|
||||
|
||||
|
||||
@app.post("/api/v1/alert/{alert_id}/respond", response_model=EmergencyResponseResponse)
|
||||
@@ -568,6 +973,285 @@ async def get_alert_responses(
|
||||
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)
|
||||
@@ -96,6 +96,52 @@ class EmergencyResponseResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""Базовая информация о пользователе для событий"""
|
||||
id: int
|
||||
username: str
|
||||
full_name: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EmergencyEventDetails(BaseModel):
|
||||
"""Полная детальная информация о событии экстренной помощи"""
|
||||
# Основная информация о событии
|
||||
id: int
|
||||
uuid: UUID
|
||||
user_id: int
|
||||
latitude: float
|
||||
longitude: float
|
||||
address: Optional[str] = None
|
||||
alert_type: AlertType
|
||||
message: Optional[str] = None
|
||||
status: AlertStatus
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
resolved_at: Optional[datetime] = None
|
||||
|
||||
# Информация о пользователе, который создал событие
|
||||
user: UserInfo
|
||||
|
||||
# Все ответы на это событие
|
||||
responses: List[EmergencyResponseResponse] = []
|
||||
|
||||
# Статистика уведомлений
|
||||
notifications_sent: int = 0
|
||||
websocket_notifications_sent: int = 0
|
||||
push_notifications_sent: int = 0
|
||||
|
||||
# Дополнительная информация
|
||||
contact_emergency_services: bool = True
|
||||
notify_emergency_contacts: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Report schemas
|
||||
class EmergencyReportCreate(BaseModel):
|
||||
latitude: float = Field(..., ge=-90, le=90)
|
||||
|
||||
Reference in New Issue
Block a user