Chepuhagram/srv/app/services/notification_service.py

110 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from firebase_admin import messaging
from app.core.config import config
from app.db import models
from app.services.crypto_service import encrypt_system_message
from app.websocket.connection_manager import manager
async def send_system_notification(db: AsyncSession, receiver_id: int, plain_text: str):
"""
Полный цикл отправки зашифрованного системного уведомления.
Безопасен к коммитам SQLAlchemy и не блокирует асинхронный Event Loop.
"""
# 1. Извлекаем получателя из БД
result = await db.execute(select(models.User).where(models.User.id == receiver_id))
user = result.scalars().first()
if not user:
print(f"[System Notice] Ошибка: Пользователь {receiver_id} не найден.")
return False
if not user.public_key:
print(f"[System Notice] Предупреждение: У пользователя {user.username} не сгенерирован ключ E2EE. Отправка отменена.")
return False
# ФИКС: Атомарно сохраняем все нужные свойства пользователя в локальные переменные.
# После db.commit() объект user станет недоступен, но эти переменные останутся в безопасности.
u_id = user.id
u_username = user.username
u_public_key = user.public_key
u_fcm_token = user.fcm_token
try:
# 2. Шифруем текст сообщения, используя сохраненный ключ
encrypted_content = encrypt_system_message(plain_text, u_public_key)
preview_text = plain_text if len(plain_text) <= 500 else f"{plain_text[:497]}..."
encrypted_preview = encrypt_system_message(preview_text, u_public_key)
# 3. Записываем сообщение в историю базы данных
new_message = models.Message(
sender_id=0, # Система
receiver_id=u_id,
content=encrypted_content,
timestamp=datetime.now(timezone.utc),
message_type="text"
)
db.add(new_message)
await db.commit()
await db.refresh(new_message)
# Подготавливаем JSON-пакет для WebSocket
socket_payload = {
"type": "private_message",
"id": new_message.id,
"sender_id": 0,
"receiver_id": u_id,
"content": encrypted_preview,
"timestamp": new_message.timestamp.isoformat(),
"message_type": "text"
}
# 4. Доставка через WebSocket (используем строковый u_id)
is_delivered_via_ws = await manager.send_personal_message(socket_payload, user_id=str(u_id))
# 5. Если пользователь офлайн, отправляем Push-уведомление через Firebase FCM
if u_fcm_token:
try:
unread_res = await db.execute(
select(func.count(models.Message.id))
.where(models.Message.receiver_id == receiver_id, models.Message.read_at == None)
)
unread_count = unread_res.scalar() or 0
fcm_message = messaging.Message(
token=u_fcm_token,
data={
"click_action": "FLUTTER_NOTIFICATION_CLICK",
"type": "enc_message",
"sender_id": "0",
"receiver_id": str(u_id),
"public_key": config.SYSTEM_CHAT_PUBLIC_KEY,
"content": encrypted_preview,
"timestamp": new_message.timestamp.isoformat(),
"username": "Chepuhagram",
"unread_count": str(unread_count),
"message_id": str(new_message.id)
},
android=messaging.AndroidConfig(priority="high"),
apns=messaging.APNSConfig(payload=messaging.APNSPayload(aps=messaging.Aps(sound="default")))
)
# ФИКС: Выполняем синхронный сетевой запрос Firebase в отдельном потоке (Thread),
# чтобы он не блокировал и не тормозил основной поток FastAPI
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, messaging.send, fcm_message)
print(f"[System Notice] Push успешно отправлен через FCM для {u_username}")
except Exception as fcm_err:
print(f"[System Notice] Не удалось отправить FCM пуш: {fcm_err}")
return True
except Exception as err:
print(f"[System Notice] Критическая ошибка во время отправки уведомления: {err}")
await db.rollback()
return False