diff --git a/backend/src/application/use_cases/collection_use_cases.py b/backend/src/application/use_cases/collection_use_cases.py
index 6e1b9f8..2c0f3c7 100644
--- a/backend/src/application/use_cases/collection_use_cases.py
+++ b/backend/src/application/use_cases/collection_use_cases.py
@@ -138,4 +138,68 @@ class CollectionUseCases:
all_collections = {c.collection_id: c for c in owned + public + accessed_collections}
return list(all_collections.values())[skip:skip+limit]
+
+ async def list_collection_access(self, collection_id: UUID, user_id: UUID) -> list[CollectionAccess]:
+ """Получить список доступа к коллекции"""
+ collection = await self.get_collection(collection_id)
+
+ has_access = await self.check_access(collection_id, user_id)
+ if not has_access:
+ raise ForbiddenError("У вас нет доступа к этой коллекции")
+
+ return await self.access_repository.list_by_collection(collection_id)
+
+ async def grant_access_by_telegram_id(
+ self,
+ collection_id: UUID,
+ telegram_id: str,
+ owner_id: UUID
+ ) -> CollectionAccess:
+ """Предоставить доступ пользователю к коллекции по Telegram ID"""
+ collection = await self.get_collection(collection_id)
+
+ if collection.owner_id != owner_id:
+ raise ForbiddenError("Только владелец может предоставлять доступ")
+
+ user = await self.user_repository.get_by_telegram_id(telegram_id)
+ if not user:
+ from src.domain.entities.user import User, UserRole
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.info(f"Creating new user with telegram_id: {telegram_id}")
+ user = User(telegram_id=telegram_id, role=UserRole.USER)
+ try:
+ user = await self.user_repository.create(user)
+ logger.info(f"User created successfully: user_id={user.user_id}, telegram_id={user.telegram_id}")
+ except Exception as e:
+ logger.error(f"Error creating user: {e}")
+ raise
+
+ if user.user_id == owner_id:
+ raise ForbiddenError("Владелец уже имеет доступ к коллекции")
+
+ existing_access = await self.access_repository.get_by_user_and_collection(user.user_id, collection_id)
+ if existing_access:
+ return existing_access
+
+ access = CollectionAccess(user_id=user.user_id, collection_id=collection_id)
+ return await self.access_repository.create(access)
+
+ async def revoke_access_by_telegram_id(
+ self,
+ collection_id: UUID,
+ telegram_id: str,
+ owner_id: UUID
+ ) -> bool:
+ """Отозвать доступ пользователя к коллекции по Telegram ID"""
+ collection = await self.get_collection(collection_id)
+
+ if collection.owner_id != owner_id:
+ raise ForbiddenError("Только владелец может отзывать доступ")
+
+ user = await self.user_repository.get_by_telegram_id(telegram_id)
+ if not user:
+ raise NotFoundError(f"Пользователь с telegram_id {telegram_id} не найден")
+
+ return await self.access_repository.delete_by_user_and_collection(user.user_id, collection_id)
diff --git a/backend/src/application/use_cases/document_use_cases.py b/backend/src/application/use_cases/document_use_cases.py
index f73b531..3ed254b 100644
--- a/backend/src/application/use_cases/document_use_cases.py
+++ b/backend/src/application/use_cases/document_use_cases.py
@@ -6,6 +6,7 @@ from typing import BinaryIO, Optional
from src.domain.entities.document import Document
from src.domain.repositories.document_repository import IDocumentRepository
from src.domain.repositories.collection_repository import ICollectionRepository
+from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.application.services.document_parser_service import DocumentParserService
from src.shared.exceptions import NotFoundError, ForbiddenError
@@ -17,12 +18,25 @@ class DocumentUseCases:
self,
document_repository: IDocumentRepository,
collection_repository: ICollectionRepository,
+ access_repository: ICollectionAccessRepository,
parser_service: DocumentParserService
):
self.document_repository = document_repository
self.collection_repository = collection_repository
+ self.access_repository = access_repository
self.parser_service = parser_service
+ async def _check_collection_access(self, user_id: UUID, collection) -> bool:
+ """Проверить доступ пользователя к коллекции"""
+ if collection.owner_id == user_id:
+ return True
+
+ if collection.is_public:
+ return True
+
+ access = await self.access_repository.get_by_user_and_collection(user_id, collection.collection_id)
+ return access is not None
+
async def create_document(
self,
collection_id: UUID,
@@ -55,8 +69,9 @@ class DocumentUseCases:
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
- if collection.owner_id != user_id:
- raise ForbiddenError("Только владелец может добавлять документы")
+ has_access = await self._check_collection_access(user_id, collection)
+ if not has_access:
+ raise ForbiddenError("У вас нет доступа к этой коллекции")
title, content = await self.parser_service.parse_pdf(file, filename)
@@ -87,8 +102,11 @@ class DocumentUseCases:
document = await self.get_document(document_id)
collection = await self.collection_repository.get_by_id(document.collection_id)
- if not collection or collection.owner_id != user_id:
- raise ForbiddenError("Только владелец коллекции может изменять документы")
+ if not collection:
+ raise NotFoundError(f"Коллекция {document.collection_id} не найдена")
+ has_access = await self._check_collection_access(user_id, collection)
+ if not has_access:
+ raise ForbiddenError("У вас нет доступа к этой коллекции")
if title is not None:
document.title = title
diff --git a/backend/src/presentation/api/v1/collections.py b/backend/src/presentation/api/v1/collections.py
index 0346e3a..6789f2e 100644
--- a/backend/src/presentation/api/v1/collections.py
+++ b/backend/src/presentation/api/v1/collections.py
@@ -13,7 +13,9 @@ from src.presentation.schemas.collection_schemas import (
CollectionUpdate,
CollectionResponse,
CollectionAccessGrant,
- CollectionAccessResponse
+ CollectionAccessResponse,
+ CollectionAccessListResponse,
+ CollectionAccessUserInfo
)
from src.application.use_cases.collection_use_cases import CollectionUseCases
from src.domain.entities.user import User
@@ -44,10 +46,19 @@ async def create_collection(
@inject
async def get_collection(
collection_id: UUID,
+ request: Request,
+ user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[CollectionUseCases, FromDishka()]
):
"""Получить коллекцию по ID"""
+ current_user = await get_current_user(request, user_repo)
collection = await use_cases.get_collection(collection_id)
+
+ has_access = await use_cases.check_access(collection_id, current_user.user_id)
+ if not has_access:
+ from fastapi import HTTPException
+ raise HTTPException(status_code=403, detail="У вас нет доступа к этой коллекции")
+
return CollectionResponse.from_entity(collection)
@@ -138,3 +149,78 @@ async def revoke_access(
await use_cases.revoke_access(collection_id, user_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
+
+@router.get("/{collection_id}/access", response_model=List[CollectionAccessListResponse])
+@inject
+async def list_collection_access(
+ collection_id: UUID,
+ request: Request,
+ user_repo: Annotated[IUserRepository, FromDishka()],
+ use_cases: Annotated[CollectionUseCases, FromDishka()]
+):
+ """Получить список пользователей с доступом к коллекции"""
+ current_user = await get_current_user(request, user_repo)
+ accesses = await use_cases.list_collection_access(collection_id, current_user.user_id)
+ result = []
+ for access in accesses:
+ user = await user_repo.get_by_id(access.user_id)
+ if user:
+ user_info = CollectionAccessUserInfo(
+ user_id=user.user_id,
+ telegram_id=user.telegram_id,
+ role=user.role.value,
+ created_at=user.created_at
+ )
+ result.append(CollectionAccessListResponse(
+ access_id=access.access_id,
+ user=user_info,
+ collection_id=access.collection_id,
+ created_at=access.created_at
+ ))
+
+ return result
+
+
+@router.post("/{collection_id}/access/telegram/{telegram_id}", response_model=CollectionAccessResponse, status_code=status.HTTP_201_CREATED)
+@inject
+async def grant_access_by_telegram_id(
+ collection_id: UUID,
+ telegram_id: str,
+ request: Request,
+ user_repo: Annotated[IUserRepository, FromDishka()],
+ use_cases: Annotated[CollectionUseCases, FromDishka()]
+):
+ """Предоставить доступ пользователю к коллекции по Telegram ID"""
+ import logging
+ logger = logging.getLogger(__name__)
+
+ current_user = await get_current_user(request, user_repo)
+ logger.info(f"Granting access: collection_id={collection_id}, target_telegram_id={telegram_id}, owner_id={current_user.user_id}")
+
+ try:
+ access = await use_cases.grant_access_by_telegram_id(
+ collection_id=collection_id,
+ telegram_id=telegram_id,
+ owner_id=current_user.user_id
+ )
+ logger.info(f"Access granted successfully: access_id={access.access_id}")
+ return CollectionAccessResponse.from_entity(access)
+ except Exception as e:
+ logger.error(f"Error granting access: {e}", exc_info=True)
+ raise
+
+
+@router.delete("/{collection_id}/access/telegram/{telegram_id}", status_code=status.HTTP_204_NO_CONTENT)
+@inject
+async def revoke_access_by_telegram_id(
+ collection_id: UUID,
+ telegram_id: str,
+ request: Request,
+ user_repo: Annotated[IUserRepository, FromDishka()],
+ use_cases: Annotated[CollectionUseCases, FromDishka()]
+):
+ """Отозвать доступ пользователя к коллекции по Telegram ID"""
+ current_user = await get_current_user(request, user_repo)
+ await use_cases.revoke_access_by_telegram_id(collection_id, telegram_id, current_user.user_id)
+ return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
+
diff --git a/backend/src/presentation/api/v1/documents.py b/backend/src/presentation/api/v1/documents.py
index d342ee4..a013850 100644
--- a/backend/src/presentation/api/v1/documents.py
+++ b/backend/src/presentation/api/v1/documents.py
@@ -2,7 +2,7 @@
API роутеры для работы с документами
"""
from uuid import UUID
-from fastapi import APIRouter, status, UploadFile, File, Depends, Request
+from fastapi import APIRouter, status, UploadFile, File, Depends, Request, Query
from fastapi.responses import JSONResponse
from typing import List, Annotated
from dishka.integrations.fastapi import FromDishka, inject
@@ -14,6 +14,7 @@ from src.presentation.schemas.document_schemas import (
DocumentResponse
)
from src.application.use_cases.document_use_cases import DocumentUseCases
+from src.application.use_cases.collection_use_cases import CollectionUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/documents", tags=["documents"])
@@ -41,10 +42,10 @@ async def create_document(
@router.post("/upload", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
@inject
async def upload_document(
- collection_id: UUID,
- request: Request,
- user_repo: Annotated[IUserRepository, FromDishka()],
- use_cases: Annotated[DocumentUseCases, FromDishka()],
+ collection_id: UUID = Query(...),
+ request: Request = None,
+ user_repo: Annotated[IUserRepository, FromDishka()] = None,
+ use_cases: Annotated[DocumentUseCases, FromDishka()] = None,
file: UploadFile = File(...)
):
"""Загрузить и распарсить PDF документ или изображение"""
@@ -123,11 +124,22 @@ async def delete_document(
@inject
async def list_collection_documents(
collection_id: UUID,
+ request: Request,
+ user_repo: Annotated[IUserRepository, FromDishka()],
use_cases: Annotated[DocumentUseCases, FromDishka()],
+ collection_use_cases: Annotated[CollectionUseCases, FromDishka()],
skip: int = 0,
limit: int = 100
):
"""Получить документы коллекции"""
+ current_user = await get_current_user(request, user_repo)
+
+
+ has_access = await collection_use_cases.check_access(collection_id, current_user.user_id)
+ if not has_access:
+ from fastapi import HTTPException
+ raise HTTPException(status_code=403, detail="У вас нет доступа к этой коллекции")
+
documents = await use_cases.list_collection_documents(
collection_id=collection_id,
skip=skip,
diff --git a/backend/src/presentation/schemas/collection_schemas.py b/backend/src/presentation/schemas/collection_schemas.py
index 08d75ec..d5ae94a 100644
--- a/backend/src/presentation/schemas/collection_schemas.py
+++ b/backend/src/presentation/schemas/collection_schemas.py
@@ -75,3 +75,22 @@ class CollectionAccessResponse(BaseModel):
class Config:
from_attributes = True
+
+class CollectionAccessUserInfo(BaseModel):
+ """Информация о пользователе с доступом"""
+ user_id: UUID
+ telegram_id: str
+ role: str
+ created_at: datetime
+
+
+class CollectionAccessListResponse(BaseModel):
+ """Схема ответа со списком доступа"""
+ access_id: UUID
+ user: CollectionAccessUserInfo
+ collection_id: UUID
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
diff --git a/backend/src/shared/di_container.py b/backend/src/shared/di_container.py
index 271f301..e773923 100644
--- a/backend/src/shared/di_container.py
+++ b/backend/src/shared/di_container.py
@@ -151,9 +151,10 @@ class UseCaseProvider(Provider):
self,
document_repo: IDocumentRepository,
collection_repo: ICollectionRepository,
+ access_repo: ICollectionAccessRepository,
parser_service: DocumentParserService
) -> DocumentUseCases:
- return DocumentUseCases(document_repo, collection_repo, parser_service)
+ return DocumentUseCases(document_repo, collection_repo, access_repo, parser_service)
@provide(scope=Scope.REQUEST)
def get_conversation_use_cases(
diff --git a/tg_bot/infrastructure/telegram/bot.py b/tg_bot/infrastructure/telegram/bot.py
index 7bcbf64..d4d0db1 100644
--- a/tg_bot/infrastructure/telegram/bot.py
+++ b/tg_bot/infrastructure/telegram/bot.py
@@ -10,7 +10,8 @@ from tg_bot.infrastructure.telegram.handlers import (
stats_handler,
question_handler,
buy_handler,
- collection_handler
+ collection_handler,
+ document_handler
)
logger = logging.getLogger(__name__)
@@ -27,6 +28,7 @@ async def create_bot() -> tuple[Bot, Dispatcher]:
dp.include_router(stats_handler.router)
dp.include_router(buy_handler.router)
dp.include_router(collection_handler.router)
+ dp.include_router(document_handler.router)
dp.include_router(question_handler.router)
return bot, dp
diff --git a/tg_bot/infrastructure/telegram/handlers/collection_handler.py b/tg_bot/infrastructure/telegram/handlers/collection_handler.py
index eea2ab8..eba6f29 100644
--- a/tg_bot/infrastructure/telegram/handlers/collection_handler.py
+++ b/tg_bot/infrastructure/telegram/handlers/collection_handler.py
@@ -1,8 +1,13 @@
-from aiogram import Router
+from aiogram import Router, F
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
-from aiogram.filters import Command
+from aiogram.filters import Command, StateFilter
+from aiogram.fsm.context import FSMContext
import aiohttp
from tg_bot.config.settings import settings
+from tg_bot.infrastructure.telegram.states.collection_states import (
+ CollectionAccessStates,
+ CollectionEditStates
+)
router = Router()
@@ -24,16 +29,29 @@ async def get_user_collections(telegram_id: str):
async def get_collection_documents(collection_id: str, telegram_id: str):
try:
+ collection_id = str(collection_id).strip()
+ url = f"{settings.BACKEND_URL}/documents/collection/{collection_id}"
+ print(f"DEBUG get_collection_documents: URL={url}, collection_id={collection_id}, telegram_id={telegram_id}")
+
async with aiohttp.ClientSession() as session:
async with session.get(
- f"{settings.BACKEND_URL}/documents/collection/{collection_id}",
+ url,
headers={"X-Telegram-ID": telegram_id}
) as response:
if response.status == 200:
return await response.json()
- return []
+ elif response.status == 422:
+ error_text = await response.text()
+ print(f"Validation error getting documents: {response.status} - {error_text}, collection_id: {collection_id}, URL: {url}")
+ return []
+ else:
+ error_text = await response.text()
+ print(f"Error getting documents: {response.status} - {error_text}, collection_id: {collection_id}, URL: {url}")
+ return []
except Exception as e:
- print(f"Error getting documents: {e}")
+ print(f"Exception getting documents: {e}, collection_id: {collection_id}, type: {type(collection_id)}")
+ import traceback
+ traceback.print_exc()
return []
@@ -53,6 +71,91 @@ async def search_in_collection(collection_id: str, query: str, telegram_id: str)
return []
+async def get_collection_info(collection_id: str, telegram_id: str):
+ """Получить информацию о коллекции"""
+ try:
+ collection_id = str(collection_id).strip()
+ url = f"{settings.BACKEND_URL}/collections/{collection_id}"
+ print(f"DEBUG get_collection_info: URL={url}, collection_id={collection_id}, telegram_id={telegram_id}")
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ url,
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ if response.status == 200:
+ return await response.json()
+ elif response.status == 422:
+ error_text = await response.text()
+ print(f"Validation error getting collection info: {response.status} - {error_text}, collection_id: {collection_id}, URL: {url}")
+ return None
+ else:
+ error_text = await response.text()
+ print(f"Error getting collection info: {response.status} - {error_text}, collection_id: {collection_id}, URL: {url}")
+ return None
+ except Exception as e:
+ print(f"Exception getting collection info: {e}, collection_id: {collection_id}, type: {type(collection_id)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+
+async def get_collection_access_list(collection_id: str, telegram_id: str):
+ """Получить список пользователей с доступом к коллекции"""
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{settings.BACKEND_URL}/collections/{collection_id}/access",
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ if response.status == 200:
+ return await response.json()
+ return []
+ except Exception as e:
+ print(f"Error getting access list: {e}")
+ return []
+
+
+async def grant_collection_access(collection_id: str, telegram_id: str, owner_telegram_id: str):
+ """Предоставить доступ к коллекции"""
+ try:
+ url = f"{settings.BACKEND_URL}/collections/{collection_id}/access/telegram/{telegram_id}"
+ print(f"DEBUG grant_collection_access: URL={url}, target_telegram_id={telegram_id}, owner_telegram_id={owner_telegram_id}")
+
+ async with aiohttp.ClientSession() as session:
+ async with session.post(
+ url,
+ headers={"X-Telegram-ID": owner_telegram_id}
+ ) as response:
+ if response.status == 201:
+ result = await response.json()
+ print(f"DEBUG: Access granted successfully: {result}")
+ return result
+ else:
+ error_text = await response.text()
+ print(f"ERROR granting access: status={response.status}, error={error_text}, target_telegram_id={telegram_id}")
+ return None
+ except Exception as e:
+ print(f"Exception granting access: {e}, target_telegram_id={telegram_id}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+
+async def revoke_collection_access(collection_id: str, telegram_id: str, owner_telegram_id: str):
+ """Отозвать доступ к коллекции"""
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.delete(
+ f"{settings.BACKEND_URL}/collections/{collection_id}/access/telegram/{telegram_id}",
+ headers={"X-Telegram-ID": owner_telegram_id}
+ ) as response:
+ return response.status == 204
+ except Exception as e:
+ print(f"Error revoking access: {e}")
+ return False
+
+
@router.message(Command("mycollections"))
async def cmd_mycollections(message: Message):
telegram_id = str(message.from_user.id)
@@ -147,36 +250,495 @@ async def cmd_search(message: Message):
await message.answer(response, parse_mode="HTML")
-@router.callback_query(lambda c: c.data.startswith("collection:"))
-async def show_collection_documents(callback: CallbackQuery):
- collection_id = callback.data.split(":")[1]
+@router.callback_query(lambda c: c.data.startswith("collection:") and not c.data.startswith("collection:documents:") and not c.data.startswith("collection:edit:") and not c.data.startswith("collection:access:") and not c.data.startswith("collection:view_access:"))
+async def show_collection_menu(callback: CallbackQuery):
+ """Показать меню коллекции с опциями в зависимости от прав"""
+ parts = callback.data.split(":", 1)
+ if len(parts) < 2:
+ await callback.message.answer(
+ "Ошибка\n\nНеверный формат данных.",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+ return
+
+ collection_id = parts[1]
telegram_id = str(callback.from_user.id)
- await callback.answer("Загружаю документы...")
+ print(f"DEBUG: collection_id from callback (menu): {collection_id}, callback_data: {callback.data}")
- documents = await get_collection_documents(collection_id, telegram_id)
+ await callback.answer("Загружаю информацию...")
- if not documents:
+ collection_info = await get_collection_info(collection_id, telegram_id)
+ if not collection_info:
await callback.message.answer(
- f"Коллекция пуста\n\n"
- f"В этой коллекции пока нет документов.\n"
- f"Обратитесь к администратору для добавления документов.",
+ "Ошибка\n\nНе удалось загрузить информацию о коллекции.",
parse_mode="HTML"
)
return
+ owner_id = collection_info.get("owner_id")
+ collection_name = collection_info.get("name", "Коллекция")
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{settings.BACKEND_URL}/users/telegram/{telegram_id}"
+ ) as response:
+ if response.status == 200:
+ user_info = await response.json()
+ current_user_id = user_info.get("user_id")
+ is_owner = str(owner_id) == str(current_user_id)
+ else:
+ is_owner = False
+ except:
+ is_owner = False
+
+ keyboard_buttons = []
+
+ collection_id_str = str(collection_id)
+
+ if is_owner:
+ keyboard_buttons = [
+ [InlineKeyboardButton(text="Просмотр документов", callback_data=f"collection:documents:{collection_id_str}")],
+ [InlineKeyboardButton(text="Редактировать коллекцию", callback_data=f"collection:edit:{collection_id_str}")],
+ [InlineKeyboardButton(text="Управление доступом", callback_data=f"collection:access:{collection_id_str}")],
+ [InlineKeyboardButton(text="Загрузить документ", callback_data=f"document:upload:{collection_id_str}")],
+ [InlineKeyboardButton(text="Назад к коллекциям", callback_data="collections:list")]
+ ]
+ else:
+ keyboard_buttons = [
+ [InlineKeyboardButton(text="Просмотр документов", callback_data=f"collection:documents:{collection_id_str}")],
+ [InlineKeyboardButton(text="Просмотр доступа", callback_data=f"collection:view_access:{collection_id_str}")],
+ [InlineKeyboardButton(text="Назад к коллекциям", callback_data="collections:list")]
+ ]
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
+
+ role_text = "Владелец" if is_owner else "Доступ"
+ response = f"{collection_name}\n\n"
+ response += f"{role_text}\n\n"
+ response += f"ID: {collection_id}\n\n"
+ response += "Выберите действие:"
+
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
+
+
+@router.callback_query(lambda c: c.data.startswith("collection:documents:"))
+async def show_collection_documents(callback: CallbackQuery):
+ """Показать документы коллекции"""
+ try:
+ parts = callback.data.split(":", 2)
+ if len(parts) < 3:
+ raise ValueError("Неверный формат callback_data")
+
+ collection_id = parts[2]
+ telegram_id = str(callback.from_user.id)
+
+ print(f"DEBUG: collection_id from callback: {collection_id}, callback_data: {callback.data}")
+
+ await callback.answer("Загружаю документы...")
+
+ collection_info = await get_collection_info(collection_id, telegram_id)
+ if not collection_info:
+ await callback.message.answer(
+ "Ошибка\n\nНе удалось загрузить информацию о коллекции. Проверьте, что у вас есть доступ к этой коллекции.",
+ parse_mode="HTML"
+ )
+ return
+
+ documents = await get_collection_documents(collection_id, telegram_id)
+
+ if not documents:
+ await callback.message.answer(
+ f"Коллекция пуста\n\n"
+ f"В этой коллекции пока нет документов.",
+ parse_mode="HTML"
+ )
+ return
+ except IndexError:
+ await callback.message.answer(
+ "Ошибка\n\nНеверный формат данных.",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+ return
+ except Exception as e:
+ print(f"Error in show_collection_documents: {e}")
+ await callback.message.answer(
+ f"Ошибка\n\nПроизошла ошибка при загрузке документов: {str(e)}",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+ return
+
response = f"Документы в коллекции:\n\n"
+ keyboard_buttons = []
+
for i, doc in enumerate(documents[:10], 1):
+ doc_id = doc.get("document_id")
title = doc.get("title", "Без названия")
content_preview = doc.get("content", "")[:100]
response += f"{i}. {title}\n"
if content_preview:
response += f" {content_preview}...\n"
response += "\n"
+
+ keyboard_buttons.append([
+ InlineKeyboardButton(
+ text=f"{title[:30]}",
+ callback_data=f"document:view:{doc_id}"
+ )
+ ])
if len(documents) > 10:
response += f"\nПоказано 10 из {len(documents)} документов"
- await callback.message.answer(response, parse_mode="HTML")
+
+ collection_id_for_back = str(collection_info.get("collection_id", collection_id))
+ keyboard_buttons.append([
+ InlineKeyboardButton(text="Назад", callback_data=f"collection:{collection_id_for_back}")
+ ])
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
+
+
+@router.callback_query(lambda c: c.data.startswith("collection:access:"))
+async def show_access_management(callback: CallbackQuery):
+ """Показать меню управления доступом (только для владельца)"""
+ collection_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ await callback.answer("Загружаю список доступа...")
+
+ access_list = await get_collection_access_list(collection_id, telegram_id)
+
+ response = "Управление доступом\n\n"
+ response += "Пользователи с доступом:\n\n"
+
+ keyboard_buttons = []
+
+ if access_list:
+ for i, access in enumerate(access_list[:10], 1):
+ user = access.get("user", {})
+ user_telegram_id = user.get("telegram_id", "N/A")
+ role = user.get("role", "user")
+ response += f"{i}. {user_telegram_id} ({role})\n"
+
+ keyboard_buttons.append([
+ InlineKeyboardButton(
+ text=f" Удалить {user_telegram_id}",
+ callback_data=f"access:remove:{collection_id}:{user_telegram_id}"
+ )
+ ])
+ else:
+ response += "Нет пользователей с доступом\n\n"
+
+ keyboard_buttons.extend([
+ [InlineKeyboardButton(text="Добавить доступ", callback_data=f"access:add:{collection_id}")],
+ [InlineKeyboardButton(text="Назад", callback_data=f"collection:{collection_id}")]
+ ])
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
+
+
+@router.callback_query(lambda c: c.data.startswith("collection:view_access:"))
+async def show_access_list(callback: CallbackQuery):
+ """Показать список пользователей с доступом (read-only для пользователей с доступом)"""
+ collection_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ await callback.answer("Загружаю список доступа...")
+
+ access_list = await get_collection_access_list(collection_id, telegram_id)
+
+ response = "Пользователи с доступом\n\n"
+
+ if access_list:
+ for i, access in enumerate(access_list[:20], 1):
+ user = access.get("user", {})
+ user_telegram_id = user.get("telegram_id", "N/A")
+ role = user.get("role", "user")
+ response += f"{i}. {user_telegram_id} ({role})\n"
+ else:
+ response += "Нет пользователей с доступом\n"
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[
+ InlineKeyboardButton(text="Назад", callback_data=f"collection:{collection_id}")
+ ]])
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
+
+
+@router.callback_query(lambda c: c.data.startswith("access:add:"))
+async def add_access_prompt(callback: CallbackQuery, state: FSMContext):
+ """Запросить пересылку сообщения для добавления доступа"""
+ collection_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ await state.update_data(collection_id=collection_id)
+ await state.set_state(CollectionAccessStates.waiting_for_username)
+
+ await callback.message.answer(
+ "Добавить доступ\n\n"
+ "Перешлите любое сообщение от пользователя, которому нужно предоставить доступ.\n\n"
+ "Просто перешлите сообщение от нужного пользователя.",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+@router.message(StateFilter(CollectionAccessStates.waiting_for_username))
+async def process_add_access(message: Message, state: FSMContext):
+ """Обработать добавление доступа через пересылку сообщения"""
+ telegram_id = str(message.from_user.id)
+ data = await state.get_data()
+ collection_id = data.get("collection_id")
+
+ if not collection_id:
+ await message.answer("Ошибка: не указана коллекция")
+ await state.clear()
+ return
+
+ target_telegram_id = None
+
+ if message.forward_from:
+ target_telegram_id = str(message.forward_from.id)
+ elif message.forward_from_chat:
+ await message.answer(
+ "Ошибка\n\n"
+ "Пожалуйста, перешлите сообщение от пользователя, а не из группы или канала.",
+ parse_mode="HTML"
+ )
+ await state.clear()
+ return
+ elif message.forward_date:
+ await message.answer(
+ "Информация о пересылке скрыта\n\n"
+ "Пользователь скрыл информацию о пересылке в настройках приватности Telegram.\n\n"
+ "Попросите пользователя временно разрешить пересылку сообщений.",
+ parse_mode="HTML"
+ )
+ await state.clear()
+ return
+ else:
+ await message.answer(
+ "Ошибка\n\n"
+ "Пожалуйста, перешлите сообщение от пользователя, которому нужно предоставить доступ.\n\n"
+ "Просто перешлите любое сообщение от нужного пользователя.",
+ parse_mode="HTML"
+ )
+ await state.clear()
+ return
+
+ if not target_telegram_id:
+ await message.answer(
+ "Ошибка\n\n"
+ "Не удалось определить Telegram ID пользователя.",
+ parse_mode="HTML"
+ )
+ await state.clear()
+ return
+
+ print(f"DEBUG: Attempting to grant access: collection_id={collection_id}, target_telegram_id={target_telegram_id}, owner_telegram_id={telegram_id}")
+ result = await grant_collection_access(collection_id, target_telegram_id, telegram_id)
+
+ if result:
+ user_info = ""
+ if message.forward_from:
+ user_name = message.forward_from.first_name or ""
+ user_username = f"@{message.forward_from.username}" if message.forward_from.username else ""
+ user_info = f"{user_name} {user_username}".strip() or target_telegram_id
+ else:
+ user_info = target_telegram_id
+
+ await message.answer(
+ f"Доступ предоставлен\n\n"
+ f"Пользователю {target_telegram_id} предоставлен доступ к коллекции.\n\n"
+ f"Пользователь: {user_info}\n\n"
+ f"Примечание: Если пользователь еще не взаимодействовал с ботом, он был автоматически создан в системе.",
+ parse_mode="HTML"
+ )
+ else:
+ await message.answer(
+ "Ошибка\n\n"
+ "Не удалось предоставить доступ. Возможно:\n"
+ "• Доступ уже предоставлен\n"
+ "• Произошла ошибка на сервере\n"
+ "• Вы не являетесь владельцем коллекции\n\n"
+ "Проверьте логи сервера для получения подробной информации.",
+ parse_mode="HTML"
+ )
+
+ await state.clear()
+
+
+@router.callback_query(lambda c: c.data.startswith("access:remove:"))
+async def remove_access(callback: CallbackQuery):
+ """Удалить доступ пользователя"""
+ parts = callback.data.split(":")
+ collection_id = parts[2]
+ target_telegram_id = parts[3]
+ owner_telegram_id = str(callback.from_user.id)
+
+ await callback.answer("Удаляю доступ...")
+
+ result = await revoke_collection_access(collection_id, target_telegram_id, owner_telegram_id)
+
+ if result:
+ await callback.message.answer(
+ f"Доступ отозван\n\n"
+ f"Доступ пользователя {target_telegram_id} отозван.",
+ parse_mode="HTML"
+ )
+ else:
+ await callback.message.answer(
+ "Ошибка\n\n"
+ "Не удалось отозвать доступ.",
+ parse_mode="HTML"
+ )
+
+
+@router.callback_query(lambda c: c.data.startswith("collection:edit:"))
+async def edit_collection_prompt(callback: CallbackQuery, state: FSMContext):
+ """Запросить данные для редактирования коллекции"""
+ collection_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ collection_info = await get_collection_info(collection_id, telegram_id)
+ if not collection_info:
+ await callback.message.answer(
+ "Ошибка\n\nНе удалось загрузить информацию о коллекции.",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+ return
+
+ await state.update_data(collection_id=collection_id)
+ await state.set_state(CollectionEditStates.waiting_for_name)
+
+ await callback.message.answer(
+ "Редактирование коллекции\n\n"
+ "Отправьте новое название коллекции или /skip чтобы оставить текущее.\n\n"
+ f"Текущее название: {collection_info.get('name', 'Без названия')}",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+@router.message(StateFilter(CollectionEditStates.waiting_for_name))
+async def process_edit_collection_name(message: Message, state: FSMContext):
+ """Обработать новое название коллекции"""
+ telegram_id = str(message.from_user.id)
+ data = await state.get_data()
+ collection_id = data.get("collection_id")
+
+ if message.text and message.text.strip() == "/skip":
+ new_name = None
+ else:
+ new_name = message.text.strip() if message.text else None
+
+ await state.update_data(name=new_name)
+ await state.set_state(CollectionEditStates.waiting_for_description)
+
+ collection_info = await get_collection_info(collection_id, telegram_id)
+ current_description = collection_info.get("description", "") if collection_info else ""
+
+ await message.answer(
+ "Описание коллекции\n\n"
+ "Отправьте новое описание коллекции или /skip чтобы оставить текущее.\n\n"
+ f"Текущее описание: {current_description[:100] if current_description else 'Нет описания'}...",
+ parse_mode="HTML"
+ )
+
+
+@router.message(StateFilter(CollectionEditStates.waiting_for_description))
+async def process_edit_collection_description(message: Message, state: FSMContext):
+ """Обработать новое описание коллекции"""
+ telegram_id = str(message.from_user.id)
+ data = await state.get_data()
+ collection_id = data.get("collection_id")
+ name = data.get("name")
+
+ if message.text and message.text.strip() == "/skip":
+ new_description = None
+ else:
+ new_description = message.text.strip() if message.text else None
+
+ try:
+ update_data = {}
+ if name:
+ update_data["name"] = name
+ if new_description:
+ update_data["description"] = new_description
+
+ async with aiohttp.ClientSession() as session:
+ async with session.put(
+ f"{settings.BACKEND_URL}/collections/{collection_id}",
+ json=update_data,
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ if response.status == 200:
+ await message.answer(
+ "Коллекция обновлена\n\n"
+ "Изменения сохранены.",
+ parse_mode="HTML"
+ )
+ else:
+ error_text = await response.text()
+ await message.answer(
+ f"Ошибка\n\n"
+ f"Не удалось обновить коллекцию: {error_text}",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ await message.answer(
+ f"Ошибка\n\n"
+ f"Произошла ошибка: {str(e)}",
+ parse_mode="HTML"
+ )
+
+ await state.clear()
+
+
+@router.callback_query(lambda c: c.data == "collections:list")
+async def back_to_collections(callback: CallbackQuery):
+ """Вернуться к списку коллекций"""
+ telegram_id = str(callback.from_user.id)
+ collections = await get_user_collections(telegram_id)
+
+ if not collections:
+ await callback.message.answer(
+ "У вас пока нет коллекций\n\n"
+ "Обратитесь к администратору для создания коллекций и добавления документов.",
+ parse_mode="HTML"
+ )
+ return
+
+ response = "Ваши коллекции документов:\n\n"
+ keyboard_buttons = []
+
+ for i, collection in enumerate(collections[:10], 1):
+ name = collection.get("name", "Без названия")
+ description = collection.get("description", "")
+ collection_id = collection.get("collection_id")
+
+ response += f"{i}. {name}\n"
+ if description:
+ response += f" {description[:50]}...\n"
+ response += f" ID: {collection_id}\n\n"
+
+ keyboard_buttons.append([
+ InlineKeyboardButton(
+ text=f"{name}",
+ callback_data=f"collection:{collection_id}"
+ )
+ ])
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
+ response += "Нажмите на коллекцию, чтобы посмотреть документы"
+
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
diff --git a/tg_bot/infrastructure/telegram/handlers/document_handler.py b/tg_bot/infrastructure/telegram/handlers/document_handler.py
new file mode 100644
index 0000000..4f671a9
--- /dev/null
+++ b/tg_bot/infrastructure/telegram/handlers/document_handler.py
@@ -0,0 +1,381 @@
+"""
+Обработчики для работы с документами
+"""
+from aiogram import Router, F
+from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
+from aiogram.filters import StateFilter
+from aiogram.fsm.context import FSMContext
+import aiohttp
+from tg_bot.config.settings import settings
+from tg_bot.infrastructure.telegram.states.collection_states import (
+ DocumentEditStates,
+ DocumentUploadStates
+)
+
+router = Router()
+
+
+async def get_document_info(document_id: str, telegram_id: str):
+ """Получить информацию о документе"""
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{settings.BACKEND_URL}/documents/{document_id}",
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ if response.status == 200:
+ return await response.json()
+ return None
+ except Exception as e:
+ print(f"Error getting document info: {e}")
+ return None
+
+
+async def delete_document(document_id: str, telegram_id: str):
+ """Удалить документ"""
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.delete(
+ f"{settings.BACKEND_URL}/documents/{document_id}",
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ return response.status == 204
+ except Exception as e:
+ print(f"Error deleting document: {e}")
+ return False
+
+
+async def update_document(document_id: str, telegram_id: str, title: str = None, content: str = None):
+ """Обновить документ"""
+ try:
+ update_data = {}
+ if title:
+ update_data["title"] = title
+ if content:
+ update_data["content"] = content
+
+ async with aiohttp.ClientSession() as session:
+ async with session.put(
+ f"{settings.BACKEND_URL}/documents/{document_id}",
+ json=update_data,
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ if response.status == 200:
+ return await response.json()
+ return None
+ except Exception as e:
+ print(f"Error updating document: {e}")
+ return None
+
+
+async def upload_document_to_collection(collection_id: str, file_data: bytes, filename: str, telegram_id: str):
+ """Загрузить документ в коллекцию"""
+ try:
+ async with aiohttp.ClientSession() as session:
+ form_data = aiohttp.FormData()
+ form_data.add_field('file', file_data, filename=filename, content_type='application/octet-stream')
+
+ async with session.post(
+ f"{settings.BACKEND_URL}/documents/upload?collection_id={collection_id}",
+ data=form_data,
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response:
+ if response.status == 201:
+ return await response.json()
+ else:
+ error_text = await response.text()
+ print(f"Upload error: {response.status} - {error_text}")
+ return None
+ except Exception as e:
+ print(f"Error uploading document: {e}")
+ return None
+
+
+@router.callback_query(lambda c: c.data.startswith("document:view:"))
+async def view_document(callback: CallbackQuery):
+ """Просмотр документа"""
+ document_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ await callback.answer("Загружаю документ...")
+
+ document = await get_document_info(document_id, telegram_id)
+ if not document:
+ await callback.message.answer(
+ "Ошибка\n\nНе удалось загрузить документ.",
+ parse_mode="HTML"
+ )
+ return
+
+ title = document.get("title", "Без названия")
+ content = document.get("content", "")
+ collection_id = document.get("collection_id")
+
+ content_preview = content[:2000] if len(content) > 2000 else content
+ has_more = len(content) > 2000
+
+ response = f"{title}\n\n"
+ response += f"{content_preview}"
+ if has_more:
+ response += "\n\n..."
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{settings.BACKEND_URL}/collections/{collection_id}",
+ headers={"X-Telegram-ID": telegram_id}
+ ) as response_collection:
+ if response_collection.status == 200:
+ collection_info = await response_collection.json()
+ owner_id = collection_info.get("owner_id")
+
+ async with session.get(
+ f"{settings.BACKEND_URL}/users/telegram/{telegram_id}"
+ ) as response_user:
+ if response_user.status == 200:
+ user_info = await response_user.json()
+ current_user_id = user_info.get("user_id")
+ is_owner = str(owner_id) == str(current_user_id)
+
+ keyboard_buttons = []
+ if is_owner:
+ keyboard_buttons = [
+ [InlineKeyboardButton(text="Редактировать", callback_data=f"document:edit:{document_id}")],
+ [InlineKeyboardButton(text="Удалить", callback_data=f"document:delete:{document_id}")],
+ [InlineKeyboardButton(text="Назад", callback_data=f"collection:documents:{collection_id}")]
+ ]
+ else:
+ keyboard_buttons = [
+ [InlineKeyboardButton(text="Редактировать", callback_data=f"document:edit:{document_id}")],
+ [InlineKeyboardButton(text="Назад", callback_data=f"collection:documents:{collection_id}")]
+ ]
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
+ return
+ except:
+ pass
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[
+ InlineKeyboardButton(text="Назад", callback_data=f"collection:documents:{collection_id}")
+ ]])
+ await callback.message.answer(response, parse_mode="HTML", reply_markup=keyboard)
+
+
+@router.callback_query(lambda c: c.data.startswith("document:edit:"))
+async def edit_document_prompt(callback: CallbackQuery, state: FSMContext):
+ """Запросить данные для редактирования документа"""
+ document_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ document = await get_document_info(document_id, telegram_id)
+ if not document:
+ await callback.message.answer(
+ "Ошибка\n\nНе удалось загрузить документ.",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+ return
+
+ await state.update_data(document_id=document_id)
+ await state.set_state(DocumentEditStates.waiting_for_title)
+
+ await callback.message.answer(
+ "Редактирование документа\n\n"
+ "Отправьте новое название документа или /skip чтобы оставить текущее.\n\n"
+ f"Текущее название: {document.get('title', 'Без названия')}",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+@router.message(StateFilter(DocumentEditStates.waiting_for_title))
+async def process_edit_title(message: Message, state: FSMContext):
+ """Обработать новое название документа"""
+ telegram_id = str(message.from_user.id)
+ data = await state.get_data()
+ document_id = data.get("document_id")
+
+ if message.text and message.text.strip() == "/skip":
+ new_title = None
+ else:
+ new_title = message.text.strip() if message.text else None
+
+ await state.update_data(title=new_title)
+ await state.set_state(DocumentEditStates.waiting_for_content)
+
+ await message.answer(
+ "Содержимое документа\n\n"
+ "Отправьте новое содержимое документа или /skip чтобы оставить текущее.",
+ parse_mode="HTML"
+ )
+
+
+@router.message(StateFilter(DocumentEditStates.waiting_for_content))
+async def process_edit_content(message: Message, state: FSMContext):
+ """Обработать новое содержимое документа"""
+ telegram_id = str(message.from_user.id)
+ data = await state.get_data()
+ document_id = data.get("document_id")
+ title = data.get("title")
+
+ if message.text and message.text.strip() == "/skip":
+ new_content = None
+ else:
+ new_content = message.text.strip() if message.text else None
+
+ result = await update_document(document_id, telegram_id, title=title, content=new_content)
+
+ if result:
+ await message.answer(
+ "Документ обновлен\n\n"
+ "Изменения сохранены.",
+ parse_mode="HTML"
+ )
+ else:
+ await message.answer(
+ "Ошибка\n\n"
+ "Не удалось обновить документ.",
+ parse_mode="HTML"
+ )
+
+ await state.clear()
+
+
+@router.callback_query(lambda c: c.data.startswith("document:delete:"))
+async def delete_document_confirm(callback: CallbackQuery):
+ """Подтверждение удаления документа"""
+ document_id = callback.data.split(":")[2]
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="Да, удалить", callback_data=f"document:delete_confirm:{document_id}")],
+ [InlineKeyboardButton(text="Отмена", callback_data=f"document:view:{document_id}")]
+ ])
+
+ await callback.message.answer(
+ "Подтверждение удаления\n\n"
+ "Вы уверены, что хотите удалить этот документ?",
+ parse_mode="HTML",
+ reply_markup=keyboard
+ )
+ await callback.answer()
+
+
+@router.callback_query(lambda c: c.data.startswith("document:delete_confirm:"))
+async def delete_document_execute(callback: CallbackQuery):
+ """Выполнить удаление документа"""
+ document_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ await callback.answer("Удаляю документ...")
+
+ # Получаем информацию о документе для возврата к коллекции
+ document = await get_document_info(document_id, telegram_id)
+ collection_id = document.get("collection_id") if document else None
+
+ result = await delete_document(document_id, telegram_id)
+
+ if result:
+ await callback.message.answer(
+ "Документ удален",
+ parse_mode="HTML"
+ )
+ else:
+ await callback.message.answer(
+ "Ошибка\n\n"
+ "Не удалось удалить документ.",
+ parse_mode="HTML"
+ )
+
+
+@router.callback_query(lambda c: c.data.startswith("document:upload:"))
+async def upload_document_prompt(callback: CallbackQuery, state: FSMContext):
+ """Запросить файл для загрузки"""
+ collection_id = callback.data.split(":")[2]
+ telegram_id = str(callback.from_user.id)
+
+ await state.update_data(collection_id=collection_id)
+ await state.set_state(DocumentUploadStates.waiting_for_file)
+
+ await callback.message.answer(
+ "Загрузка документа\n\n"
+ "Отправьте файл (PDF, PNG, JPG, JPEG, TIFF, BMP).\n\n"
+ "Поддерживаемые форматы:\n"
+ "• PDF\n"
+ "• Изображения: PNG, JPG, JPEG, TIFF, BMP",
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+@router.message(StateFilter(DocumentUploadStates.waiting_for_file), F.document | F.photo)
+async def process_upload_document(message: Message, state: FSMContext):
+ """Обработать загрузку документа"""
+ telegram_id = str(message.from_user.id)
+ data = await state.get_data()
+ collection_id = data.get("collection_id")
+
+ if not collection_id:
+ await message.answer("Ошибка: не указана коллекция")
+ await state.clear()
+ return
+
+ file_id = None
+ filename = None
+
+ if message.document:
+ file_id = message.document.file_id
+ filename = message.document.file_name or "document.pdf"
+
+ supported_extensions = ['.pdf', '.png', '.jpg', '.jpeg', '.tiff', '.bmp']
+ file_ext = filename.lower().rsplit('.', 1)[-1] if '.' in filename else ''
+ if f'.{file_ext}' not in supported_extensions:
+ await message.answer(
+ "Ошибка\n\n"
+ f"Неподдерживаемый формат файла: {file_ext}\n\n"
+ "Поддерживаются: PDF, PNG, JPG, JPEG, TIFF, BMP",
+ parse_mode="HTML"
+ )
+ await state.clear()
+ return
+ elif message.photo:
+ file_id = message.photo[-1].file_id
+ filename = "photo.jpg"
+ else:
+ await message.answer(
+ "Ошибка\n\n"
+ "Пожалуйста, отправьте файл (PDF или изображение).",
+ parse_mode="HTML"
+ )
+ await state.clear()
+ return
+
+ try:
+ file = await message.bot.get_file(file_id)
+ file_data = await message.bot.download_file(file.file_path)
+ file_bytes = file_data.read()
+
+ result = await upload_document_to_collection(collection_id, file_bytes, filename, telegram_id)
+
+ if result:
+ await message.answer(
+ f"Документ загружен\n\n"
+ f"Название: {result.get('title', filename)}",
+ parse_mode="HTML"
+ )
+ else:
+ await message.answer(
+ "Ошибка\n\n"
+ "Не удалось загрузить документ.",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ print(f"Error uploading document: {e}")
+ await message.answer(
+ "Ошибка\n\n"
+ f"Произошла ошибка при загрузке: {str(e)}",
+ parse_mode="HTML"
+ )
+
+ await state.clear()
+
diff --git a/tg_bot/infrastructure/telegram/states/collection_states.py b/tg_bot/infrastructure/telegram/states/collection_states.py
new file mode 100644
index 0000000..f7f0939
--- /dev/null
+++ b/tg_bot/infrastructure/telegram/states/collection_states.py
@@ -0,0 +1,27 @@
+"""
+FSM состояния для работы с коллекциями
+"""
+from aiogram.fsm.state import State, StatesGroup
+
+
+class CollectionAccessStates(StatesGroup):
+ """Состояния для управления доступом к коллекции"""
+ waiting_for_username = State()
+
+
+class CollectionEditStates(StatesGroup):
+ """Состояния для редактирования коллекции"""
+ waiting_for_name = State()
+ waiting_for_description = State()
+
+
+class DocumentEditStates(StatesGroup):
+ """Состояния для редактирования документа"""
+ waiting_for_title = State()
+ waiting_for_content = State()
+
+
+class DocumentUploadStates(StatesGroup):
+ """Состояния для загрузки документа"""
+ waiting_for_file = State()
+