From 169d874dad12a06437f6caf41852d7b2a0e15354 Mon Sep 17 00:00:00 2001 From: bokho Date: Wed, 24 Dec 2025 04:38:38 +0300 Subject: [PATCH] fixed bot and server connectivity issues --- backend/run.py | 2 +- backend/src/presentation/main.py | 2 - backend/src/shared/di_container.py | 8 +- docker-compose.yml | 1 + tg_bot/config/settings.py | 19 ++-- tg_bot/domain/services/user_service.py | 47 +++++++--- tg_bot/infrastructure/__init__.py | 2 + tg_bot/infrastructure/http_client.py | 88 +++++++++++++++++++ tg_bot/infrastructure/telegram/bot.py | 2 +- .../telegram/handlers/buy_handler.py | 7 +- 10 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 tg_bot/infrastructure/__init__.py create mode 100644 tg_bot/infrastructure/http_client.py diff --git a/backend/run.py b/backend/run.py index ee0c7eb..87ff6db 100644 --- a/backend/run.py +++ b/backend/run.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 + import sys import os diff --git a/backend/src/presentation/main.py b/backend/src/presentation/main.py index 0666d4d..f6a1fae 100644 --- a/backend/src/presentation/main.py +++ b/backend/src/presentation/main.py @@ -29,7 +29,6 @@ async def lifespan(app: FastAPI): except Exception as e: print(f"Примечание при создании таблиц: {e}") yield - # Cleanup container if needed if hasattr(app.state, 'container') and hasattr(app.state.container, 'close'): if asyncio.iscoroutinefunction(app.state.container.close): await app.state.container.close() @@ -45,7 +44,6 @@ app = FastAPI( lifespan=lifespan ) -# Настройка Dishka ДО добавления middleware container = create_container() setup_dishka(container, app) app.state.container = container diff --git a/backend/src/shared/di_container.py b/backend/src/shared/di_container.py index 4908389..271f301 100644 --- a/backend/src/shared/di_container.py +++ b/backend/src/shared/di_container.py @@ -39,13 +39,9 @@ from src.application.use_cases.rag_use_cases import RAGUseCases class DatabaseProvider(Provider): @provide(scope=Scope.REQUEST) - @asynccontextmanager async def get_db(self) -> AsyncSession: - async with AsyncSessionLocal() as session: - try: - yield session - finally: - await session.close() + session = AsyncSessionLocal() + return session class RepositoryProvider(Provider): diff --git a/docker-compose.yml b/docker-compose.yml index 495c444..33fbac6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,7 @@ services: DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} DEEPSEEK_API_URL: ${DEEPSEEK_API_URL:-https://api.deepseek.com/v1/chat/completions} YANDEX_OCR_API_KEY: ${YANDEX_OCR_API_KEY} + BACKEND_URL: ${BACKEND_URL:-http://backend:8000/api/v1} DEBUG: "true" depends_on: - postgres diff --git a/tg_bot/config/settings.py b/tg_bot/config/settings.py index bf695df..e7d67e1 100644 --- a/tg_bot/config/settings.py +++ b/tg_bot/config/settings.py @@ -1,9 +1,10 @@ -import os from typing import List, Optional from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + """Настройки приложения (загружаются из .env файла в корне проекта)""" + model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", @@ -13,27 +14,35 @@ class Settings(BaseSettings): APP_NAME: str = "VibeLawyerBot" VERSION: str = "0.1.0" - DEBUG: bool = True + DEBUG: bool = False + TELEGRAM_BOT_TOKEN: str = "" + FREE_QUESTIONS_LIMIT: int = 5 PAYMENT_AMOUNT: float = 500.0 + LOG_LEVEL: str = "INFO" LOG_FILE: str = "logs/bot.log" - YOOKASSA_SHOP_ID: str = "1230200" - YOOKASSA_SECRET_KEY: str = "test_GVoixmlp0FqohXcyFzFHbRlAUoA3B1I2aMtAkAE_ubw" + + YOOKASSA_SHOP_ID: str = "" + YOOKASSA_SECRET_KEY: str = "" YOOKASSA_RETURN_URL: str = "https://t.me/vibelawyer_bot" YOOKASSA_WEBHOOK_SECRET: Optional[str] = None + DEEPSEEK_API_KEY: Optional[str] = None DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions" - BACKEND_URL: str = "http://localhost:8001/api/v1" + + BACKEND_URL: str = "http://localhost:8000/api/v1" + ADMIN_IDS_STR: str = "" @property def ADMIN_IDS(self) -> List[int]: + """Список ID администраторов из строки через запятую""" if not self.ADMIN_IDS_STR: return [] try: diff --git a/tg_bot/domain/services/user_service.py b/tg_bot/domain/services/user_service.py index 3fb2a25..67bccd9 100644 --- a/tg_bot/domain/services/user_service.py +++ b/tg_bot/domain/services/user_service.py @@ -3,6 +3,7 @@ import aiohttp from datetime import datetime from typing import Optional from tg_bot.config.settings import settings +from tg_bot.infrastructure.http_client import create_http_session, normalize_backend_url class User: @@ -39,19 +40,22 @@ class UserService: """Сервис для работы с пользователями через API бэкенда""" def __init__(self): - self.backend_url = settings.BACKEND_URL + self.backend_url = normalize_backend_url(settings.BACKEND_URL) + print(f"UserService initialized with BACKEND_URL: {self.backend_url}") async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]: """Получить пользователя по Telegram ID""" try: - async with aiohttp.ClientSession() as session: - async with session.get( - f"{self.backend_url}/users/telegram/{telegram_id}" - ) as response: + url = f"{self.backend_url}/users/telegram/{telegram_id}" + async with create_http_session() as session: + async with session.get(url, ssl=False) as response: if response.status == 200: data = await response.json() return User(data) return None + except aiohttp.ClientConnectorError as e: + print(f"Backend not available at {self.backend_url}: {e}") + return None except Exception as e: print(f"Error getting user: {e}") return None @@ -67,25 +71,43 @@ class UserService: user = await self.get_user_by_telegram_id(telegram_id) if not user: try: - async with aiohttp.ClientSession() as session: + async with create_http_session() as session: async with session.post( f"{self.backend_url}/users", - json={"telegram_id": str(telegram_id), "role": "user"} + json={"telegram_id": str(telegram_id), "role": "user"}, + ssl=False ) as response: if response.status in [200, 201]: data = await response.json() return User(data) + else: + error_text = await response.text() + raise Exception( + f"Backend API returned status {response.status}: {error_text}. " + f"Make sure the backend server is running at {self.backend_url}" + ) + except aiohttp.ClientConnectorError as e: + error_msg = ( + f"Cannot connect to backend API at {self.backend_url}. " + f"Please ensure the backend server is running on port 8000. " + f"Start it with: cd project/backend && python run.py" + ) + print(f"Error creating user: {error_msg}") + print(f"Original error: {e}") + raise ConnectionError(error_msg) from e except Exception as e: - print(f"Error creating user: {e}") + error_msg = f"Error creating user: {e}. Backend URL: {self.backend_url}" + print(error_msg) raise return user async def update_user_questions(self, telegram_id: int) -> bool: """Увеличить счетчик использованных вопросов""" try: - async with aiohttp.ClientSession() as session: + async with create_http_session() as session: async with session.post( - f"{self.backend_url}/users/telegram/{telegram_id}/increment-questions" + f"{self.backend_url}/users/telegram/{telegram_id}/increment-questions", + ssl=False ) as response: return response.status == 200 except Exception as e: @@ -95,10 +117,11 @@ class UserService: async def activate_premium(self, telegram_id: int, days: int = 30) -> bool: """Активировать premium статус""" try: - async with aiohttp.ClientSession() as session: + async with create_http_session() as session: async with session.post( f"{self.backend_url}/users/telegram/{telegram_id}/activate-premium", - params={"days": days} + params={"days": days}, + ssl=False ) as response: return response.status == 200 except Exception as e: diff --git a/tg_bot/infrastructure/__init__.py b/tg_bot/infrastructure/__init__.py new file mode 100644 index 0000000..f738cfb --- /dev/null +++ b/tg_bot/infrastructure/__init__.py @@ -0,0 +1,2 @@ +"""Infrastructure layer for the Telegram bot""" + diff --git a/tg_bot/infrastructure/http_client.py b/tg_bot/infrastructure/http_client.py new file mode 100644 index 0000000..4549d63 --- /dev/null +++ b/tg_bot/infrastructure/http_client.py @@ -0,0 +1,88 @@ +"""HTTP client utilities for making requests to the backend API""" +import aiohttp +from typing import Optional +import ssl +import os + + +def get_windows_host_ip() -> Optional[str]: + """ + Get the Windows host IP address when running in WSL. + In WSL2, the Windows host IP is typically the first nameserver in /etc/resolv.conf. + """ + try: + if os.path.exists("/etc/resolv.conf"): + with open("/etc/resolv.conf", "r") as f: + for line in f: + if line.startswith("nameserver"): + ip = line.split()[1] + if ip not in ["127.0.0.1", "127.0.0.53"] and not ip.startswith("fe80"): + return ip + except Exception: + pass + return None + + +def normalize_backend_url(url: str) -> str: + """ + Normalize backend URL for better compatibility, especially on WSL and Docker. + """ + if not ("localhost" in url or "127.0.0.1" in url): + return url + if os.path.exists("/.dockerenv"): + print(f"Warning: Running in Docker but URL contains localhost: {url}") + print("Please set BACKEND_URL environment variable in docker-compose.yml to use Docker service name (e.g., http://backend:8000/api/v1)") + return url.replace("localhost", "127.0.0.1") + try: + if os.path.exists("/proc/version"): + with open("/proc/version", "r") as f: + version_content = f.read().lower() + if "microsoft" in version_content: + windows_ip = get_windows_host_ip() + if windows_ip: + if "localhost" in url or "127.0.0.1" in url: + url = url.replace("localhost", windows_ip).replace("127.0.0.1", windows_ip) + print(f"WSL detected: Using Windows host IP {windows_ip} for backend connection") + return url + except Exception as e: + print(f"Warning: Could not detect WSL environment: {e}") + + if url.startswith("http://localhost") or url.startswith("https://localhost"): + return url.replace("localhost", "127.0.0.1") + return url + + +def create_http_session(timeout: Optional[aiohttp.ClientTimeout] = None) -> aiohttp.ClientSession: + """ + Create a configured aiohttp ClientSession for backend API requests. + + Args: + timeout: Optional timeout configuration. Defaults to 30 seconds total timeout. + + Returns: + Configured aiohttp.ClientSession + """ + if timeout is None: + timeout = aiohttp.ClientTimeout(total=30, connect=10) + + connector = aiohttp.TCPConnector( + ssl=False, + limit=100, + limit_per_host=30, + force_close=True, + enable_cleanup_closed=True + ) + + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + return aiohttp.ClientSession( + connector=connector, + timeout=timeout, + headers={ + "Content-Type": "application/json", + "Accept": "application/json" + } + ) + diff --git a/tg_bot/infrastructure/telegram/bot.py b/tg_bot/infrastructure/telegram/bot.py index c36228a..7bcbf64 100644 --- a/tg_bot/infrastructure/telegram/bot.py +++ b/tg_bot/infrastructure/telegram/bot.py @@ -25,9 +25,9 @@ async def create_bot() -> tuple[Bot, Dispatcher]: dp.include_router(start_handler.router) dp.include_router(help_handler.router) dp.include_router(stats_handler.router) - dp.include_router(question_handler.router) dp.include_router(buy_handler.router) dp.include_router(collection_handler.router) + dp.include_router(question_handler.router) return bot, dp diff --git a/tg_bot/infrastructure/telegram/handlers/buy_handler.py b/tg_bot/infrastructure/telegram/handlers/buy_handler.py index f7d9b6e..1b4f0a3 100644 --- a/tg_bot/infrastructure/telegram/handlers/buy_handler.py +++ b/tg_bot/infrastructure/telegram/handlers/buy_handler.py @@ -2,6 +2,7 @@ from aiogram import Router, types from aiogram.filters import Command from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton from decimal import Decimal +import aiohttp from tg_bot.config.settings import settings from tg_bot.payment.yookassa.client import yookassa_client from tg_bot.domain.services.user_service import UserService @@ -29,8 +30,10 @@ async def cmd_buy(message: Message): f"Новая подписка будет добавлена к текущей.", parse_mode="HTML" ) - except Exception: - pass + except aiohttp.ClientError as e: + print(f"Не удалось подключиться к backend при проверке подписки: {e}") + except Exception as e: + print(f"Ошибка при проверке подписки: {e}") await message.answer( "*Создаю ссылку для оплаты...*\n\n"