This commit is contained in:
bokho 2025-12-23 22:20:42 +03:00
parent 71e8d1079e
commit 493c385cb1
31 changed files with 574 additions and 507 deletions

View File

@ -0,0 +1,28 @@
"""Add premium fields to users
Revision ID: 002
Revises: 001
Create Date: 2024-01-02 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = '002'
down_revision = '001'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column('users', sa.Column('is_premium', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('users', sa.Column('premium_until', sa.DateTime(), nullable=True))
op.add_column('users', sa.Column('questions_used', sa.Integer(), nullable=False, server_default='0'))
def downgrade() -> None:
op.drop_column('users', 'questions_used')
op.drop_column('users', 'premium_until')
op.drop_column('users', 'is_premium')

View File

@ -1,4 +1,4 @@
fastapi==0.104.1
fastapi==0.100.1
uvicorn[standard]==0.24.0
sqlalchemy[asyncio]==2.0.23
asyncpg==0.29.0

23
backend/run.py Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import sys
import os
from pathlib import Path
backend_dir = Path(__file__).parent
sys.path.insert(0, str(backend_dir))
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"src.presentation.main:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)

View File

@ -3,6 +3,7 @@ Use cases для работы с пользователями
"""
from uuid import UUID
from typing import Optional
from datetime import datetime, timedelta
from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository
from src.shared.exceptions import NotFoundError, ValidationError
@ -52,4 +53,27 @@ class UserUseCases:
async def list_users(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список пользователей"""
return await self.user_repository.list_all(skip=skip, limit=limit)
async def increment_questions_used(self, telegram_id: str) -> User:
"""Увеличить счетчик использованных вопросов"""
user = await self.user_repository.get_by_telegram_id(telegram_id)
if not user:
raise NotFoundError(f"Пользователь с telegram_id {telegram_id} не найден")
user.questions_used += 1
return await self.user_repository.update(user)
async def activate_premium(self, telegram_id: str, days: int = 30) -> User:
"""Активировать premium статус"""
user = await self.user_repository.get_by_telegram_id(telegram_id)
if not user:
raise NotFoundError(f"Пользователь с telegram_id {telegram_id} не найден")
user.is_premium = True
if user.premium_until and user.premium_until > datetime.utcnow():
user.premium_until = user.premium_until + timedelta(days=days)
else:
user.premium_until = datetime.utcnow() + timedelta(days=days)
return await self.user_repository.update(user)

View File

@ -20,12 +20,18 @@ class User:
telegram_id: str,
role: UserRole = UserRole.USER,
user_id: UUID | None = None,
created_at: datetime | None = None
created_at: datetime | None = None,
is_premium: bool = False,
premium_until: datetime | None = None,
questions_used: int = 0
):
self.user_id = user_id or uuid4()
self.telegram_id = telegram_id
self.role = role
self.created_at = created_at or datetime.utcnow()
self.is_premium = is_premium
self.premium_until = premium_until
self.questions_used = questions_used
def is_admin(self) -> bool:
"""проверка, является ли пользователь администратором"""

View File

@ -17,6 +17,10 @@ class UserModel(Base):
telegram_id = Column(String, unique=True, nullable=False, index=True)
role = Column(String, nullable=False, default="user")
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
is_premium = Column(Boolean, default=False, nullable=False)
premium_until = Column(DateTime, nullable=True)
questions_used = Column(Integer, default=0, nullable=False)
collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan")
collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan")

View File

@ -23,7 +23,10 @@ class PostgreSQLUserRepository(IUserRepository):
user_id=user.user_id,
telegram_id=user.telegram_id,
role=user.role.value,
created_at=user.created_at
created_at=user.created_at,
is_premium=user.is_premium,
premium_until=user.premium_until,
questions_used=user.questions_used
)
self.session.add(db_user)
await self.session.commit()
@ -57,6 +60,9 @@ class PostgreSQLUserRepository(IUserRepository):
db_user.telegram_id = user.telegram_id
db_user.role = user.role.value
db_user.is_premium = user.is_premium
db_user.premium_until = user.premium_until
db_user.questions_used = user.questions_used
await self.session.commit()
await self.session.refresh(db_user)
return self._to_entity(db_user)
@ -90,6 +96,9 @@ class PostgreSQLUserRepository(IUserRepository):
user_id=db_user.user_id,
telegram_id=db_user.telegram_id,
role=UserRole(db_user.role),
created_at=db_user.created_at
created_at=db_user.created_at,
is_premium=db_user.is_premium,
premium_until=db_user.premium_until,
questions_used=db_user.questions_used
)

View File

@ -24,8 +24,8 @@ router = APIRouter(prefix="/admin", tags=["admin"])
async def admin_list_users(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[UserUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: UserUseCases = FromDishka()
):
"""Получить список всех пользователей (только для админов)"""
if not current_user.is_admin():
@ -38,8 +38,8 @@ async def admin_list_users(
async def admin_list_collections(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Получить список всех коллекций (только для админов)"""
from src.infrastructure.database.base import AsyncSessionLocal

View File

@ -24,8 +24,8 @@ router = APIRouter(prefix="/collections", tags=["collections"])
@router.post("", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED)
async def create_collection(
collection_data: CollectionCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Создать коллекцию"""
collection = await use_cases.create_collection(
@ -40,7 +40,7 @@ async def create_collection(
@router.get("/{collection_id}", response_model=CollectionResponse)
async def get_collection(
collection_id: UUID,
use_cases: FromDishka[CollectionUseCases] = FromDishka()
use_cases: CollectionUseCases = FromDishka()
):
"""Получить коллекцию по ID"""
collection = await use_cases.get_collection(collection_id)
@ -51,8 +51,8 @@ async def get_collection(
async def update_collection(
collection_id: UUID,
collection_data: CollectionUpdate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Обновить коллекцию"""
collection = await use_cases.update_collection(
@ -68,8 +68,8 @@ async def update_collection(
@router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_collection(
collection_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Удалить коллекцию"""
await use_cases.delete_collection(collection_id, current_user.user_id)
@ -80,8 +80,8 @@ async def delete_collection(
async def list_collections(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Получить список коллекций, доступных пользователю"""
collections = await use_cases.list_user_collections(
@ -96,8 +96,8 @@ async def list_collections(
async def grant_access(
collection_id: UUID,
access_data: CollectionAccessGrant,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Предоставить доступ пользователю к коллекции"""
access = await use_cases.grant_access(
@ -112,8 +112,8 @@ async def grant_access(
async def revoke_access(
collection_id: UUID,
user_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: CollectionUseCases = FromDishka()
):
"""Отозвать доступ пользователя к коллекции"""
await use_cases.revoke_access(collection_id, user_id, current_user.user_id)

View File

@ -21,8 +21,8 @@ router = APIRouter(prefix="/conversations", tags=["conversations"])
@router.post("", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED)
async def create_conversation(
conversation_data: ConversationCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: ConversationUseCases = FromDishka()
):
"""Создать беседу"""
conversation = await use_cases.create_conversation(
@ -35,8 +35,8 @@ async def create_conversation(
@router.get("/{conversation_id}", response_model=ConversationResponse)
async def get_conversation(
conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: ConversationUseCases = FromDishka()
):
"""Получить беседу по ID"""
conversation = await use_cases.get_conversation(conversation_id, current_user.user_id)
@ -46,8 +46,8 @@ async def get_conversation(
@router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_conversation(
conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: ConversationUseCases = FromDishka()
):
"""Удалить беседу"""
await use_cases.delete_conversation(conversation_id, current_user.user_id)
@ -58,8 +58,8 @@ async def delete_conversation(
async def list_conversations(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: ConversationUseCases = FromDishka()
):
"""Получить список бесед пользователя"""
conversations = await use_cases.list_user_conversations(

View File

@ -22,8 +22,8 @@ router = APIRouter(prefix="/documents", tags=["documents"])
@router.post("", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
async def create_document(
document_data: DocumentCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: DocumentUseCases = FromDishka()
):
"""Создать документ"""
document = await use_cases.create_document(
@ -39,8 +39,8 @@ async def create_document(
async def upload_document(
collection_id: UUID,
file: UploadFile = File(...),
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: DocumentUseCases = FromDishka()
):
"""Загрузить и распарсить PDF документ или изображение"""
if not file.filename:
@ -70,7 +70,7 @@ async def upload_document(
@router.get("/{document_id}", response_model=DocumentResponse)
async def get_document(
document_id: UUID,
use_cases: FromDishka[DocumentUseCases] = FromDishka()
use_cases: DocumentUseCases = FromDishka()
):
"""Получить документ по ID"""
document = await use_cases.get_document(document_id)
@ -81,8 +81,8 @@ async def get_document(
async def update_document(
document_id: UUID,
document_data: DocumentUpdate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: DocumentUseCases = FromDishka()
):
"""Обновить документ"""
document = await use_cases.update_document(
@ -98,8 +98,8 @@ async def update_document(
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_document(
document_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: DocumentUseCases = FromDishka()
):
"""Удалить документ"""
await use_cases.delete_document(document_id, current_user.user_id)
@ -111,7 +111,7 @@ async def list_collection_documents(
collection_id: UUID,
skip: int = 0,
limit: int = 100,
use_cases: FromDishka[DocumentUseCases] = FromDishka()
use_cases: DocumentUseCases = FromDishka()
):
"""Получить документы коллекции"""
documents = await use_cases.list_collection_documents(

View File

@ -22,8 +22,8 @@ router = APIRouter(prefix="/messages", tags=["messages"])
@router.post("", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def create_message(
message_data: MessageCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: MessageUseCases = FromDishka()
):
"""Создать сообщение"""
message = await use_cases.create_message(
@ -39,7 +39,7 @@ async def create_message(
@router.get("/{message_id}", response_model=MessageResponse)
async def get_message(
message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka()
use_cases: MessageUseCases = FromDishka()
):
"""Получить сообщение по ID"""
message = await use_cases.get_message(message_id)
@ -50,7 +50,7 @@ async def get_message(
async def update_message(
message_id: UUID,
message_data: MessageUpdate,
use_cases: FromDishka[MessageUseCases] = FromDishka()
use_cases: MessageUseCases = FromDishka()
):
"""Обновить сообщение"""
message = await use_cases.update_message(
@ -64,7 +64,7 @@ async def update_message(
@router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_message(
message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka()
use_cases: MessageUseCases = FromDishka()
):
"""Удалить сообщение"""
await use_cases.delete_message(message_id)
@ -76,8 +76,8 @@ async def list_conversation_messages(
conversation_id: UUID,
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka()
current_user: User = FromDishka(),
use_cases: MessageUseCases = FromDishka()
):
"""Получить сообщения беседы"""
messages = await use_cases.list_conversation_messages(

View File

@ -21,8 +21,8 @@ router = APIRouter(prefix="/rag", tags=["rag"])
@router.post("/index", response_model=IndexDocumentResponse, status_code=status.HTTP_200_OK)
async def index_document(
body: IndexDocumentRequest,
use_cases: FromDishka[RAGUseCases] = FromDishka(),
current_user: FromDishka[User] = FromDishka(),
use_cases: RAGUseCases = FromDishka(),
current_user: User = FromDishka(),
):
"""Индексирование идет через чанкирование, далее эмбеддинг и загрузка в векторную бд"""
result = await use_cases.index_document(body.document_id)
@ -32,8 +32,8 @@ async def index_document(
@router.post("/question", response_model=RAGAnswer, status_code=status.HTTP_200_OK)
async def ask_question(
body: QuestionRequest,
use_cases: FromDishka[RAGUseCases] = FromDishka(),
current_user: FromDishka[User] = FromDishka(),
use_cases: RAGUseCases = FromDishka(),
current_user: User = FromDishka(),
):
"""Отвечает на вопрос, используя RAG в рамках беседы"""
result = await use_cases.ask_question(

View File

@ -1,83 +1,126 @@
"""
API роутеры для работы с пользователями
"""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse
from src.application.use_cases.user_use_cases import UserUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/users", tags=["users"])
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Создать пользователя"""
user = await use_cases.create_user(
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: FromDishka[User] = FromDishka()
):
"""Получить информацию о текущем пользователе"""
return UserResponse.from_entity(current_user)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить пользователя по ID"""
user = await use_cases.get_user(user_id)
return UserResponse.from_entity(user)
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: UUID,
user_data: UserUpdate,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Обновить пользователя"""
user = await use_cases.update_user(
user_id=user_id,
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Удалить пользователя"""
await use_cases.delete_user(user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[UserResponse])
async def list_users(
skip: int = 0,
limit: int = 100,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить список пользователей"""
users = await use_cases.list_users(skip=skip, limit=limit)
return [UserResponse.from_entity(user) for user in users]
"""
API роутеры для работы с пользователями
"""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, status, Depends
from fastapi.responses import JSONResponse
from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka, inject
from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse
from src.application.use_cases.user_use_cases import UserUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/users", tags=["users"])
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
@inject
async def create_user(
user_data: UserCreate,
use_cases: UserUseCases = FromDishka()
) -> UserResponse:
"""Создать пользователя"""
user = await use_cases.create_user(
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.get("/me", response_model=UserResponse)
@inject
async def get_current_user_info(
current_user: User = FromDishka()
):
"""Получить информацию о текущем пользователе"""
return UserResponse.from_entity(current_user)
@router.get("/telegram/{telegram_id}", response_model=UserResponse)
@inject
async def get_user_by_telegram_id(
telegram_id: str,
use_cases: UserUseCases = FromDishka()
):
"""Получить пользователя по Telegram ID"""
user = await use_cases.get_user_by_telegram_id(telegram_id)
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"Пользователь с telegram_id {telegram_id} не найден")
return UserResponse.from_entity(user)
@router.post("/telegram/{telegram_id}/increment-questions", response_model=UserResponse)
@inject
async def increment_questions(
telegram_id: str,
use_cases: UserUseCases = FromDishka()
):
"""Увеличить счетчик использованных вопросов"""
user = await use_cases.increment_questions_used(telegram_id)
return UserResponse.from_entity(user)
@router.post("/telegram/{telegram_id}/activate-premium", response_model=UserResponse)
@inject
async def activate_premium(
telegram_id: str,
days: int = 30,
use_cases: UserUseCases = FromDishka()
):
"""Активировать premium статус"""
user = await use_cases.activate_premium(telegram_id, days=days)
return UserResponse.from_entity(user)
@router.get("/{user_id}", response_model=UserResponse)
@inject
async def get_user(
user_id: UUID,
use_cases: UserUseCases = FromDishka()
):
"""Получить пользователя по ID"""
user = await use_cases.get_user(user_id)
return UserResponse.from_entity(user)
@router.put("/{user_id}", response_model=UserResponse)
@inject
async def update_user(
user_id: UUID,
user_data: UserUpdate,
use_cases: UserUseCases = FromDishka()
):
"""Обновить пользователя"""
user = await use_cases.update_user(
user_id=user_id,
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def delete_user(
user_id: UUID,
use_cases: UserUseCases = FromDishka()
):
"""Удалить пользователя"""
await use_cases.delete_user(user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[UserResponse])
@inject
async def list_users(
skip: int = 0,
limit: int = 100,
use_cases: UserUseCases = FromDishka()
):
"""Получить список пользователей"""
users = await use_cases.list_users(skip=skip, limit=limit)
return [UserResponse.from_entity(user) for user in users]

View File

@ -2,9 +2,11 @@ from __future__ import annotations
import sys
import os
from pathlib import Path
if '/app' not in sys.path:
sys.path.insert(0, '/app')
backend_dir = Path(__file__).parent.parent.parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

View File

@ -30,6 +30,9 @@ class UserResponse(BaseModel):
telegram_id: str
role: UserRole
created_at: datetime
is_premium: bool = False
premium_until: datetime | None = None
questions_used: int = 0
@classmethod
def from_entity(cls, user: "User") -> "UserResponse":
@ -38,7 +41,10 @@ class UserResponse(BaseModel):
user_id=user.user_id,
telegram_id=user.telegram_id,
role=user.role,
created_at=user.created_at
created_at=user.created_at,
is_premium=user.is_premium,
premium_until=user.premium_until,
questions_used=user.questions_used
)
class Config:

View File

@ -1,7 +1,7 @@
from dishka import Container, Provider, Scope, provide
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from src.infrastructure.database.base import AsyncSessionLocal
from src.infrastructure.repositories.postgresql.user_repository import PostgreSQLUserRepository
@ -39,8 +39,7 @@ 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 def get_db(self) -> AsyncIterator[AsyncSession]:
async with AsyncSessionLocal() as session:
try:
yield session
@ -77,7 +76,7 @@ class RepositoryProvider(Provider):
class ServiceProvider(Provider):
@provide(scope=Scope.APP)
def get_redis_client(self) -> RedisClient:
return RedisClient()
return RedisClient(host=settings.REDIS_HOST, port=settings.REDIS_PORT)
@provide(scope=Scope.APP)
def get_cache_service(self, redis_client: RedisClient) -> CacheService:

View File

@ -2,8 +2,6 @@ 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:
@ -19,7 +17,7 @@ class RAGService:
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/users/telegram/{user_telegram_id}"
f"{settings.BACKEND_URL}/users/telegram/{user_telegram_id}"
) as user_response:
if user_response.status != 200:
return []
@ -31,7 +29,7 @@ class RAGService:
return []
async with session.get(
f"{BACKEND_URL}/collections/",
f"{settings.BACKEND_URL}/collections/",
headers={"X-Telegram-ID": user_telegram_id}
) as collections_response:
if collections_response.status != 200:
@ -48,7 +46,7 @@ class RAGService:
try:
async with aiohttp.ClientSession() as search_session:
async with search_session.get(
f"{BACKEND_URL}/documents/collection/{collection_id}",
f"{settings.BACKEND_URL}/documents/collection/{collection_id}",
params={"search": query, "limit": limit_per_collection},
headers={"X-Telegram-ID": user_telegram_id}
) as search_response:

View File

@ -17,7 +17,6 @@ class Settings(BaseSettings):
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"
@ -28,6 +27,8 @@ class Settings(BaseSettings):
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"
ADMIN_IDS_STR: str = ""

View File

@ -1,20 +1,60 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime, timedelta
import aiohttp
from datetime import datetime
from typing import Optional
from tg_bot.infrastructure.database.models import UserModel
from tg_bot.config.settings import settings
class User:
"""Модель пользователя для телеграм-бота"""
def __init__(self, data: dict):
self.user_id = data.get("user_id")
self.telegram_id = data.get("telegram_id")
self.role = data.get("role")
created_at_str = data.get("created_at")
if created_at_str:
try:
created_at_str = created_at_str.replace("Z", "+00:00")
self.created_at = datetime.fromisoformat(created_at_str)
except (ValueError, AttributeError):
self.created_at = None
else:
self.created_at = None
premium_until_str = data.get("premium_until")
if premium_until_str:
try:
premium_until_str = premium_until_str.replace("Z", "+00:00")
self.premium_until = datetime.fromisoformat(premium_until_str)
except (ValueError, AttributeError):
self.premium_until = None
else:
self.premium_until = None
self.is_premium = data.get("is_premium", False)
self.questions_used = data.get("questions_used", 0)
class UserService:
"""Сервис для работы с пользователями через API бэкенда"""
def __init__(self, session: AsyncSession):
self.session = session
def __init__(self):
self.backend_url = settings.BACKEND_URL
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_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:
if response.status == 200:
data = await response.json()
return User(data)
return None
except Exception as e:
print(f"Error getting user: {e}")
return None
async def get_or_create_user(
self,
@ -22,46 +62,45 @@ class UserService:
username: str = "",
first_name: str = "",
last_name: str = ""
) -> UserModel:
) -> User:
"""Получить или создать пользователя"""
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()
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.backend_url}/users",
json={"telegram_id": str(telegram_id), "role": "user"}
) as response:
if response.status in [200, 201]:
data = await response.json()
return User(data)
except Exception as e:
print(f"Error creating user: {e}")
raise
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
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.backend_url}/users/telegram/{telegram_id}/increment-questions"
) as response:
return response.status == 200
except Exception as e:
print(f"Error updating questions: {e}")
return False
async def activate_premium(self, telegram_id: int, days: int = 30) -> bool:
"""Активировать premium статус"""
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.backend_url}/users/telegram/{telegram_id}/activate-premium",
params={"days": days}
) as response:
return response.status == 200
except Exception as e:
print(f"Error activating premium: {e}")
await self.session.rollback()
return False

View File

@ -1,19 +0,0 @@
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

@ -1,39 +0,0 @@
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

@ -34,8 +34,18 @@ async def create_bot() -> tuple[Bot, Dispatcher]:
async def start_bot():
bot = None
try:
if not settings.TELEGRAM_BOT_TOKEN or not settings.TELEGRAM_BOT_TOKEN.strip():
raise ValueError("TELEGRAM_BOT_TOKEN не установлен в переменных окружения или файле .env")
bot, dp = await create_bot()
try:
bot_info = await bot.get_me()
username = bot_info.username if bot_info.username else f"ID: {bot_info.id}"
logger.info(f"Бот успешно подключен: @{username}")
except Exception as e:
raise ValueError(f"Неверный токен Telegram бота: {e}")
try:
webhook_info = await bot.get_webhook_info()
if webhook_info.url:

View File

@ -4,14 +4,11 @@ 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
from datetime import datetime
router = Router()
user_service = UserService()
@router.message(Command("buy"))
@ -19,23 +16,21 @@ 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)
try:
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
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"
@ -49,24 +44,8 @@ async def cmd_buy(message: Message):
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()
print(f"Платёж создан в ЮKассе: {payment_data['id']}")
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
@ -139,27 +118,15 @@ async def check_payment_status(callback_query: types.CallbackQuery):
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)
try:
success = await user_service.activate_premium(user_id)
if success:
user = await user_service.get_user_by_telegram_id(user_id)
if user:
await callback_query.message.answer(
"<b>Оплата подтверждена!</b>\n\n"
f"Ваш premium-доступ активирован до: "
f"<b>{user.premium_until.strftime('%d.%m.%Y')}</b>\n\n"
f"<b>{user.premium_until.strftime('%d.%m.%Y') if user.premium_until else 'Не указано'}</b>\n\n"
"Теперь вы можете:\n"
"• Задавать неограниченное количество вопросов\n"
"• Получать приоритетные ответы\n"
@ -169,12 +136,23 @@ async def check_payment_status(callback_query: types.CallbackQuery):
)
else:
await callback_query.message.answer(
"<b>Платёж найден в ЮKассе, но не в нашей БД</b>\n\n"
"<b>Оплата подтверждена, но не удалось активировать premium</b>\n\n"
"Пожалуйста, обратитесь к администратору.",
parse_mode="HTML"
)
except Exception as e:
print(f"Ошибка обработки платежа: {e}")
else:
await callback_query.message.answer(
"<b>Оплата подтверждена, но не удалось активировать premium</b>\n\n"
"Пожалуйста, обратитесь к администратору.",
parse_mode="HTML"
)
except Exception as e:
print(f"Ошибка обработки платежа: {e}")
await callback_query.message.answer(
"<b>Ошибка активации premium</b>\n\n"
"Пожалуйста, обратитесь к администратору.",
parse_mode="HTML"
)
elif payment.status == "pending":
await callback_query.message.answer(
@ -206,42 +184,13 @@ async def check_payment_status(callback_query: types.CallbackQuery):
@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}")
await message.answer(
"<b>История платежей</b>\n\n"
"История платежей хранится в системе оплаты ЮKassa.\n"
"Для проверки статуса подписки используйте команду /stats.\n\n"
"Для оформления новой подписки используйте команду /buy",
parse_mode="HTML"
)
@router.message(Command("testcards"))

View File

@ -2,17 +2,16 @@ from aiogram import Router
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.filters import Command
import aiohttp
from tg_bot.config.settings import settings
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/",
f"{settings.BACKEND_URL}/collections/",
headers={"X-Telegram-ID": telegram_id}
) as response:
if response.status == 200:
@ -27,7 +26,7 @@ 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}",
f"{settings.BACKEND_URL}/documents/collection/{collection_id}",
headers={"X-Telegram-ID": telegram_id}
) as response:
if response.status == 200:
@ -42,7 +41,7 @@ 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}",
f"{settings.BACKEND_URL}/documents/collection/{collection_id}",
params={"search": query},
headers={"X-Telegram-ID": telegram_id}
) as response:

View File

@ -3,14 +3,12 @@ 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.domain.services.user_service import UserService, User
from tg_bot.application.services.rag_service import RAGService
router = Router()
BACKEND_URL = "http://localhost:8001/api/v1"
rag_service = RAGService()
user_service = UserService()
@router.message()
async def handle_question(message: Message):
@ -19,58 +17,37 @@ async def handle_question(message: Message):
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)
try:
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"
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 ""
)
if user.is_premium:
await process_premium_question(message, user, question_text)
elif user.questions_used < settings.FREE_QUESTIONS_LIMIT:
await process_free_question(message, user, question_text)
else:
await handle_limit_exceeded(message, user)
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}")
print(f"Error processing question: {e}")
await message.answer(
"Произошла ошибка. Попробуйте позже.",
parse_mode="HTML"
)
async def process_premium_question(message: Message, user: UserModel, question_text: str, user_service: UserService):
await user_service.update_user_questions(user.telegram_id)
async def process_premium_question(message: Message, user: User, question_text: str):
await user_service.update_user_questions(int(user.telegram_id))
user = await user_service.get_user_by_telegram_id(int(user.telegram_id))
await message.bot.send_chat_action(message.chat.id, "typing")
@ -129,9 +106,9 @@ async def process_premium_question(message: Message, user: UserModel, question_t
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)
async def process_free_question(message: Message, user: User, question_text: str):
await user_service.update_user_questions(int(user.telegram_id))
user = await user_service.get_user_by_telegram_id(int(user.telegram_id))
remaining = settings.FREE_QUESTIONS_LIMIT - user.questions_used
await message.bot.send_chat_action(message.chat.id, "typing")
@ -201,9 +178,11 @@ async def process_free_question(message: Message, user: UserModel, question_text
async def save_conversation_to_backend(telegram_id: str, question: str, answer: str, sources: list):
try:
from tg_bot.config.settings import settings
backend_url = settings.BACKEND_URL
async with aiohttp.ClientSession() as session:
async with session.get(
f"{BACKEND_URL}/users/telegram/{telegram_id}"
f"{backend_url}/users/telegram/{telegram_id}"
) as user_response:
if user_response.status != 200:
return
@ -211,7 +190,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer:
user_uuid = user_data.get("user_id")
async with session.get(
f"{BACKEND_URL}/collections/",
f"{backend_url}/collections/",
headers={"X-Telegram-ID": telegram_id}
) as collections_response:
collections = []
@ -223,7 +202,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer:
collection_id = collections[0].get("collection_id")
else:
async with session.post(
f"{BACKEND_URL}/collections",
f"{backend_url}/collections",
json={
"name": "Основная коллекция",
"description": "Коллекция по умолчанию",
@ -239,7 +218,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer:
return
async with session.post(
f"{BACKEND_URL}/conversations",
f"{backend_url}/conversations",
json={"collection_id": str(collection_id)},
headers={"X-Telegram-ID": telegram_id}
) as conversation_response:
@ -252,7 +231,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer:
return
await session.post(
f"{BACKEND_URL}/messages",
f"{backend_url}/messages",
json={
"conversation_id": str(conversation_id),
"content": question,
@ -262,7 +241,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer:
)
await session.post(
f"{BACKEND_URL}/messages",
f"{backend_url}/messages",
json={
"conversation_id": str(conversation_id),
"content": answer,
@ -276,7 +255,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer:
print(f"Error saving conversation: {e}")
async def handle_limit_exceeded(message: Message, user: UserModel):
async def handle_limit_exceeded(message: Message, user: User):
response = (
f"<b>Лимит бесплатных вопросов исчерпан!</b>\n\n"

View File

@ -4,10 +4,10 @@ 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()
user_service = UserService()
@router.message(Command("start"))
async def cmd_start(message: Message):
@ -16,22 +16,19 @@ async def cmd_start(message: Message):
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()
try:
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}")
welcome_text = (
f"<b>Привет, {first_name}!</b>\n\n"
f"Я <b>VibeLawyerBot</b> - ваш помощник в юридических вопросах.\n\n"

View File

@ -4,58 +4,56 @@ 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()
user_service = UserService()
@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)
try:
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"
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"
)

View File

@ -19,9 +19,6 @@ async def handle_yookassa_webhook(request: Request):
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":
@ -29,38 +26,34 @@ async def handle_yookassa_webhook(request: Request):
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")
user_service = UserService()
success = await user_service.activate_premium(int(user_id))
if success:
print(f"Premium activated for user {user_id}")
user = await user_service.get_user_by_telegram_id(int(user_id))
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 or failed to activate premium")
except ImportError as e:
print(f"Import error: {e}")

View File

@ -2,8 +2,6 @@ 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.2
yookassa>=2.4.0
aiohttp>=3.9.1

20
tg_bot/run.py Normal file
View File

@ -0,0 +1,20 @@
"""
Скрипт для запуска Telegram бота без Docker
"""
import sys
import os
from pathlib import Path
tg_bot_dir = Path(__file__).parent
sys.path.insert(0, str(tg_bot_dir))
if __name__ == "__main__":
from main import main
import asyncio
asyncio.run(main())