350 lines
14 KiB
Python
350 lines
14 KiB
Python
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query, Depends
|
||
from app.core.security import test_token
|
||
from typing import Dict
|
||
from datetime import datetime, timezone
|
||
import json
|
||
from sqlalchemy.orm import Session
|
||
from app.db import models
|
||
from firebase_admin import messaging, credentials, exceptions
|
||
import firebase_admin
|
||
|
||
cred = credentials.Certificate(
|
||
"chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json")
|
||
firebase_admin.initialize_app(cred)
|
||
|
||
# бд
|
||
|
||
|
||
def get_db():
|
||
db = models.SessionLocal()
|
||
try:
|
||
yield db
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
wsRouter = APIRouter(
|
||
prefix='/ws'
|
||
)
|
||
|
||
|
||
@wsRouter.websocket("")
|
||
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: Session = Depends(get_db)):
|
||
if token is None:
|
||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||
return
|
||
try:
|
||
user_id = int(await test_token(token=token))
|
||
except HTTPException:
|
||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||
return
|
||
print("ПОДКЛЮЧЕНИЕ")
|
||
await manager.connect(user_id, websocket)
|
||
print("ПОДКЛЮЧЕНО")
|
||
|
||
db.query(models.User).filter(models.User.id == user_id).update({"last_online": datetime.now(timezone.utc)},
|
||
synchronize_session="fetch")
|
||
db.commit()
|
||
await manager.broadcast({
|
||
"type": "user_online",
|
||
"user_id": user_id,
|
||
})
|
||
try:
|
||
while True:
|
||
print("ОЖИДАНИЕ СООБЩЕНИЙ")
|
||
data = await websocket.receive_text()
|
||
message_data = json.loads(data)
|
||
print(f"DEBUG: Получены данные: {message_data}")
|
||
|
||
db.query(models.User).filter(models.User.id == user_id).update({"last_online": datetime.now(timezone.utc)},
|
||
synchronize_session="fetch")
|
||
db.commit()
|
||
|
||
if message_data.get("type") == "private_message":
|
||
|
||
user = db.query(models.User).filter(
|
||
models.User.id == user_id).first()
|
||
receiver_id = message_data.get("receiver_id")
|
||
temp_id = message_data.get("temp_id")
|
||
content = message_data.get("content")
|
||
content50 = message_data.get("content50")
|
||
|
||
if receiver_id is None or content is None:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"detail": "receiver_id/content required",
|
||
})
|
||
continue
|
||
|
||
try:
|
||
receiver_id = int(receiver_id)
|
||
except (TypeError, ValueError):
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"detail": "receiver_id must be int",
|
||
})
|
||
continue
|
||
|
||
new_msg = models.Message(
|
||
sender_id=user_id,
|
||
receiver_id=receiver_id,
|
||
content=content,
|
||
reply_to_id=message_data.get("reply_to_id"),
|
||
reply_to_text=message_data.get("reply_to_text")
|
||
)
|
||
db.add(new_msg)
|
||
db.commit()
|
||
db.refresh(new_msg)
|
||
|
||
# ACK отправителю: сервер принял и сохранил сообщение (нужно для статусов клиента).
|
||
await manager.send_personal_message({
|
||
"type": "message_sent",
|
||
"temp_id": temp_id,
|
||
"server_id": new_msg.id,
|
||
"timestamp": (new_msg.timestamp or datetime.now()).isoformat(),
|
||
}, str(user_id))
|
||
|
||
# Если получатель оффлайн — отправим пуш (если есть токен и ключи).
|
||
if user.public_key:
|
||
receiver = db.query(models.User).filter(
|
||
models.User.id == receiver_id).first()
|
||
if receiver.fcm_token:
|
||
send_fcm_notification(
|
||
receiver.fcm_token,
|
||
user_id,
|
||
user.first_name,
|
||
user.public_key,
|
||
content50 if content50 else content,
|
||
datetime.now(),
|
||
)
|
||
# Формируем пакет для получателя
|
||
outgoing_message = {
|
||
"id": new_msg.id,
|
||
"type": "private_message",
|
||
"sender_id": user_id,
|
||
"receiver_id": receiver_id,
|
||
"content": message_data.get("content"),
|
||
"timestamp": (new_msg.timestamp or datetime.now()).isoformat(),
|
||
"reply_to_id": new_msg.reply_to_id,
|
||
"reply_to_text": new_msg.reply_to_text,
|
||
}
|
||
|
||
# Пересылаем получателю, если он в сети
|
||
sent_to_receiver = await manager.send_personal_message(outgoing_message, str(receiver_id))
|
||
|
||
# Если сообщение реально ушло по сокету получателю — отмечаем delivered_at.
|
||
if sent_to_receiver:
|
||
try:
|
||
delivered_at = datetime.now()
|
||
new_msg.delivered_at = delivered_at
|
||
db.add(new_msg)
|
||
db.commit()
|
||
await manager.send_personal_message({
|
||
"type": "message_delivered",
|
||
"message_id": new_msg.id,
|
||
"timestamp": delivered_at.isoformat(),
|
||
}, str(user_id))
|
||
except Exception:
|
||
db.rollback()
|
||
|
||
elif message_data.get("type") == "edit_message":
|
||
message_id = message_data.get("message_id")
|
||
content = message_data.get("content")
|
||
if message_id is None or content is None:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"detail": "message_id/content required",
|
||
})
|
||
continue
|
||
try:
|
||
message_id = int(message_id)
|
||
except (TypeError, ValueError):
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"detail": "message_id must be int",
|
||
})
|
||
continue
|
||
msg = db.query(models.Message).filter(
|
||
models.Message.id == message_id).first()
|
||
if msg is None or msg.sender_id != user_id:
|
||
continue
|
||
try:
|
||
msg.content = content
|
||
msg.edited_at = datetime.now()
|
||
db.add(msg)
|
||
db.commit()
|
||
except Exception:
|
||
db.rollback()
|
||
continue
|
||
event = {
|
||
"type": "message_edited",
|
||
"message_id": msg.id,
|
||
"content": msg.content,
|
||
"edited_at": msg.edited_at.isoformat() if msg.edited_at else None,
|
||
}
|
||
await manager.send_personal_message(event, str(msg.receiver_id))
|
||
await manager.send_personal_message(event, str(msg.sender_id))
|
||
|
||
elif message_data.get("type") == "delete_message":
|
||
message_id = message_data.get("message_id")
|
||
if message_id is None:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"detail": "message_id required",
|
||
})
|
||
continue
|
||
try:
|
||
message_id = int(message_id)
|
||
except (TypeError, ValueError):
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"detail": "message_id must be int",
|
||
})
|
||
continue
|
||
msg = db.query(models.Message).filter(
|
||
models.Message.id == message_id).first()
|
||
if msg is None or msg.sender_id != user_id:
|
||
continue
|
||
receiver_id = msg.receiver_id
|
||
try:
|
||
db.delete(msg)
|
||
db.commit()
|
||
except Exception:
|
||
db.rollback()
|
||
continue
|
||
event = {
|
||
"type": "message_deleted",
|
||
"message_id": message_id,
|
||
}
|
||
await manager.send_personal_message(event, str(receiver_id))
|
||
await manager.send_personal_message(event, str(user_id))
|
||
|
||
elif message_data.get("type") == "read_receipt":
|
||
message_id = message_data.get("message_id")
|
||
try:
|
||
message_id = int(message_id)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
|
||
msg = db.query(models.Message).filter(
|
||
models.Message.id == message_id).first()
|
||
if msg is None:
|
||
continue
|
||
|
||
# Безопасность: read_receipt может отправлять только получатель.
|
||
if int(msg.receiver_id) != int(user_id):
|
||
continue
|
||
|
||
# Сохраняем read_at в БД
|
||
try:
|
||
read_at = datetime.now()
|
||
msg.read_at = read_at
|
||
db.add(msg)
|
||
db.commit()
|
||
except Exception:
|
||
db.rollback()
|
||
|
||
sender_id = int(msg.sender_id)
|
||
await manager.send_personal_message({
|
||
"type": "message_read",
|
||
"message_id": message_id,
|
||
"timestamp": read_at.isoformat() if 'read_at' in locals() else datetime.now().isoformat(),
|
||
}, str(sender_id))
|
||
elif message_data.get("type") == "typing":
|
||
receiver_id = message_data.get("receiver_id")
|
||
if receiver_id is None:
|
||
continue
|
||
try:
|
||
receiver_id = int(receiver_id)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
await manager.send_personal_message({
|
||
"type": "typing",
|
||
"sender_id": user_id,
|
||
}, str(receiver_id))
|
||
elif message_data.get("type") == "stop_typing":
|
||
receiver_id = message_data.get("receiver_id")
|
||
if receiver_id is None:
|
||
continue
|
||
try:
|
||
receiver_id = int(receiver_id)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
await manager.send_personal_message({
|
||
"type": "stop_typing",
|
||
"sender_id": user_id,
|
||
}, str(receiver_id))
|
||
except WebSocketDisconnect:
|
||
pass
|
||
finally:
|
||
manager.disconnect(user_id)
|
||
db.query(models.User).filter(models.User.id == user_id).update(
|
||
{"last_online": datetime.now(timezone.utc)}, synchronize_session="fetch")
|
||
db.commit()
|
||
print("ОТКЛЮЧЕНИЕ")
|
||
|
||
await manager.broadcast({
|
||
"type": "user_offline",
|
||
"user_id": user_id,
|
||
})
|
||
|
||
|
||
def send_fcm_notification(token, user_id, username, public_key, encrypted_text, timestamp):
|
||
print(
|
||
f"DEBUG: Отправляем FCM уведомление пользователю {user_id} с токеном {token}")
|
||
message = messaging.Message(
|
||
data={
|
||
"type": "enc_message",
|
||
"sender_id": str(user_id),
|
||
"username": username,
|
||
"public_key": public_key,
|
||
"content": encrypted_text, # Зашифрованный текст
|
||
"timestamp": timestamp.isoformat(),
|
||
},
|
||
android=messaging.AndroidConfig(
|
||
priority='high',
|
||
),
|
||
token=token,
|
||
)
|
||
try:
|
||
response = messaging.send(message)
|
||
print('Successfully sent message:', response)
|
||
except Exception as e:
|
||
print('Unexpected error sending push:', e)
|
||
|
||
|
||
class ConnectionManager:
|
||
def __init__(self):
|
||
# Храним активные соединения: {user_id: websocket}
|
||
self.active_connections: Dict[str, WebSocket] = {}
|
||
|
||
async def connect(self, user_id: int, websocket: WebSocket):
|
||
await websocket.accept()
|
||
self.active_connections[str(user_id)] = websocket
|
||
|
||
def disconnect(self, user_id: int):
|
||
key = str(user_id)
|
||
if key in self.active_connections:
|
||
del self.active_connections[key]
|
||
|
||
async def send_personal_message(self, message: dict, user_id: str) -> bool:
|
||
if str(user_id) in self.active_connections:
|
||
try:
|
||
await self.active_connections[str(user_id)].send_json(message)
|
||
print('Sent to socket')
|
||
return True
|
||
except Exception as e:
|
||
print(f'Failed to send to socket: {e}')
|
||
return False
|
||
else:
|
||
print('User not active')
|
||
return False
|
||
|
||
async def broadcast(self, message: dict):
|
||
# Рассылка вообще всем (например, системное уведомление)
|
||
for connection in self.active_connections.values():
|
||
await connection.send_json(message)
|
||
|
||
|
||
manager = ConnectionManager()
|