Merge pull request 'polina_tg' (#1) from polina_tg into main

Reviewed-on: HSE_team/BetterCallPraskovia#1
This commit is contained in:
Arxip222 2025-12-22 21:41:08 +03:00
commit 4a9fab7fba
33 changed files with 1811 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
.env
venv/
.venv/

93
create_database.py Normal file
View File

@ -0,0 +1,93 @@
import os
import sys
from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy import Column, String, DateTime, Boolean, Integer, Text
import uuid
from datetime import datetime
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, 'data', 'bot.db')
DATABASE_URL = f"sqlite:///{DB_PATH}"
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
if os.path.exists(DB_PATH):
try:
temp_engine = create_engine(DATABASE_URL)
inspector = inspect(temp_engine)
tables = inspector.get_table_names()
if tables:
sys.exit(0)
except:
pass
choice = input("Перезаписать БД? (y/N): ")
if choice.lower() != 'y':
sys.exit(0)
engine = create_engine(DATABASE_URL, echo=False)
Base = declarative_base()
class UserModel(Base):
__tablename__ = "users"
user_id = Column("user_id", String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
telegram_id = Column("telegram_id", String(100), nullable=False, unique=True)
created_at = Column("created_at", DateTime, default=datetime.utcnow, nullable=False)
role = Column("role", String(20), default="user", nullable=False)
is_premium = Column(Boolean, default=False, nullable=False)
premium_until = Column(DateTime, nullable=True)
questions_used = Column(Integer, default=0, nullable=False)
username = Column(String(100), nullable=True)
first_name = Column(String(100), nullable=True)
last_name = Column(String(100), nullable=True)
class PaymentModel(Base):
__tablename__ = "payments"
id = Column(Integer, primary_key=True, autoincrement=True)
payment_id = Column(String(36), default=lambda: str(uuid.uuid4()), nullable=False, unique=True)
user_id = Column(Integer, nullable=False)
amount = Column(String(20), nullable=False)
currency = Column(String(3), default="RUB", nullable=False)
status = Column(String(20), default="pending", nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
yookassa_payment_id = Column(String(100), unique=True, nullable=True)
description = Column(Text, nullable=True)
try:
Base.metadata.create_all(bind=engine)
session = Session(bind=engine)
existing = session.query(UserModel).filter_by(telegram_id="123456789").first()
if not existing:
test_user = UserModel(
telegram_id="123456789",
username="test_user",
first_name="Test",
last_name="User",
is_premium=True
)
session.add(test_user)
existing_payment = session.query(PaymentModel).filter_by(yookassa_payment_id="test_yoo_001").first()
if not existing_payment:
test_payment = PaymentModel(
user_id=123456789,
amount="500.00",
status="succeeded",
description="Test payment",
yookassa_payment_id="test_yoo_001"
)
session.add(test_payment)
session.commit()
session.close()
except Exception as e:
print(f"Ошибка: {e}")
import traceback
traceback.print_exc()

31
create_tables.py Executable file
View File

@ -0,0 +1,31 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from tg_bot.infrastructure.database.database import engine, Base
from tg_bot.infrastructure.database import models
print("СОЗДАНИЕ ТАБЛИЦ БАЗЫ ДАННЫХ")
Base.metadata.create_all(bind=engine)
print("Таблицы успешно созданы!")
print(" • users")
print(" • payments")
print()
print(f"База данных: {engine.url}")
db_path = "data/bot.db"
if os.path.exists(db_path):
size = os.path.getsize(db_path)
print(f"Размер файла: {size} байт")
else:
print("Файл БД не найден, но таблицы созданы")
except Exception as e:
print(f"Ошибка: {e}")
import traceback
traceback.print_exc()
print("=" * 50)

11
requirements.txt Normal file
View File

@ -0,0 +1,11 @@
pydantic>=2.5.0
pydantic-settings>=2.1.0
python-dotenv>=1.0.0
aiogram>=3.10.0
sqlalchemy>=2.0.0
aiosqlite>=0.19.0
httpx>=0.25.0
yookassa>=2.4.0
fastapi>=0.104.0
uvicorn>=0.24.0
python-multipart>=0.0.6

View File

View File

View File

@ -0,0 +1,139 @@
import aiohttp
from tg_bot.infrastructure.external.deepseek_client import DeepSeekClient
from tg_bot.config.settings import settings
BACKEND_URL = "http://localhost:8001/api/v1"
class RAGService:
def __init__(self):
self.deepseek_client = DeepSeekClient()
async def search_documents_in_collections(
self,
user_telegram_id: str,
query: str,
limit_per_collection: int = 5
) -> list[dict]:
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/users/telegram/{user_telegram_id}"
) as user_response:
if user_response.status != 200:
return []
user_data = await user_response.json()
user_uuid = str(user_data.get("user_id"))
if not user_uuid:
return []
async with session.get(
f"{BACKEND_URL}/collections/",
headers={"X-Telegram-ID": user_telegram_id}
) as collections_response:
if collections_response.status != 200:
return []
collections = await collections_response.json()
all_documents = []
for collection in collections:
collection_id = collection.get("collection_id")
if not collection_id:
continue
try:
async with aiohttp.ClientSession() as search_session:
async with search_session.get(
f"{BACKEND_URL}/documents/collection/{collection_id}",
params={"search": query, "limit": limit_per_collection},
headers={"X-Telegram-ID": user_telegram_id}
) as search_response:
if search_response.status == 200:
documents = await search_response.json()
for doc in documents:
doc["collection_name"] = collection.get("name", "Unknown")
all_documents.append(doc)
except Exception as e:
print(f"Error searching collection {collection_id}: {e}")
continue
return all_documents[:20]
except Exception as e:
print(f"Error searching documents: {e}")
return []
async def generate_answer_with_rag(
self,
question: str,
user_telegram_id: str
) -> dict:
documents = await self.search_documents_in_collections(
user_telegram_id,
question
)
context_parts = []
sources = []
for doc in documents[:5]:
title = doc.get("title", "Без названия")
content = doc.get("content", "")[:1000]
collection_name = doc.get("collection_name", "Unknown")
context_parts.append(f"Документ: {title}\nКоллекция: {collection_name}\nСодержание: {content[:500]}...")
sources.append({
"title": title,
"collection": collection_name,
"document_id": doc.get("document_id")
})
context = "\n\n".join(context_parts) if context_parts else "Релевантные документы не найдены."
system_prompt = """Ты - помощник-юрист, который отвечает на вопросы на основе предоставленных документов.
Используй информацию из документов для формирования точного и полезного ответа.
Если в документах нет информации для ответа, честно скажи об этом."""
user_prompt = f"""Контекст из документов:
{context}
Вопрос пользователя: {question}
Ответь на вопрос, используя информацию из предоставленных документов. Если информации недостаточно, укажи это."""
try:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response = await self.deepseek_client.chat_completion(
messages=messages,
temperature=0.7,
max_tokens=2000
)
return {
"answer": response.get("content", "Failed to generate answer"),
"sources": sources,
"usage": response.get("usage", {})
}
except Exception as e:
print(f"Error generating answer: {e}")
if documents:
return {
"answer": f"Found {len(documents)} documents but failed to generate answer",
"sources": sources[:3],
"usage": {}
}
else:
return {
"answer": "No relevant documents found",
"sources": [],
"usage": {}
}

View File

View File

44
tg_bot/config/settings.py Normal file
View File

@ -0,0 +1,44 @@
import os
from typing import List, Optional
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True,
extra="allow"
)
APP_NAME: str = "VibeLawyerBot"
VERSION: str = "0.1.0"
DEBUG: bool = True
TELEGRAM_BOT_TOKEN: str = ""
FREE_QUESTIONS_LIMIT: int = 5
PAYMENT_AMOUNT: float = 500.0
DATABASE_URL: str = "sqlite:///data/bot.db"
LOG_LEVEL: str = "INFO"
LOG_FILE: str = "logs/bot.log"
YOOKASSA_SHOP_ID: str = "1230200"
YOOKASSA_SECRET_KEY: str = "test_GVoixmlp0FqohXcyFzFHbRlAUoA3B1I2aMtAkAE_ubw"
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"
ADMIN_IDS_STR: str = ""
@property
def ADMIN_IDS(self) -> List[int]:
if not self.ADMIN_IDS_STR:
return []
try:
return [int(x.strip()) for x in self.ADMIN_IDS_STR.split(",")]
except:
return []
settings = Settings()

View File

View File

View File

@ -0,0 +1,67 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime, timedelta
from typing import Optional
from tg_bot.infrastructure.database.models import UserModel
class UserService:
def __init__(self, session: AsyncSession):
self.session = session
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[UserModel]:
result = await self.session.execute(
select(UserModel).filter_by(telegram_id=str(telegram_id))
)
return result.scalar_one_or_none()
async def get_or_create_user(
self,
telegram_id: int,
username: str = "",
first_name: str = "",
last_name: str = ""
) -> UserModel:
user = await self.get_user_by_telegram_id(telegram_id)
if not user:
user = UserModel(
telegram_id=str(telegram_id),
username=username,
first_name=first_name,
last_name=last_name
)
self.session.add(user)
await self.session.commit()
else:
user.username = username
user.first_name = first_name
user.last_name = last_name
await self.session.commit()
return user
async def update_user_questions(self, telegram_id: int) -> bool:
user = await self.get_user_by_telegram_id(telegram_id)
if user:
user.questions_used += 1
await self.session.commit()
return True
return False
async def activate_premium(self, telegram_id: int) -> bool:
try:
user = await self.get_user_by_telegram_id(telegram_id)
if user:
user.is_premium = True
if user.premium_until and user.premium_until > datetime.now():
user.premium_until = user.premium_until + timedelta(days=30)
else:
user.premium_until = datetime.now() + timedelta(days=30)
await self.session.commit()
return True
else:
return False
except Exception as e:
print(f"Error activating premium: {e}")
await self.session.rollback()
return False

View File

@ -0,0 +1,19 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from tg_bot.config.settings import settings
database_url = settings.DATABASE_URL
if database_url.startswith("sqlite:///"):
database_url = database_url.replace("sqlite:///", "sqlite+aiosqlite:///")
engine = create_async_engine(
database_url,
echo=settings.DEBUG
)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def create_tables():
from .models import Base
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
print(f"Таблицы созданы: {settings.DATABASE_URL}")

View File

@ -0,0 +1,39 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime, Boolean, Integer, Text
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class UserModel(Base):
__tablename__ = "users"
user_id = Column("user_id", String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
telegram_id = Column("telegram_id", String(100), nullable=False, unique=True)
created_at = Column("created_at", DateTime, default=datetime.utcnow, nullable=False)
role = Column("role", String(20), default="user", nullable=False)
is_premium = Column(Boolean, default=False, nullable=False)
premium_until = Column(DateTime, nullable=True)
questions_used = Column(Integer, default=0, nullable=False)
username = Column(String(100), nullable=True)
first_name = Column(String(100), nullable=True)
last_name = Column(String(100), nullable=True)
class PaymentModel(Base):
__tablename__ = "payments"
id = Column(Integer, primary_key=True, autoincrement=True)
payment_id = Column(String(36), default=lambda: str(uuid.uuid4()), nullable=False, unique=True)
user_id = Column(Integer, nullable=False)
amount = Column(String(20), nullable=False)
currency = Column(String(3), default="RUB", nullable=False)
status = Column(String(20), default="pending", nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
yookassa_payment_id = Column(String(100), unique=True, nullable=True)
description = Column(Text, nullable=True)
def __repr__(self):
return f"<Payment(user_id={self.user_id}, amount={self.amount}, status={self.status})>"

View File

View File

@ -0,0 +1,172 @@
import json
from typing import Optional, AsyncIterator
import httpx
from tg_bot.config.settings import settings
class DeepSeekAPIError(Exception):
pass
class DeepSeekClient:
def __init__(self, api_key: str | None = None, api_url: str | None = None):
self.api_key = api_key or settings.DEEPSEEK_API_KEY
self.api_url = api_url or settings.DEEPSEEK_API_URL
self.timeout = 60.0
def _get_headers(self) -> dict[str, str]:
if not self.api_key:
raise DeepSeekAPIError("API key not set")
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
async def chat_completion(
self,
messages: list[dict[str, str]],
model: str = "deepseek-chat",
temperature: float = 0.7,
max_tokens: Optional[int] = None,
stream: bool = False
) -> dict:
if not self.api_key:
return {
"content": "API key not configured",
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"stream": stream
}
if max_tokens is not None:
payload["max_tokens"] = max_tokens
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
self.api_url,
headers=self._get_headers(),
json=payload
)
response.raise_for_status()
data = response.json()
if "choices" in data and len(data["choices"]) > 0:
content = data["choices"][0]["message"]["content"]
else:
raise DeepSeekAPIError("Invalid response format")
usage = data.get("usage", {})
return {
"content": content,
"usage": {
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0)
}
}
except httpx.HTTPStatusError as e:
error_msg = f"API error: {e.response.status_code}"
try:
error_data = e.response.json()
if "error" in error_data:
error_msg = error_data['error'].get('message', error_msg)
except:
pass
raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e:
raise DeepSeekAPIError(f"Connection error: {str(e)}") from e
except Exception as e:
raise DeepSeekAPIError(str(e)) from e
async def stream_chat_completion(
self,
messages: list[dict[str, str]],
model: str = "deepseek-chat",
temperature: float = 0.7,
max_tokens: Optional[int] = None
) -> AsyncIterator[str]:
if not self.api_key:
yield "API key not configured"
return
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"stream": True
}
if max_tokens is not None:
payload["max_tokens"] = max_tokens
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
async with client.stream(
"POST",
self.api_url,
headers=self._get_headers(),
json=payload
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if not line.strip():
continue
if line.startswith("data: "):
line = line[6:]
if line.strip() == "[DONE]":
break
try:
data = json.loads(line)
if "choices" in data and len(data["choices"]) > 0:
delta = data["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
yield content
except json.JSONDecodeError:
continue
except httpx.HTTPStatusError as e:
error_msg = f"API error: {e.response.status_code}"
try:
error_data = e.response.json()
if "error" in error_data:
error_msg = error_data['error'].get('message', error_msg)
except:
pass
raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e:
raise DeepSeekAPIError(f"Connection error: {str(e)}") from e
except Exception as e:
raise DeepSeekAPIError(str(e)) from e
async def health_check(self) -> bool:
if not self.api_key:
return False
try:
test_messages = [{"role": "user", "content": "test"}]
await self.chat_completion(test_messages, max_tokens=1)
return True
except Exception:
return False

View File

@ -0,0 +1,59 @@
import logging
from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode
from aiogram.client.default import DefaultBotProperties
from tg_bot.config.settings import settings
from tg_bot.infrastructure.telegram.handlers import (
start_handler,
help_handler,
stats_handler,
question_handler,
buy_handler,
collection_handler
)
logger = logging.getLogger(__name__)
async def create_bot() -> tuple[Bot, Dispatcher]:
bot = Bot(
token=settings.TELEGRAM_BOT_TOKEN,
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
dp = 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)
return bot, dp
async def start_bot():
bot = None
try:
bot, dp = await create_bot()
try:
webhook_info = await bot.get_webhook_info()
if webhook_info.url:
await bot.delete_webhook(drop_pending_updates=True)
except Exception:
pass
print("=" * 50)
print("Telegram бот запускается")
print(f"Бот: @vibelawyer_bot")
print(f"Лимит: {settings.FREE_QUESTIONS_LIMIT} вопросов")
print(f"Команды: /start, /help, /buy, /stats, /mycollections, /search")
print("=" * 50)
await dp.start_polling(bot, drop_pending_updates=True)
except Exception as e:
logger.error(f"Ошибка запуска: {e}")
raise
finally:
if bot:
await bot.session.close()

View File

@ -0,0 +1,274 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from decimal import Decimal
from tg_bot.config.settings import settings
from tg_bot.payment.yookassa.client import yookassa_client
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.infrastructure.database.models import PaymentModel
from tg_bot.domain.services.user_service import UserService
from sqlalchemy import select
import uuid
from datetime import datetime, timedelta
router = Router()
@router.message(Command("buy"))
async def cmd_buy(message: Message):
user_id = message.from_user.id
username = message.from_user.username or f"user_{user_id}"
async with AsyncSessionLocal() as session:
try:
user_service = UserService(session)
user = await user_service.get_user_by_telegram_id(user_id)
if user and user.is_premium and user.premium_until and user.premium_until > datetime.now():
days_left = (user.premium_until - datetime.now()).days
await message.answer(
f"<b>У вас уже есть активная подписка!</b>\n\n"
f"• Статус: Premium активен\n"
f"• Действует до: {user.premium_until.strftime('%d.%m.%Y')}\n"
f"• Осталось дней: {days_left}\n\n"
f"Новая подписка будет добавлена к текущей.",
parse_mode="HTML"
)
except Exception:
pass
await message.answer(
"*Создаю ссылку для оплаты...*\n\n"
"Пожалуйста, подождите несколько секунд.",
parse_mode="Markdown"
)
try:
payment_data = await yookassa_client.create_payment(
amount=Decimal(str(settings.PAYMENT_AMOUNT)),
description=f"Подписка VibeLawyerBot для @{username}",
user_id=user_id
)
async with AsyncSessionLocal() as session:
try:
payment = PaymentModel(
payment_id=str(uuid.uuid4()),
user_id=user_id,
amount=str(settings.PAYMENT_AMOUNT),
currency="RUB",
status="pending",
yookassa_payment_id=payment_data["id"],
description="Оплата подписки VibeLawyerBot"
)
session.add(payment)
await session.commit()
print(f"Платёж сохранён в БД: {payment.payment_id}")
except Exception as e:
print(f"Ошибка сохранения платежа в БД: {e}")
await session.rollback()
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Оплатить онлайн",
url=payment_data["confirmation_url"]
)
],
[
InlineKeyboardButton(
text="Проверить статус оплаты",
callback_data=f"check_payment:{payment_data['id']}"
)
]
]
)
response_text = (
f"<b>Оплата подписки VibeLawyerBot</b>\n\n"
f"<b>Детали платежа:</b>\n"
f"• Сумма: {settings.PAYMENT_AMOUNT} руб.\n"
f"• Описание: Подписка на 30 дней\n"
f"• ID платежа: <code>{payment_data['id'][:20]}...</code>\n\n"
f"<b>Что даёт подписка:</b>\n"
f"• Неограниченное число вопросов\n"
f"• Приоритетная обработка\n"
f"• Доступ ко всем функциям\n\n"
f"<i>После оплаты доступ активируется автоматически в течение 1-2 минут.</i>"
)
await message.answer(
response_text,
parse_mode="HTML",
reply_markup=keyboard,
disable_web_page_preview=True
)
await message.answer(
"<b>Инструкция по оплате:</b>\n\n"
"1. Нажмите кнопку 'Оплатить онлайн'\n"
"2. Введите данные банковской карты\n"
"3. Подтвердите оплату\n"
"4. После успешной оплаты нажмите 'Проверить статус оплаты'\n\n"
"<b>Тестовые карты для проверки:</b>\n"
"• <code>5555 5555 5555 4477</code> - успешная оплата\n"
" Срок: <b>любой будущий</b> (напр. 12/30)\n"
" CVV: <b>любые 3 цифры</b> (напр. 123)\n\n"
"<i>Это тестовые карты, реальные деньги не списываются!</i>",
parse_mode="HTML"
)
except Exception as e:
print(f"Ошибка создания платежа: {e}")
await message.answer(
"<b>Произошла ошибка при создании платежа</b>\n\n"
"Пожалуйста, попробуйте позже или обратитесь к администратору.\n\n"
f"<code>Ошибка: {str(e)[:100]}</code>",
parse_mode="HTML"
)
@router.callback_query(lambda c: c.data.startswith("check_payment:"))
async def check_payment_status(callback_query: types.CallbackQuery):
yookassa_id = callback_query.data.split(":")[1]
user_id = callback_query.from_user.id
await callback_query.answer("Проверяю статус оплаты...")
try:
from yookassa import Payment as YooPayment
payment = YooPayment.find_one(yookassa_id)
if payment.status == "succeeded":
async with AsyncSessionLocal() as session:
try:
result = await session.execute(
select(PaymentModel).filter_by(yookassa_payment_id=yookassa_id)
)
db_payment = result.scalar_one_or_none()
if db_payment:
db_payment.status = "succeeded"
user_service = UserService(session)
success = await user_service.activate_premium(user_id)
if success:
user = await user_service.get_user_by_telegram_id(user_id)
await session.commit()
if not user:
user = await user_service.get_user_by_telegram_id(user_id)
await callback_query.message.answer(
"<b>Оплата подтверждена!</b>\n\n"
f"Ваш premium-доступ активирован до: "
f"<b>{user.premium_until.strftime('%d.%m.%Y')}</b>\n\n"
"Теперь вы можете:\n"
"• Задавать неограниченное количество вопросов\n"
"• Получать приоритетные ответы\n"
"• Использовать все функции бота\n\n"
"<i>Спасибо за покупку!</i>",
parse_mode="HTML"
)
else:
await callback_query.message.answer(
"<b>Платёж найден в ЮKассе, но не в нашей БД</b>\n\n"
"Пожалуйста, обратитесь к администратору.",
parse_mode="HTML"
)
except Exception as e:
print(f"Ошибка обработки платежа: {e}")
elif payment.status == "pending":
await callback_query.message.answer(
"<b>Платёж ещё не завершён</b>\n\n"
"Если вы уже оплатили, пожалуйста, подождите 1-2 минуты "
"и проверьте статус снова.\n\n"
"<i>Проверьте правильность данных карты:</i>\n"
"• Срок действия должен быть <b>будущим</b>\n"
"• CVV - <b>3 цифры</b> на обратной стороне карты",
parse_mode="HTML"
)
else:
await callback_query.message.answer(
f"<b>Статус платежа: {payment.status}</b>\n\n"
"Попробуйте оплатить ещё раз или обратитесь в поддержку.\n\n"
"<i>Для теста используйте карту:</i>\n"
"<code>5555 5555 5555 4477</code>\n"
"Срок: <b>12/30</b>, CVV: <b>123</b>",
parse_mode="HTML"
)
except Exception as e:
print(f"Ошибка проверки статуса: {e}")
await callback_query.message.answer(
"<b>Не удалось проверить статус платежа</b>\n\n"
"Попробуйте позже или обратитесь к администратору.",
parse_mode="HTML"
)
@router.message(Command("mypayments"))
async def cmd_my_payments(message: Message):
user_id = message.from_user.id
async with AsyncSessionLocal() as session:
try:
result = await session.execute(
select(PaymentModel).filter_by(user_id=user_id).order_by(PaymentModel.created_at.desc()).limit(10)
)
payments = result.scalars().all()
if not payments:
await message.answer(
"<b>У вас пока нет платежей</b>\n\n"
"Используйте команду /buy чтобы оформить подписку.",
parse_mode="HTML"
)
return
response = ["<b>Ваши последние платежи:</b>\n"]
for i, payment in enumerate(payments, 1):
status_text = "Успешно" if payment.status == "succeeded" else "Ожидание" if payment.status == "pending" else "Ошибка"
response.append(
f"\n<b>{i}. {payment.amount} руб. ({status_text})</b>\n"
f"Статус: {payment.status}\n"
f"Дата: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n"
f"ID: <code>{payment.payment_id[:8]}...</code>"
)
response.append("\n\n<i>Полный доступ открывается после успешной оплаты</i>")
await message.answer(
"\n".join(response),
parse_mode="HTML"
)
except Exception as e:
print(f"Ошибка получения платежей: {e}")
@router.message(Command("testcards"))
async def cmd_testcards(message: Message):
testcards_text = (
f"<b>Тестовые банковские карты для оплаты</b>\n\n"
f"<b>Для тестирования оплаты используйте:</b>\n\n"
f"<b>Карта для успешной оплаты:</b>\n"
f"• Номер: <code>5555 5555 5555 4477</code>\n"
f"• Срок действия: <b>ЛЮБОЙ будущий</b> (например: 12/30)\n"
f"• CVV код: <b>ЛЮБЫЕ 3 цифры</b> (например: 123)\n"
f"• Результат: Оплата пройдёт успешно\n\n"
f"<b>Карта для отказа в оплате:</b>\n"
f"• Номер: <code>5555 5555 5555 4445</code>\n"
f"• Срок действия: <b>ЛЮБОЙ будущий</b>\n"
f"• CVV код: <b>ЛЮБЫЕ 3 цифры</b>\n"
f"• Результат: Оплата будет отклонена\n\n"
f"<b>Важно:</b>\n"
f"• Это тестовые карты, реальные деньги не списываются\n"
f"• Используются только для проверки работы оплаты\n"
f"• После успешной тестовой оплаты premium активируется\n\n"
f"Для оплаты подписки используйте команду /buy"
)
await message.answer(testcards_text, parse_mode="HTML")

View File

@ -0,0 +1,183 @@
from aiogram import Router
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.filters import Command
import aiohttp
router = Router()
BACKEND_URL = "http://localhost:8001/api/v1"
async def get_user_collections(telegram_id: str):
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/collections/",
headers={"X-Telegram-ID": telegram_id}
) as response:
if response.status == 200:
return await response.json()
return []
except Exception as e:
print(f"Error getting collections: {e}")
return []
async def get_collection_documents(collection_id: str, telegram_id: str):
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/documents/collection/{collection_id}",
headers={"X-Telegram-ID": telegram_id}
) as response:
if response.status == 200:
return await response.json()
return []
except Exception as e:
print(f"Error getting documents: {e}")
return []
async def search_in_collection(collection_id: str, query: str, telegram_id: str):
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/documents/collection/{collection_id}",
params={"search": query},
headers={"X-Telegram-ID": telegram_id}
) as response:
if response.status == 200:
return await response.json()
return []
except Exception as e:
print(f"Error searching: {e}")
return []
@router.message(Command("mycollections"))
async def cmd_mycollections(message: Message):
telegram_id = str(message.from_user.id)
collections = await get_user_collections(telegram_id)
if not collections:
await message.answer(
"<b>У вас пока нет коллекций</b>\n\n"
"Обратитесь к администратору для создания коллекций и добавления документов.",
parse_mode="HTML"
)
return
response = "<b>Ваши коллекции документов:</b>\n\n"
keyboard_buttons = []
for i, collection in enumerate(collections[:10], 1):
name = collection.get("name", "Без названия")
description = collection.get("description", "")
collection_id = collection.get("collection_id")
response += f"{i}. <b>{name}</b>\n"
if description:
response += f" <i>{description[:50]}...</i>\n"
response += f" ID: <code>{collection_id}</code>\n\n"
keyboard_buttons.append([
InlineKeyboardButton(
text=f"{name}",
callback_data=f"collection:{collection_id}"
)
])
keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
response += "<i>Нажмите на коллекцию, чтобы посмотреть документы</i>"
await message.answer(response, parse_mode="HTML", reply_markup=keyboard)
@router.message(Command("search"))
async def cmd_search(message: Message):
parts = message.text.split(maxsplit=2)
if len(parts) < 3:
telegram_id = str(message.from_user.id)
collections = await get_user_collections(telegram_id)
if not collections:
await message.answer(
"<b>Использование:</b> /search &lt;collection_id&gt; &lt;запрос&gt;\n\n"
"У вас пока нет коллекций. Обратитесь к администратору.",
parse_mode="HTML"
)
return
response = "<b>Выберите коллекцию для поиска:</b>\n\n"
response += "Использование: /search &lt;collection_id&gt; &lt;запрос&gt;\n\n"
response += "<b>Доступные коллекции:</b>\n"
for collection in collections[:5]:
name = collection.get("name", "Без названия")
collection_id = collection.get("collection_id")
response += f"• <b>{name}</b>\n <code>{collection_id}</code>\n\n"
response += "Пример: /search " + collections[0].get("collection_id", "")[:8] + "... Как оформить договор?"
await message.answer(response, parse_mode="HTML")
return
collection_id = parts[1]
query = parts[2]
telegram_id = str(message.from_user.id)
await message.bot.send_chat_action(message.chat.id, "typing")
results = await search_in_collection(collection_id, query, telegram_id)
if not results:
await message.answer(
f"<b>Ничего не найдено</b>\n\n"
f"По запросу <i>\"{query}\"</i> в коллекции ничего не найдено.\n\n"
f"Попробуйте другой запрос или используйте /mycollections для просмотра доступных коллекций.",
parse_mode="HTML"
)
return
response = f"<b>Результаты поиска:</b> \"{query}\"\n\n"
for i, doc in enumerate(results[:5], 1):
title = doc.get("title", "Без названия")
content = doc.get("content", "")[:200]
response += f"{i}. <b>{title}</b>\n"
response += f" <i>{content}...</i>\n\n"
await message.answer(response, parse_mode="HTML")
@router.callback_query(lambda c: c.data.startswith("collection:"))
async def show_collection_documents(callback: CallbackQuery):
collection_id = callback.data.split(":")[1]
telegram_id = str(callback.from_user.id)
await callback.answer("Загружаю документы...")
documents = await get_collection_documents(collection_id, telegram_id)
if not documents:
await callback.message.answer(
f"<b>Коллекция пуста</b>\n\n"
f"В этой коллекции пока нет документов.\n"
f"Обратитесь к администратору для добавления документов.",
parse_mode="HTML"
)
return
response = f"<b>Документы в коллекции:</b>\n\n"
for i, doc in enumerate(documents[:10], 1):
title = doc.get("title", "Без названия")
content_preview = doc.get("content", "")[:100]
response += f"{i}. <b>{title}</b>\n"
if content_preview:
response += f" <i>{content_preview}...</i>\n"
response += "\n"
if len(documents) > 10:
response += f"\n<i>Показано 10 из {len(documents)} документов</i>"
await callback.message.answer(response, parse_mode="HTML")

View File

@ -0,0 +1,82 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.types import Message
from tg_bot.config.settings import settings
router = Router()
@router.message(Command("help"))
async def cmd_help(message: Message):
help_text = (
f"<b>VibeLawyerBot - помощь</b>\n\n"
f"<b>Основные команды:</b>\n"
f"• /start - начать работу с ботом\n"
f"• /help - показать это сообщение\n"
f"• /buy - купить подписку\n"
f"• /stats - статистика и лимиты\n"
f"• /mypayments - история платежей\n\n"
f"<b>Работа с коллекциями:</b>\n"
f"• /mycollections - показать мои коллекции документов\n"
f"• /search - поиск документов в коллекции\n\n"
f"<b>Как работает бот:</b>\n"
f"1. У вас есть <b>{settings.FREE_QUESTIONS_LIMIT}</b> бесплатных вопросов\n"
f"2. Бот ищет ответы в ваших коллекциях документов\n"
f"3. После исчерпания лимита нужна подписка\n"
f"4. Подписка даёт неограниченный доступ\n\n"
f"<b>О коллекциях:</b>\n"
f"• Администратор загружает документы в коллекции\n"
f"• Вам предоставляется доступ к коллекциям\n"
f"• При задаче вопроса бот ищет ответы в ваших коллекциях\n"
f"• Используйте /mycollections для просмотра коллекций\n\n"
f"<b>Оплата (тестовый режим):</b>\n"
f"• Безопасно через ЮKассу\n"
f"• Сразу после оплаты доступ открывается\n"
f"• <b>Тестовые карты для проверки:</b>\n"
f" Успешная оплата: <code>5555 5555 5555 4477</code>\n"
f" Срок: <b>любой будущий</b> (напр. 12/30)\n"
f" CVV: <b>любой 3 цифры</b> (напр. 123)\n\n"
f" Отказ в оплате: <code>5555 5555 5555 4445</code>\n"
f" Срок: <b>любой будущий</b>\n"
f" CVV: <b>любой 3 цифры</b>\n\n"
f"• Поддержка: @vibelawyer_support\n\n"
f"<i>Задавайте юридические вопросы, и бот поможет с ответами!</i>"
)
await message.answer(help_text, parse_mode="HTML")
@router.message(Command("testcards"))
async def cmd_testcards(message: Message):
testcards_text = (
f"<b>Тестовые банковские карты для оплаты</b>\n\n"
f"<b>Для тестирования оплаты используйте:</b>\n\n"
f"<b>Карта для успешной оплаты:</b>\n"
f"• Номер: <code>5555 5555 5555 4477</code>\n"
f"• Срок действия: <b>ЛЮБОЙ будущий</b> (например: 12/30)\n"
f"• CVV код: <b>ЛЮБЫЕ 3 цифры</b> (например: 123)\n"
f"• Результат: Оплата пройдёт успешно\n\n"
f"<b>Карта для отказа в оплате:</b>\n"
f"• Номер: <code>5555 5555 5555 4445</code>\n"
f"• Срок действия: <b>ЛЮБОЙ будущий</b>\n"
f"• CVV код: <b>ЛЮБЫЕ 3 цифры</b>\n"
f"• Результат: Оплата будет отклонена\n\n"
f"<b>Важно:</b>\n"
f"• Это тестовые карты, реальные деньги не списываются\n"
f"• Используются только для проверки работы оплаты\n"
f"• После успешной тестовой оплаты premium активируется\n\n"
f"Для оплаты подписки используйте команду /buy"
)
await message.answer(testcards_text, parse_mode="HTML")

View File

@ -0,0 +1,300 @@
from aiogram import Router, types
from aiogram.types import Message
from datetime import datetime
import aiohttp
from tg_bot.config.settings import settings
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.infrastructure.database.models import UserModel
from tg_bot.domain.services.user_service import UserService
from tg_bot.application.services.rag_service import RAGService
router = Router()
BACKEND_URL = "http://localhost:8001/api/v1"
rag_service = RAGService()
@router.message()
async def handle_question(message: Message):
user_id = message.from_user.id
question_text = message.text.strip()
if question_text.startswith('/'):
return
async with AsyncSessionLocal() as session:
try:
user_service = UserService(session)
user = await user_service.get_user_by_telegram_id(user_id)
if not user:
user = await user_service.get_or_create_user(
user_id,
message.from_user.username or "",
message.from_user.first_name or "",
message.from_user.last_name or ""
)
await ensure_user_in_backend(str(user_id), message.from_user)
if user.is_premium:
await process_premium_question(message, user, question_text, user_service)
elif user.questions_used < settings.FREE_QUESTIONS_LIMIT:
await process_free_question(message, user, question_text, user_service)
else:
await handle_limit_exceeded(message, user)
except Exception as e:
print(f"Error processing question: {e}")
await message.answer(
"Произошла ошибка. Попробуйте позже.",
parse_mode="HTML"
)
async def ensure_user_in_backend(telegram_id: str, telegram_user):
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/users/telegram/{telegram_id}"
) as response:
if response.status == 200:
return
async with session.post(
f"{BACKEND_URL}/users",
json={"telegram_id": telegram_id, "role": "user"}
) as create_response:
if create_response.status in [200, 201]:
print(f"Пользователь {telegram_id} создан в backend")
except Exception as e:
print(f"Error creating user in backend: {e}")
async def process_premium_question(message: Message, user: UserModel, question_text: str, user_service: UserService):
await user_service.update_user_questions(user.telegram_id)
await message.bot.send_chat_action(message.chat.id, "typing")
try:
rag_result = await rag_service.generate_answer_with_rag(
question_text,
str(message.from_user.id)
)
answer = rag_result.get("answer", "Извините, не удалось сгенерировать ответ.")
sources = rag_result.get("sources", [])
await save_conversation_to_backend(
str(message.from_user.id),
question_text,
answer,
sources
)
response = (
f"<b>Ваш вопрос:</b>\n"
f"<i>{question_text[:200]}</i>\n\n"
f"<b>Ответ:</b>\n{answer}\n\n"
)
if sources:
response += f"<b>Источники из коллекций:</b>\n"
collections_used = {}
for source in sources[:5]:
collection_name = source.get('collection', 'Неизвестно')
if collection_name not in collections_used:
collections_used[collection_name] = []
collections_used[collection_name].append(source.get('title', 'Без названия'))
for i, (collection_name, titles) in enumerate(collections_used.items(), 1):
response += f"{i}. <b>Коллекция:</b> {collection_name}\n"
for title in titles[:2]:
response += f"{title}\n"
response += "\n<i>Используйте /mycollections для просмотра всех коллекций</i>\n\n"
response += (
f"<b>Статус:</b> Premium (вопросов безлимитно)\n"
f"<b>Всего вопросов:</b> {user.questions_used}"
)
except Exception as e:
print(f"Error generating answer: {e}")
response = (
f"<b>Ваш вопрос:</b>\n"
f"<i>{question_text[:200]}</i>\n\n"
f"Ошибка при генерации ответа. Попробуйте позже.\n\n"
f"<b>Статус:</b> Premium\n"
f"<b>Всего вопросов:</b> {user.questions_used}"
)
await message.answer(response, parse_mode="HTML")
async def process_free_question(message: Message, user: UserModel, question_text: str, user_service: UserService):
await user_service.update_user_questions(user.telegram_id)
user = await user_service.get_user_by_telegram_id(user.telegram_id)
remaining = settings.FREE_QUESTIONS_LIMIT - user.questions_used
await message.bot.send_chat_action(message.chat.id, "typing")
try:
rag_result = await rag_service.generate_answer_with_rag(
question_text,
str(message.from_user.id)
)
answer = rag_result.get("answer", "Извините, не удалось сгенерировать ответ.")
sources = rag_result.get("sources", [])
await save_conversation_to_backend(
str(message.from_user.id),
question_text,
answer,
sources
)
response = (
f"<b>Ваш вопрос:</b>\n"
f"<i>{question_text[:200]}</i>\n\n"
f"<b>Ответ:</b>\n{answer}\n\n"
)
if sources:
response += f"<b>Источники из коллекций:</b>\n"
collections_used = {}
for source in sources[:5]:
collection_name = source.get('collection', 'Неизвестно')
if collection_name not in collections_used:
collections_used[collection_name] = []
collections_used[collection_name].append(source.get('title', 'Без названия'))
for i, (collection_name, titles) in enumerate(collections_used.items(), 1):
response += f"{i}. <b>Коллекция:</b> {collection_name}\n"
for title in titles[:2]:
response += f"{title}\n"
response += "\n<i>Используйте /mycollections для просмотра всех коллекций</i>\n\n"
response += (
f"<b>Статус:</b> Бесплатный доступ\n"
f"<b>Использовано вопросов:</b> {user.questions_used}/{settings.FREE_QUESTIONS_LIMIT}\n"
f"<b>Осталось бесплатных:</b> {remaining}\n\n"
)
if remaining <= 3 and remaining > 0:
response += f"<i>Осталось мало вопросов! Для продолжения используйте /buy</i>\n\n"
response += f"<i>Для безлимитного доступа: /buy</i>"
except Exception as e:
print(f"Error generating answer: {e}")
response = (
f"<b>Ваш вопрос:</b>\n"
f"<i>{question_text[:200]}</i>\n\n"
f"Ошибка при генерации ответа. Попробуйте позже.\n\n"
f"<b>Статус:</b> Бесплатный доступ\n"
f"<b>Использовано вопросов:</b> {user.questions_used}/{settings.FREE_QUESTIONS_LIMIT}\n"
f"<b>Осталось бесплатных:</b> {remaining}\n\n"
f"<i>Для безлимитного доступа: /buy</i>"
)
await message.answer(response, parse_mode="HTML")
async def save_conversation_to_backend(telegram_id: str, question: str, answer: str, sources: list):
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/users/telegram/{telegram_id}"
) as user_response:
if user_response.status != 200:
return
user_data = await user_response.json()
user_uuid = user_data.get("user_id")
async with session.get(
f"{BACKEND_URL}/collections/",
headers={"X-Telegram-ID": telegram_id}
) as collections_response:
collections = []
if collections_response.status == 200:
collections = await collections_response.json()
collection_id = None
if collections:
collection_id = collections[0].get("collection_id")
else:
async with session.post(
f"{BACKEND_URL}/collections",
json={
"name": "Основная коллекция",
"description": "Коллекция по умолчанию",
"is_public": False
},
headers={"X-Telegram-ID": telegram_id}
) as create_collection_response:
if create_collection_response.status in [200, 201]:
collection_data = await create_collection_response.json()
collection_id = collection_data.get("collection_id")
if not collection_id:
return
async with session.post(
f"{BACKEND_URL}/conversations",
json={"collection_id": str(collection_id)},
headers={"X-Telegram-ID": telegram_id}
) as conversation_response:
if conversation_response.status not in [200, 201]:
return
conversation_data = await conversation_response.json()
conversation_id = conversation_data.get("conversation_id")
if not conversation_id:
return
await session.post(
f"{BACKEND_URL}/messages",
json={
"conversation_id": str(conversation_id),
"content": question,
"role": "user"
},
headers={"X-Telegram-ID": telegram_id}
)
await session.post(
f"{BACKEND_URL}/messages",
json={
"conversation_id": str(conversation_id),
"content": answer,
"role": "assistant",
"sources": {"documents": sources}
},
headers={"X-Telegram-ID": telegram_id}
)
except Exception as e:
print(f"Error saving conversation: {e}")
async def handle_limit_exceeded(message: Message, user: UserModel):
response = (
f"<b>Лимит бесплатных вопросов исчерпан!</b>\n\n"
f"<b>Ваша статистика:</b>\n"
f"• Использовано вопросов: {user.questions_used}\n"
f"• Бесплатный лимит: {settings.FREE_QUESTIONS_LIMIT}\n\n"
f"<b>Что делать дальше?</b>\n"
f"1. Купите подписку командой /buy\n"
f"2. Получите неограниченный доступ к вопросам\n"
f"3. Продолжайте использовать бот без ограничений\n\n"
f"<b>Подписка включает:</b>\n"
f"• Неограниченное количество вопросов\n"
f"• Приоритетную обработку\n"
f"• Доступ ко всем функциям\n\n"
f"<b>Нажмите /buy чтобы продолжить</b>"
)
await message.answer(response, parse_mode="HTML")

View File

@ -0,0 +1,59 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.types import Message
from datetime import datetime
from tg_bot.config.settings import settings
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.domain.services.user_service import UserService
router = Router()
@router.message(Command("start"))
async def cmd_start(message: Message):
user_id = message.from_user.id
username = message.from_user.username or ""
first_name = message.from_user.first_name or ""
last_name = message.from_user.last_name or ""
async with AsyncSessionLocal() as session:
try:
user_service = UserService(session)
existing_user = await user_service.get_user_by_telegram_id(user_id)
user = await user_service.get_or_create_user(
user_id,
username,
first_name,
last_name
)
if not existing_user:
print(f"Новый пользователь: {user_id}")
except Exception as e:
print(f"Ошибка сохранения пользователя: {e}")
await session.rollback()
welcome_text = (
f"<b>Привет, {first_name}!</b>\n\n"
f"Я <b>VibeLawyerBot</b> - ваш помощник в юридических вопросах.\n\n"
f"<b>Как я работаю:</b>\n"
f"1. Администратор загружает документы в коллекции\n"
f"2. Вы задаёте вопрос на любую юридическую тему\n"
f"3. Я ищу ответы в ваших коллекциях документов\n"
f"4. Даю развернутый ответ на основе найденных документов\n"
f"5. Первые {settings.FREE_QUESTIONS_LIMIT} вопросов - бесплатно\n"
f"6. Для продолжения нужна подписка (/buy)\n\n"
f"<b>Основные команды:</b>\n"
f"• /help - подробная помощь\n"
f"• /buy - купить подписку\n"
f"• /stats - ваша статистика\n"
f"• /mypayments - история платежей\n"
f"• /mycollections - мои коллекции документов\n"
f"• /search - поиск в коллекции\n\n"
f"<b>Готовы начать?</b> Просто напишите ваш вопрос!\n\n"
f"<i>Для получения полного доступа используйте /buy</i>"
)
await message.answer(welcome_text, parse_mode="HTML")

View File

@ -0,0 +1,61 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.types import Message
from tg_bot.config.settings import settings
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.domain.services.user_service import UserService
router = Router()
@router.message(Command("stats"))
async def cmd_stats(message: Message):
user_id = message.from_user.id
async with AsyncSessionLocal() as session:
try:
user_service = UserService(session)
user = await user_service.get_user_by_telegram_id(user_id)
if user:
stats_text = (
f"<b>Ваша статистика</b>\n\n"
f"<b>Основное:</b>\n"
f"• ID: <code>{user_id}</code>\n"
f"• Premium: {'Да' if user.is_premium else 'Нет'}\n"
f"• Вопросов использовано: {user.questions_used}/{settings.FREE_QUESTIONS_LIMIT}\n\n"
)
if user.is_premium:
stats_text += (
f"<b>Premium статус:</b>\n"
f"• Активен до: {user.premium_until.strftime('%d.%m.%Y') if user.premium_until else 'Не указано'}\n"
f"• Лимит вопросов: безлимитно\n\n"
)
else:
remaining = max(0, settings.FREE_QUESTIONS_LIMIT - user.questions_used)
stats_text += (
f"<b>Бесплатный доступ:</b>\n"
f"• Осталось вопросов: {remaining}\n"
f"• Для безлимита: /buy\n\n"
)
else:
stats_text = (
f"<b>Добро пожаловать!</b>\n\n"
f"Вы новый пользователь.\n"
f"• Ваш ID: <code>{user_id}</code>\n"
f"• Бесплатных вопросов: {settings.FREE_QUESTIONS_LIMIT}\n"
f"• Для начала работы просто задайте вопрос!\n\n"
f"<i>Используйте /buy для получения полного доступа</i>"
)
await message.answer(stats_text, parse_mode="HTML")
except Exception as e:
await message.answer(
f"<b>Ошибка получения статистики</b>\n\n"
f"Попробуйте позже.",
parse_mode="HTML"
)

43
tg_bot/main.py Normal file
View File

@ -0,0 +1,43 @@
import asyncio
import logging
import sys
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.insert(0, parent_dir)
from tg_bot.config.settings import settings
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(settings.LOG_FILE),
logging.StreamHandler()
]
)
logger = logging.getLogger("vibelawyer_bot")
async def main():
logger.info("=" * 50)
logger.info(f"Запуск {settings.APP_NAME} v{settings.VERSION}")
logger.info(f"Режим: {'РАЗРАБОТКА' if settings.DEBUG else 'ПРОДАКШН'}")
logger.info(f"Лимит вопросов: {settings.FREE_QUESTIONS_LIMIT}")
logger.info("=" * 50)
try:
from tg_bot.infrastructure.telegram.bot import start_bot
await start_bot()
except KeyboardInterrupt:
logger.info("Бот остановлен пользователем")
print("\nБот остановлен")
except Exception as e:
logger.error(f"Ошибка запуска: {e}")
print(f"Ошибка запуска: {e}")
if __name__ == "__main__":
asyncio.run(main())

View File

View File

View File

@ -0,0 +1,71 @@
import json
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import JSONResponse
import sys
import os
from datetime import datetime, timedelta
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
router = APIRouter()
@router.post("/payment/webhook")
async def handle_yookassa_webhook(request: Request):
try:
data = await request.json()
event_type = data.get("event")
print(f"Webhook received: {event_type}")
try:
from tg_bot.config.settings import settings
from tg_bot.domain.services.user_service import UserService
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.infrastructure.database.models import UserModel
from sqlalchemy import select
from aiogram import Bot
if event_type == "payment.succeeded":
payment = data.get("object", {})
user_id = payment.get("metadata", {}).get("user_id")
if user_id:
async with AsyncSessionLocal() as session:
user_service = UserService(session)
success = await user_service.activate_premium(int(user_id))
if success:
print(f"Premium activated for user {user_id}")
result = await session.execute(
select(UserModel).filter_by(telegram_id=str(user_id))
)
user = result.scalar_one_or_none()
if user and settings.TELEGRAM_BOT_TOKEN:
try:
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
premium_until = user.premium_until or datetime.now() + timedelta(days=30)
notification = (
f"<b>Оплата подтверждена!</b>\n\n"
f"Premium активирован до {premium_until.strftime('%d.%m.%Y')}"
)
await bot.send_message(
chat_id=int(user_id),
text=notification,
parse_mode="HTML"
)
print(f"Notification sent to user {user_id}")
await bot.session.close()
except Exception as e:
print(f"Error sending notification: {e}")
else:
print(f"User {user_id} not found")
except ImportError as e:
print(f"Import error: {e}")
return JSONResponse({"status": "ok", "message": "Webhook processed"})
except Exception as e:
print(f"Error processing webhook: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

View File

@ -0,0 +1,55 @@
from decimal import Decimal
import uuid
from typing import Dict, Any
from yookassa import Configuration, Payment as YooPayment
from tg_bot.config.settings import settings
class YookassaClient:
def __init__(self):
Configuration.configure(
account_id=settings.YOOKASSA_SHOP_ID,
secret_key=settings.YOOKASSA_SECRET_KEY
)
async def create_payment(
self,
amount: Decimal,
description: str,
user_id: int
) -> Dict[str, Any]:
try:
payment = YooPayment.create({
"amount": {
"value": f"{amount:.2f}",
"currency": "RUB"
},
"payment_method_data": {
"type": "bank_card"
},
"confirmation": {
"type": "redirect",
"return_url": settings.YOOKASSA_RETURN_URL
},
"capture": True,
"description": description,
"metadata": {
"user_id": str(user_id),
"telegram_payment": "true"
},
"save_payment_method": False
})
return {
"id": payment.id,
"status": payment.status,
"confirmation_url": payment.confirmation.confirmation_url,
"amount": payment.amount.value,
"description": payment.description,
"metadata": payment.metadata
}
except Exception as e:
print(f"Error creating payment: {e}")
raise
yookassa_client = YookassaClient()