Edit message listener

This commit is contained in:
2026-01-25 10:29:17 +03:00
parent 8c0c063bac
commit 2711270d9b
10 changed files with 147 additions and 36 deletions
+33
View File
@@ -0,0 +1,33 @@
name: Deploy to Server
on:
push:
branches:
- main
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create .env file
run: |
echo "${{ secrets.ENV_FILE }}" > .env
- name: Build
run: |
echo "Building to server"
docker compose build
- name: Deploying
run: |
echo "Deploying to server"
docker compose up -d
- name: Cleanup old images
run: |
docker image prune -f
+1 -1
View File
@@ -2,7 +2,7 @@
# 🌌 Aether # 🌌 Aether
<img src="assets/logo.png" alt="Aether logo" width="150" style="border-radius: 15px;"> <img src="assets/mini-logo.png" alt="Aether logo" width="150" style="border-radius: 15px;">
**Современная full-stack платформа для чатов с мощным backend и элегантным frontend** **Современная full-stack платформа для чатов с мощным backend и элегантным frontend**
+1 -1
View File
@@ -7,7 +7,7 @@ from jose import jwt
from sqlalchemy import or_ from sqlalchemy import or_
from app.utils.hash_password import verify_password from app.utils.hash_password import verify_password
from app.services.redis_service import RefreshTokenStorage from app.services.storage_service import RefreshTokenStorage
from app.core.exceptions import InvalidTokenException from app.core.exceptions import InvalidTokenException
from app.users.models import UserModel from app.users.models import UserModel
from app.users.dao import UserDAO from app.users.dao import UserDAO
+6 -29
View File
@@ -12,12 +12,12 @@ from app.chats.models import ChatModel, MessageModel, ParticipantModel
from app.chats.schemas import Chat, MessageCreate, MessageCreateDB, ChatCreateDB, ParticipantCreateDB, Message, MessageUpdateDB, MessageUpdate from app.chats.schemas import Chat, MessageCreate, MessageCreateDB, ChatCreateDB, ParticipantCreateDB, Message, MessageUpdateDB, MessageUpdate
from app.users.models import UserModel from app.users.models import UserModel
from app.core.redis import get_redis from app.core.redis import get_redis
from app.utils.connect_manager import manager
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class ChatService: class ChatService:
active_connections: Dict[str, WebSocket] = {}
@classmethod @classmethod
async def get_chats(cls, user: UserModel, offset: int, limit: int) -> List[Chat]: async def get_chats(cls, user: UserModel, offset: int, limit: int) -> List[Chat]:
@@ -130,38 +130,15 @@ class ChatService:
@classmethod @classmethod
async def save_websocket(cls, user: UserModel, ws: WebSocket): async def save_websocket(cls, user: UserModel, ws: WebSocket):
cls.active_connections[str(user.id)] = ws manager.add_connection(user_id=str(user.id), ws=ws)
log.info("WebSocket connection saved", extra={"user_id": user.id, "active_connections": len(cls.active_connections) + 1}) log.info("WebSocket connection saved", extra={"user_id": user.id, "active_connections": manager.count_connections + 1})
@classmethod @classmethod
async def delete_websocket(cls, user: UserModel): async def delete_websocket(cls, user: UserModel):
cls.active_connections.pop(str(user.id)) manager.delete_connection(user_id=str(user.id))
log.info("WebSocket connection deleted", extra={"user_id": user.id, "active_connections": len(cls.active_connections) - 1}) log.info("WebSocket connection deleted", extra={"user_id": user.id, "active_connections": manager.count_connections - 1})
@classmethod
async def message_listener(cls):
redis_client = await get_redis()
pubsub = redis_client.pubsub()
await pubsub.subscribe("messenger_updates")
async for message in pubsub.listen():
log.debug(f"Received message from Redis: {message}")
if message["type"] == "message":
payload = json.loads(message["data"])
user_id = payload["user_id"]
if user_id in cls.active_connections:
ws = cls.active_connections[user_id]
await ws.send_json(payload["message"])
log.info(f"Message sent to user {user_id} via WebSocket")
else:
log.debug(f"User {user_id} not connected")
@classmethod @classmethod
@@ -170,7 +147,7 @@ class ChatService:
for user_id in user_ids: for user_id in user_ids:
payload = { payload = {
"user_id": str(user_id), "user_id": str(user_id),
"message": message.model_dump(mode='json') "data": message.model_dump(mode='json')
} }
await redis_client.publish("messenger_updates", json.dumps(payload)) await redis_client.publish("messenger_updates", json.dumps(payload))
log.debug(f"Published message for user_id: {user_id}") log.debug(f"Published message for user_id: {user_id}")
+2 -2
View File
@@ -12,9 +12,9 @@ from app.core.redis import close_redis, init_redis
from app.users.router import router as user_router from app.users.router import router as user_router
from app.auth.router import router as auth_router from app.auth.router import router as auth_router
from app.chats.router import router as chat_router from app.chats.router import router as chat_router
from app.chats.service import ChatService
from app.core.log_config import set_logging from app.core.log_config import set_logging
from app.core.config import settings from app.core.config import settings
from app.services.messenger_service import PubSubMessenger
set_logging() set_logging()
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -24,7 +24,7 @@ log = logging.getLogger(__name__)
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
await init_redis() await init_redis()
log.info("Redis connected") log.info("Redis connected")
task_send_message = asyncio.create_task(ChatService.message_listener()) task_send_message = asyncio.create_task(PubSubMessenger.subscribe_to_channels())
log.info("Message sender started") log.info("Message sender started")
yield yield
await close_redis() await close_redis()
+73
View File
@@ -0,0 +1,73 @@
import json
import logging
from fastapi import WebSocket
from app.core.redis import get_redis
from app.utils.connect_manager import manager
log = logging.getLogger(__name__)
class PubSubMessenger:
@classmethod
def get_handlers(cls) -> dict:
return dict(
messenger_updates=cls.handle_message
)
@classmethod
async def subscribe_to_channels(cls):
print("test")
redis_client = await get_redis()
pubsub = redis_client.pubsub()
await pubsub.subscribe(*cls.get_handlers().keys())
async for message in pubsub.listen():
log.debug(f"Received message from Redis: {message}")
if message["type"] != "message":
continue
payload = json.loads(message["data"])
user_id = payload["user_id"]
ws = manager.get_connection(str(user_id))
if ws is None:
log.debug(f"User {user_id} not connected")
continue
handler = cls.get_handlers().get(message['channel'])
if handler:
await handler(payload["data"], ws)
@classmethod
async def handle_message(cls, message, ws: WebSocket):
await ws.send_json(message)
log.info(f"Message sent to user via WebSocket")
# async def message_listener():
# redis_client = await get_redis()
#
# pubsub = redis_client.pubsub()
#
# await pubsub.subscribe("messenger_updates")
#
# async for message in pubsub.listen():
# log.debug(f"Received message from Redis: {message}")
# if message["type"] == "message":
# payload = json.loads(message["data"])
# user_id = payload["user_id"]
#
# ws = manager.get_connection(str(user_id))
#
# if ws is None:
# log.debug(f"User {user_id} not connected")
# continue
#
# await ws.send_json(payload["message"])
# log.info(f"Message sent to user {user_id} via WebSocket")
@@ -1,8 +1,10 @@
import logging import logging
import uuid import uuid
from typing import Optional from typing import Optional
import json
from app.core.redis import get_redis from app.core.redis import get_redis
from app.utils.connect_manager import manager
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
+1 -1
View File
@@ -7,7 +7,7 @@ from fastapi import HTTPException, status, UploadFile
from sqlalchemy import or_ from sqlalchemy import or_
from app.utils.hash_password import hash_password, verify_password from app.utils.hash_password import hash_password, verify_password
from app.services.redis_service import EmailTokenStorage, ChangePasswordTokenStorage from app.services.storage_service import EmailTokenStorage, ChangePasswordTokenStorage
from app.utils.S3_client import s3_client from app.utils.S3_client import s3_client
from app.core.exceptions import InvalidTokenException, TokenExpiredException, UserNotFoundException from app.core.exceptions import InvalidTokenException, TokenExpiredException, UserNotFoundException
from app.users.models import UserModel from app.users.models import UserModel
-2
View File
@@ -32,8 +32,6 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No connection found") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No connection found")
authorization: str = connection.cookies.get("access_token") authorization: str = connection.cookies.get("access_token")
print(authorization)
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
+28
View File
@@ -0,0 +1,28 @@
from typing import Dict, Optional
from fastapi import WebSocket
class ConnectManager:
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {}
def get_connection(self, user_id: str) -> Optional[WebSocket]:
return self.active_connections.get(user_id)
def add_connection(self, user_id: str, ws: WebSocket):
self.active_connections[user_id] = ws
def delete_connection(self, user_id: str):
self.active_connections.pop(user_id)
@property
def count_connections(self) -> int:
return len(self.active_connections)
manager = ConnectManager()