import os import logging from aiogram import Bot, Dispatcher, Router, types, F from aiogram.filters import CommandStart, StateFilter from aiogram.fsm.state import StatesGroup, State from aiogram.fsm.context import FSMContext from aiogram.types import ReplyKeyboardMarkup, KeyboardButton from dotenv import load_dotenv import kanboard # Логирование logging.basicConfig(level=logging.INFO) # Загрузка токена из .env load_dotenv() BOT_TOKEN = os.getenv("TELEGRAM_TOKEN") KB_URI = os.getenv("KB_URI") BOT_LOGIN = os.getenv("BOT_LOGIN") BOT_PASS = os.getenv("BOT_PASS") bot = Bot(token=BOT_TOKEN) dp = Dispatcher() router = Router() # Kanboard client (укажи свои адрес и логин/пароль) kb_connect = "http://"+KB_URI+"/jsonrpc.php" kb = kanboard.Client(kb_connect,BOT_LOGIN, BOT_PASS) # FSM состояния class ProjectStates(StatesGroup): waiting_project_choice = State() project_menu = State() waiting_for_task_title = State() # /start — главное меню @router.message(CommandStart()) async def start(msg: types.Message, state: FSMContext): keyboard = ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="Список проектов")]], resize_keyboard=True ) await msg.answer( "Добро пожаловать! Выберите действие:", reply_markup=keyboard ) await state.clear() # Кнопка "Список проектов" @router.message(F.text == "Список проектов") async def show_projects(msg: types.Message, state: FSMContext): try: projects = kb.get_my_projects() await state.update_data(projects=projects) except Exception as e: await msg.answer(f"Ошибка получения проектов: {e}") return # Список проектов с номерами lines = [f"{idx + 1}. {proj['name']}" for idx, proj in enumerate(projects)] text = "Выберите проект (отправьте его номер):\n" + "\n".join(lines) await msg.answer(text) await state.set_state(ProjectStates.waiting_project_choice) # Выбор проекта по номеру @router.message(StateFilter(ProjectStates.waiting_project_choice), F.text) async def project_chosen(msg: types.Message, state: FSMContext): data = await state.get_data() projects = data.get("projects", []) # Проверяем, что введено число try: idx = int(msg.text.strip()) - 1 except ValueError: await msg.answer("Пожалуйста, введите номер проекта.") return if not (0 <= idx < len(projects)): await msg.answer("Нет проекта с таким номером. Попробуйте ещё раз.") return selected = projects[idx] await state.update_data(selected_project=selected) keyboard = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="Список задач")], [KeyboardButton(text="Добавить задачу")], [KeyboardButton(text="Назад")], ], resize_keyboard=True ) await msg.answer( f"Вы выбрали проект: {selected['name']}\nВыберите действие:", reply_markup=keyboard ) await state.set_state(ProjectStates.project_menu) # Кнопка "Список задач" @router.message(StateFilter(ProjectStates.project_menu), F.text == "Список задач") async def show_tasks(msg: types.Message, state: FSMContext): data = await state.get_data() project = data.get("selected_project") if not project: await msg.answer("Ошибка: проект не выбран.") return try: tasks = kb.get_all_tasks(project_id=project['id']) if not tasks: await msg.answer("В этом проекте нет задач.") else: text = "\n".join([ f"{t['title']}" for t in tasks ]) await msg.answer(f"Текущие задачи:\n{text}") except Exception as e: await msg.answer(f"Ошибка получения задач: {e}") # Кнопка "Добавить задачу" @router.message(StateFilter(ProjectStates.project_menu), F.text == "Добавить задачу") async def add_task(msg: types.Message, state: FSMContext): await msg.answer("Напишите название задачи одним сообщением:") await state.set_state(ProjectStates.waiting_for_task_title) # Кнопка "Назад" — возвращает к выбору проекта по номеру @router.message(StateFilter(ProjectStates.project_menu), F.text == "Назад") async def go_back_to_projects(msg: types.Message, state: FSMContext): data = await state.get_data() projects = data.get("projects", []) if not projects: # если вдруг projects нет в state (например, после сброса) try: projects = kb.get_my_projects() await state.update_data(projects=projects) except Exception as e: await msg.answer(f"Ошибка получения проектов: {e}") return lines = [f"{idx + 1}. {proj['name']}" for idx, proj in enumerate(projects)] text = "Выберите проект (отправьте его номер):\n" + "\n".join(lines) await state.update_data(selected_project=None) keyboard = ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="Список проектов")]], resize_keyboard=True ) await msg.answer( text, reply_markup=keyboard ) await state.set_state(ProjectStates.waiting_project_choice) # Принимаем название задачи и создаём её @router.message(StateFilter(ProjectStates.waiting_for_task_title), F.text) async def task_title_received(msg: types.Message, state: FSMContext): data = await state.get_data() project = data.get("selected_project") title = msg.text try: kb.create_task(project_id=project['id'], title=title) await msg.answer(f"Задача '{title}' добавлена!") except Exception as e: await msg.answer(f"Ошибка добавления задачи: {e}") # Меню проекта после добавления задачи keyboard = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="Список задач")], [KeyboardButton(text="Добавить задачу")], [KeyboardButton(text="Назад")], ], resize_keyboard=True ) await msg.answer("Выберите действие:", reply_markup=keyboard) await state.set_state(ProjectStates.project_menu) # Регистрация роутера dp.include_router(router) async def main(): await dp.start_polling(bot) if __name__ == "__main__": import asyncio asyncio.run(main())