patch chat

This commit is contained in:
unknown 2025-09-30 01:07:43 +03:00
parent 6f8bc0e762
commit 22b6fd272f
5 changed files with 241 additions and 214 deletions

View File

@ -105,7 +105,7 @@ class MainController(QStackedWidget):
# return
token = session['access_token']
success, data = await get_private_chats(token=token, offset=0, limit=50)
success, data = await get_private_chats(login=username, token=token, offset=0, limit=50)
if success:
# Отправляем данные в основной поток через сигнал
@ -113,3 +113,5 @@ class MainController(QStackedWidget):
else:
# Отправляем ошибку в основной поток через сигнал
self.notification_requested.emit(f"Не удалось загрузить чаты: {data}", True)
print("data", data)

View File

@ -1,17 +1,92 @@
import ssl
import httpx
from contextvars import ContextVar
from typing import Dict, Any, Optional
ssl_transport = httpx.AsyncHTTPTransport(http2=True)
# Ограничения пула соединений
# Connection pool & timeouts
limits = httpx.Limits(max_connections=200, max_keepalive_connections=50)
# Таймауты
timeout = httpx.Timeout(connect=5.0, read=10.0, write=5.0, pool=5.0)
# Глобальный клиент
client = httpx.AsyncClient(
transport=ssl_transport,
limits=limits,
timeout=timeout,
)
# Keep a client per event loop/context to avoid cross-loop issues
_client_ctx: ContextVar[httpx.AsyncClient | None] = ContextVar("http_client_ctx", default=None)
def get_client() -> httpx.AsyncClient:
client = _client_ctx.get()
if client is None or client.is_closed:
transport = httpx.AsyncHTTPTransport(http2=True)
client = httpx.AsyncClient(transport=transport, limits=limits, timeout=timeout)
_client_ctx.set(client)
return client
async def aclose_client():
client = _client_ctx.get()
if client is not None and not client.is_closed:
await client.aclose()
_client_ctx.set(None)
# --- Authorized helpers with auto-refresh on 401 ---
async def authorized_get(
url: str,
*,
login: Optional[str] = None,
access_token: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, Any]] = None,
) -> httpx.Response:
hdrs = dict(headers or {})
if access_token:
hdrs["Authorization"] = f"Bearer {access_token}"
resp = await get_client().get(url, headers=hdrs, params=params)
if resp.status_code != 401 or not login or not access_token:
return resp
# Try refresh flow lazily to avoid import cycle at import time
from app.core.database import get_session, logout
from app.core.services.auth_service import refresh_token as do_refresh
session = get_session(login)
if not session or not session.get("refresh_token"):
return resp
ok, data = await do_refresh(access_token, session["refresh_token"])
if ok:
new_access = data["access_token"]
hdrs["Authorization"] = f"Bearer {new_access}"
return await get_client().get(url, headers=hdrs, params=params)
logout(access_token)
return resp
async def authorized_post(
url: str,
*,
login: Optional[str] = None,
access_token: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, Any]] = None,
json: Any = None,
data: Any = None,
) -> httpx.Response:
hdrs = dict(headers or {})
if access_token:
hdrs["Authorization"] = f"Bearer {access_token}"
resp = await get_client().post(url, headers=hdrs, params=params, json=json, data=data)
if resp.status_code != 401 or not login or not access_token:
return resp
from app.core.database import get_session, logout
from app.core.services.auth_service import refresh_token as do_refresh
session = get_session(login)
if not session or not session.get("refresh_token"):
return resp
ok, data = await do_refresh(access_token, session["refresh_token"])
if ok:
new_access = data["access_token"]
hdrs["Authorization"] = f"Bearer {new_access}"
return await get_client().post(url, headers=hdrs, params=params, json=json, data=data)
logout(access_token)
return resp

View File

@ -3,62 +3,61 @@ import asyncio
from app.core import config
from app.core.database import add_session, logout, get_session
from app.core.localizer import localizer
from app.core.http_client import get_client, authorized_get
async def login(login, password):
"""
Отправляет запрос на аутентификацию и в случае успеха сохраняет сессию.
Логин пользователя по логину и паролю.
:param login: Логин пользователя
:param password: Пароль пользователя
:return: Кортеж (успех: bool, сообщение: str)
:return: tuple (ok: bool, message: str)
"""
url = f"{config.BASE_URL}/v1/auth/login"
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.post(url, json={"login": login, "password": password})
response = await get_client().post(url, json={"login": login, "password": password})
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
token_data = data["data"]
add_session(
login=login,
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"],
user_id=token_data["user_id"]
)
return True, localizer.translate("Успешный вход")
else:
return False, data.get("detail", localizer.translate("Неизвестная ошибка ответа"))
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
token_data = data["data"]
add_session(
login=login,
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"],
user_id=token_data.get("user_id"),
)
return True, localizer.translate("Вход выполнен успешно")
return False, data.get("detail", localizer.translate("Неизвестная ошибка"))
elif response.status_code in [401]:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Неверный логин или пароль"))
if response.status_code == 401:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Неверный логин или пароль"))
elif response.status_code in [403]:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Учетная запись пользователя отключена"))
if response.status_code == 403:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Доступ запрещен"))
elif response.status_code == 422:
return False, localizer.translate("Некорректные данные для входа")
if response.status_code == 422:
return False, localizer.translate("Некорректные входные данные")
else:
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e:
return False, f"{localizer.translate('Ошибка сети')}: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def register(login, password, invite=None):
"""
Отправляет запрос на регистрацию нового пользователя.
Регистрация нового пользователя.
:param login: Логин
:param password: Пароль
:param invite: Инвайт-код (опционально)
:return: Кортеж (успех: bool, сообщение: str)
:return: tuple (ok: bool, message: str)
"""
url = f"{config.BASE_URL}/v1/auth/register"
payload = {"login": login, "password": password}
@ -66,116 +65,90 @@ async def register(login, password, invite=None):
payload["invite"] = invite
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.post(url, json=payload)
response = await get_client().post(url, json=payload)
if response.status_code == 201:
return True, localizer.translate("Регистрация прошла успешно!")
if response.status_code == 201:
return True, localizer.translate("Регистрация прошла успешно!")
error_data = response.json()
error_message = error_data.get("detail", localizer.translate("Произошла ошибка"))
error_data = response.json()
error_message = error_data.get("detail", localizer.translate("Произошла ошибка"))
if response.status_code == 409:
return False, localizer.translate("Этот логин уже занят.")
elif response.status_code == 400:
return False, localizer.translate("Неверный инвайт-код.")
elif response.status_code == 403:
return False, localizer.translate("Регистрация в данный момент отключена.")
elif response.status_code == 422:
return False, localizer.translate("Данные не прошли валидацию. Проверьте длину логина и пароля.")
else:
return False, f"{localizer.translate('Ошибка сервера')} ({response.status_code}): {error_message}"
if response.status_code == 409:
return False, localizer.translate("Пользователь уже существует.")
if response.status_code == 400:
return False, localizer.translate("Некорректный запрос.")
if response.status_code == 403:
return False, localizer.translate("Регистрация по приглашению отключена.")
if response.status_code == 422:
return False, localizer.translate("Некорректные входные данные. Проверьте логин и пароль.")
return False, f"{localizer.translate('Ошибка сервера')} ({response.status_code}): {error_message}"
except httpx.RequestError as e:
return False, f"{localizer.translate('Ошибка сети')}: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def refresh_token(access_token: str, refresh_token: str):
"""
Обновляет токен доступа, используя токен обновления.
Обновление пары токенов по refresh_token.
:param access_token: Истекший токен доступа
:param refresh_token: Токен обновления
:return: Кортеж (успех: bool, данные: dict | str)
:param access_token: Текущий access token
:param refresh_token: Refresh token
:return: tuple (ok: bool, data: dict | str)
"""
url = f"{config.BASE_URL}/v1/auth/token/refresh"
payload = {"access_token": access_token, "refresh_token": refresh_token}
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.post(url, json=payload)
response = await get_client().post(url, json=payload)
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
token_data = data["data"]
# Обновляем сессию с новыми токенами
add_session(
login=None, # Логин не требуется для обновления
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"],
update_existing=True
)
return True, token_data
else:
return False, data.get("detail", localizer.translate("Unknown error"))
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
token_data = data["data"]
add_session(
login=None,
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"],
update_existing=True,
)
return True, token_data
return False, data.get("detail", localizer.translate("Неизвестная ошибка"))
elif response.status_code == 401:
return False, localizer.translate("Refresh token is invalid or expired")
if response.status_code == 401:
return False, localizer.translate("Refresh token недействителен или истек")
else:
return False, f"{localizer.translate('Server error')}: {response.status_code}"
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e:
return False, f"{localizer.translate('Network error')}: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"{localizer.translate('An error occurred')}: {e}"
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def get_user_role(access_token: str, login: str):
"""
Получает роль и права пользователя по токену доступа.
В случае истечения срока действия токена, пытается его обновить.
Получение роли пользователя. При 401 выполняется автообновление токена внутри authorized_get.
"""
url = f"{config.BASE_URL}/v1/user/role"
headers = {"Authorization": f"Bearer {access_token}"}
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.get(url, headers=headers)
response = await authorized_get(url, login=login, access_token=access_token, headers=headers)
if response.status_code == 200:
data = response.json()
return (True, data['data']) if data.get("status") == "fine" else (False, data.get("detail", localizer.translate("Unknown error")))
if response.status_code == 200:
data = response.json()
return (True, data['data']) if data.get("status") == "fine" else (False, data.get("detail", localizer.translate("Неизвестная ошибка")))
elif response.status_code == 401:
# Токен истек, пытаемся обновить
session = get_session(login)
if not session or not session['refresh_token']:
return False, localizer.translate("No refresh token found")
if response.status_code == 401:
return False, localizer.translate("Сессия истекла, войдите снова")
refresh_success, refresh_data = await refresh_token(access_token, session['refresh_token'])
if refresh_success:
# Повторяем запрос с новым токеном
new_access_token = refresh_data['access_token']
headers["Authorization"] = f"Bearer {new_access_token}"
response = await client.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
return (True, data['data']) if data.get("status") == "fine" else (False, localizer.translate("Failed to get role after refresh"))
# Если обновление не удалось, выходим из системы
logout(access_token)
return False, localizer.translate("Session expired, please log in again")
else:
return False, f"{localizer.translate('Server error')}: {response.status_code}"
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e:
return False, f"{localizer.translate('Network error')}: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"{localizer.translate('An error occurred')}: {e}"
return False, f"{localizer.translate('Произошла ошибка')}: {e}"

View File

@ -7,61 +7,49 @@ from app.core.models.chat_models import (
PrivateMessageSendRequest, PrivateMessageSendResponse
)
from uuid import UUID
from app.core.http_client import get_client, authorized_get, authorized_post
async def get_private_chats(token: str, offset: int = 0, limit: int = 20):
"""
Получает список приватных чатов пользователя.
:param token: Токен доступа пользователя
:param offset: Смещение для пагинации
:param limit: Количество чатов для загрузки
:return: Кортеж (успех: bool, данные: PrivateChatListData | str)
async def get_private_chats(login: str, token: str, offset: int = 0, limit: int = 20):
"""
Получить список приватных чатов.
:param token: Bearer токен
:param offset: Смещение
:param limit: Количество
:return: tuple (ok: bool, data: PrivateChatListData | str)
"""
# TODO: Добавить логику обновления токена, как в auth_service.py
url = f"{config.BASE_URL}/v1/chat/private/list"
headers = {"Authorization": f"Bearer {token}"}
params = {"offset": offset, "limit": limit}
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.get(url, headers=headers, params=params)
print("response.status_code", response.status_code)
response = await authorized_get(url, login=login, access_token=token, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
response_model = PrivateChatListResponse(**data)
return True, response_model.data
return False, data.get("detail", localizer.translate("Неизвестная ошибка"))
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
# Используем Pydantic модель для парсинга ответа
response_model = PrivateChatListResponse(**data)
# print("response_model.data", response_model.data)
print("data", data)
return True, response_model.data
else:
return False, data.get("detail", localizer.translate("Неизвестная ошибка ответа"))
if response.status_code in [401, 403]:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Недостаточно прав или неавторизован"))
elif response.status_code in [401, 403]:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Ошибка аутентификации или авторизации"))
if response.status_code == 422:
return False, localizer.translate("Некорректные параметры запроса")
elif response.status_code == 422:
return False, localizer.translate("Некорректные параметры запроса")
else:
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e:
return False, f"{localizer.translate('Ошибка сети')}: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def get_chat_history(token: str, chat_id: UUID, before_message_id: int = None, limit: int = 30):
"""
Получает историю сообщений для указанного приватного чата.
:param token: Токен доступа
:param chat_id: ID чата
:param before_message_id: ID сообщения для пагинации (загрузка более старых)
:param limit: Количество сообщений для загрузки
:return: Кортеж (успех: bool, данные: PrivateChatHistoryData | str)
async def get_chat_history(login: str, token: str, chat_id: UUID, before_message_id: int = None, limit: int = 30):
"""
История сообщений приватного чата.
"""
url = f"{config.BASE_URL}/v1/chat/private/history"
headers = {"Authorization": f"Bearer {token}"}
@ -70,73 +58,61 @@ async def get_chat_history(token: str, chat_id: UUID, before_message_id: int = N
params["before_message_id"] = before_message_id
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.get(url, headers=headers, params=params)
response = await authorized_get(url, login=login, access_token=token, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
response_model = PrivateChatHistoryResponse(**data)
return True, response_model.data
else:
return False, data.get("detail", "Неизвестная ошибка ответа")
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
response_model = PrivateChatHistoryResponse(**data)
return True, response_model.data
return False, data.get("detail", localizer.translate("Неизвестная ошибка"))
elif response.status_code in [401, 403]:
error_data = response.json()
return False, error_data.get("detail", "Ошибка аутентификации или доступа")
if response.status_code in [401, 403]:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Недостаточно прав или неавторизован"))
elif response.status_code == 422:
return False, "Некорректные параметры запроса"
if response.status_code == 422:
return False, localizer.translate("Некорректные параметры запроса")
else:
return False, f"Ошибка сервера: {response.status_code}"
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e:
return False, f"Ошибка сети: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"Произошла ошибка: {e}"
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def send_private_message(token: str, payload: "PrivateMessageSendRequest"):
async def send_private_message(login: str, token: str, payload: "PrivateMessageSendRequest"):
"""
Отправляет приватное сообщение в чат.
:param token: Токен доступа
:param payload: Данные сообщения (Pydantic модель PrivateMessageSendRequest)
:return: Кортеж (успех: bool, данные: PrivateMessageSendData | str)
Отправка приватного сообщения.
"""
url = f"{config.BASE_URL}/v1/chat/private/send"
headers = {"Authorization": f"Bearer {token}"}
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.post(url, headers=headers, json=payload.model_dump(mode='json'))
response = await authorized_post(url, login=login, access_token=token, headers=headers, json=payload.model_dump(mode='json'))
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
response_model = PrivateMessageSendResponse(**data)
return True, response_model.data
else:
return False, data.get("detail", "Неизвестная ошибка ответа")
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
response_model = PrivateMessageSendResponse(**data)
return True, response_model.data
return False, data.get("detail", localizer.translate("Неизвестная ошибка"))
elif response.status_code in [401, 403, 404]:
error_data = response.json()
print("error_data", error_data)
return False, error_data.get("detail", "Ошибка доступа или чат не найден")
if response.status_code in [401, 403, 404]:
error_data = response.json()
return False, error_data.get("detail", localizer.translate("Недостаточно прав или ресурс не найден"))
elif response.status_code == 422:
error_data = response.json()
# Может быть список ошибок
detail = error_data.get("detail")
if isinstance(detail, list):
return False, ", ".join([e.get("msg", "Неизвестная ошибка валидации") for e in detail])
return False, detail or "Некорректные данные"
if response.status_code == 422:
error_data = response.json()
detail = error_data.get("detail")
if isinstance(detail, list):
return False, ", ".join([e.get("msg", localizer.translate("Некорректные параметры")) for e in detail])
return False, detail or localizer.translate("Некорректный запрос")
else:
return False, f"Ошибка сервера: {response.status_code}"
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e:
return False, f"Ошибка сети: {e}"
return False, f"{localizer.translate('Сетевая ошибка')}: {e}"
except Exception as e:
return False, f"Произошла ошибка: {e}"
return False, f"{localizer.translate('Произошла ошибка')}: {e}"

View File

@ -489,7 +489,7 @@ class YobbleHomeView(QWidget):
return
self.show_notification("Загрузка истории...", is_error=False, duration=1000)
success, data = await get_chat_history(token, chat_id)
success, data = await get_chat_history(self.username, token, chat_id)
if success:
if self.chat_view:
@ -532,7 +532,7 @@ class YobbleHomeView(QWidget):
self.show_notification("Ошибка: сессия не найдена.", is_error=True)
return
success, data = await send_private_message(token, payload)
success, data = await send_private_message(self.username, token, payload)
if success:
# В случае успеха, создаем объект сообщения и добавляем в чат
@ -791,3 +791,4 @@ class YobbleHomeView(QWidget):
);
}}
"""