diff --git a/.env.example b/.env.example
index fb1983e..97849da 100644
--- a/.env.example
+++ b/.env.example
@@ -13,7 +13,11 @@ SSL_ENABLED=True
SSL_KEY_PATH=""
SSL_CERT_PATH=""
-# Параметры для Nextcloud
+# Параметры для Nextcloud Deck
NC_URL="https://yourdomain"
NC_LOGIN=""
-NC_PASSWORD=""
\ No newline at end of file
+NC_PASSWORD=""
+
+# ID доски и стека для списка покупок
+DECK_BOARD_ID=1
+DECK_STACK_ID=1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c032bde..8082d76 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,9 +20,3 @@ repos:
# hooks:
# - id: isort
# # сортирует импорты в Python
-
- - repo: https://github.com/pycqa/flake8
- rev: 6.0.0
- hooks:
- - id: flake8
- args: [--max-line-length=88]
diff --git a/.scripts/project_tree.py b/.scripts/project_tree.py
index c01fb98..6c07a16 100644
--- a/.scripts/project_tree.py
+++ b/.scripts/project_tree.py
@@ -37,7 +37,10 @@ def print_tree(directory: Path, ignore_dirs: list[str] = None, prefix: str = "")
yield from print_tree(path, ignore_dirs, prefix=prefix + extension)
-def main():
+def main() -> None:
+ """
+ Основная функция запуска отрисовки дерева файлов
+ """
parser = argparse.ArgumentParser(
description="Отображение древовидной структуры проекта"
)
diff --git a/config/settings.py b/config/settings.py
index 81df07d..39eba72 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -1,7 +1,6 @@
"""Файл настроек приложения"""
from typing import Optional
-
from pydantic import Field
from pydantic_settings import BaseSettings
@@ -9,32 +8,36 @@
class Settings(BaseSettings):
"""Конфигурация приложения"""
- VERSION: str = Field("0.0.2.2")
+ VERSION: str = "0.0.3.0"
# Токен бота Telegram
BOT_TOKEN: Optional[str] = Field(None, description="Токен бота от @BotFather")
# Настройки сервера
- HOST: str = Field("0.0.0.0", description="Хост для запуска сервера")
- PORT: int = Field(8443, description="Порт для запуска сервера")
+ HOST: str = "0.0.0.0"
+ PORT: int = 8443
# SSL
- SSL_ENABLED: bool = Field(False, description="Использование локального SSL")
- SSL_KEY_PATH: Optional[str] = Field(None, description="Ключ сертификата")
- SSL_CERT_PATH: Optional[str] = Field(None, description="Сертификат")
+ SSL_ENABLED: bool = False
+ SSL_KEY_PATH: Optional[str] = None
+ SSL_CERT_PATH: Optional[str] = None
- # Настройки вебхука (опционально для продакшена)
- WEBHOOK_HOST: Optional[str] = Field(None, description="Публичный URL для вебхука")
+ # Настройки вебхука
+ WEBHOOK_HOST: Optional[str] = None
- # Настройки NextCloud
+ # Настройки NextCloud Deck
NC_URL: Optional[str] = Field(None, description="Адрес Nextcloud")
NC_LOGIN: Optional[str] = Field(None, description="Логин Nextcloud")
NC_PASSWORD: Optional[str] = Field(None, description="Пароль Nextcloud")
+ # ID доски и стека для списка покупок
+ DECK_BOARD_ID: int = 1
+ DECK_STACK_ID: int = 1
+
# Настройки логирования
- LOG_LEVEL: str = Field("INFO", description="Уровень логирования")
+ LOG_LEVEL: str = "INFO"
- class Config: # pylint: disable=R0903
+ class Config:
"""Конфигурация Pydantic"""
env_file = ".env"
diff --git a/handlers/bot_routes/route_deck_boards.py b/handlers/bot_routes/route_deck_boards.py
deleted file mode 100644
index 5b49565..0000000
--- a/handlers/bot_routes/route_deck_boards.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""Обработчик для работы с досками"""
-
-from aiogram import types
-from aiogram.filters.callback_data import CallbackData
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-
-from handlers.handler_logging import logger
-from handlers.handler_nc_deck import get_boards
-
-KBD_COLUMNS = 1
-
-
-class NavigationCallback(CallbackData, prefix="nav"):
- """Универсальный callback для навигации по доскам и стекам"""
-
- screen: str # 'boards_list', 'stacks_list', 'cards_list'
- target_id: int = 0 # ID доски или стека
-
-
-async def show_boards_command(message: types.Message) -> None:
- """
- Показывает список досок в виде инлайн-кнопок
-
- :param message: Сообщение от пользователя
- :type message: types.Message
-
- Пользователь -> /decks ->
- Бот показывает кнопки с досками
- """
- logger.info(f"Пользователь {message.from_user.id} запросил список досок")
- decks = await get_boards()
-
- if not decks:
- await message.answer("❌ Доски не найдены")
- return
-
- keyboard_builder = InlineKeyboardBuilder()
-
- for deck in decks:
- keyboard_builder.button(
- text=f"🗂️ {deck.title}",
- callback_data=NavigationCallback(screen="stacks_list", target_id=deck.id),
- )
-
- keyboard_builder.adjust(KBD_COLUMNS)
-
- await message.answer(
- "🗃️ Выберите доску:",
- reply_markup=keyboard_builder.as_markup(),
- parse_mode="HTML",
- )
diff --git a/handlers/bot_routes/route_deck_cards.py b/handlers/bot_routes/route_deck_cards.py
deleted file mode 100644
index 15bb253..0000000
--- a/handlers/bot_routes/route_deck_cards.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Обработчик для работы с карточками"""
-
-from aiogram import types
-
-from handlers.bot_routes.route_deck_boards import NavigationCallback
-from handlers.handler_logging import logger
-
-
-async def show_cards_in_stack(
- callback: types.CallbackQuery, callback_data: NavigationCallback
-) -> None:
- """
- Показывает карточки в стеке (заглушка)
-
- :param callback: Callback от нажатой кнопки в предыдущем меню
- :type callback: types.CallbackQuery
- :param callback_data: Данные callback'а
- :type callback_data: NavigationCallback
-
- Пользователь нажимает на кнопку стека ->
- Бот показывает карточки (пока заглушка)
- """
- stack_id = callback_data.target_id
- logger.info(f"Пользователь {callback.from_user.id} выбрал стек {stack_id}")
-
- # NOTE: Здесь будет логика получения и отображения карточек
- await callback.message.edit_text(
- f"📋 Карточки стека {stack_id}\n\n"
- f"Функционал отображения карточек в разработке...",
- parse_mode="HTML",
- )
- await callback.answer()
diff --git a/handlers/bot_routes/route_deck_stacks.py b/handlers/bot_routes/route_deck_stacks.py
deleted file mode 100644
index 14d1c0f..0000000
--- a/handlers/bot_routes/route_deck_stacks.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""Обработчик для работы со стеками"""
-
-from aiogram import types
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-
-from handlers.bot_routes.route_deck_boards import (
- NavigationCallback,
- show_boards_command,
-)
-from handlers.handler_logging import logger
-from handlers.handler_nc_deck import get_stacks
-
-KBD_COLUMNS = 1
-
-
-async def show_stacks_for_board(
- callback: types.CallbackQuery, callback_data: NavigationCallback
-) -> None:
- """
- Показывает стеки выбранной доски
-
- :param callback: Callback от нажатой кнопки в предыдущем меню
- :type callback: types.CallbackQuery
- :param callback_data: Данные callback'а
- :type callback_data: NavigationCallback
-
- Пользователь нажимает на кнопку доски ->
- Бот показывает кнопки со стеками этой доски
- """
- board_id = callback_data.target_id
- logger.info(f"Пользователь {callback.from_user.id} выбрал доску {board_id}")
- stacks = await get_stacks(board_id)
-
- if not stacks:
- await callback.message.edit_text("❌ Стеки не найдены для этой доски")
- await callback.answer()
- return
-
- keyboard_builder = InlineKeyboardBuilder()
-
- for stack in stacks:
- keyboard_builder.button(
- text=f"📑 {stack.title}",
- callback_data=NavigationCallback(screen="cards_list", target_id=stack.id),
- )
-
- keyboard_builder.button(
- text="⬅️ Назад к доскам", callback_data=NavigationCallback(screen="boards_list")
- )
-
- keyboard_builder.adjust(KBD_COLUMNS)
-
- await callback.message.edit_text(
- "📚 Стеки выбранной доски:",
- reply_markup=keyboard_builder.as_markup(),
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-async def back_to_boards_list(callback: types.CallbackQuery) -> None:
- """
- Возвращает к списку досок
-
- :param callback: Callback от нажатия кнопки "Назад"
- :type callback: types.CallbackQuery
-
- Пользователь нажимает "Назад" ->
- Бот снова показывает список досок
- """
-
- await show_boards_command(callback.message)
- await callback.answer()
diff --git a/handlers/bot_routes/route_edit_items.py b/handlers/bot_routes/route_edit_items.py
new file mode 100644
index 0000000..64f3e3f
--- /dev/null
+++ b/handlers/bot_routes/route_edit_items.py
@@ -0,0 +1,151 @@
+"""Логика редактирования элементов карточки"""
+
+from aiogram import Router, types, F
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from handlers.handler_nc_deck import update_card_description
+from handlers.bot_routes.states import CardCallback
+from handlers.bot_routes.route_view_card import (
+ get_cached_card,
+ _show_card_view,
+)
+from handlers.bot_routes.route_list_cards import list_handler
+
+# from handlers.handler_logging import logger
+
+edit_router = Router()
+
+
+@edit_router.callback_query(CardCallback.filter(F.action == "toggle"))
+async def toggle_item_handler(
+ callback: types.CallbackQuery, callback_data: CardCallback
+) -> None:
+ """
+ Переключить статус элемента списка
+
+ :param callback: Callback запрос
+ :type callback: types.CallbackQuery
+ :param callback_data: Данные callback
+ :type callback_data: CardCallback
+ """
+ # Пытаемся получить карточку из кэша
+ target_card = await get_cached_card(callback_data.card_id)
+
+ if not target_card or not target_card.description:
+ await callback.answer("❌ Карточка не найдена")
+ return
+
+ lines = target_card.description.split("\n")
+ if 0 <= callback_data.item_index < len(lines):
+ line = lines[callback_data.item_index]
+
+ if "[ ]" in line:
+ lines[callback_data.item_index] = line.replace("[ ]", "[x]")
+ elif "[x]" in line:
+ lines[callback_data.item_index] = line.replace("[x]", "[ ]")
+
+ new_description = "\n".join(lines)
+ success = await update_card_description(target_card.id, new_description)
+
+ if success:
+ await callback.answer("✅ Статус обновлен")
+ # Обновляем кэш и показываем обновленную карточку
+ target_card.description = new_description
+ await _show_card_view(callback.message, target_card)
+ else:
+ await callback.answer("❌ Ошибка обновления")
+ else:
+ await callback.answer("❌ Элемент не найден")
+
+
+@edit_router.callback_query(CardCallback.filter(F.action == "remove"))
+async def remove_items_handler(
+ callback: types.CallbackQuery, callback_data: CardCallback
+) -> None:
+ """
+ Начать удаление элементов
+
+ :param callback: Callback запрос
+ :type callback: types.CallbackQuery
+ :param callback_data: Данные callback
+ :type callback_data: CardCallback
+ """
+ # Пытаемся получить карточку из кэша
+ target_card = await get_cached_card(callback_data.card_id)
+
+ if not target_card:
+ await callback.answer("❌ Карточка не найдена")
+ return
+
+ items = target_card.get_list_items()
+ if not items:
+ await callback.answer("📝 Список пуст")
+ await list_handler(callback.message)
+ return
+
+ keyboard_builder = InlineKeyboardBuilder()
+ for index, item in enumerate(items):
+ keyboard_builder.button(
+ text=f"🗑️ {item}",
+ callback_data=CardCallback(
+ action="delete", card_id=target_card.id, item_index=index
+ ),
+ )
+
+ keyboard_builder.button(
+ text="⬅️ Назад",
+ callback_data=CardCallback(action="view", card_id=target_card.id),
+ )
+ keyboard_builder.adjust(1)
+
+ await callback.message.edit_text(
+ "🗑️ Выберите элементы для удаления:",
+ reply_markup=keyboard_builder.as_markup(),
+ parse_mode="HTML",
+ )
+ await callback.answer()
+
+
+@edit_router.callback_query(CardCallback.filter(F.action == "delete"))
+async def delete_item_handler(
+ callback: types.CallbackQuery, callback_data: CardCallback
+) -> None:
+ """
+ Удалить элемент из списка
+
+ :param callback: Callback запрос
+ :type callback: types.CallbackQuery
+ :param callback_data: Данные callback
+ :type callback_data: CardCallback
+ """
+ # Пытаемся получить карточку из кэша
+ target_card = await get_cached_card(callback_data.card_id)
+
+ if not target_card or not target_card.description:
+ await callback.answer("❌ Карточка не найдена")
+ return
+
+ lines = target_card.description.split("\n")
+ if 0 <= callback_data.item_index < len(lines):
+ lines.pop(callback_data.item_index)
+
+ new_description = "\n".join(lines) if lines else ""
+ success = await update_card_description(target_card.id, new_description)
+
+ if success:
+ await callback.answer("✅ Элемент удален")
+
+ # Обновляем кэш
+ target_card.description = new_description
+ items = target_card.get_list_items()
+
+ if items:
+ await remove_items_handler(callback, callback_data)
+ else:
+ await callback.answer("📝 Список пуст")
+ callback_data.action = "view"
+ await _show_card_view(callback.message, target_card)
+ else:
+ await callback.answer("❌ Ошибка удаления")
+ else:
+ await callback.answer("❌ Элемент не найден")
diff --git a/handlers/bot_routes/route_list_cards.py b/handlers/bot_routes/route_list_cards.py
new file mode 100644
index 0000000..6451a45
--- /dev/null
+++ b/handlers/bot_routes/route_list_cards.py
@@ -0,0 +1,91 @@
+"""Логика отображения списка карточек"""
+
+from aiogram import Router, types, F
+from aiogram.filters import Command, CommandStart
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from handlers.handler_logging import logger
+from handlers.handler_nc_deck import get_shopping_cards
+from handlers.bot_routes.states import CardCallback
+
+list_router = Router()
+
+MAX_TITLE_LENGTH = 30
+
+
+@list_router.message(CommandStart())
+@list_router.message(Command("help"))
+async def start_help_handler(message: types.Message) -> None:
+ """
+ Обработчик команд /start и /help
+
+ :param message: Входящее сообщение
+ :type message: types.Message
+ """
+ help_text = """🛒 Бот для списка покупок
+
+Команды:
+/list - Показать список покупок
+/help - Эта справка
+
+💡 Просто введите элементы покупок сообщением, чтобы добавить их в список!
+"""
+ await message.answer(help_text)
+
+
+@list_router.message(Command("list"))
+async def list_handler(message: types.Message) -> None:
+ """
+ Показать список покупок в виде инлайн кнопок
+
+ :param message: Входящее сообщение
+ :type message: types.Message
+ """
+ logger.info(
+ f"Пользователь {message.from_user.id} "
+ f"({message.from_user.username}) запросил список покупок"
+ )
+ cards = await get_shopping_cards()
+
+ if cards is None:
+ await message.answer("❌ Ошибка загрузки списка покупок")
+ return
+
+ if not cards:
+ await message.answer("📝 Список покупок пуст")
+ return
+
+ keyboard_builder = InlineKeyboardBuilder()
+ for card in cards:
+ keyboard_builder.button(
+ text=f"📋 {card.short_title}",
+ callback_data=CardCallback(action="view", card_id=card.id),
+ )
+
+ keyboard_builder.adjust(1)
+ await message.answer(
+ f"🛒 Список покупок ({len(cards)}):", # noqa: E231
+ reply_markup=keyboard_builder.as_markup(),
+ )
+
+
+@list_router.callback_query(CardCallback.filter(F.action == "back"))
+async def back_to_list_handler(
+ callback: types.CallbackQuery, callback_data: CardCallback
+) -> None:
+ """
+ Вернуться к списку карточек
+
+ :param callback: Callback запрос
+ :type callback: types.CallbackQuery
+ :param callback_data: Данные callback
+ :type callback_data: CardCallback
+ """
+ try:
+ del callback_data
+ await callback.answer()
+ await list_handler(callback.message)
+
+ except Exception as error:
+ logger.error(f"Ошибка в back_to_list_handler: {error}")
+ await callback.answer("❌ Ошибка возврата к списку")
diff --git a/handlers/bot_routes/route_view_card.py b/handlers/bot_routes/route_view_card.py
new file mode 100644
index 0000000..e0110c1
--- /dev/null
+++ b/handlers/bot_routes/route_view_card.py
@@ -0,0 +1,278 @@
+"""Логика просмотра и управления карточкой"""
+
+from aiogram import Router, types, F
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from handlers.handler_nc_deck import get_shopping_cards, update_card_description
+from handlers.bot_routes.states import CardCallback
+from handlers.bot_routes.route_list_cards import list_handler
+from handlers.handler_logging import logger
+
+view_router = Router()
+
+MAX_ITEM_PREVIEW_LENGTH = 50
+ELLIPSIS_LENGTH = 3
+
+# Простой кэш карточек на время сессии
+_card_cache = {}
+
+# Кэш для отслеживания активной карточки по пользователю
+_user_active_card = {}
+
+
+@view_router.callback_query(CardCallback.filter(F.action == "view"))
+async def view_card_handler(
+ callback: types.CallbackQuery, callback_data: CardCallback
+) -> None:
+ """
+ Показать детали карточки с элементами списка
+
+ :param callback: Callback запрос
+ :type callback: types.CallbackQuery
+ :param callback_data: Данные callback
+ :type callback_data: CardCallback
+ """
+ cards = await get_shopping_cards()
+ if not cards:
+ await callback.message.edit_text("❌ Карточка не найдена")
+ return
+
+ target_card = next(
+ (card for card in cards if card.id == callback_data.card_id), None
+ )
+ if not target_card:
+ await callback.message.edit_text("❌ Карточка не найдена")
+ return
+
+ # Кэшируем карточку
+ _card_cache[callback_data.card_id] = target_card
+
+ # Сохраняем активную карточку для пользователя
+ user_id = callback.from_user.id
+ _user_active_card[user_id] = callback_data.card_id
+
+ await _show_card_view(callback.message, target_card)
+ await callback.answer()
+
+
+async def _show_card_view(message: types.Message, card) -> None:
+ """
+ Показать представление карточки
+
+ :param message: Сообщение для редактирования/отправки
+ :type message: types.Message
+ :param card: Карточка для отображения
+ """
+ items = card.get_list_items()
+ keyboard_builder = InlineKeyboardBuilder()
+
+ for index, item in enumerate(items):
+ description = card.description or ""
+ lines = description.split("\n")
+ EMOJI_CHECKED = "✅"
+ EMOJI_UNCHECKED = "🔳"
+
+ emoji = (
+ EMOJI_CHECKED
+ if index < len(lines) and "[x]" in lines[index]
+ else EMOJI_UNCHECKED
+ )
+
+ display_item = item
+ if len(item) > MAX_ITEM_PREVIEW_LENGTH:
+ display_item = item[: MAX_ITEM_PREVIEW_LENGTH - ELLIPSIS_LENGTH] + "..."
+
+ keyboard_builder.button(
+ text=f"{emoji} {display_item}",
+ callback_data=CardCallback(
+ action="toggle", card_id=card.id, item_index=index
+ ),
+ )
+
+ if items:
+ keyboard_builder.button(
+ text="🗑️ Удалить",
+ callback_data=CardCallback(action="remove", card_id=card.id),
+ )
+
+ keyboard_builder.button(text="⬅️ Назад", callback_data=CardCallback(action="back"))
+ keyboard_builder.adjust(1)
+
+ card_text = await _generate_card_text(card.title, items)
+
+ if hasattr(message, "edit_text"):
+ await message.edit_text(
+ card_text, reply_markup=keyboard_builder.as_markup(), parse_mode="HTML"
+ )
+ else:
+ await message.answer(
+ card_text, reply_markup=keyboard_builder.as_markup(), parse_mode="HTML"
+ )
+
+
+async def _generate_card_text(title: str, items: list) -> str:
+ """
+ Сгенерировать текст для отображения карточки
+
+ :param title: Заголовок карточки
+ :type title: str
+ :param items: Список элементов
+ :type items: list
+ :return: Сформированный текст
+ :rtype: str
+ """
+ if items:
+ items_text = f"Элементов: {len(items)}\n\n"
+ else:
+ items_text = "📝 Список пуст\n\n"
+
+ return f"""{title}
+
+{items_text}
+💡 Просто введите новые элементы сообщением:
+• Один элемент
+• Или несколько через запятую"""
+
+
+@view_router.message()
+async def handle_message_input(message: types.Message) -> None:
+ """
+ Обработчик текстовых сообщений для автоматического добавления элементов
+
+ :param message: Входящее сообщение
+ :type message: types.Message
+ """
+ if message.text.startswith("/"):
+ return
+
+ logger.info(f"Получено сообщение для добавления: {message.text}")
+
+ # Получаем активную карточку для пользователя
+ user_id = message.from_user.id
+ active_card_id = _user_active_card.get(user_id)
+
+ if active_card_id and active_card_id in _card_cache:
+ # Используем активную карточку пользователя
+ target_card = _card_cache[active_card_id]
+ await _add_items_to_card(message, target_card)
+ elif _card_cache:
+ # Fallback - берем первую карточку из кэша
+ target_card = next(iter(_card_cache.values()))
+ await _add_items_to_card(message, target_card)
+ logger.warning(
+ f"Активная карточка не найдена для пользователя {user_id}, использована первая из кэша"
+ )
+ else:
+ # Fallback - загружаем карточки если кэш пуст
+ cards = await get_shopping_cards()
+ if not cards:
+ await message.answer("❌ Нет доступных карточек")
+ return
+ target_card = cards[0]
+ await _add_items_to_card(message, target_card)
+ logger.warning(
+ f"Кэш пуст для пользователя {user_id}, использована первая карточка"
+ )
+
+
+async def _parse_new_items(text: str) -> list:
+ """
+ Парсит новые элементы из текста
+
+ :param text: Текст с элементами для парсинга
+ :type text: str
+ :return: Список очищенных элементов
+ :rtype: list
+ """
+ ITEM_SEPARATOR = ","
+
+ new_items_text = text.strip()
+ if not new_items_text:
+ return []
+
+ if ITEM_SEPARATOR in new_items_text:
+ return [
+ item.strip()
+ for item in new_items_text.split(ITEM_SEPARATOR)
+ if item.strip()
+ ]
+ return [new_items_text]
+
+
+async def _add_items_to_card(message: types.Message, card) -> None:
+ """
+ Добавить элементы в карточку
+
+ :param message: Сообщение с элементами
+ :type message: types.Message
+ :param card: Карточка для обновления
+ """
+ new_items = await _parse_new_items(message.text)
+ if not new_items:
+ await message.answer("❌ Не указаны элементы для добавления")
+ return
+
+ logger.info(f"Парсинг элементов: {new_items}")
+
+ # Получаем текущие элементы с их состояниями
+ current_items_with_states = card.get_list_items_with_states()
+ current_item_texts = [item["text"] for item in current_items_with_states]
+
+ # Добавляем только новые элементы (исключаем дубликаты)
+ for new_item in new_items:
+ if new_item not in current_item_texts:
+ current_items_with_states.append({"text": new_item, "checked": False})
+
+ # Создаем обновленный список текстов элементов для обновления
+ updated_item_texts = [item["text"] for item in current_items_with_states]
+
+ # Обновляем описание карточки
+ new_description = card.update_list_items(updated_item_texts)
+
+ logger.info(f"Обновление карточки {card.id}: {len(updated_item_texts)} элементов")
+
+ success = await update_card_description(card.id, new_description)
+
+ if success:
+ await message.answer(f"✅ Добавлено {len(new_items)} элементов в '{card.title}'")
+ # Обновляем кэш
+ card.description = new_description
+ _card_cache[card.id] = card
+ await list_handler(message)
+ else:
+ await message.answer("❌ Ошибка при добавлении элементов")
+
+
+async def get_cached_card(card_id: int):
+ """
+ Получить карточку из кэша
+
+ :param card_id: ID карточки
+ :type card_id: int
+ :return: Карточка из кэша или None
+ """
+ return _card_cache.get(card_id)
+
+
+def set_user_active_card(user_id: int, card_id: int) -> None:
+ """
+ Установить активную карточку для пользователя
+
+ :param user_id: ID пользователя
+ :type user_id: int
+ :param card_id: ID карточки
+ :type card_id: int
+ """
+ _user_active_card[user_id] = card_id
+
+
+def get_user_active_card(user_id: int) -> int:
+ """
+ Получить активную карточку пользователя
+
+ :param user_id: ID пользователя
+ :type user_id: int
+ :return: ID активной карточки или None
+ :rtype: int
+ """
+ return _user_active_card.get(user_id)
diff --git a/handlers/bot_routes/routes_deck.py b/handlers/bot_routes/routes_deck.py
index 0d12103..c105b34 100644
--- a/handlers/bot_routes/routes_deck.py
+++ b/handlers/bot_routes/routes_deck.py
@@ -1,152 +1,18 @@
-"""Главный роутер для всех команд бота (базовые + deck)"""
+"""Главный роутер для бота списка покупок"""
-from aiogram import F, Router, types
-from aiogram.filters import Command, CommandStart
+from aiogram import Router
-from handlers.bot_routes.route_deck_boards import (
- NavigationCallback,
- show_boards_command,
-)
-from handlers.bot_routes.route_deck_cards import show_cards_in_stack
-from handlers.bot_routes.route_deck_stacks import (
- back_to_boards_list,
- show_stacks_for_board,
-)
-from handlers.handler_logging import logger
+from handlers.bot_routes.route_list_cards import list_router
+from handlers.bot_routes.route_view_card import view_router
+from handlers.bot_routes.route_edit_items import edit_router
nc_deck_router = Router()
+ROUTERS = (
+ list_router,
+ view_router,
+ edit_router,
+)
-# ========== БАЗОВЫЕ КОМАНДЫ ==========
-@nc_deck_router.message(CommandStart())
-async def start_handler(message: types.Message) -> None:
- """
- Обработчик команды /start
-
- :param message: Сообщение пользователя
- :type message: types.Message
- """
- await message.answer("🚀 Бот запущен! Используйте /help для списка команд")
- logger.info(f"Пользователь {message.from_user.id} запустил бота")
-
-
-@nc_deck_router.message(Command("help"))
-async def help_handler(message: types.Message) -> None:
- """
- Обработчик команды /help
-
- :param message: Сообщение от пользователя
- :type message: types.Message
- """
- help_text = """
-📋 Доступные команды:
-
-/start - Запуск бота
-/help - Помощь
-/test - Тестовая команда
-/about - О боте
-/decks - Показать доски
-
-Просто напишите "test" для проверки текстового хендлера
- """
- await message.answer(help_text)
-
-
-@nc_deck_router.message(Command("test"))
-async def test_command_handler(message: types.Message) -> None:
- """
- Обработчик команды /test
-
- :param message: Сообщение от пользователя
- :type message: types.Message
-
- """
- await message.answer("✅ Тест пройден! Бот работает корректно!")
- logger.info(f"Пользователь {message.from_user.id} выполнил тест-команду")
-
-
-@nc_deck_router.message(Command("about"))
-async def about_handler(message: types.Message) -> None:
- """
- Обработчик команды /about
-
- :param message: Сообщение от пользователя
- :type message: types.Message
-
- """
- await message.answer(
- "🤖 Это тестовый бот с модульной архитектурой\n\n"
- "⚡ Быстрое масштабирование\n"
- "📁 Чистая структура проекта\n"
- "🔧 Легкое обслуживание"
- )
-
-
-@nc_deck_router.message(lambda message: message.text and message.text.lower() == "test")
-async def test_text_handler(message: types.Message) -> None:
- """
- Обработчик текстового сообщения 'test'
-
- :param message: Сообщение от пользователя
- :type message: types.Message
- """
- await message.answer("🔤 Вы написали 'test'! Текстовые хендлеры работают!")
- logger.info(f"Пользователь {message.from_user.id} отправил текстовый тест")
-
-
-# ========== DECK КОМАНДЫ ==========
-@nc_deck_router.message(Command("decks"))
-async def decks_handler(message: types.Message) -> None:
- """
- Обработчик команды /decks
-
- :param message: Сообщение от пользователя
- :type message: types.Message
- """
- await show_boards_command(message)
-
-
-@nc_deck_router.callback_query(NavigationCallback.filter(F.screen == "stacks_list"))
-async def stacks_handler(
- callback: types.CallbackQuery, callback_data: NavigationCallback
-) -> None:
- """
- Обработчик выбора доски
-
- :param callback: Callback от нажатой кнопки меню
- :type callback: types.CallbackQuery
- :param callback_data: Callback данные
- :type callback_data: NavigationCallback
- """
- await show_stacks_for_board(callback, callback_data)
-
-
-@nc_deck_router.callback_query(NavigationCallback.filter(F.screen == "boards_list"))
-async def boards_handler(
- callback: types.CallbackQuery, callback_data: NavigationCallback
-) -> None:
- """
- Обработчик возврата к доскам
-
- :param callback: Callback от нажатой кнопки
- :type callback: types.CallbackQuery
- :param callback_data: Callback данные
- :type callback_data: NavigationCallback
- """
- del callback_data
- await back_to_boards_list(callback)
-
-
-@nc_deck_router.callback_query(NavigationCallback.filter(F.screen == "cards_list"))
-async def cards_handler(
- callback: types.CallbackQuery, callback_data: NavigationCallback
-) -> None:
- """
- Обработчик выбора стека
-
- :param callback: Callback от нажатой кнопки
- :type callback: types.CallbackQuery
- :param callback_data: Callback данные
- :type callback_data: NavigationCallback
- """
- await show_cards_in_stack(callback, callback_data)
+for router in ROUTERS:
+ nc_deck_router.include_router(router)
diff --git a/handlers/bot_routes/states.py b/handlers/bot_routes/states.py
new file mode 100644
index 0000000..34b2a7a
--- /dev/null
+++ b/handlers/bot_routes/states.py
@@ -0,0 +1,20 @@
+"""Состояния и callback данные для бота списка покупок"""
+
+from aiogram.filters.callback_data import CallbackData
+
+
+class CardCallback(CallbackData, prefix="card"):
+ """
+ Callback для навигации по карточкам
+
+ :param action: Действие (view, back, toggle, delete)
+ :type action: str
+ :param card_id: ID карточки
+ :type card_id: int
+ :param item_index: Индекс элемента
+ :type item_index: int
+ """
+
+ action: str # 'view', 'back', 'toggle', 'delete'
+ card_id: int = 0
+ item_index: int = -1
diff --git a/handlers/handler_bot.py b/handlers/handler_bot.py
index ddb06a3..bfa823f 100644
--- a/handlers/handler_bot.py
+++ b/handlers/handler_bot.py
@@ -10,20 +10,13 @@
# Инициализация бота
BOT_INSTANCE = Bot(token=settings.BOT_TOKEN) if settings.BOT_TOKEN else None
dispatcher = Dispatcher()
-
-# Главный роутер
dispatcher.include_router(nc_deck_router)
-logger.info("Бот инициализирован")
+logger.info("Бот списка покупок инициализирован")
async def process_update(update_data: dict) -> None:
- """
- Обработка обновления от Telegram
-
- :param update_data: Описание
- :type update_data: dict
- """
+ """Обработка обновления от Telegram"""
if not BOT_INSTANCE:
logger.warning("Получено обновление, но бот не инициализирован")
return
@@ -33,12 +26,7 @@ async def process_update(update_data: dict) -> None:
async def setup_webhook(webhook_url: str) -> None:
- """
- Настройка вебхука
-
- :param webhook_url: Описание
- :type webhook_url: str
- """
+ """Настройка вебхука"""
if not BOT_INSTANCE:
logger.warning("Попытка настроить вебхук без бота")
return
@@ -49,8 +37,6 @@ async def setup_webhook(webhook_url: str) -> None:
async def close_bot_session() -> None:
- """
- Закрытие сессии бота
- """
+ """Закрытие сессии бота"""
if BOT_INSTANCE:
await BOT_INSTANCE.session.close()
diff --git a/handlers/handler_logging.py b/handlers/handler_logging.py
index 16e09cd..e81b28a 100644
--- a/handlers/handler_logging.py
+++ b/handlers/handler_logging.py
@@ -3,16 +3,17 @@
import sys
from loguru import logger
+from config.settings import settings
logger.remove()
logger.add(
sys.stderr,
- format="{time:YYYY-MM-DD HH:mm:ss} | "
+ format="{time:DD-MM-YYYY HH:mm:ss} | "
"{level} | "
"{module}:{function}:{line} - "
"{message}",
colorize=True,
- level="DEBUG",
+ level=settings.LOG_LEVEL.upper(),
)
logger.level("DEBUG", color="")
diff --git a/handlers/handler_nc_deck.py b/handlers/handler_nc_deck.py
index fe673e6..9a1cae9 100644
--- a/handlers/handler_nc_deck.py
+++ b/handlers/handler_nc_deck.py
@@ -1,58 +1,91 @@
-"""Обработчик получения досок и данных в них"""
+"""Обработчик для работы с карточками списка покупок"""
from typing import List, Optional
-
-from pydantic import ValidationError
-
from config.settings import settings
from handlers.handler_logging import logger
from handlers.handler_requests import send_request
-from models.model_board import ModelBoard
-from models.model_stack import ModelStack
+from models.model_card import ModelCard
DECK_ENDPOINT = "/apps/deck/api/v1.0/boards"
-async def get_boards() -> Optional[List[ModelBoard]]:
+async def get_shopping_cards() -> Optional[List[ModelCard]]:
"""
- Получение всех Дэк
+ Получение карточек из стека списка покупок
- :return: JSON со списком Дэк
- :rtype: List[ModelBoard] | None
+ :return: Список карточек или None при ошибке
+ :rtype: Optional[List[ShoppingCard]]
"""
- boards_endpoint = f"{settings.NC_URL}{DECK_ENDPOINT}"
- decklist = await send_request(url=boards_endpoint, method="GET")
- if decklist is not None:
+ base_url = f"{settings.NC_URL}{DECK_ENDPOINT}"
+ stacks_endpoint = f"{base_url}/{settings.DECK_BOARD_ID}/stacks"
+
+ stacks_data = await send_request(url=stacks_endpoint, method="GET")
+
+ if not stacks_data:
+ logger.error("Не удалось получить данные стеков")
+ return None
+
+ target_stack = next(
+ (stack for stack in stacks_data if stack.get("id") == settings.DECK_STACK_ID),
+ None,
+ )
+
+ if not target_stack:
+ logger.error(f"Стек {settings.DECK_STACK_ID} не найден")
+ return None
+
+ cards_data = target_stack.get("cards", [])
+ cards = []
+
+ for card_data in cards_data:
try:
- decks = [ModelBoard(**deck) for deck in decklist]
- return decks
- except ValidationError as err:
- logger.critical(f"Получены невалидные данные: {err}")
- else:
- logger.critical("Данные по доскам не получены")
- return None
+ shopping_card = ModelCard(
+ id=card_data.get("id", 0),
+ title=card_data.get("title", ""),
+ description=card_data.get("description"),
+ stack_id=card_data.get("stackId", 0),
+ )
+ cards.append(shopping_card)
+ except Exception as error:
+ logger.warning(
+ f"Ошибка преобразования карточки {card_data.get('id')}: {error}"
+ )
+ continue
+
+ logger.info(f"Успешно загружено {len(cards)} карточек")
+ return cards
-async def get_stacks(board_id: int) -> Optional[List[ModelStack]]:
+async def update_card_description(card_id: int, description: str) -> bool:
"""
- Получение стеков в доске
+ Обновление описания карточки
- :param board_id: ID доски
- :type board_id: int
- :return: Список стеков в доске
- :rtype: List[ModelStack] | None
+ :param card_id: ID карточки для обновления
+ :type card_id: int
+ :param description: Новое описание карточки
+ :type description: str
+ :return: True если обновление успешно, иначе False
+ :rtype: bool
"""
- if board_id is not None:
- stacks_endpoint = f"{settings.NC_URL}{DECK_ENDPOINT}/{board_id}/stacks"
- stacklist = await send_request(url=stacks_endpoint, method="GET")
- if stacklist is not None:
- try:
- stacks = [ModelStack(**stack) for stack in stacklist]
- return stacks
- except ValidationError as err:
- logger.critical(f"Получены невалидные данные: {err}")
- else:
- logger.critical("Данные по стекам не получены")
- else:
- logger.critical('"board_id" не задан')
- return None
+ base_url = f"{settings.NC_URL}{DECK_ENDPOINT}"
+ card_endpoint = (
+ f"{base_url}/{settings.DECK_BOARD_ID}/stacks/"
+ f"{settings.DECK_STACK_ID}/cards/{card_id}"
+ )
+
+ # Получаем текущие данные карточки для сохранения всех полей
+ card_data = await send_request(url=card_endpoint, method="GET")
+ if not card_data:
+ logger.error(f"Не удалось получить данные карточки {card_id}")
+ return False
+
+ # Обновляем только описание, сохраняя остальные поля
+ update_data = {**card_data, "description": description}
+ result = await send_request(url=card_endpoint, method="PUT", data=update_data)
+
+ if result:
+ logger.info(f"Карточка {card_id} успешно обновлена")
+ return True
+
+ logger.error(f"Ошибка обновления карточки {card_id}")
+ return False
diff --git a/handlers/handler_requests.py b/handlers/handler_requests.py
index 0862be0..e1bd91c 100644
--- a/handlers/handler_requests.py
+++ b/handlers/handler_requests.py
@@ -1,45 +1,55 @@
"""Обработчик отправки запросов к серверу NextCloud"""
from typing import Any, Dict, Optional
-
-from httpx import AsyncClient, BasicAuth, RequestError
+from httpx import AsyncClient, BasicAuth, HTTPStatusError, RequestError
from config.settings import settings
from handlers.handler_logging import logger
+TIMEOUT = 20
+
headers = {
"OCS-APIRequest": "true",
"accept": "application/json",
+ "Content-Type": "application/json",
}
auth = BasicAuth(username=settings.NC_LOGIN, password=settings.NC_PASSWORD)
-async def send_request(url: str, method: str) -> Optional[Dict[Any, Any]]:
+async def send_request(
+ url: str, method: str, data: Optional[Dict] = None
+) -> Optional[Dict[Any, Any]]:
"""
Клиент для отправки запросов в API NextCloud
:param url: Полный адрес вебхука куда отправляются данные
:type url: str
- :param method: Используемый метод (пока GET / POST)
+ :param method: Используемый метод (GET/POST/PUT)
:type method: str
+ :param data: Данные для отправки (опционально)
+ :type data: Optional[Dict]
:return: JSON ответа от сервера
:rtype: Dict[Any, Any] | None
"""
if not url:
- logger.critical("Не определен URL!")
+ logger.error("Не определен URL!")
return None
- async with AsyncClient(auth=auth, headers=headers) as client:
+
+ async with AsyncClient(auth=auth, headers=headers, timeout=30.0) as client:
try:
- match method.upper():
+ method_upper = method.upper()
+ match method_upper:
case "GET":
response = await client.get(url=url)
case "POST":
- response = await client.post(url=url)
+ response = await client.post(url=url, json=data)
+ case "PUT":
+ response = await client.put(url=url, json=data)
case _:
- logger.critical(f"Неподдерживаемый метод: {method}")
+ logger.error(f"Неподдерживаемый метод: {method}")
return None
response.raise_for_status()
return response.json()
- except RequestError as err:
- logger.critical(f'Ошибка запроса к "{settings.NC_URL}": {err}')
- return None
+ except (RequestError, HTTPStatusError) as error:
+ logger.error(f'Ошибка запроса к "{url}" : {error}')
+ return None
diff --git a/main.py b/main.py
index 2e24bd2..046db47 100644
--- a/main.py
+++ b/main.py
@@ -8,18 +8,22 @@
from handlers.handler_logging import logger
from handlers.handler_server import server_app
-if __name__ == "__main__":
+
+def main() -> None:
+ """
+ Основная функция запуска приложения
+ """
logger.info(f"🚀 Сервер запускается (v.{settings.VERSION})")
logger.info(f"📍 Хост: {settings.HOST}, Порт: {settings.PORT}")
logger.info(f"🌐 WEBHOOK_HOST: {settings.WEBHOOK_HOST or 'не установлен'}")
ssl_keyfile = settings.SSL_KEY_PATH
ssl_certfile = settings.SSL_CERT_PATH
- SSL_ENABLED = False
+ ssl_enabled = False
if settings.SSL_ENABLED:
if os.path.exists(ssl_keyfile) and os.path.exists(ssl_certfile):
- SSL_ENABLED = True
+ ssl_enabled = True
logger.success("🔐 SSL сертификаты найдены, запуск с HTTPS")
else:
logger.warning("⚠️ SSL сертификаты не найдены, запуск с HTTP")
@@ -30,6 +34,10 @@
port=settings.PORT,
log_level=settings.LOG_LEVEL,
access_log=True,
- ssl_keyfile=ssl_keyfile if SSL_ENABLED else None,
- ssl_certfile=ssl_certfile if SSL_ENABLED else None,
+ ssl_keyfile=ssl_keyfile if ssl_enabled else None,
+ ssl_certfile=ssl_certfile if ssl_enabled else None,
)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/models/model_board.py b/models/model_board.py
deleted file mode 100644
index 1f64197..0000000
--- a/models/model_board.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from pydantic import BaseModel
-from typing import Dict, List, Optional, Union
-
-# TODO: Референсные данные - удалить после тестов
-# "title": "Board title",
-# "owner": {
-# "primaryKey": "admin",
-# "uid": "admin",
-# "displayname": "Administrator"
-# },
-# "color": "ff0000",
-# "archived": false,
-# "labels": [],
-# "acl": [],
-# "permissions": {
-# "PERMISSION_READ": true,
-# "PERMISSION_EDIT": true,
-# "PERMISSION_MANAGE": true,
-# "PERMISSION_SHARE": true
-# },
-# "users": [],
-# "shared": 0,
-# "deletedAt": 0,
-# "id": 10,
-# "lastModified": 1586269585,
-# "settings": {
-# "notify-due": "off",
-# "calendar": true
-
-
-class Owner(BaseModel):
- primaryKey: str
- uid: str
- displayname: str
-
-
-class Permissions(BaseModel):
- PERMISSION_READ: bool
- PERMISSION_EDIT: bool
- PERMISSION_MANAGE: bool
- PERMISSION_SHARE: bool
-
-
-class Settings(BaseModel):
- notify_due: str
- calendar: bool
-
-
-class ModelBoard(BaseModel):
- title: str
- owner: Owner
- color: str
- archived: bool
- labels: List[str]
- acl: List[Dict]
- permissions: Permissions
- users: List[Dict]
- shared: int
- deletedAt: int
- id: int
- lastModified: int
- settings: Optional[Union[Settings, List]] = None
diff --git a/models/model_card.py b/models/model_card.py
new file mode 100644
index 0000000..c86a77b
--- /dev/null
+++ b/models/model_card.py
@@ -0,0 +1,172 @@
+"""Модели данных для карточек NextCloud Deck"""
+
+from typing import Optional, List
+from pydantic import BaseModel
+
+
+class ModelCard(BaseModel):
+ """
+ Упрощенная модель карточки для списка покупок
+
+ :param id: Уникальный идентификатор карточки
+ :type id: int
+ :param title: Заголовок карточки
+ :type title: str
+ :param description: Описание карточки (список покупок)
+ :type description: Optional[str]
+ :param stack_id: ID стека в котором находится карточка
+ :type stack_id: int
+ """
+
+ id: int
+ title: str
+ description: Optional[str] = None
+ stack_id: int
+
+ @property
+ def short_title(self) -> str:
+ """Сокращенный заголовок для кнопок"""
+ MAX_TITLE_LENGTH = 30
+ ELLIPSIS_LENGTH = 3
+
+ if len(self.title) > MAX_TITLE_LENGTH:
+ return self.title[: MAX_TITLE_LENGTH - ELLIPSIS_LENGTH] + "..."
+ return self.title
+
+ def get_list_items(self) -> List[str]:
+ """
+ Получить элементы списка из описания
+
+ :return: Список очищенных элементов
+ :rtype: List[str]
+ """
+ if not self.description:
+ return []
+
+ items = []
+ lines = self.description.split("\n")
+
+ for line in lines:
+ line = line.strip()
+ if not line:
+ continue
+
+ # Очищаем от маркеров списка
+ clean_line = self._clean_list_item(line)
+ if clean_line:
+ items.append(clean_line)
+
+ return items
+
+ def _clean_list_item(self, line: str) -> Optional[str]:
+ """Очистить элемент списка от маркеров"""
+ MARKER_CHECKBOX_UNCHECKED = "- [ ] "
+ MARKER_CHECKBOX_CHECKED = "- [x] "
+ MARKER_DASH = "- "
+ MARKER_ASTERISK = "* "
+
+ if line.startswith(MARKER_CHECKBOX_UNCHECKED) or line.startswith(
+ MARKER_CHECKBOX_CHECKED
+ ):
+ return line[6:].strip() # Убираем "- [ ] " или "- [x] "
+ elif line.startswith(MARKER_DASH) or line.startswith(MARKER_ASTERISK):
+ return line[2:].strip() # Убираем "- " или "* "
+ elif line[0].isdigit() and ". " in line:
+ return line.split(". ", 1)[1].strip() # Убираем "1. "
+
+ return line.strip() if line.strip() else None
+
+ def update_list_items(self, items: List[str]) -> str:
+ """
+ Обновить элементы списка в формате задач, сохраняя состояние выполненных элементов
+
+ :param items: Список элементов для добавления
+ :type items: List[str]
+ :return: Отформатированное описание карточки
+ :rtype: str
+ """
+ if not items:
+ return ""
+
+ # Создаем словарь для отслеживания состояния элементов
+ item_states = {}
+
+ # Парсим текущее описание, чтобы сохранить состояния
+ if self.description:
+ lines = self.description.split("\n")
+ for line in lines:
+ line = line.strip()
+ if not line:
+ continue
+
+ # Определяем состояние элемента
+ if line.startswith("- [x] "):
+ item_text = line[6:].strip()
+ item_states[item_text] = "checked"
+ elif line.startswith("- [ ] "):
+ item_text = line[6:].strip()
+ item_states[item_text] = "unchecked"
+ elif line.startswith("- "):
+ item_text = line[2:].strip()
+ item_states[item_text] = "unchecked"
+ elif line.startswith("* "):
+ item_text = line[2:].strip()
+ item_states[item_text] = "unchecked"
+ elif line[0].isdigit() and ". " in line:
+ item_text = line.split(". ", 1)[1].strip()
+ item_states[item_text] = "unchecked"
+ else:
+ item_text = line.strip()
+ item_states[item_text] = "unchecked"
+
+ # Создаем новые строки с сохранением состояний
+ list_lines = []
+ for item in items:
+ # Используем сохраненное состояние или создаем новый unchecked элемент
+ state = item_states.get(item, "unchecked")
+ if state == "checked":
+ list_lines.append(f"- [x] {item}")
+ else:
+ list_lines.append(f"- [ ] {item}")
+
+ return "\n".join(list_lines)
+
+ def get_list_items_with_states(self) -> List[dict]:
+ """
+ Получить элементы списка с их состояниями
+
+ :return: Список словарей с элементами и их состояниями
+ :rtype: List[dict]
+ """
+ if not self.description:
+ return []
+
+ items = []
+ lines = self.description.split("\n")
+
+ for line in lines:
+ line = line.strip()
+ if not line:
+ continue
+
+ # Определяем состояние элемента
+ if line.startswith("- [x] "):
+ item_text = line[6:].strip()
+ items.append({"text": item_text, "checked": True})
+ elif line.startswith("- [ ] "):
+ item_text = line[6:].strip()
+ items.append({"text": item_text, "checked": False})
+ elif line.startswith("- "):
+ item_text = line[2:].strip()
+ items.append({"text": item_text, "checked": False})
+ elif line.startswith("* "):
+ item_text = line[2:].strip()
+ items.append({"text": item_text, "checked": False})
+ elif line[0].isdigit() and ". " in line:
+ item_text = line.split(". ", 1)[1].strip()
+ items.append({"text": item_text, "checked": False})
+ else:
+ item_text = line.strip()
+ items.append({"text": item_text, "checked": False})
+
+ return items
diff --git a/models/model_stack.py b/models/model_stack.py
deleted file mode 100644
index 8296882..0000000
--- a/models/model_stack.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from typing import List, Optional
-
-from pydantic import BaseModel
-
-# "title": "ToDo",
-# "boardId": 2,
-# "deletedAt": 0,
-# "lastModified": 1541426139,
-# "cards": [...],
-# "order": 999,
-# "id": 4
-
-
-class ModelStack(BaseModel):
- title: str
- boardId: int
- deletedAt: int
- lastModified: int
- cards: Optional[List] = None
- order: int
- id: int
diff --git a/routes/routes_base.py b/routes/routes_base.py
index 18f6dfb..d0ea323 100644
--- a/routes/routes_base.py
+++ b/routes/routes_base.py
@@ -1,3 +1,5 @@
+"""Базовые API эндпоинты приложения"""
+
from fastapi import APIRouter
# Создаем роутер для базовых API эндпоинтов
diff --git a/routes/routes_webhook.py b/routes/routes_webhook.py
index 0b8de28..6132853 100644
--- a/routes/routes_webhook.py
+++ b/routes/routes_webhook.py
@@ -1,11 +1,11 @@
+"""Роуты для обработки вебхуков Telegram"""
+
from fastapi import APIRouter, Request
-from handlers.handler_logging import logger
+
from handlers.handler_bot import process_update
+from handlers.handler_logging import logger
-# Создаем роутер для вебхука
webhook_router = APIRouter(tags=["webhook"])
-
-# Простой путь без токена
WEBHOOK_PATH = "/webhook"
@@ -19,13 +19,3 @@ async def telegram_webhook(update: Request):
except Exception as e:
logger.error(f"Ошибка обработки вебхука: {e}")
return {"ok": False, "error": str(e)}
-
-
-@webhook_router.get(WEBHOOK_PATH)
-async def webhook_debug():
- """Отладочная ручка для проверки вебхука"""
- return {
- "message": "Webhook endpoint is active",
- "path": WEBHOOK_PATH,
- "method": "Use POST for Telegram webhooks",
- }