andrewbokh #6

Merged
Arxip222 merged 4 commits from andrewbokh into main 2025-12-24 10:36:03 +03:00
31 changed files with 574 additions and 507 deletions
Showing only changes of commit 493c385cb1 - Show all commits

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 uvicorn[standard]==0.24.0
sqlalchemy[asyncio]==2.0.23 sqlalchemy[asyncio]==2.0.23
asyncpg==0.29.0 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 uuid import UUID
from typing import Optional from typing import Optional
from datetime import datetime, timedelta
from src.domain.entities.user import User, UserRole from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository from src.domain.repositories.user_repository import IUserRepository
from src.shared.exceptions import NotFoundError, ValidationError from src.shared.exceptions import NotFoundError, ValidationError
@ -53,3 +54,26 @@ class UserUseCases:
"""Получить список пользователей""" """Получить список пользователей"""
return await self.user_repository.list_all(skip=skip, limit=limit) 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, telegram_id: str,
role: UserRole = UserRole.USER, role: UserRole = UserRole.USER,
user_id: UUID | None = None, 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.user_id = user_id or uuid4()
self.telegram_id = telegram_id self.telegram_id = telegram_id
self.role = role self.role = role
self.created_at = created_at or datetime.utcnow() 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: def is_admin(self) -> bool:
"""проверка, является ли пользователь администратором""" """проверка, является ли пользователь администратором"""

View File

@ -17,6 +17,10 @@ class UserModel(Base):
telegram_id = Column(String, unique=True, nullable=False, index=True) telegram_id = Column(String, unique=True, nullable=False, index=True)
role = Column(String, nullable=False, default="user") role = Column(String, nullable=False, default="user")
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) 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") collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="user", 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") 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, user_id=user.user_id,
telegram_id=user.telegram_id, telegram_id=user.telegram_id,
role=user.role.value, 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) self.session.add(db_user)
await self.session.commit() await self.session.commit()
@ -57,6 +60,9 @@ class PostgreSQLUserRepository(IUserRepository):
db_user.telegram_id = user.telegram_id db_user.telegram_id = user.telegram_id
db_user.role = user.role.value 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.commit()
await self.session.refresh(db_user) await self.session.refresh(db_user)
return self._to_entity(db_user) return self._to_entity(db_user)
@ -90,6 +96,9 @@ class PostgreSQLUserRepository(IUserRepository):
user_id=db_user.user_id, user_id=db_user.user_id,
telegram_id=db_user.telegram_id, telegram_id=db_user.telegram_id,
role=UserRole(db_user.role), 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( async def admin_list_users(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[UserUseCases] = FromDishka() use_cases: UserUseCases = FromDishka()
): ):
"""Получить список всех пользователей (только для админов)""" """Получить список всех пользователей (только для админов)"""
if not current_user.is_admin(): if not current_user.is_admin():
@ -38,8 +38,8 @@ async def admin_list_users(
async def admin_list_collections( async def admin_list_collections(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Получить список всех коллекций (только для админов)""" """Получить список всех коллекций (только для админов)"""
from src.infrastructure.database.base import AsyncSessionLocal 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) @router.post("", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED)
async def create_collection( async def create_collection(
collection_data: CollectionCreate, collection_data: CollectionCreate,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Создать коллекцию""" """Создать коллекцию"""
collection = await use_cases.create_collection( collection = await use_cases.create_collection(
@ -40,7 +40,7 @@ async def create_collection(
@router.get("/{collection_id}", response_model=CollectionResponse) @router.get("/{collection_id}", response_model=CollectionResponse)
async def get_collection( async def get_collection(
collection_id: UUID, collection_id: UUID,
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Получить коллекцию по ID""" """Получить коллекцию по ID"""
collection = await use_cases.get_collection(collection_id) collection = await use_cases.get_collection(collection_id)
@ -51,8 +51,8 @@ async def get_collection(
async def update_collection( async def update_collection(
collection_id: UUID, collection_id: UUID,
collection_data: CollectionUpdate, collection_data: CollectionUpdate,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Обновить коллекцию""" """Обновить коллекцию"""
collection = await use_cases.update_collection( 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) @router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_collection( async def delete_collection(
collection_id: UUID, collection_id: UUID,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Удалить коллекцию""" """Удалить коллекцию"""
await use_cases.delete_collection(collection_id, current_user.user_id) await use_cases.delete_collection(collection_id, current_user.user_id)
@ -80,8 +80,8 @@ async def delete_collection(
async def list_collections( async def list_collections(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Получить список коллекций, доступных пользователю""" """Получить список коллекций, доступных пользователю"""
collections = await use_cases.list_user_collections( collections = await use_cases.list_user_collections(
@ -96,8 +96,8 @@ async def list_collections(
async def grant_access( async def grant_access(
collection_id: UUID, collection_id: UUID,
access_data: CollectionAccessGrant, access_data: CollectionAccessGrant,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Предоставить доступ пользователю к коллекции""" """Предоставить доступ пользователю к коллекции"""
access = await use_cases.grant_access( access = await use_cases.grant_access(
@ -112,8 +112,8 @@ async def grant_access(
async def revoke_access( async def revoke_access(
collection_id: UUID, collection_id: UUID,
user_id: UUID, user_id: UUID,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka() use_cases: CollectionUseCases = FromDishka()
): ):
"""Отозвать доступ пользователя к коллекции""" """Отозвать доступ пользователя к коллекции"""
await use_cases.revoke_access(collection_id, user_id, current_user.user_id) 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) @router.post("", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED)
async def create_conversation( async def create_conversation(
conversation_data: ConversationCreate, conversation_data: ConversationCreate,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka() use_cases: ConversationUseCases = FromDishka()
): ):
"""Создать беседу""" """Создать беседу"""
conversation = await use_cases.create_conversation( conversation = await use_cases.create_conversation(
@ -35,8 +35,8 @@ async def create_conversation(
@router.get("/{conversation_id}", response_model=ConversationResponse) @router.get("/{conversation_id}", response_model=ConversationResponse)
async def get_conversation( async def get_conversation(
conversation_id: UUID, conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka() use_cases: ConversationUseCases = FromDishka()
): ):
"""Получить беседу по ID""" """Получить беседу по ID"""
conversation = await use_cases.get_conversation(conversation_id, current_user.user_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) @router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_conversation( async def delete_conversation(
conversation_id: UUID, conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka() use_cases: ConversationUseCases = FromDishka()
): ):
"""Удалить беседу""" """Удалить беседу"""
await use_cases.delete_conversation(conversation_id, current_user.user_id) await use_cases.delete_conversation(conversation_id, current_user.user_id)
@ -58,8 +58,8 @@ async def delete_conversation(
async def list_conversations( async def list_conversations(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka() use_cases: ConversationUseCases = FromDishka()
): ):
"""Получить список бесед пользователя""" """Получить список бесед пользователя"""
conversations = await use_cases.list_user_conversations( 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) @router.post("", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
async def create_document( async def create_document(
document_data: DocumentCreate, document_data: DocumentCreate,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka() use_cases: DocumentUseCases = FromDishka()
): ):
"""Создать документ""" """Создать документ"""
document = await use_cases.create_document( document = await use_cases.create_document(
@ -39,8 +39,8 @@ async def create_document(
async def upload_document( async def upload_document(
collection_id: UUID, collection_id: UUID,
file: UploadFile = File(...), file: UploadFile = File(...),
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka() use_cases: DocumentUseCases = FromDishka()
): ):
"""Загрузить и распарсить PDF документ или изображение""" """Загрузить и распарсить PDF документ или изображение"""
if not file.filename: if not file.filename:
@ -70,7 +70,7 @@ async def upload_document(
@router.get("/{document_id}", response_model=DocumentResponse) @router.get("/{document_id}", response_model=DocumentResponse)
async def get_document( async def get_document(
document_id: UUID, document_id: UUID,
use_cases: FromDishka[DocumentUseCases] = FromDishka() use_cases: DocumentUseCases = FromDishka()
): ):
"""Получить документ по ID""" """Получить документ по ID"""
document = await use_cases.get_document(document_id) document = await use_cases.get_document(document_id)
@ -81,8 +81,8 @@ async def get_document(
async def update_document( async def update_document(
document_id: UUID, document_id: UUID,
document_data: DocumentUpdate, document_data: DocumentUpdate,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka() use_cases: DocumentUseCases = FromDishka()
): ):
"""Обновить документ""" """Обновить документ"""
document = await use_cases.update_document( 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) @router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_document( async def delete_document(
document_id: UUID, document_id: UUID,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka() use_cases: DocumentUseCases = FromDishka()
): ):
"""Удалить документ""" """Удалить документ"""
await use_cases.delete_document(document_id, current_user.user_id) await use_cases.delete_document(document_id, current_user.user_id)
@ -111,7 +111,7 @@ async def list_collection_documents(
collection_id: UUID, collection_id: UUID,
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
use_cases: FromDishka[DocumentUseCases] = FromDishka() use_cases: DocumentUseCases = FromDishka()
): ):
"""Получить документы коллекции""" """Получить документы коллекции"""
documents = await use_cases.list_collection_documents( 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) @router.post("", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def create_message( async def create_message(
message_data: MessageCreate, message_data: MessageCreate,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka() use_cases: MessageUseCases = FromDishka()
): ):
"""Создать сообщение""" """Создать сообщение"""
message = await use_cases.create_message( message = await use_cases.create_message(
@ -39,7 +39,7 @@ async def create_message(
@router.get("/{message_id}", response_model=MessageResponse) @router.get("/{message_id}", response_model=MessageResponse)
async def get_message( async def get_message(
message_id: UUID, message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka() use_cases: MessageUseCases = FromDishka()
): ):
"""Получить сообщение по ID""" """Получить сообщение по ID"""
message = await use_cases.get_message(message_id) message = await use_cases.get_message(message_id)
@ -50,7 +50,7 @@ async def get_message(
async def update_message( async def update_message(
message_id: UUID, message_id: UUID,
message_data: MessageUpdate, message_data: MessageUpdate,
use_cases: FromDishka[MessageUseCases] = FromDishka() use_cases: MessageUseCases = FromDishka()
): ):
"""Обновить сообщение""" """Обновить сообщение"""
message = await use_cases.update_message( 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) @router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_message( async def delete_message(
message_id: UUID, message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka() use_cases: MessageUseCases = FromDishka()
): ):
"""Удалить сообщение""" """Удалить сообщение"""
await use_cases.delete_message(message_id) await use_cases.delete_message(message_id)
@ -76,8 +76,8 @@ async def list_conversation_messages(
conversation_id: UUID, conversation_id: UUID,
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka() use_cases: MessageUseCases = FromDishka()
): ):
"""Получить сообщения беседы""" """Получить сообщения беседы"""
messages = await use_cases.list_conversation_messages( 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) @router.post("/index", response_model=IndexDocumentResponse, status_code=status.HTTP_200_OK)
async def index_document( async def index_document(
body: IndexDocumentRequest, body: IndexDocumentRequest,
use_cases: FromDishka[RAGUseCases] = FromDishka(), use_cases: RAGUseCases = FromDishka(),
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
): ):
"""Индексирование идет через чанкирование, далее эмбеддинг и загрузка в векторную бд""" """Индексирование идет через чанкирование, далее эмбеддинг и загрузка в векторную бд"""
result = await use_cases.index_document(body.document_id) 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) @router.post("/question", response_model=RAGAnswer, status_code=status.HTTP_200_OK)
async def ask_question( async def ask_question(
body: QuestionRequest, body: QuestionRequest,
use_cases: FromDishka[RAGUseCases] = FromDishka(), use_cases: RAGUseCases = FromDishka(),
current_user: FromDishka[User] = FromDishka(), current_user: User = FromDishka(),
): ):
"""Отвечает на вопрос, используя RAG в рамках беседы""" """Отвечает на вопрос, используя RAG в рамках беседы"""
result = await use_cases.ask_question( result = await use_cases.ask_question(

View File

@ -4,10 +4,10 @@ API роутеры для работы с пользователями
from __future__ import annotations from __future__ import annotations
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, status from fastapi import APIRouter, status, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka from dishka.integrations.fastapi import FromDishka, inject
from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse
from src.application.use_cases.user_use_cases import UserUseCases from src.application.use_cases.user_use_cases import UserUseCases
from src.domain.entities.user import User from src.domain.entities.user import User
@ -16,10 +16,11 @@ router = APIRouter(prefix="/users", tags=["users"])
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED) @router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
@inject
async def create_user( async def create_user(
user_data: UserCreate, user_data: UserCreate,
use_cases: FromDishka[UserUseCases] = FromDishka() use_cases: UserUseCases = FromDishka()
): ) -> UserResponse:
"""Создать пользователя""" """Создать пользователя"""
user = await use_cases.create_user( user = await use_cases.create_user(
telegram_id=user_data.telegram_id, telegram_id=user_data.telegram_id,
@ -29,17 +30,56 @@ async def create_user(
@router.get("/me", response_model=UserResponse) @router.get("/me", response_model=UserResponse)
@inject
async def get_current_user_info( async def get_current_user_info(
current_user: FromDishka[User] = FromDishka() current_user: User = FromDishka()
): ):
"""Получить информацию о текущем пользователе""" """Получить информацию о текущем пользователе"""
return UserResponse.from_entity(current_user) 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) @router.get("/{user_id}", response_model=UserResponse)
@inject
async def get_user( async def get_user(
user_id: UUID, user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka() use_cases: UserUseCases = FromDishka()
): ):
"""Получить пользователя по ID""" """Получить пользователя по ID"""
user = await use_cases.get_user(user_id) user = await use_cases.get_user(user_id)
@ -47,10 +87,11 @@ async def get_user(
@router.put("/{user_id}", response_model=UserResponse) @router.put("/{user_id}", response_model=UserResponse)
@inject
async def update_user( async def update_user(
user_id: UUID, user_id: UUID,
user_data: UserUpdate, user_data: UserUpdate,
use_cases: FromDishka[UserUseCases] = FromDishka() use_cases: UserUseCases = FromDishka()
): ):
"""Обновить пользователя""" """Обновить пользователя"""
user = await use_cases.update_user( user = await use_cases.update_user(
@ -62,9 +103,10 @@ async def update_user(
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
@inject
async def delete_user( async def delete_user(
user_id: UUID, user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka() use_cases: UserUseCases = FromDishka()
): ):
"""Удалить пользователя""" """Удалить пользователя"""
await use_cases.delete_user(user_id) await use_cases.delete_user(user_id)
@ -72,10 +114,11 @@ async def delete_user(
@router.get("", response_model=List[UserResponse]) @router.get("", response_model=List[UserResponse])
@inject
async def list_users( async def list_users(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
use_cases: FromDishka[UserUseCases] = FromDishka() use_cases: UserUseCases = FromDishka()
): ):
"""Получить список пользователей""" """Получить список пользователей"""
users = await use_cases.list_users(skip=skip, limit=limit) users = await use_cases.list_users(skip=skip, limit=limit)

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from dishka import Container, Provider, Scope, provide from dishka import Container, Provider, Scope, provide
from fastapi import Request from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession 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.database.base import AsyncSessionLocal
from src.infrastructure.repositories.postgresql.user_repository import PostgreSQLUserRepository 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): class DatabaseProvider(Provider):
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
@asynccontextmanager async def get_db(self) -> AsyncIterator[AsyncSession]:
async def get_db(self) -> AsyncSession:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
try: try:
yield session yield session
@ -77,7 +76,7 @@ class RepositoryProvider(Provider):
class ServiceProvider(Provider): class ServiceProvider(Provider):
@provide(scope=Scope.APP) @provide(scope=Scope.APP)
def get_redis_client(self) -> RedisClient: def get_redis_client(self) -> RedisClient:
return RedisClient() return RedisClient(host=settings.REDIS_HOST, port=settings.REDIS_PORT)
@provide(scope=Scope.APP) @provide(scope=Scope.APP)
def get_cache_service(self, redis_client: RedisClient) -> CacheService: 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.infrastructure.external.deepseek_client import DeepSeekClient
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
BACKEND_URL = "http://localhost:8001/api/v1"
class RAGService: class RAGService:
@ -19,7 +17,7 @@ class RAGService:
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
f"{BACKEND_URL}/users/telegram/{user_telegram_id}" f"{settings.BACKEND_URL}/users/telegram/{user_telegram_id}"
) as user_response: ) as user_response:
if user_response.status != 200: if user_response.status != 200:
return [] return []
@ -31,7 +29,7 @@ class RAGService:
return [] return []
async with session.get( async with session.get(
f"{BACKEND_URL}/collections/", f"{settings.BACKEND_URL}/collections/",
headers={"X-Telegram-ID": user_telegram_id} headers={"X-Telegram-ID": user_telegram_id}
) as collections_response: ) as collections_response:
if collections_response.status != 200: if collections_response.status != 200:
@ -48,7 +46,7 @@ class RAGService:
try: try:
async with aiohttp.ClientSession() as search_session: async with aiohttp.ClientSession() as search_session:
async with search_session.get( 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}, params={"search": query, "limit": limit_per_collection},
headers={"X-Telegram-ID": user_telegram_id} headers={"X-Telegram-ID": user_telegram_id}
) as search_response: ) as search_response:

View File

@ -17,7 +17,6 @@ class Settings(BaseSettings):
TELEGRAM_BOT_TOKEN: str = "" TELEGRAM_BOT_TOKEN: str = ""
FREE_QUESTIONS_LIMIT: int = 5 FREE_QUESTIONS_LIMIT: int = 5
PAYMENT_AMOUNT: float = 500.0 PAYMENT_AMOUNT: float = 500.0
DATABASE_URL: str = "sqlite:///data/bot.db"
LOG_LEVEL: str = "INFO" LOG_LEVEL: str = "INFO"
LOG_FILE: str = "logs/bot.log" LOG_FILE: str = "logs/bot.log"
@ -29,6 +28,8 @@ class Settings(BaseSettings):
DEEPSEEK_API_KEY: Optional[str] = None DEEPSEEK_API_KEY: Optional[str] = None
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions" DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
BACKEND_URL: str = "http://localhost:8001/api/v1"
ADMIN_IDS_STR: str = "" ADMIN_IDS_STR: str = ""
@property @property

View File

@ -1,20 +1,60 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select import aiohttp
from datetime import datetime, timedelta from datetime import datetime
from typing import Optional 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: class UserService:
"""Сервис для работы с пользователями через API бэкенда"""
def __init__(self, session: AsyncSession): def __init__(self):
self.session = session self.backend_url = settings.BACKEND_URL
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[UserModel]: async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]:
result = await self.session.execute( """Получить пользователя по Telegram ID"""
select(UserModel).filter_by(telegram_id=str(telegram_id)) try:
) async with aiohttp.ClientSession() as session:
return result.scalar_one_or_none() 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( async def get_or_create_user(
self, self,
@ -22,46 +62,45 @@ class UserService:
username: str = "", username: str = "",
first_name: str = "", first_name: str = "",
last_name: str = "" last_name: str = ""
) -> UserModel: ) -> User:
"""Получить или создать пользователя"""
user = await self.get_user_by_telegram_id(telegram_id) user = await self.get_user_by_telegram_id(telegram_id)
if not user: if not user:
user = UserModel( try:
telegram_id=str(telegram_id), async with aiohttp.ClientSession() as session:
username=username, async with session.post(
first_name=first_name, f"{self.backend_url}/users",
last_name=last_name json={"telegram_id": str(telegram_id), "role": "user"}
) ) as response:
self.session.add(user) if response.status in [200, 201]:
await self.session.commit() data = await response.json()
else: return User(data)
user.username = username except Exception as e:
user.first_name = first_name print(f"Error creating user: {e}")
user.last_name = last_name raise
await self.session.commit()
return user return user
async def update_user_questions(self, telegram_id: int) -> bool: async def update_user_questions(self, telegram_id: int) -> bool:
user = await self.get_user_by_telegram_id(telegram_id) """Увеличить счетчик использованных вопросов"""
if user: try:
user.questions_used += 1 async with aiohttp.ClientSession() as session:
await self.session.commit() async with session.post(
return True 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 return False
async def activate_premium(self, telegram_id: int) -> bool: async def activate_premium(self, telegram_id: int, days: int = 30) -> bool:
"""Активировать premium статус"""
try: try:
user = await self.get_user_by_telegram_id(telegram_id) async with aiohttp.ClientSession() as session:
if user: async with session.post(
user.is_premium = True f"{self.backend_url}/users/telegram/{telegram_id}/activate-premium",
if user.premium_until and user.premium_until > datetime.now(): params={"days": days}
user.premium_until = user.premium_until + timedelta(days=30) ) as response:
else: return response.status == 200
user.premium_until = datetime.now() + timedelta(days=30)
await self.session.commit()
return True
else:
return False
except Exception as e: except Exception as e:
print(f"Error activating premium: {e}") print(f"Error activating premium: {e}")
await self.session.rollback()
return False 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(): async def start_bot():
bot = None bot = None
try: 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() 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: try:
webhook_info = await bot.get_webhook_info() webhook_info = await bot.get_webhook_info()
if webhook_info.url: if webhook_info.url:

View File

@ -4,14 +4,11 @@ from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from decimal import Decimal from decimal import Decimal
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.payment.yookassa.client import yookassa_client from tg_bot.payment.yookassa.client import yookassa_client
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.infrastructure.database.models import PaymentModel
from tg_bot.domain.services.user_service import UserService from tg_bot.domain.services.user_service import UserService
from sqlalchemy import select from datetime import datetime
import uuid
from datetime import datetime, timedelta
router = Router() router = Router()
user_service = UserService()
@router.message(Command("buy")) @router.message(Command("buy"))
@ -19,9 +16,7 @@ async def cmd_buy(message: Message):
user_id = message.from_user.id user_id = message.from_user.id
username = message.from_user.username or f"user_{user_id}" username = message.from_user.username or f"user_{user_id}"
async with AsyncSessionLocal() as session:
try: try:
user_service = UserService(session)
user = await user_service.get_user_by_telegram_id(user_id) 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(): if user and user.is_premium and user.premium_until and user.premium_until > datetime.now():
@ -50,23 +45,7 @@ async def cmd_buy(message: Message):
user_id=user_id user_id=user_id
) )
async with AsyncSessionLocal() as session: print(f"Платёж создан в ЮKассе: {payment_data['id']}")
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( keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
@ -139,27 +118,15 @@ async def check_payment_status(callback_query: types.CallbackQuery):
payment = YooPayment.find_one(yookassa_id) payment = YooPayment.find_one(yookassa_id)
if payment.status == "succeeded": if payment.status == "succeeded":
async with AsyncSessionLocal() as session:
try: 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) success = await user_service.activate_premium(user_id)
if success: if success:
user = await user_service.get_user_by_telegram_id(user_id) user = await user_service.get_user_by_telegram_id(user_id)
await session.commit() if user:
if not user:
user = await user_service.get_user_by_telegram_id(user_id)
await callback_query.message.answer( await callback_query.message.answer(
"<b>Оплата подтверждена!</b>\n\n" "<b>Оплата подтверждена!</b>\n\n"
f"Ваш premium-доступ активирован до: " 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" "• Задавать неограниченное количество вопросов\n"
"• Получать приоритетные ответы\n" "• Получать приоритетные ответы\n"
@ -169,12 +136,23 @@ async def check_payment_status(callback_query: types.CallbackQuery):
) )
else: else:
await callback_query.message.answer( await callback_query.message.answer(
"<b>Платёж найден в ЮKассе, но не в нашей БД</b>\n\n" "<b>Оплата подтверждена, но не удалось активировать premium</b>\n\n"
"Пожалуйста, обратитесь к администратору.",
parse_mode="HTML"
)
else:
await callback_query.message.answer(
"<b>Оплата подтверждена, но не удалось активировать premium</b>\n\n"
"Пожалуйста, обратитесь к администратору.", "Пожалуйста, обратитесь к администратору.",
parse_mode="HTML" parse_mode="HTML"
) )
except Exception as e: except Exception as e:
print(f"Ошибка обработки платежа: {e}") print(f"Ошибка обработки платежа: {e}")
await callback_query.message.answer(
"<b>Ошибка активации premium</b>\n\n"
"Пожалуйста, обратитесь к администратору.",
parse_mode="HTML"
)
elif payment.status == "pending": elif payment.status == "pending":
await callback_query.message.answer( await callback_query.message.answer(
@ -206,42 +184,13 @@ async def check_payment_status(callback_query: types.CallbackQuery):
@router.message(Command("mypayments")) @router.message(Command("mypayments"))
async def cmd_my_payments(message: Message): 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( await message.answer(
"<b>У вас пока нет платежей</b>\n\n" "<b>История платежей</b>\n\n"
"Используйте команду /buy чтобы оформить подписку.", "История платежей хранится в системе оплаты ЮKassa.\n"
"Для проверки статуса подписки используйте команду /stats.\n\n"
"Для оформления новой подписки используйте команду /buy",
parse_mode="HTML" 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")) @router.message(Command("testcards"))

View File

@ -2,17 +2,16 @@ from aiogram import Router
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.filters import Command from aiogram.filters import Command
import aiohttp import aiohttp
from tg_bot.config.settings import settings
router = Router() router = Router()
BACKEND_URL = "http://localhost:8001/api/v1"
async def get_user_collections(telegram_id: str): async def get_user_collections(telegram_id: str):
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
f"{BACKEND_URL}/collections/", f"{settings.BACKEND_URL}/collections/",
headers={"X-Telegram-ID": telegram_id} headers={"X-Telegram-ID": telegram_id}
) as response: ) as response:
if response.status == 200: if response.status == 200:
@ -27,7 +26,7 @@ async def get_collection_documents(collection_id: str, telegram_id: str):
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( 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} headers={"X-Telegram-ID": telegram_id}
) as response: ) as response:
if response.status == 200: if response.status == 200:
@ -42,7 +41,7 @@ async def search_in_collection(collection_id: str, query: str, telegram_id: str)
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
f"{BACKEND_URL}/documents/collection/{collection_id}", f"{settings.BACKEND_URL}/documents/collection/{collection_id}",
params={"search": query}, params={"search": query},
headers={"X-Telegram-ID": telegram_id} headers={"X-Telegram-ID": telegram_id}
) as response: ) as response:

View File

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

View File

@ -4,10 +4,10 @@ from aiogram.types import Message
from datetime import datetime from datetime import datetime
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.domain.services.user_service import UserService from tg_bot.domain.services.user_service import UserService
router = Router() router = Router()
user_service = UserService()
@router.message(Command("start")) @router.message(Command("start"))
async def cmd_start(message: Message): async def cmd_start(message: Message):
@ -16,9 +16,7 @@ async def cmd_start(message: Message):
username = message.from_user.username or "" username = message.from_user.username or ""
first_name = message.from_user.first_name or "" first_name = message.from_user.first_name or ""
last_name = message.from_user.last_name or "" last_name = message.from_user.last_name or ""
async with AsyncSessionLocal() as session:
try: try:
user_service = UserService(session)
existing_user = await user_service.get_user_by_telegram_id(user_id) existing_user = await user_service.get_user_by_telegram_id(user_id)
user = await user_service.get_or_create_user( user = await user_service.get_or_create_user(
user_id, user_id,
@ -31,7 +29,6 @@ async def cmd_start(message: Message):
except Exception as e: except Exception as e:
print(f"Ошибка сохранения пользователя: {e}") print(f"Ошибка сохранения пользователя: {e}")
await session.rollback()
welcome_text = ( welcome_text = (
f"<b>Привет, {first_name}!</b>\n\n" f"<b>Привет, {first_name}!</b>\n\n"
f"Я <b>VibeLawyerBot</b> - ваш помощник в юридических вопросах.\n\n" f"Я <b>VibeLawyerBot</b> - ваш помощник в юридических вопросах.\n\n"

View File

@ -4,19 +4,17 @@ from aiogram.filters import Command
from aiogram.types import Message from aiogram.types import Message
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.infrastructure.database.database import AsyncSessionLocal
from tg_bot.domain.services.user_service import UserService from tg_bot.domain.services.user_service import UserService
router = Router() router = Router()
user_service = UserService()
@router.message(Command("stats")) @router.message(Command("stats"))
async def cmd_stats(message: Message): async def cmd_stats(message: Message):
user_id = message.from_user.id user_id = message.from_user.id
async with AsyncSessionLocal() as session:
try: try:
user_service = UserService(session)
user = await user_service.get_user_by_telegram_id(user_id) user = await user_service.get_user_by_telegram_id(user_id)
if user: if user:

View File

@ -19,9 +19,6 @@ async def handle_yookassa_webhook(request: Request):
try: try:
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.domain.services.user_service import UserService 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 from aiogram import Bot
if event_type == "payment.succeeded": if event_type == "payment.succeeded":
@ -29,16 +26,12 @@ async def handle_yookassa_webhook(request: Request):
user_id = payment.get("metadata", {}).get("user_id") user_id = payment.get("metadata", {}).get("user_id")
if user_id: if user_id:
async with AsyncSessionLocal() as session: user_service = UserService()
user_service = UserService(session)
success = await user_service.activate_premium(int(user_id)) success = await user_service.activate_premium(int(user_id))
if success: if success:
print(f"Premium activated for user {user_id}") print(f"Premium activated for user {user_id}")
result = await session.execute( user = await user_service.get_user_by_telegram_id(int(user_id))
select(UserModel).filter_by(telegram_id=str(user_id))
)
user = result.scalar_one_or_none()
if user and settings.TELEGRAM_BOT_TOKEN: if user and settings.TELEGRAM_BOT_TOKEN:
try: try:
@ -60,7 +53,7 @@ async def handle_yookassa_webhook(request: Request):
except Exception as e: except Exception as e:
print(f"Error sending notification: {e}") print(f"Error sending notification: {e}")
else: else:
print(f"User {user_id} not found") print(f"User {user_id} not found or failed to activate premium")
except ImportError as e: except ImportError as e:
print(f"Import error: {e}") print(f"Import error: {e}")

View File

@ -2,8 +2,6 @@ pydantic>=2.5.0
pydantic-settings>=2.1.0 pydantic-settings>=2.1.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
aiogram>=3.10.0 aiogram>=3.10.0
sqlalchemy>=2.0.0
aiosqlite>=0.19.0
httpx>=0.25.2 httpx>=0.25.2
yookassa>=2.4.0 yookassa>=2.4.0
aiohttp>=3.9.1 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())