forked from HSE_team/BetterCallPraskovia
temp
This commit is contained in:
parent
71e8d1079e
commit
493c385cb1
28
backend/alembic/versions/002_add_premium_fields.py
Normal file
28
backend/alembic/versions/002_add_premium_fields.py
Normal 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')
|
||||||
|
|
||||||
@ -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
23
backend/run.py
Normal 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
"""проверка, является ли пользователь администратором"""
|
"""проверка, является ли пользователь администратором"""
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}")
|
|
||||||
@ -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})>"
|
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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"))
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|||||||
@ -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
20
tg_bot/run.py
Normal 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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user