forked from HSE_team/BetterCallPraskovia
fixed bot and server connectivity issues
This commit is contained in:
parent
dfc188e179
commit
169d874dad
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
2
tg_bot/infrastructure/__init__.py
Normal file
2
tg_bot/infrastructure/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
"""Infrastructure layer for the Telegram bot"""
|
||||||
|
|
||||||
88
tg_bot/infrastructure/http_client.py
Normal file
88
tg_bot/infrastructure/http_client.py
Normal 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"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user