andrewbokh #6

Merged
Arxip222 merged 4 commits from andrewbokh into main 2025-12-24 10:36:03 +03:00
10 changed files with 149 additions and 29 deletions
Showing only changes of commit 169d874dad - Show all commits

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python3
import sys import sys
import os import os

View File

@ -29,7 +29,6 @@ async def lifespan(app: FastAPI):
except Exception as e: except Exception as e:
print(f"Примечание при создании таблиц: {e}") print(f"Примечание при создании таблиц: {e}")
yield yield
# Cleanup container if needed
if hasattr(app.state, 'container') and hasattr(app.state.container, 'close'): if hasattr(app.state, 'container') and hasattr(app.state.container, 'close'):
if asyncio.iscoroutinefunction(app.state.container.close): if asyncio.iscoroutinefunction(app.state.container.close):
await app.state.container.close() await app.state.container.close()
@ -45,7 +44,6 @@ app = FastAPI(
lifespan=lifespan lifespan=lifespan
) )
# Настройка Dishka ДО добавления middleware
container = create_container() container = create_container()
setup_dishka(container, app) setup_dishka(container, app)
app.state.container = container app.state.container = container

View File

@ -39,13 +39,9 @@ from src.application.use_cases.rag_use_cases import RAGUseCases
class DatabaseProvider(Provider): class DatabaseProvider(Provider):
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
@asynccontextmanager
async def get_db(self) -> AsyncSession: async def get_db(self) -> AsyncSession:
async with AsyncSessionLocal() as session: session = AsyncSessionLocal()
try: return session
yield session
finally:
await session.close()
class RepositoryProvider(Provider): class RepositoryProvider(Provider):

View File

@ -70,6 +70,7 @@ services:
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
DEEPSEEK_API_URL: ${DEEPSEEK_API_URL:-https://api.deepseek.com/v1/chat/completions} DEEPSEEK_API_URL: ${DEEPSEEK_API_URL:-https://api.deepseek.com/v1/chat/completions}
YANDEX_OCR_API_KEY: ${YANDEX_OCR_API_KEY} YANDEX_OCR_API_KEY: ${YANDEX_OCR_API_KEY}
BACKEND_URL: ${BACKEND_URL:-http://backend:8000/api/v1}
DEBUG: "true" DEBUG: "true"
depends_on: depends_on:
- postgres - postgres

View File

@ -1,9 +1,10 @@
import os
from typing import List, Optional from typing import List, Optional
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
"""Настройки приложения (загружаются из .env файла в корне проекта)"""
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file=".env", env_file=".env",
env_file_encoding="utf-8", env_file_encoding="utf-8",
@ -13,27 +14,35 @@ class Settings(BaseSettings):
APP_NAME: str = "VibeLawyerBot" APP_NAME: str = "VibeLawyerBot"
VERSION: str = "0.1.0" VERSION: str = "0.1.0"
DEBUG: bool = True DEBUG: bool = False
TELEGRAM_BOT_TOKEN: str = "" TELEGRAM_BOT_TOKEN: str = ""
FREE_QUESTIONS_LIMIT: int = 5 FREE_QUESTIONS_LIMIT: int = 5
PAYMENT_AMOUNT: float = 500.0 PAYMENT_AMOUNT: float = 500.0
LOG_LEVEL: str = "INFO" LOG_LEVEL: str = "INFO"
LOG_FILE: str = "logs/bot.log" 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_RETURN_URL: str = "https://t.me/vibelawyer_bot"
YOOKASSA_WEBHOOK_SECRET: Optional[str] = None YOOKASSA_WEBHOOK_SECRET: Optional[str] = None
DEEPSEEK_API_KEY: Optional[str] = None DEEPSEEK_API_KEY: Optional[str] = None
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions" 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 = "" ADMIN_IDS_STR: str = ""
@property @property
def ADMIN_IDS(self) -> List[int]: def ADMIN_IDS(self) -> List[int]:
"""Список ID администраторов из строки через запятую"""
if not self.ADMIN_IDS_STR: if not self.ADMIN_IDS_STR:
return [] return []
try: try:

View File

@ -3,6 +3,7 @@ import aiohttp
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.infrastructure.http_client import create_http_session, normalize_backend_url
class User: class User:
@ -39,19 +40,22 @@ class UserService:
"""Сервис для работы с пользователями через API бэкенда""" """Сервис для работы с пользователями через API бэкенда"""
def __init__(self): 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]: async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]:
"""Получить пользователя по Telegram ID""" """Получить пользователя по Telegram ID"""
try: try:
async with aiohttp.ClientSession() as session: url = f"{self.backend_url}/users/telegram/{telegram_id}"
async with session.get( async with create_http_session() as session:
f"{self.backend_url}/users/telegram/{telegram_id}" async with session.get(url, ssl=False) as response:
) as response:
if response.status == 200: if response.status == 200:
data = await response.json() data = await response.json()
return User(data) return User(data)
return None return None
except aiohttp.ClientConnectorError as e:
print(f"Backend not available at {self.backend_url}: {e}")
return None
except Exception as e: except Exception as e:
print(f"Error getting user: {e}") print(f"Error getting user: {e}")
return None return None
@ -67,25 +71,43 @@ class UserService:
user = await self.get_user_by_telegram_id(telegram_id) user = await self.get_user_by_telegram_id(telegram_id)
if not user: if not user:
try: try:
async with aiohttp.ClientSession() as session: async with create_http_session() as session:
async with session.post( async with session.post(
f"{self.backend_url}/users", 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: ) as response:
if response.status in [200, 201]: if response.status in [200, 201]:
data = await response.json() data = await response.json()
return User(data) 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: 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 raise
return user return user
async def update_user_questions(self, telegram_id: int) -> bool: async def update_user_questions(self, telegram_id: int) -> bool:
"""Увеличить счетчик использованных вопросов""" """Увеличить счетчик использованных вопросов"""
try: try:
async with aiohttp.ClientSession() as session: async with create_http_session() as session:
async with session.post( 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: ) as response:
return response.status == 200 return response.status == 200
except Exception as e: except Exception as e:
@ -95,10 +117,11 @@ class UserService:
async def activate_premium(self, telegram_id: int, days: int = 30) -> bool: async def activate_premium(self, telegram_id: int, days: int = 30) -> bool:
"""Активировать premium статус""" """Активировать premium статус"""
try: try:
async with aiohttp.ClientSession() as session: async with create_http_session() as session:
async with session.post( async with session.post(
f"{self.backend_url}/users/telegram/{telegram_id}/activate-premium", f"{self.backend_url}/users/telegram/{telegram_id}/activate-premium",
params={"days": days} params={"days": days},
ssl=False
) as response: ) as response:
return response.status == 200 return response.status == 200
except Exception as e: except Exception as e:

View File

@ -0,0 +1,2 @@
"""Infrastructure layer for the Telegram bot"""

View File

@ -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"
}
)

View File

@ -25,9 +25,9 @@ async def create_bot() -> tuple[Bot, Dispatcher]:
dp.include_router(start_handler.router) dp.include_router(start_handler.router)
dp.include_router(help_handler.router) dp.include_router(help_handler.router)
dp.include_router(stats_handler.router) dp.include_router(stats_handler.router)
dp.include_router(question_handler.router)
dp.include_router(buy_handler.router) dp.include_router(buy_handler.router)
dp.include_router(collection_handler.router) dp.include_router(collection_handler.router)
dp.include_router(question_handler.router)
return bot, dp return bot, dp

View File

@ -2,6 +2,7 @@ from aiogram import Router, types
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from decimal import Decimal from decimal import Decimal
import aiohttp
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.payment.yookassa.client import yookassa_client from tg_bot.payment.yookassa.client import yookassa_client
from tg_bot.domain.services.user_service import UserService from tg_bot.domain.services.user_service import UserService
@ -29,8 +30,10 @@ async def cmd_buy(message: Message):
f"Новая подписка будет добавлена к текущей.", f"Новая подписка будет добавлена к текущей.",
parse_mode="HTML" parse_mode="HTML"
) )
except Exception: except aiohttp.ClientError as e:
pass print(f"Не удалось подключиться к backend при проверке подписки: {e}")
except Exception as e:
print(f"Ошибка при проверке подписки: {e}")
await message.answer( await message.answer(
"*Создаю ссылку для оплаты...*\n\n" "*Создаю ссылку для оплаты...*\n\n"