/
tari.gpt
/
ServitorBot
Обзор
Документация
Войти
/
tari.gpt
/
ServitorBot
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
notes.py
3 007 строк
121 KB
tarrri
feat: add task/note creation from text and forwarded messages
08 дек 2025, 18:55
08 дек 2025, 18:55
f9e4fe0
Код
Авторство
О чём код?
""" Модуль для работы с заметками Обработчики для создания, просмотра, редактирования и удаления заметок """ import os import logging from dotenv import load_dotenv from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ( ContextTypes, ConversationHandler, CommandHandler, MessageHandler, CallbackQueryHandler, filters ) import notes_database import ai_processor # Загружаем переменные окружения load_dotenv() # Настройка логирования logger = logging.getLogger(__name__) # Константы состояний для ConversationHandler заметок NOTE_WAITING_CONTENT = 11 NOTE_WAITING_TAGS = 12 NOTE_PREVIEW = 13 NOTE_WAITING_AI_CHOICE = 14 NOTE_WAITING_COMPANION_TEXT = 15 NOTE_CHOICE_TASK_OR_NOTE = 16 VOICE_TASK_PREVIEW = 17 EDIT_NOTE_WAITING_CONTENT = 18 EDIT_NOTE_WAITING_TAGS = 19 NOTE_SEARCH_WAITING_QUERY = 20 REFLECTION_WAITING_CONTENT = 21 TEXT_CHOICE_TASK_OR_NOTE = 22 FORWARDED_CHOICE_TASK_OR_NOTE = 23 TEXT_TASK_PREVIEW = 24 FORWARDED_TASK_PREVIEW = 25 # ============================================================================ # Команда /addnote - Создание заметки # ============================================================================ async def addnote_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /addnote - начало создания заметки """ user = update.effective_user logger.info(f"Пользователь {user.id} начал создание заметки") # Инициализируем временное хранилище для заметки context.user_data['temp_note'] = {} context.user_data['note_mode'] = True await update.message.reply_text( "📝 Создание заметки\n\n" "Отправьте содержимое заметки:\n" "• Текст (до 4096 символов)\n" "• Голосовое сообщение\n" "• Пересланное сообщение\n" "• Ссылку\n\n" "Используйте /cancel для отмены" ) return NOTE_WAITING_CONTENT async def receive_note_text(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение текстового содержимого заметки """ text = update.message.text.strip() user_id = update.effective_user.id # Проверяем длину if len(text) > 4096: await update.message.reply_text( f"❌ Слишком длинная заметка ({len(text)} символов).\n" f"Максимум: 4096 символов.\n\n" f"Попробуйте сократить текст или разбить на несколько заметок." ) return NOTE_WAITING_CONTENT # Сохраняем в временное хранилище context.user_data['temp_note']['content'] = text context.user_data['temp_note']['raw_content'] = text logger.info(f"Пользователь {user_id} ввел заметку длиной {len(text)} символов") # Переходим к выбору тегов return await show_tag_selection(update, context) async def receive_note_voice_in_context(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение голосового сообщения в контексте /addnote """ user_id = update.effective_user.id voice = update.message.voice logger.info(f"Пользователь {user_id} отправил голосовое сообщение в /addnote") # Показываем индикатор обработки processing_msg = await update.message.reply_text("🎤 Обрабатываю голосовое сообщение...") try: # Импортируем voice_processor import voice_processor # Скачиваем и транскрибируем voice_file = await context.bot.get_file(voice.file_id) file_path = f"temp_audio/voice_{user_id}_{voice.file_id}.ogg" # Создаем директорию если нет os.makedirs("temp_audio", exist_ok=True) await voice_file.download_to_drive(file_path) # Транскрибируем groq_api_key = os.getenv('GROQ_API_KEY') if not groq_api_key: await processing_msg.delete() await update.message.reply_text( "❌ Голосовые сообщения недоступны (API ключ не настроен)." ) return NOTE_WAITING_CONTENT success, transcription = voice_processor.transcribe_audio(file_path, groq_api_key, user_id) # Удаляем временный файл if os.path.exists(file_path): os.remove(file_path) # Удаляем сообщение об обработке await processing_msg.delete() if not success: await update.message.reply_text( "❌ Не удалось распознать голосовое сообщение.\n" "Попробуйте еще раз или отправьте текстом." ) return NOTE_WAITING_CONTENT # Сохраняем транскрипцию context.user_data['temp_note']['raw_content'] = transcription logger.info(f"Голосовое сообщение распознано: {len(transcription)} символов") # Показываем кнопки выбора AI структурирования keyboard = [ [InlineKeyboardButton("✨ Структурировать с AI", callback_data="note_structure_ai")], [InlineKeyboardButton("💾 Сохранить как есть", callback_data="note_save_as_is")] ] reply_markup = InlineKeyboardMarkup(keyboard) # Показываем превью транскрипции preview = transcription[:300] + "..." if len(transcription) > 300 else transcription await update.message.reply_text( f"📝 Распознанный текст:\n\n{preview}\n\n" f"Длина: {len(transcription)} символов\n\n" f"Хотите структурировать текст с помощью AI?", reply_markup=reply_markup ) return NOTE_WAITING_AI_CHOICE except Exception as e: logger.error(f"Ошибка при обработке голосового сообщения: {e}") await processing_msg.delete() await update.message.reply_text( "❌ Произошла ошибка при обработке голосового сообщения.\n" "Попробуйте отправить текстом." ) return NOTE_WAITING_CONTENT async def button_structure_with_ai(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработка кнопки "Структурировать с AI" """ query = update.callback_query await query.answer() user_id = update.effective_user.id raw_content = context.user_data['temp_note'].get('raw_content', '') logger.info(f"Пользователь {user_id} выбрал AI структурирование") # Показываем индикатор await query.edit_message_text("⏳ AI обрабатывает вашу заметку...") try: # Получаем API ключ api_key = os.getenv('OPENROUTER_API_KEY') if not api_key: await query.edit_message_text( "❌ Ошибка: AI обработка недоступна (не настроен API ключ).\n" "Заметка будет сохранена в исходном виде." ) context.user_data['temp_note']['content'] = raw_content return await show_tag_selection(update, context, edit_message=False) # Вызываем AI success, result = ai_processor.structure_note(raw_content, api_key) if success: context.user_data['temp_note']['content'] = result logger.info(f"AI успешно обработал заметку: {len(result)} символов") await query.edit_message_text( f"✅ AI обработка завершена!\n\n" f"Исходный текст: {len(raw_content)} символов\n" f"Структурированный: {len(result)} символов" ) else: # Если ошибка - сохраняем как есть context.user_data['temp_note']['content'] = raw_content await query.edit_message_text( f"⚠️ AI обработка не удалась: {result}\n\n" f"Заметка будет сохранена в исходном виде." ) # Переходим к выбору тегов return await show_tag_selection(update, context, edit_message=False) except Exception as e: logger.error(f"Ошибка при AI обработке: {e}") context.user_data['temp_note']['content'] = raw_content await query.edit_message_text( "❌ Произошла ошибка при AI обработке.\n" "Заметка будет сохранена в исходном виде." ) return await show_tag_selection(update, context, edit_message=False) async def button_save_as_is(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработка кнопки "Сохранить как есть" (без AI) """ query = update.callback_query await query.answer("Сохраняем без изменений") user_id = update.effective_user.id raw_content = context.user_data['temp_note'].get('raw_content', '') context.user_data['temp_note']['content'] = raw_content logger.info(f"Пользователь {user_id} выбрал сохранить без AI") await query.edit_message_text("💾 Сохраняем без структурирования...") # Переходим к выбору тегов return await show_tag_selection(update, context, edit_message=False) # ============================================================================ # Работа с тегами # ============================================================================ async def show_tag_selection(update: Update, context: ContextTypes.DEFAULT_TYPE, edit_message=True): """ Показывает интерфейс выбора тегов """ user_id = update.effective_user.id # Получаем существующие теги пользователя existing_tags = notes_database.get_user_tags_with_count(user_id) keyboard = [] # Показываем топ-10 наиболее используемых тегов for tag in existing_tags[:10]: keyboard.append([ InlineKeyboardButton( f"#{tag['name']} ({tag['count']})", callback_data=f"select_tag_{tag['id']}" ) ]) # Кнопка "Без тегов" keyboard.append([InlineKeyboardButton("⏩ Без тегов", callback_data="skip_note_tags")]) text = ( "🏷 Выберите теги для заметки\n\n" "Нажмите на существующий тег из списка выше.\n" "Или введите новые теги текстом (через пробел или запятую):\n\n" "Например: работа, идеи, важное" ) reply_markup = InlineKeyboardMarkup(keyboard) if edit_message and update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: # Отправляем новое сообщение if update.callback_query: await update.callback_query.message.reply_text(text, reply_markup=reply_markup) else: await update.message.reply_text(text, reply_markup=reply_markup) return NOTE_WAITING_TAGS async def button_skip_tags(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пропуск добавления тегов """ query = update.callback_query await query.answer("Сохраняем без тегов") context.user_data['temp_note']['tags'] = [] logger.info(f"Пользователь {update.effective_user.id} пропустил добавление тегов") # Переходим к превью return await show_note_preview(update, context) async def button_select_tag(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Выбор существующего тега из списка """ query = update.callback_query tag_id = int(query.data.split('_')[2]) # Получаем название тега user_id = update.effective_user.id tags = notes_database.get_user_tags_with_count(user_id) selected_tag = next((t for t in tags if t['id'] == tag_id), None) if not selected_tag: await query.answer("Тег не найден", show_alert=True) return NOTE_WAITING_TAGS # Инициализируем список тегов если нет if 'tags' not in context.user_data['temp_note']: context.user_data['temp_note']['tags'] = [] # Добавляем тег tag_name = selected_tag['name'] if tag_name not in context.user_data['temp_note']['tags']: context.user_data['temp_note']['tags'].append(tag_name) await query.answer(f"✓ Добавлен тег #{tag_name}") else: await query.answer(f"Тег #{tag_name} уже добавлен") logger.info(f"Пользователь {user_id} добавил тег {tag_name}") # Обновляем сообщение с текущими выбранными тегами current_tags = context.user_data['temp_note']['tags'] tags_str = ", ".join([f"#{t}" for t in current_tags]) # Переходим к превью return await show_note_preview(update, context) async def receive_note_tags(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение тегов введенных текстом """ tags_text = update.message.text.strip() user_id = update.effective_user.id # Парсим теги import re tags = [] if '#' in tags_text: # Хэштеги формата tags = re.findall(r'#(\w+)', tags_text) else: # Разделение по пробелам или запятым tags = re.split(r'[,\s]+', tags_text) tags = [t.strip().lower() for t in tags if t.strip()] # Ограничение на количество if len(tags) > 10: await update.message.reply_text( "❌ Максимум 10 тегов.\n" "Попробуйте еще раз." ) return NOTE_WAITING_TAGS context.user_data['temp_note']['tags'] = tags logger.info(f"Пользователь {user_id} добавил {len(tags)} тегов: {tags}") # Переходим к превью return await show_note_preview(update, context) # ============================================================================ # Превью и сохранение заметки # ============================================================================ async def show_note_preview(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Показывает превью заметки перед сохранением """ temp_note = context.user_data.get('temp_note', {}) content = temp_note.get('content', '') tags = temp_note.get('tags', []) # Превью контента (первые 500 символов) if len(content) > 500: preview_content = content[:200] + f"...\n\n[Показано 200 из {len(content)} символов]" else: preview_content = content tags_str = ", ".join([f"#{t}" for t in tags]) if tags else "без тегов" text = ( f"👁 Превью заметки:\n\n" f"{preview_content}\n\n" f"🏷 Теги: {tags_str}\n" f"📊 Длина: {len(content)} символов" ) keyboard = [ [InlineKeyboardButton("✏️ Изменить текст", callback_data="edit_note_content")], [InlineKeyboardButton("🏷 Изменить теги", callback_data="edit_note_tags")], [ InlineKeyboardButton("💾 Сохранить", callback_data="save_note"), InlineKeyboardButton("❌ Отменить", callback_data="cancel_note") ] ] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await update.message.reply_text(text, reply_markup=reply_markup) return NOTE_PREVIEW async def button_save_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Сохранение заметки в базу данных """ query = update.callback_query await query.answer() user_id = update.effective_user.id temp_note = context.user_data.get('temp_note', {}) try: # Сохраняем заметку в БД note_id = notes_database.add_note( user_id=user_id, content=temp_note.get('content', ''), raw_content=temp_note.get('raw_content'), forwarded_message_id=temp_note.get('forwarded_message_id'), forwarded_link=temp_note.get('forwarded_link') ) if not note_id: await query.edit_message_text("❌ Ошибка при сохранении заметки. Попробуйте еще раз.") return ConversationHandler.END # Сохраняем теги if temp_note.get('tags'): tag_ids = [] for tag_name in temp_note['tags']: tag_id = notes_database.add_tag(user_id, tag_name) tag_ids.append(tag_id) notes_database.link_note_tags(note_id, tag_ids) logger.info(f"Пользователь {user_id} создал заметку #{note_id}") tags_str = ", ".join([f"#{t}" for t in temp_note.get('tags', [])]) if temp_note.get('tags') else "без тегов" await query.edit_message_text( f"✅ Заметка #{note_id} сохранена!\n\n" f"🏷 Теги: {tags_str}\n\n" f"Просмотр всех заметок: /notes" ) # Очищаем временные данные context.user_data.pop('temp_note', None) context.user_data.pop('note_mode', None) return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при сохранении заметки: {e}") await query.edit_message_text( "❌ Произошла ошибка при сохранении заметки.\n" "Попробуйте еще раз позже." ) return ConversationHandler.END async def button_cancel_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Отмена создания заметки """ query = update.callback_query await query.answer() logger.info(f"Пользователь {update.effective_user.id} отменил создание заметки") await query.edit_message_text("❌ Создание заметки отменено.") # Очищаем временные данные context.user_data.pop('temp_note', None) context.user_data.pop('note_mode', None) return ConversationHandler.END async def cancel_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /cancel - отмена создания заметки """ await update.message.reply_text("❌ Создание заметки отменено.") # Очищаем временные данные context.user_data.pop('temp_note', None) context.user_data.pop('note_mode', None) return ConversationHandler.END # ============================================================================ # ConversationHandler для /addnote # ============================================================================ text_note_handler = ConversationHandler( entry_points=[CommandHandler('addnote', addnote_command)], states={ NOTE_WAITING_CONTENT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_note_text), MessageHandler(filters.VOICE, receive_note_voice_in_context) ], NOTE_WAITING_AI_CHOICE: [ CallbackQueryHandler(button_structure_with_ai, pattern="^note_structure_ai$"), CallbackQueryHandler(button_save_as_is, pattern="^note_save_as_is$") ], NOTE_WAITING_TAGS: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_note_tags), CallbackQueryHandler(button_select_tag, pattern="^select_tag_\\d+$"), CallbackQueryHandler(button_skip_tags, pattern="^skip_note_tags$") ], NOTE_PREVIEW: [ CallbackQueryHandler(button_save_note, pattern="^save_note$"), CallbackQueryHandler(button_cancel_note, pattern="^cancel_note$") ] }, fallbacks=[CommandHandler('cancel', cancel_note)], per_message=False, allow_reentry=True ) # ============================================================================ # Обработчик голосовых сообщений БЕЗ команды (с выбором задача/заметка) # ============================================================================ async def handle_voice_choice(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработка голосового сообщения без команды Транскрибирует и предлагает выбор: задача или заметка """ user_id = update.effective_user.id voice = update.message.voice # ВАЖНО: Проверяем, не находится ли пользователь в режиме рефлексии # Если да, то пропускаем обработку здесь и позволяем reflection_handler обработать temp_note = context.user_data.get('temp_note', {}) if temp_note.get('category') == 'reflection': logger.info(f"Пользователь {user_id} в режиме рефлексии, пропускаем voice_choice_handler") return ConversationHandler.END logger.info(f"Пользователь {user_id} отправил голосовое сообщение без команды") # Показываем индикатор обработки processing_msg = await update.message.reply_text("🎤 Обрабатываю голосовое сообщение...") try: # Скачиваем и транскрибируем voice_file = await context.bot.get_file(voice.file_id) file_path = f"temp_audio/voice_{user_id}_{voice.file_id}.ogg" # Создаем директорию если нет os.makedirs("temp_audio", exist_ok=True) await voice_file.download_to_drive(file_path) # Транскрибируем import voice_processor groq_api_key = os.getenv('GROQ_API_KEY') if not groq_api_key: await processing_msg.delete() await update.message.reply_text( "❌ Голосовые сообщения недоступны (API ключ не настроен)." ) return ConversationHandler.END success, transcription = voice_processor.transcribe_audio(file_path, groq_api_key, user_id) # Удаляем временный файл if os.path.exists(file_path): os.remove(file_path) # Удаляем сообщение об обработке await processing_msg.delete() if not success: await update.message.reply_text( "❌ Не удалось распознать голосовое сообщение.\n" "Попробуйте еще раз." ) return ConversationHandler.END # Сохраняем транскрипцию context.user_data['temp_voice_transcription'] = transcription logger.info(f"Голосовое сообщение распознано: {len(transcription)} символов") # Показываем превью и выбор preview = transcription[:200] + "..." if len(transcription) > 200 else transcription keyboard = [ [InlineKeyboardButton("📋 Это задача", callback_data="voice_to_task")], [InlineKeyboardButton("📝 Это заметка", callback_data="voice_to_note")] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f"🎤 Распознано:\n\n\"{preview}\"\n\n" f"Длина: {len(transcription)} символов\n\n" f"Что вы хотите создать?", reply_markup=reply_markup ) return NOTE_CHOICE_TASK_OR_NOTE except Exception as e: logger.error(f"Ошибка при обработке голосового сообщения: {e}") await processing_msg.delete() await update.message.reply_text( "❌ Произошла ошибка при обработке голосового сообщения.\n" "Попробуйте еще раз." ) return ConversationHandler.END async def handle_text_choice(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработка обычного текстового сообщения без команды Предлагает выбор: задача или заметка """ user_id = update.effective_user.id message_text = update.message.text.strip() # Пропускаем если текст слишком короткий if len(message_text) < 5: return ConversationHandler.END # Пропускаем если пользователь в другом ConversationHandler if context.user_data.get('temp_task') or context.user_data.get('temp_note'): return ConversationHandler.END logger.info(f"Пользователь {user_id} отправил текст без команды: {len(message_text)} символов") # Сохраняем текст context.user_data['temp_text_message'] = message_text # Показываем превью и выбор preview = message_text[:200] + "..." if len(message_text) > 200 else message_text keyboard = [ [InlineKeyboardButton("📋 Это задача", callback_data="text_to_task")], [InlineKeyboardButton("📝 Это заметка", callback_data="text_to_note")] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f"💬 Получено сообщение:\n\n\"{preview}\"\n\n" f"Длина: {len(message_text)} символов\n\n" f"Что вы хотите создать?", reply_markup=reply_markup ) return TEXT_CHOICE_TASK_OR_NOTE async def button_text_to_task(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал "задача" для текстового сообщения """ query = update.callback_query await query.answer() user_id = update.effective_user.id message_text = context.user_data.get('temp_text_message') if not message_text: await query.edit_message_text("❌ Ошибка: текст не найден. Попробуйте еще раз.") return ConversationHandler.END logger.info(f"Пользователь {user_id} выбрал создание задачи из текста") await query.edit_message_text( f"📋 Создание задачи из текста\n\n" f"💬 Текст:\n{message_text[:200]}{'...' if len(message_text) > 200 else ''}\n\n" f"⏳ Обрабатываю с помощью AI..." ) try: openrouter_api_key = os.getenv('OPENROUTER_API_KEY') if not openrouter_api_key: await query.edit_message_text( "❌ Ошибка конфигурации: API ключ не найден.\n" "Обратитесь к администратору." ) return ConversationHandler.END # Обрабатываем текст через AI import voice_processor success, result = voice_processor.parse_task_from_text( message_text, openrouter_api_key, user_id ) if not success: await query.edit_message_text( f"❌ Не удалось обработать текст:\n{result}\n\n" f"Попробуйте создать задачу вручную: /addtask" ) return ConversationHandler.END # result содержит структурированные данные задачи task_data = result # Инициализируем временную задачу context.user_data['temp_task'] = { 'client': task_data.get('client_name', 'Не указан'), 'task': task_data.get('task', message_text[:600]), 'deadline': task_data.get('deadline', 'нет'), 'contact_info': task_data.get('contact_info', 'нет'), 'deadline_time': task_data.get('deadline_time') } # Парсим дедлайн, если он есть import date_utils if context.user_data['temp_task']['deadline'] and context.user_data['temp_task']['deadline'] != 'нет': deadline_input = context.user_data['temp_task']['deadline'] success_parse, parsed_date, message = date_utils.parse_date(deadline_input) if success_parse: context.user_data['temp_task']['deadline'] = parsed_date else: logger.warning(f"Не удалось распарсить дедлайн '{deadline_input}': {message}") # Парсим время, если оно есть if task_data.get('deadline_time'): time_input = task_data.get('deadline_time') success_parse, parsed_time, message = date_utils.parse_time(time_input) if success_parse: context.user_data['temp_task']['deadline_time'] = parsed_time else: context.user_data['temp_task']['deadline_time'] = None # Форматируем превью task = context.user_data['temp_task'] deadline_display = date_utils.format_datetime_for_display( task.get('deadline'), task.get('deadline_time') ) is_personal = task.get('client') == "✨ Для себя" if is_personal: preview_text = ( f"📋 Превью задачи:\n\n" f"✨ Личная задача\n" f"📝 Задача: {task['task'][:200]}{'...' if len(task['task']) > 200 else ''}\n" f"📅 Дедлайн: {deadline_display}" ) else: preview_text = ( f"📋 Превью задачи:\n\n" f"👤 Клиент: {task.get('client', 'не указан')}\n" f"📞 Контакт: {task.get('contact_info', 'нет')}\n" f"📝 Задача: {task['task'][:200]}{'...' if len(task['task']) > 200 else ''}\n" f"📅 Дедлайн: {deadline_display}" ) # Создаём кнопки для редактирования if is_personal: keyboard = [ [ InlineKeyboardButton("✏️ Изменить задачу", callback_data="text_edit_task"), InlineKeyboardButton("✏️ Изменить дедлайн", callback_data="text_edit_deadline") ], [ InlineKeyboardButton("🕐 Изменить время", callback_data="text_edit_time"), InlineKeyboardButton("🗑️ Удалить время", callback_data="text_clear_time") ], [ InlineKeyboardButton("💾 Сохранить", callback_data="text_save_task"), InlineKeyboardButton("❌ Отменить", callback_data="text_cancel_task") ] ] else: keyboard = [ [ InlineKeyboardButton("✏️ Изменить клиента", callback_data="text_edit_client"), InlineKeyboardButton("✏️ Изменить контакт", callback_data="text_edit_contact") ], [ InlineKeyboardButton("✏️ Изменить задачу", callback_data="text_edit_task"), InlineKeyboardButton("✏️ Изменить дедлайн", callback_data="text_edit_deadline") ], [ InlineKeyboardButton("🕐 Изменить время", callback_data="text_edit_time"), InlineKeyboardButton("🗑️ Удалить время", callback_data="text_clear_time") ], [ InlineKeyboardButton("💾 Сохранить", callback_data="text_save_task"), InlineKeyboardButton("❌ Отменить", callback_data="text_cancel_task") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(preview_text, reply_markup=reply_markup) # Очищаем временные данные текстового сообщения context.user_data.pop('temp_text_message', None) return TEXT_TASK_PREVIEW except Exception as e: logger.error(f"Ошибка при обработке текстовой задачи: {e}") await query.edit_message_text( f"❌ Произошла ошибка при обработке.\n" f"Попробуйте создать задачу вручную: /addtask" ) return ConversationHandler.END async def button_text_to_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал "заметка" для текстового сообщения """ query = update.callback_query await query.answer() user_id = update.effective_user.id message_text = context.user_data.get('temp_text_message') if not message_text: await query.edit_message_text("❌ Ошибка: текст не найден. Попробуйте еще раз.") return ConversationHandler.END logger.info(f"Пользователь {user_id} выбрал создание заметки из текста") # Инициализируем заметку context.user_data['temp_note'] = { 'raw_content': message_text } context.user_data['note_mode'] = True # Показываем выбор AI структурирования keyboard = [ [InlineKeyboardButton("✨ Структурировать с AI", callback_data="note_structure_ai")], [InlineKeyboardButton("💾 Сохранить как есть", callback_data="note_save_as_is")] ] reply_markup = InlineKeyboardMarkup(keyboard) preview = message_text[:300] + "..." if len(message_text) > 300 else message_text await query.edit_message_text( f"📝 Создание заметки\n\n" f"Текст:\n{preview}\n\n" f"Длина: {len(message_text)} символов\n\n" f"Хотите структурировать текст с помощью AI?", reply_markup=reply_markup ) # Очищаем временные данные текстового сообщения context.user_data.pop('temp_text_message', None) return NOTE_WAITING_AI_CHOICE async def button_voice_to_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал "заметка" для голосового сообщения """ query = update.callback_query await query.answer() user_id = update.effective_user.id transcription = context.user_data.get('temp_voice_transcription') if not transcription: await query.edit_message_text("❌ Ошибка: транскрипция не найдена. Попробуйте еще раз.") return ConversationHandler.END logger.info(f"Пользователь {user_id} выбрал создание заметки из голосового") # Инициализируем заметку context.user_data['temp_note'] = { 'raw_content': transcription } context.user_data['note_mode'] = True # Показываем выбор AI структурирования keyboard = [ [InlineKeyboardButton("✨ Структурировать с AI", callback_data="note_structure_ai")], [InlineKeyboardButton("💾 Сохранить как есть", callback_data="note_save_as_is")] ] reply_markup = InlineKeyboardMarkup(keyboard) preview = transcription[:300] + "..." if len(transcription) > 300 else transcription await query.edit_message_text( f"📝 Создание заметки\n\n" f"Текст:\n{preview}\n\n" f"Длина: {len(transcription)} символов\n\n" f"Хотите структурировать текст с помощью AI?", reply_markup=reply_markup ) return NOTE_WAITING_AI_CHOICE async def button_voice_to_task(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал "задача" для голосового сообщения Перенаправляем в поток создания задачи через voice_processor """ query = update.callback_query await query.answer() user_id = update.effective_user.id transcription = context.user_data.get('temp_voice_transcription') if not transcription: await query.edit_message_text("❌ Ошибка: транскрипция не найдена. Попробуйте еще раз.") return ConversationHandler.END logger.info(f"Пользователь {user_id} выбрал создание задачи из голосового") await query.edit_message_text( f"📋 Создание задачи из голосового\n\n" f"🎤 Распознанный текст:\n{transcription[:200]}...\n\n" f"⏳ Обрабатываю с помощью AI..." ) try: # Получаем API ключи groq_api_key = os.getenv('GROQ_API_KEY') openrouter_api_key = os.getenv('OPENROUTER_API_KEY') if not groq_api_key or not openrouter_api_key: await query.edit_message_text( "❌ Ошибка конфигурации: API ключи не найдены.\n" "Обратитесь к администратору." ) return ConversationHandler.END # Обрабатываем транскрипцию через AI для извлечения полей задачи import voice_processor # voice_processor.process_voice_message требует файл, # но у нас уже есть транскрипция, поэтому используем parse_task_from_text success, result = voice_processor.parse_task_from_text( transcription, openrouter_api_key, user_id ) if not success: await query.edit_message_text( f"❌ Не удалось обработать голосовое сообщение:\n{result}\n\n" f"Попробуйте создать задачу вручную: /addtask" ) return ConversationHandler.END # result содержит структурированные данные задачи task_data = result # Инициализируем временную задачу context.user_data['temp_task'] = { 'client': task_data.get('client_name', 'Не указан'), 'task': task_data.get('task', transcription[:600]), 'deadline': task_data.get('deadline', 'нет'), 'contact_info': task_data.get('contact_info', 'нет'), 'deadline_time': task_data.get('deadline_time') } # Парсим дедлайн, если он есть import date_utils if context.user_data['temp_task']['deadline'] and context.user_data['temp_task']['deadline'] != 'нет': deadline_input = context.user_data['temp_task']['deadline'] success_parse, parsed_date, message = date_utils.parse_date(deadline_input) if success_parse: context.user_data['temp_task']['deadline'] = parsed_date else: # Если не удалось распарсить, оставляем как есть для отображения logger.warning(f"Не удалось распарсить дедлайн '{deadline_input}': {message}") # Парсим время, если оно есть if task_data.get('deadline_time'): time_input = task_data.get('deadline_time') success_parse, parsed_time, message = date_utils.parse_time(time_input) if success_parse: context.user_data['temp_task']['deadline_time'] = parsed_time else: context.user_data['temp_task']['deadline_time'] = None # Импортируем функцию превью из bot from telegram import InlineKeyboardButton, InlineKeyboardMarkup task = context.user_data['temp_task'] # Форматируем дедлайн для отображения deadline_display = date_utils.format_datetime_for_display( task.get('deadline'), task.get('deadline_time') ) # Проверяем, является ли задача личной is_personal = task.get('client') == "✨ Для себя" # Формируем превью if is_personal: preview_text = ( f"📋 Превью задачи:\n\n" f"✨ Личная задача\n" f"📝 Задача: {task['task'][:200]}{'...' if len(task['task']) > 200 else ''}\n" f"📅 Дедлайн: {deadline_display}" ) else: preview_text = ( f"📋 Превью задачи:\n\n" f"👤 Клиент: {task.get('client', 'не указан')}\n" f"📞 Контакт: {task.get('contact_info', 'нет')}\n" f"📝 Задача: {task['task'][:200]}{'...' if len(task['task']) > 200 else ''}\n" f"📅 Дедлайн: {deadline_display}" ) # Создаём кнопки для редактирования if is_personal: keyboard = [ [ InlineKeyboardButton("✏️ Изменить задачу", callback_data="voice_edit_task"), InlineKeyboardButton("✏️ Изменить дедлайн", callback_data="voice_edit_deadline") ], [ InlineKeyboardButton("🕐 Изменить время", callback_data="voice_edit_time"), InlineKeyboardButton("🗑️ Удалить время", callback_data="voice_clear_time") ], [ InlineKeyboardButton("💾 Сохранить", callback_data="voice_save_task"), InlineKeyboardButton("❌ Отменить", callback_data="voice_cancel_task") ] ] else: keyboard = [ [ InlineKeyboardButton("✏️ Изменить клиента", callback_data="voice_edit_client"), InlineKeyboardButton("✏️ Изменить контакт", callback_data="voice_edit_contact") ], [ InlineKeyboardButton("✏️ Изменить задачу", callback_data="voice_edit_task"), InlineKeyboardButton("✏️ Изменить дедлайн", callback_data="voice_edit_deadline") ], [ InlineKeyboardButton("🕐 Изменить время", callback_data="voice_edit_time"), InlineKeyboardButton("🗑️ Удалить время", callback_data="voice_clear_time") ], [ InlineKeyboardButton("💾 Сохранить", callback_data="voice_save_task"), InlineKeyboardButton("❌ Отменить", callback_data="voice_cancel_task") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(preview_text, reply_markup=reply_markup) # Очищаем временные данные голосовой заметки context.user_data.pop('temp_voice_transcription', None) # Переходим в состояние предпросмотра задачи return VOICE_TASK_PREVIEW except Exception as e: logger.error(f"Ошибка при обработке голосовой задачи: {e}") await query.edit_message_text( f"❌ Произошла ошибка при обработке.\n" f"Попробуйте создать задачу вручную: /addtask" ) return ConversationHandler.END async def voice_save_task_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Сохранение задачи из голосового сообщения""" query = update.callback_query await query.answer() try: task_data = context.user_data.get('temp_task', {}) if not task_data: await query.edit_message_text("❌ Ошибка: данные задачи не найдены") return ConversationHandler.END user_id = update.effective_user.id # Импортируем database из bot import database # Сохраняем задачу в БД task_id = database.add_task( user_id=user_id, client_name=task_data.get('client', 'Не указан'), task=task_data.get('task'), deadline=task_data.get('deadline'), contact_info=task_data.get('contact_info'), deadline_time=task_data.get('deadline_time') ) if task_id: await query.edit_message_text( f"✅ Задача #{task_id} успешно создана!\n\n" f"📝 {task_data.get('task', '')[:100]}..." ) else: await query.edit_message_text("❌ Ошибка при сохранении задачи") # Очищаем временные данные context.user_data.pop('temp_task', None) return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при сохранении голосовой задачи: {e}") await query.edit_message_text("❌ Произошла ошибка при сохранении задачи") return ConversationHandler.END async def voice_cancel_task_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Отмена создания задачи из голосового сообщения""" query = update.callback_query await query.answer() # Очищаем временные данные context.user_data.pop('temp_task', None) await query.edit_message_text("❌ Создание задачи отменено") return ConversationHandler.END async def text_save_task_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Сохранение задачи из текстового сообщения""" query = update.callback_query await query.answer() try: task_data = context.user_data.get('temp_task', {}) if not task_data: await query.edit_message_text("❌ Ошибка: данные задачи не найдены") return ConversationHandler.END user_id = update.effective_user.id # Импортируем database из bot import database # Сохраняем задачу в БД task_id = database.add_task( user_id=user_id, client_name=task_data.get('client', 'Не указан'), task=task_data.get('task'), deadline=task_data.get('deadline'), contact_info=task_data.get('contact_info'), deadline_time=task_data.get('deadline_time') ) # Очищаем временные данные context.user_data.pop('temp_task', None) await query.edit_message_text( f"✅ Задача #{task_id} создана из текстового сообщения!\n\n" f"Используйте /tasks для просмотра всех задач." ) return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при сохранении задачи из текста: {e}") await query.edit_message_text("❌ Произошла ошибка при сохранении задачи") return ConversationHandler.END async def text_cancel_task_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Отмена создания задачи из текстового сообщения""" query = update.callback_query await query.answer() # Очищаем временные данные context.user_data.pop('temp_task', None) await query.edit_message_text("❌ Создание задачи отменено") return ConversationHandler.END # ============================================================================ # ConversationHandler для голосовых сообщений без команды # ============================================================================ voice_choice_handler = ConversationHandler( entry_points=[MessageHandler(filters.VOICE & ~filters.COMMAND, handle_voice_choice)], states={ NOTE_CHOICE_TASK_OR_NOTE: [ CallbackQueryHandler(button_voice_to_task, pattern="^voice_to_task$"), CallbackQueryHandler(button_voice_to_note, pattern="^voice_to_note$") ], NOTE_WAITING_AI_CHOICE: [ CallbackQueryHandler(button_structure_with_ai, pattern="^note_structure_ai$"), CallbackQueryHandler(button_save_as_is, pattern="^note_save_as_is$") ], NOTE_WAITING_TAGS: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_note_tags), CallbackQueryHandler(button_select_tag, pattern="^select_tag_\\d+$"), CallbackQueryHandler(button_skip_tags, pattern="^skip_note_tags$") ], NOTE_PREVIEW: [ CallbackQueryHandler(button_save_note, pattern="^save_note$"), CallbackQueryHandler(button_cancel_note, pattern="^cancel_note$") ], VOICE_TASK_PREVIEW: [ CallbackQueryHandler(voice_save_task_handler, pattern="^voice_save_task$"), CallbackQueryHandler(voice_cancel_task_handler, pattern="^voice_cancel_task$") ] }, fallbacks=[CommandHandler('cancel', cancel_note)], per_message=False, allow_reentry=True ) # ============================================================================ # ConversationHandler для текстовых сообщений без команды # ============================================================================ text_choice_handler = ConversationHandler( entry_points=[MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_choice)], states={ TEXT_CHOICE_TASK_OR_NOTE: [ CallbackQueryHandler(button_text_to_task, pattern="^text_to_task$"), CallbackQueryHandler(button_text_to_note, pattern="^text_to_note$") ], NOTE_WAITING_AI_CHOICE: [ CallbackQueryHandler(button_structure_with_ai, pattern="^note_structure_ai$"), CallbackQueryHandler(button_save_as_is, pattern="^note_save_as_is$") ], NOTE_WAITING_TAGS: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_note_tags), CallbackQueryHandler(button_select_tag, pattern="^select_tag_\\d+$"), CallbackQueryHandler(button_skip_tags, pattern="^skip_note_tags$") ], NOTE_PREVIEW: [ CallbackQueryHandler(button_save_note, pattern="^save_note$"), CallbackQueryHandler(button_cancel_note, pattern="^cancel_note$") ], TEXT_TASK_PREVIEW: [ CallbackQueryHandler(text_save_task_handler, pattern="^text_save_task$"), CallbackQueryHandler(text_cancel_task_handler, pattern="^text_cancel_task$") ] }, fallbacks=[CommandHandler('cancel', cancel_note)], per_message=False, allow_reentry=True ) # ============================================================================ # Обработчик пересланных сообщений и URL # ============================================================================ async def handle_forwarded(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработка пересланного сообщения Предлагает выбор: задача или заметка """ message = update.message user_id = update.effective_user.id logger.info(f"Пользователь {user_id} переслал сообщение") # Извлекаем текст из пересланного сообщения forwarded_text = message.text or message.caption or "[Пересланное сообщение]" # Формируем ссылку на пересланное сообщение (если возможно) forwarded_link = None if message.forward_from_chat: # Пересланное из канала/группы chat_username = message.forward_from_chat.username message_id = message.forward_from_message_id if chat_username: forwarded_link = f"https://t.me/{chat_username}/{message_id}" # Сохраняем данные пересланного сообщения context.user_data['temp_forwarded'] = { 'text': forwarded_text[:4096], 'raw_text': forwarded_text, 'message_id': message.forward_from_message_id if message.forward_from_message_id else message.message_id, 'link': forwarded_link } # Показываем превью пересланного сообщения и выбор preview = forwarded_text[:300] + "..." if len(forwarded_text) > 300 else forwarded_text keyboard = [ [InlineKeyboardButton("📋 Это задача", callback_data="forwarded_to_task")], [InlineKeyboardButton("📝 Это заметка", callback_data="forwarded_to_note")] ] reply_markup = InlineKeyboardMarkup(keyboard) link_info = f"\n🔗 Ссылка: {forwarded_link}" if forwarded_link else "" await message.reply_text( f"📨 Пересланное сообщение\n\n" f"{preview}{link_info}\n\n" f"Что вы хотите создать?", reply_markup=reply_markup ) return FORWARDED_CHOICE_TASK_OR_NOTE async def button_forwarded_to_task(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал "задача" для пересланного сообщения """ query = update.callback_query await query.answer() user_id = update.effective_user.id forwarded_data = context.user_data.get('temp_forwarded') if not forwarded_data: await query.edit_message_text("❌ Ошибка: данные не найдены. Попробуйте еще раз.") return ConversationHandler.END forwarded_text = forwarded_data['raw_text'] logger.info(f"Пользователь {user_id} выбрал создание задачи из пересланного сообщения") await query.edit_message_text( f"📋 Создание задачи из пересланного сообщения\n\n" f"⏳ Обрабатываю с помощью AI..." ) try: openrouter_api_key = os.getenv('OPENROUTER_API_KEY') if not openrouter_api_key: await query.edit_message_text( "❌ Ошибка конфигурации: API ключ не найден.\n" "Обратитесь к администратору." ) return ConversationHandler.END import voice_processor success, result = voice_processor.parse_task_from_text( forwarded_text, openrouter_api_key, user_id ) if not success: await query.edit_message_text( f"❌ Не удалось обработать текст:\n{result}\n\n" f"Попробуйте создать задачу вручную: /addtask" ) return ConversationHandler.END task_data = result # Инициализируем временную задачу context.user_data['temp_task'] = { 'client': task_data.get('client_name', 'Не указан'), 'task': task_data.get('task', forwarded_text[:600]), 'deadline': task_data.get('deadline', 'нет'), 'contact_info': task_data.get('contact_info', 'нет'), 'deadline_time': task_data.get('deadline_time') } # Парсим дедлайн import date_utils if context.user_data['temp_task']['deadline'] and context.user_data['temp_task']['deadline'] != 'нет': deadline_input = context.user_data['temp_task']['deadline'] success_parse, parsed_date, message = date_utils.parse_date(deadline_input) if success_parse: context.user_data['temp_task']['deadline'] = parsed_date else: logger.warning(f"Не удалось распарсить дедлайн '{deadline_input}': {message}") # Парсим время if task_data.get('deadline_time'): time_input = task_data.get('deadline_time') success_parse, parsed_time, message = date_utils.parse_time(time_input) if success_parse: context.user_data['temp_task']['deadline_time'] = parsed_time else: context.user_data['temp_task']['deadline_time'] = None # Форматируем превью task = context.user_data['temp_task'] deadline_display = date_utils.format_datetime_for_display( task.get('deadline'), task.get('deadline_time') ) is_personal = task.get('client') == "✨ Для себя" if is_personal: preview_text = ( f"📋 Превью задачи:\n\n" f"✨ Личная задача\n" f"📝 Задача: {task['task'][:200]}{'...' if len(task['task']) > 200 else ''}\n" f"📅 Дедлайн: {deadline_display}" ) else: preview_text = ( f"📋 Превью задачи:\n\n" f"👤 Клиент: {task.get('client', 'не указан')}\n" f"📞 Контакт: {task.get('contact_info', 'нет')}\n" f"📝 Задача: {task['task'][:200]}{'...' if len(task['task']) > 200 else ''}\n" f"📅 Дедлайн: {deadline_display}" ) # Создаём кнопки для редактирования if is_personal: keyboard = [ [ InlineKeyboardButton("✏️ Изменить задачу", callback_data="forwarded_edit_task"), InlineKeyboardButton("✏️ Изменить дедлайн", callback_data="forwarded_edit_deadline") ], [ InlineKeyboardButton("🕐 Изменить время", callback_data="forwarded_edit_time"), InlineKeyboardButton("🗑️ Удалить время", callback_data="forwarded_clear_time") ], [ InlineKeyboardButton("💾 Сохранить", callback_data="forwarded_save_task"), InlineKeyboardButton("❌ Отменить", callback_data="forwarded_cancel_task") ] ] else: keyboard = [ [ InlineKeyboardButton("✏️ Изменить клиента", callback_data="forwarded_edit_client"), InlineKeyboardButton("✏️ Изменить контакт", callback_data="forwarded_edit_contact") ], [ InlineKeyboardButton("✏️ Изменить задачу", callback_data="forwarded_edit_task"), InlineKeyboardButton("✏️ Изменить дедлайн", callback_data="forwarded_edit_deadline") ], [ InlineKeyboardButton("🕐 Изменить время", callback_data="forwarded_edit_time"), InlineKeyboardButton("🗑️ Удалить время", callback_data="forwarded_clear_time") ], [ InlineKeyboardButton("💾 Сохранить", callback_data="forwarded_save_task"), InlineKeyboardButton("❌ Отменить", callback_data="forwarded_cancel_task") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(preview_text, reply_markup=reply_markup) # Очищаем временные данные пересланного сообщения context.user_data.pop('temp_forwarded', None) return FORWARDED_TASK_PREVIEW except Exception as e: logger.error(f"Ошибка при обработке пересланного как задачи: {e}") await query.edit_message_text( f"❌ Произошла ошибка при обработке.\n" f"Попробуйте создать задачу вручную: /addtask" ) return ConversationHandler.END async def button_forwarded_to_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал "заметка" для пересланного сообщения Возвращаем к оригинальному потоку с описанием """ query = update.callback_query await query.answer() user_id = update.effective_user.id forwarded_data = context.user_data.get('temp_forwarded') if not forwarded_data: await query.edit_message_text("❌ Ошибка: данные не найдены.") return ConversationHandler.END logger.info(f"Пользователь {user_id} выбрал создание заметки из пересланного") # Инициализируем заметку context.user_data['temp_note'] = { 'content': forwarded_data['text'], 'raw_content': forwarded_data['raw_text'], 'forwarded_message_id': forwarded_data['message_id'], 'forwarded_link': forwarded_data['link'] } context.user_data['note_mode'] = True # Показываем кнопки добавления описания (оригинальный поток) preview = forwarded_data['text'][:300] + "..." if len(forwarded_data['text']) > 300 else forwarded_data['text'] keyboard = [ [InlineKeyboardButton("📝 Добавить описание", callback_data="add_description")], [InlineKeyboardButton("⏩ Без описания", callback_data="no_description")] ] reply_markup = InlineKeyboardMarkup(keyboard) link_info = f"\n🔗 Ссылка: {forwarded_data['link']}" if forwarded_data['link'] else "" await query.edit_message_text( f"📨 Сохранить в заметки\n\n" f"Пересланное сообщение:\n{preview}{link_info}\n\n" f"Хотите добавить описание?", reply_markup=reply_markup ) # Очищаем временные данные пересланного сообщения context.user_data.pop('temp_forwarded', None) return NOTE_WAITING_COMPANION_TEXT async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработка сообщения со ссылкой """ message = update.message user_id = update.effective_user.id logger.info(f"Пользователь {user_id} отправил сообщение со ссылкой") # Извлекаем все URL из сообщения entities = message.entities or [] urls = [] for entity in entities: if entity.type in ["url", "text_link"]: if entity.type == "url": url = message.text[entity.offset:entity.offset + entity.length] else: url = entity.url urls.append(url) if not urls: # Если нет URL, пропускаем return ConversationHandler.END # Берём первую ссылку url = urls[0] # Текст сообщения без ссылки (или весь текст если есть дополнительный) text = message.text.strip() # Инициализируем заметку context.user_data['temp_note'] = { 'content': text if text != url else f"Ссылка: {url}", 'raw_content': text, 'forwarded_link': url } context.user_data['note_mode'] = True keyboard = [ [InlineKeyboardButton("📝 Добавить описание", callback_data="add_description")], [InlineKeyboardButton("⏩ Без описания", callback_data="no_description")] ] reply_markup = InlineKeyboardMarkup(keyboard) await message.reply_text( f"🔗 Сохранить ссылку в заметки\n\n" f"URL: {url}\n\n" f"Хотите добавить описание к ссылке?", reply_markup=reply_markup ) return NOTE_WAITING_COMPANION_TEXT async def button_add_description(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь хочет добавить описание """ query = update.callback_query await query.answer() await query.edit_message_text( "📝 Добавление описания\n\n" "Напишите описание или комментарий к сохраненному сообщению.\n" "Можно использовать:\n" "• Текст\n" "• Голосовое сообщение\n\n" "Или используйте /cancel для отмены" ) return NOTE_WAITING_COMPANION_TEXT async def button_no_description(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Пользователь выбрал сохранить без описания """ query = update.callback_query await query.answer("Сохраняем без описания") logger.info(f"Пользователь {update.effective_user.id} пропустил добавление описания") # Переходим к выбору тегов return await show_tag_selection(update, context, edit_message=True) async def receive_companion_text(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение сопроводительного текста (описания) """ companion_text = update.message.text.strip() user_id = update.effective_user.id # Проверяем длину if len(companion_text) > 4000: await update.message.reply_text( f"❌ Слишком длинное описание ({len(companion_text)} символов).\n" f"Максимум: 4000 символов." ) return NOTE_WAITING_COMPANION_TEXT # Получаем сохраненный контент temp_note = context.user_data.get('temp_note', {}) original_content = temp_note.get('content', '') forwarded_link = temp_note.get('forwarded_link') # Формируем итоговый контент if forwarded_link: # Если есть ссылка, добавляем её final_content = f"{companion_text}\n\n🔗 {forwarded_link}" else: # Если пересланное сообщение без ссылки final_content = f"{companion_text}\n\n---\nПересланное:\n{original_content}" # Ограничиваем общую длину if len(final_content) > 4096: final_content = final_content[:4096] # Обновляем заметку context.user_data['temp_note']['content'] = final_content context.user_data['temp_note']['raw_content'] = companion_text logger.info(f"Пользователь {user_id} добавил описание длиной {len(companion_text)} символов") # Переходим к выбору тегов return await show_tag_selection(update, context) async def receive_companion_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение голосового описания """ user_id = update.effective_user.id voice = update.message.voice logger.info(f"Пользователь {user_id} отправил голосовое описание") # Показываем индикатор обработки processing_msg = await update.message.reply_text("🎤 Обрабатываю голосовое описание...") try: # Скачиваем и транскрибируем voice_file = await context.bot.get_file(voice.file_id) file_path = f"temp_audio/voice_{user_id}_{voice.file_id}.ogg" os.makedirs("temp_audio", exist_ok=True) await voice_file.download_to_drive(file_path) import voice_processor groq_api_key = os.getenv('GROQ_API_KEY') if not groq_api_key: await processing_msg.delete() await update.message.reply_text( "❌ Голосовые сообщения недоступны (API ключ не настроен)." ) return NOTE_WAITING_COMPANION_TEXT success, transcription = voice_processor.transcribe_audio(file_path, groq_api_key, user_id) if os.path.exists(file_path): os.remove(file_path) await processing_msg.delete() if not success: await update.message.reply_text( "❌ Не удалось распознать голосовое сообщение.\n" "Попробуйте написать текстом." ) return NOTE_WAITING_COMPANION_TEXT # Получаем сохраненный контент temp_note = context.user_data.get('temp_note', {}) original_content = temp_note.get('content', '') forwarded_link = temp_note.get('forwarded_link') # Формируем итоговый контент if forwarded_link: final_content = f"{transcription}\n\n🔗 {forwarded_link}" else: final_content = f"{transcription}\n\n---\nПересланное:\n{original_content}" if len(final_content) > 4096: final_content = final_content[:4096] # Обновляем заметку context.user_data['temp_note']['content'] = final_content context.user_data['temp_note']['raw_content'] = transcription logger.info(f"Голосовое описание распознано: {len(transcription)} символов") # Переходим к выбору тегов return await show_tag_selection(update, context) except Exception as e: logger.error(f"Ошибка при обработке голосового описания: {e}") await processing_msg.delete() await update.message.reply_text( "❌ Произошла ошибка при обработке голосового сообщения.\n" "Попробуйте написать текстом." ) return NOTE_WAITING_COMPANION_TEXT async def forwarded_save_task_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Сохранение задачи из пересланного сообщения""" query = update.callback_query await query.answer() try: task_data = context.user_data.get('temp_task', {}) if not task_data: await query.edit_message_text("❌ Ошибка: данные задачи не найдены") return ConversationHandler.END user_id = update.effective_user.id # Импортируем database из bot import database # Сохраняем задачу в БД task_id = database.add_task( user_id=user_id, client_name=task_data.get('client', 'Не указан'), task=task_data.get('task'), deadline=task_data.get('deadline'), contact_info=task_data.get('contact_info'), deadline_time=task_data.get('deadline_time') ) # Очищаем временные данные context.user_data.pop('temp_task', None) await query.edit_message_text( f"✅ Задача #{task_id} создана из пересланного сообщения!\n\n" f"Используйте /tasks для просмотра всех задач." ) return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при сохранении задачи из пересланного: {e}") await query.edit_message_text("❌ Произошла ошибка при сохранении задачи") return ConversationHandler.END async def forwarded_cancel_task_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Отмена создания задачи из пересланного сообщения""" query = update.callback_query await query.answer() # Очищаем временные данные context.user_data.pop('temp_task', None) await query.edit_message_text("❌ Создание задачи отменено") return ConversationHandler.END # ============================================================================ # ConversationHandler для пересланных сообщений и URL # ============================================================================ forwarded_handler = ConversationHandler( entry_points=[ MessageHandler(filters.FORWARDED & ~filters.COMMAND, handle_forwarded), MessageHandler(filters.Entity("url") & ~filters.COMMAND & ~filters.FORWARDED, handle_url) ], states={ FORWARDED_CHOICE_TASK_OR_NOTE: [ CallbackQueryHandler(button_forwarded_to_task, pattern="^forwarded_to_task$"), CallbackQueryHandler(button_forwarded_to_note, pattern="^forwarded_to_note$") ], NOTE_WAITING_COMPANION_TEXT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_companion_text), MessageHandler(filters.VOICE, receive_companion_voice), CallbackQueryHandler(button_add_description, pattern="^add_description$"), CallbackQueryHandler(button_no_description, pattern="^no_description$") ], NOTE_WAITING_TAGS: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_note_tags), CallbackQueryHandler(button_select_tag, pattern="^select_tag_\\d+$"), CallbackQueryHandler(button_skip_tags, pattern="^skip_note_tags$") ], NOTE_PREVIEW: [ CallbackQueryHandler(button_save_note, pattern="^save_note$"), CallbackQueryHandler(button_cancel_note, pattern="^cancel_note$") ], FORWARDED_TASK_PREVIEW: [ CallbackQueryHandler(forwarded_save_task_handler, pattern="^forwarded_save_task$"), CallbackQueryHandler(forwarded_cancel_task_handler, pattern="^forwarded_cancel_task$") ] }, fallbacks=[CommandHandler('cancel', cancel_note)], per_message=False, allow_reentry=True ) # ============================================================================ # Редактирование заметок # ============================================================================ async def button_edit_note_content(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Начало редактирования содержимого заметки из превью """ query = update.callback_query await query.answer() await query.edit_message_text( "✏️ Редактирование содержимого\n\n" "Отправьте новое содержимое заметки (до 4096 символов).\n\n" "Или используйте /cancel для отмены" ) return EDIT_NOTE_WAITING_CONTENT async def receive_edited_note_content(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение нового содержимого заметки при редактировании """ new_content = update.message.text.strip() user_id = update.effective_user.id # Проверяем длину if len(new_content) > 4096: await update.message.reply_text( f"❌ Слишком длинная заметка ({len(new_content)} символов).\n" f"Максимум: 4096 символов." ) return EDIT_NOTE_WAITING_CONTENT # Обновляем содержимое context.user_data['temp_note']['content'] = new_content context.user_data['temp_note']['raw_content'] = new_content logger.info(f"Пользователь {user_id} отредактировал содержимое заметки: {len(new_content)} символов") # Если редактируем существующую заметку (есть ID), сохраняем изменения note_id = context.user_data['temp_note'].get('id') if note_id: # Обновляем заметку в БД success = notes_database.update_note_content(note_id, user_id, new_content) if success: # Очищаем временные данные context.user_data.pop('temp_note', None) context.user_data.pop('note_mode', None) keyboard = [ [InlineKeyboardButton("👁 Просмотреть", callback_data=f"view_note_{note_id}")], [InlineKeyboardButton("« К списку", callback_data="notes_all_page_0")] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f"✅ Заметка #{note_id} обновлена!", reply_markup=reply_markup ) return ConversationHandler.END else: await update.message.reply_text("❌ Ошибка при сохранении изменений") return ConversationHandler.END # Если создаем новую заметку, показываем превью await update.message.reply_text("✅ Содержимое обновлено") return await show_note_preview(update, context) async def button_edit_note_tags(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Начало редактирования тегов заметки из превью """ query = update.callback_query await query.answer() user_id = update.effective_user.id # Получаем существующие теги current_tags = context.user_data.get('temp_note', {}).get('tags', []) tags_str = ", ".join([f"#{t}" for t in current_tags]) if current_tags else "нет" await query.edit_message_text( f"🏷 Редактирование тегов\n\n" f"Текущие теги: {tags_str}\n\n" f"Введите новые теги (через пробел или запятую).\n" f"Например: работа, идеи, важное\n\n" f"Или используйте /cancel для отмены" ) return EDIT_NOTE_WAITING_TAGS async def receive_edited_note_tags(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Получение новых тегов при редактировании """ tags_text = update.message.text.strip() user_id = update.effective_user.id # Парсим теги import re tags = [] if '#' in tags_text: tags = re.findall(r'#(\w+)', tags_text) else: tags = re.split(r'[,\s]+', tags_text) tags = [t.strip().lower() for t in tags if t.strip()] if len(tags) > 10: await update.message.reply_text( "❌ Максимум 10 тегов.\n" "Попробуйте еще раз." ) return EDIT_NOTE_WAITING_TAGS context.user_data['temp_note']['tags'] = tags logger.info(f"Пользователь {user_id} обновил теги: {tags}") await update.message.reply_text(f"✅ Теги обновлены: {', '.join([f'#{t}' for t in tags])}") # Показываем обновлённое превью return await show_note_preview(update, context) # ============================================================================ # Удаление заметок # ============================================================================ async def button_edit_note_by_id(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Редактирование заметки по ID из списка или просмотра Извлекает ID из callback_data и загружает заметку для редактирования """ query = update.callback_query await query.answer() # Извлекаем ID заметки из callback_data (формат: edit_note_content_123) note_id = int(query.data.split('_')[3]) user_id = update.effective_user.id # Загружаем заметку из БД note = notes_database.get_note_by_id(note_id, user_id) if not note: await query.answer("❌ Заметка не найдена", show_alert=True) return # Инициализируем temp_note для редактирования context.user_data['temp_note'] = { 'id': note_id, 'content': note['content'], 'raw_content': note.get('raw_content', note['content']), 'tags': note.get('tags', []), 'category': note.get('category') } context.user_data['note_mode'] = True await query.edit_message_text( f"✏️ Редактирование заметки #{note_id}\n\n" f"Отправьте новое содержимое заметки (до 4096 символов).\n\n" f"Или используйте /cancel для отмены" ) return EDIT_NOTE_WAITING_CONTENT async def button_delete_note_start(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Запрос подтверждения удаления заметки Вызывается из просмотра заметки (будет реализовано в Фазе 4) """ query = update.callback_query note_id = int(query.data.split('_')[2]) user_id = update.effective_user.id # Получаем заметку для превью note = notes_database.get_note_by_id(note_id, user_id) if not note: await query.answer("❌ Заметка не найдена", show_alert=True) return # Показываем превью и запрашиваем подтверждение preview = note['content'][:100] + "..." if len(note['content']) > 100 else note['content'] keyboard = [ [ InlineKeyboardButton("✅ Да, удалить", callback_data=f"confirm_delete_note_{note_id}"), InlineKeyboardButton("❌ Отмена", callback_data=f"cancel_delete_note_{note_id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( f"⚠️ Удалить заметку #{note_id}?\n\n" f"{preview}\n\n" f"Это действие нельзя отменить!", reply_markup=reply_markup ) async def button_confirm_delete_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Подтверждение удаления заметки """ query = update.callback_query await query.answer() note_id = int(query.data.split('_')[3]) user_id = update.effective_user.id # Удаляем заметку success = notes_database.delete_note(note_id, user_id) if success: logger.info(f"Пользователь {user_id} удалил заметку #{note_id}") await query.edit_message_text( f"✅ Заметка #{note_id} удалена\n\n" f"Все заметки: /notes" ) else: await query.edit_message_text( "❌ Ошибка при удалении заметки.\n" "Возможно, она уже была удалена." ) async def button_cancel_delete_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Отмена удаления заметки """ query = update.callback_query await query.answer("Удаление отменено") note_id = int(query.data.split('_')[3]) await query.edit_message_text( f"❌ Удаление заметки #{note_id} отменено\n\n" f"Используйте /notes для просмотра заметок" ) # ============================================================================ # ConversationHandler для редактирования заметок # ============================================================================ edit_note_handler = ConversationHandler( entry_points=[ CallbackQueryHandler(button_edit_note_by_id, pattern="^edit_note_content_\\d+$"), CallbackQueryHandler(button_edit_note_content, pattern="^edit_note_content$"), CallbackQueryHandler(button_edit_note_tags, pattern="^edit_note_tags$") ], states={ EDIT_NOTE_WAITING_CONTENT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_edited_note_content) ], EDIT_NOTE_WAITING_TAGS: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_edited_note_tags) ], NOTE_PREVIEW: [ CallbackQueryHandler(button_edit_note_content, pattern="^edit_note_content$"), CallbackQueryHandler(button_edit_note_tags, pattern="^edit_note_tags$"), CallbackQueryHandler(button_save_note, pattern="^save_note$"), CallbackQueryHandler(button_cancel_note, pattern="^cancel_note$") ] }, fallbacks=[CommandHandler('cancel', cancel_note)], per_message=False, allow_reentry=True ) # ============================================================================ # НАВИГАЦИЯ И КАТАЛОГ ЗАМЕТОК # ============================================================================ async def notes_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Команда /notes - главное меню заметок""" user_id = update.effective_user.id total_notes = notes_database.get_total_notes_count(user_id) # Клавиатура с действиями и навигацией keyboard = [ [ InlineKeyboardButton("➕ Создать заметку", callback_data="add_note"), InlineKeyboardButton("🌙 Рефлексии", callback_data="notes_reflections") ] ] # Если есть заметки, добавляем навигацию if total_notes > 0: keyboard.extend([ [InlineKeyboardButton(f"📋 Все заметки ({total_notes})", callback_data="notes_all_page_0")], [InlineKeyboardButton("🏷 По тегам", callback_data="notes_by_tags")], [InlineKeyboardButton("🔍 Поиск", callback_data="notes_search")] ]) reply_markup = InlineKeyboardMarkup(keyboard) message_text = "📔 <b>Мои заметки</b>\n\n" if total_notes == 0: message_text += "У вас пока нет заметок.\nСоздайте первую заметку!" else: message_text += f"Всего заметок: {total_notes}\n\nВыбери действие:" await update.message.reply_text(message_text, reply_markup=reply_markup, parse_mode='HTML') async def show_tags_catalog(update: Update, context: ContextTypes.DEFAULT_TYPE): """Каталог тегов с количеством заметок""" query = update.callback_query await query.answer() user_id = update.effective_user.id tags = notes_database.get_user_tags_with_count(user_id) if not tags: await query.edit_message_text( "У вас пока нет тегов.\n\n" "Создайте первую заметку с тегами: /addnote" ) return keyboard = [] for tag in tags: keyboard.append([ InlineKeyboardButton( f"#{tag['name']} ({tag['count']})", callback_data=f"notes_tag_{tag['id']}_page_0" ) ]) # Заметки без тегов untagged_count = notes_database.get_untagged_notes_count(user_id) if untagged_count > 0: keyboard.append([ InlineKeyboardButton( f"Без тега ({untagged_count})", callback_data="notes_untagged_page_0" ) ]) keyboard.append([InlineKeyboardButton("« Главное меню", callback_data="notes_main_menu")]) reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text("🏷 Ваши теги:", reply_markup=reply_markup) async def show_notes_page(update: Update, context: ContextTypes.DEFAULT_TYPE): """Показ страницы заметок (5 на страницу)""" query = update.callback_query await query.answer() user_id = update.effective_user.id # Парсинг callback_data # Формат: notes_all_page_0 или notes_tag_5_page_1 или notes_untagged_page_0 parts = query.data.split('_') if 'tag' in parts: # notes_tag_5_page_1 -> ['notes', 'tag', '5', 'page', '1'] tag_id = int(parts[2]) page = int(parts[4]) notes = notes_database.get_notes_by_tag(user_id, tag_id, limit=5, offset=page*5) total = notes_database.get_tag_notes_count(user_id, tag_id) back_callback = "notes_by_tags" # Получаем имя тега для заголовка all_tags = notes_database.get_user_tags_with_count(user_id) tag_name = next((t['name'] for t in all_tags if t['id'] == tag_id), "тег") title = f"🏷 Заметки с тегом #{tag_name}" elif 'untagged' in parts: # notes_untagged_page_0 -> ['notes', 'untagged', 'page', '0'] page = int(parts[3]) notes = notes_database.get_untagged_notes(user_id, limit=5, offset=page*5) total = notes_database.get_untagged_notes_count(user_id) back_callback = "notes_by_tags" title = "📋 Заметки без тегов" else: # all # notes_all_page_0 -> ['notes', 'all', 'page', '0'] page = int(parts[3]) notes = notes_database.get_all_notes(user_id, limit=5, offset=page*5) total = notes_database.get_total_notes_count(user_id) back_callback = "notes_main_menu" title = f"📋 Все заметки ({total})" if not notes: await query.edit_message_text("Нет заметок") return # Формирование текста text = f"{title}\n\n" keyboard = [] for note in notes: # Превью (первые 200 символов) preview = note['content'][:200] if len(note['content']) > 200: preview += "..." tags_str = ", ".join([f"#{t}" for t in note['tags']]) if note['tags'] else "" date_str = note['created_at'].split()[0] # Только дата text += f"#{note['id']}: {preview}\n" if tags_str: text += f"🏷 {tags_str}\n" text += f"📅 {date_str}\n\n" # Кнопки для каждой заметки keyboard.append([ InlineKeyboardButton(f"👁 #{note['id']}", callback_data=f"view_note_{note['id']}"), InlineKeyboardButton(f"✏️ #{note['id']}", callback_data=f"edit_note_content_{note['id']}"), InlineKeyboardButton(f"🗑 #{note['id']}", callback_data=f"delete_note_{note['id']}") ]) # Пагинация nav_buttons = [] total_pages = (total + 4) // 5 if page > 0: nav_buttons.append( InlineKeyboardButton("◀️", callback_data=query.data.replace(f"page_{page}", f"page_{page-1}")) ) nav_buttons.append(InlineKeyboardButton(f"{page+1}/{total_pages}", callback_data="noop")) if (page + 1) * 5 < total: nav_buttons.append( InlineKeyboardButton("▶️", callback_data=query.data.replace(f"page_{page}", f"page_{page+1}")) ) if nav_buttons: keyboard.append(nav_buttons) keyboard.append([InlineKeyboardButton("« Назад", callback_data=back_callback)]) reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(text, reply_markup=reply_markup) async def view_note_full(update: Update, context: ContextTypes.DEFAULT_TYPE): """Показ полной заметки""" query = update.callback_query await query.answer() note_id = int(query.data.split('_')[2]) user_id = update.effective_user.id note = notes_database.get_note_by_id(note_id, user_id) if not note: await query.answer("⚠️ Заметка не найдена", show_alert=True) return tags_str = ", ".join([f"#{t}" for t in note['tags']]) if note['tags'] else "без тегов" # Если заметка очень длинная (>3500), разбиваем if len(note['content']) > 3500: # Отправляем несколькими сообщениями chunks = [note['content'][i:i+3500] for i in range(0, len(note['content']), 3500)] for i, chunk in enumerate(chunks): if i == 0: text = f"📝 Заметка #{note['id']} (часть {i+1}/{len(chunks)})\n\n{chunk}" else: text = f"(часть {i+1}/{len(chunks)})\n\n{chunk}" await query.message.reply_text(text) # Метаданные и кнопки в последнем сообщении metadata = ( f"\n\n🏷 {tags_str}\n" f"📅 Создана: {note['created_at']}\n" f"🔄 Обновлена: {note['updated_at']}" ) keyboard = [ [ InlineKeyboardButton("✏️ Редактировать", callback_data=f"edit_note_content_{note['id']}"), InlineKeyboardButton("🗑 Удалить", callback_data=f"delete_note_{note['id']}") ], [InlineKeyboardButton("« К списку", callback_data="notes_all_page_0")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.message.reply_text(metadata, reply_markup=reply_markup) else: # Обычный вывод text = ( f"📝 Заметка #{note['id']}\n\n" f"{note['content']}\n\n" f"🏷 {tags_str}\n" f"📅 Создана: {note['created_at']}\n" f"🔄 Обновлена: {note['updated_at']}" ) keyboard = [ [ InlineKeyboardButton("✏️ Редактировать", callback_data=f"edit_note_content_{note['id']}"), InlineKeyboardButton("🗑 Удалить", callback_data=f"delete_note_{note['id']}") ], [InlineKeyboardButton("« К списку", callback_data="notes_all_page_0")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(text, reply_markup=reply_markup) async def button_notes_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): """Возврат в главное меню заметок""" query = update.callback_query await query.answer() user_id = update.effective_user.id total_notes = notes_database.get_total_notes_count(user_id) keyboard = [ [InlineKeyboardButton(f"📋 Все заметки ({total_notes})", callback_data="notes_all_page_0")], [InlineKeyboardButton("🏷 По тегам", callback_data="notes_by_tags")], [InlineKeyboardButton("🔍 Поиск", callback_data="notes_search")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text("📔 Ваши заметки:", reply_markup=reply_markup) async def button_noop(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик для кнопок-заглушек (номер страницы)""" query = update.callback_query await query.answer() # ============================================================================ # ПОИСК ПО ЗАМЕТКАМ # ============================================================================ async def search_notes_start(update: Update, context: ContextTypes.DEFAULT_TYPE): """Начало поиска по заметкам""" query = update.callback_query await query.answer() await query.edit_message_text( "🔍 Поиск по заметкам\n\n" "Введите поисковый запрос:" ) return NOTE_SEARCH_WAITING_QUERY async def search_notes_execute(update: Update, context: ContextTypes.DEFAULT_TYPE): """Выполнение поиска по заметкам""" query_text = update.message.text.strip() user_id = update.effective_user.id if not query_text: await update.message.reply_text("Поисковый запрос не может быть пустым") return ConversationHandler.END # Ищем заметки results = notes_database.search_notes(user_id, query_text, limit=10, offset=0) total = notes_database.get_search_count(user_id, query_text) if not results: await update.message.reply_text( f"🔍 По запросу \"{query_text}\" ничего не найдено" ) return ConversationHandler.END # Показываем результаты text = f"🔍 Результаты поиска: \"{query_text}\"\nНайдено: {total}\n\n" keyboard = [] for note in results: # Превью (первые 150 символов) preview = note['content'][:150] if len(note['content']) > 150: preview += "..." tags_str = ", ".join([f"#{t}" for t in note['tags']]) if note['tags'] else "" date_str = note['created_at'].split()[0] text += f"#{note['id']}: {preview}\n" if tags_str: text += f"🏷 {tags_str}\n" text += f"📅 {date_str}\n\n" # Кнопки для каждой заметки keyboard.append([ InlineKeyboardButton(f"👁 #{note['id']}", callback_data=f"view_note_{note['id']}"), InlineKeyboardButton(f"✏️ #{note['id']}", callback_data=f"edit_note_content_{note['id']}"), InlineKeyboardButton(f"🗑 #{note['id']}", callback_data=f"delete_note_{note['id']}") ]) if total > 10: text += f"\n(Показаны первые 10 из {total} результатов)" keyboard.append([InlineKeyboardButton("« Главное меню", callback_data="notes_main_menu")]) reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(text, reply_markup=reply_markup) return ConversationHandler.END async def cancel_search(update: Update, context: ContextTypes.DEFAULT_TYPE): """Отмена поиска""" await update.message.reply_text("Поиск отменен") return ConversationHandler.END # ============================================================================ # ConversationHandler для поиска # ============================================================================ search_notes_handler = ConversationHandler( entry_points=[ CallbackQueryHandler(search_notes_start, pattern="^notes_search$") ], states={ NOTE_SEARCH_WAITING_QUERY: [ MessageHandler(filters.TEXT & ~filters.COMMAND, search_notes_execute) ] }, fallbacks=[CommandHandler('cancel', cancel_search)], per_message=False, allow_reentry=True ) # ============================================================================ # ВЕЧЕРНИЕ РЕФЛЕКСИИ # ============================================================================ # Кастомный фильтр для распознавания триггеров рефлексии class ReflectionTriggerFilter(filters.MessageFilter): """Фильтр для распознавания фраз, запускающих рефлексию""" def filter(self, message): if not message.text: return False text = message.text.lower().strip() # Ключевые слова для запуска рефлексии reflection_keywords = [ 'рефлексия', 'рефлекси', 'reflect', 'reflection', 'вечерняя рефлексия', 'сейчас будет рефлексия', 'начать рефлексию' ] return any(keyword in text for keyword in reflection_keywords) # Создаем экземпляр фильтра reflection_trigger_filter = ReflectionTriggerFilter() async def handle_reflection_trigger(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработчик текстовых сообщений, которые могут быть триггером для рефлексии Распознает фразы типа "рефлексия", "reflect", "сейчас будет рефлексия" и т.д. """ text = update.message.text.strip() logger.info(f"Обнаружен триггер рефлексии: {text}") # Запускаем режим рефлексии from datetime import datetime context.user_data['temp_note'] = { 'category': 'reflection', 'reflection_date': datetime.now().strftime('%Y-%m-%d') } await update.message.reply_text( "🌙 Вечерняя рефлексия\n\n" "Как прошёл ваш день?\n" "Поделитесь своими мыслями (текст или голос):" ) return REFLECTION_WAITING_CONTENT async def reflect_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Команда /reflect - создание вечерней рефлексии""" from datetime import datetime # Инициализируем temp_note для рефлексии context.user_data['temp_note'] = { 'category': 'reflection', 'reflection_date': datetime.now().strftime('%Y-%m-%d') } await update.message.reply_text( "🌙 Вечерняя рефлексия\n\n" "Как прошёл ваш день?\n" "Поделитесь своими мыслями (текст или голос):" ) return REFLECTION_WAITING_CONTENT async def receive_reflection_text(update: Update, context: ContextTypes.DEFAULT_TYPE): """Получение текстовой рефлексии""" text = update.message.text.strip() user_id = update.effective_user.id if not text: await update.message.reply_text("Рефлексия не может быть пустой. Попробуйте еще раз:") return REFLECTION_WAITING_CONTENT # Проверяем лимит 4096 символов if len(text) > 4096: await update.message.reply_text( f"⚠️ Рефлексия слишком длинная ({len(text)} символов).\n" f"Максимум: 4096 символов.\n\n" f"Попробуйте сократить:" ) return REFLECTION_WAITING_CONTENT # Сохраняем оригинальный текст context.user_data['temp_note']['raw_content'] = text # Предлагаем AI структурирование keyboard = [ [InlineKeyboardButton("✨ Структурировать с AI", callback_data="reflection_structure_ai")], [InlineKeyboardButton("💾 Сохранить как есть", callback_data="reflection_save_as_is")] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f"📝 Вы написали ({len(text)} символов):\n\n" f"{text[:300]}{'...' if len(text) > 300 else ''}\n\n" f"Хотите, чтобы AI структурировал вашу рефлексию?", reply_markup=reply_markup ) return ConversationHandler.END async def receive_reflection_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): """Получение голосовой рефлексии""" user_id = update.effective_user.id # Отправляем сообщение о распознавании processing_msg = await update.message.reply_text("🎤 Распознаю голосовое сообщение...") try: # Импортируем voice_processor для транскрипции import voice_processor # Получаем файл voice_file = await update.message.voice.get_file() # Скачиваем файл import os voice_path = f"voice_{user_id}_{update.message.voice.file_unique_id}.ogg" await voice_file.download_to_drive(voice_path) # Транскрибируем groq_api_key = os.getenv('GROQ_API_KEY') if not groq_api_key: await processing_msg.edit_text( "❌ Голосовые сообщения недоступны (API ключ не настроен)." ) return REFLECTION_WAITING_CONTENT success, transcription = voice_processor.transcribe_audio(voice_path, groq_api_key, user_id) # Удаляем временный файл if os.path.exists(voice_path): os.remove(voice_path) if not success: await processing_msg.edit_text("❌ Не удалось распознать голос. Попробуйте еще раз:") return REFLECTION_WAITING_CONTENT # Сохраняем в temp_note context.user_data['temp_note']['raw_content'] = transcription # Автоматически структурируем с AI api_key = os.getenv('OPENROUTER_API_KEY') if not api_key: # Если нет API ключа, сохраняем как есть context.user_data['temp_note']['content'] = transcription await save_reflection(update, context, processing_msg) return ConversationHandler.END # Структурируем рефлексию await processing_msg.edit_text("✨ Структурирую рефлексию...") success, structured = ai_processor.structure_reflection(transcription, api_key) if success: context.user_data['temp_note']['content'] = structured # Показываем результат preview = structured[:500] + "..." if len(structured) > 500 else structured keyboard = [ [InlineKeyboardButton("💾 Сохранить", callback_data="reflection_save")], [InlineKeyboardButton("🔄 Редактировать", callback_data="reflection_edit")], [InlineKeyboardButton("❌ Отменить", callback_data="cancel_note")] ] reply_markup = InlineKeyboardMarkup(keyboard) await processing_msg.edit_text( f"🌙 Ваша рефлексия:\n\n{preview}", reply_markup=reply_markup ) else: # Если AI не сработал, сохраняем как есть context.user_data['temp_note']['content'] = transcription await save_reflection(update, context, processing_msg) return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при обработке голосовой рефлексии: {e}") await processing_msg.edit_text( "❌ Ошибка при обработке голоса. Попробуйте написать текстом:" ) return REFLECTION_WAITING_CONTENT async def button_reflection_structure_ai(update: Update, context: ContextTypes.DEFAULT_TYPE): """Структурирование рефлексии с AI""" query = update.callback_query await query.answer() raw_content = context.user_data['temp_note'].get('raw_content', '') user_id = update.effective_user.id api_key = os.getenv('OPENROUTER_API_KEY') if not api_key: await query.edit_message_text( "⚠️ AI недоступен. Сохраняю как есть...\n\n" f"{raw_content}" ) context.user_data['temp_note']['content'] = raw_content await save_reflection_after_button(query, context) return ConversationHandler.END await query.edit_message_text("✨ Структурирую рефлексию с помощью AI...") success, structured = ai_processor.structure_reflection(raw_content, api_key) if success: context.user_data['temp_note']['content'] = structured preview = structured[:500] + "..." if len(structured) > 500 else structured keyboard = [ [InlineKeyboardButton("💾 Сохранить", callback_data="reflection_save")], [InlineKeyboardButton("🔄 Редактировать", callback_data="reflection_edit")], [InlineKeyboardButton("❌ Отменить", callback_data="cancel_note")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( f"🌙 Ваша рефлексия:\n\n{preview}", reply_markup=reply_markup ) else: await query.edit_message_text( f"❌ Ошибка AI. Сохраняю оригинал:\n\n{raw_content}" ) context.user_data['temp_note']['content'] = raw_content await save_reflection_after_button(query, context) return ConversationHandler.END async def button_reflection_save_as_is(update: Update, context: ContextTypes.DEFAULT_TYPE): """Сохранение рефлексии без AI""" query = update.callback_query await query.answer() raw_content = context.user_data['temp_note'].get('raw_content', '') context.user_data['temp_note']['content'] = raw_content await save_reflection_after_button(query, context) return ConversationHandler.END async def button_reflection_edit(update: Update, context: ContextTypes.DEFAULT_TYPE): """Редактирование рефлексии после структурирования""" query = update.callback_query await query.answer() await query.edit_message_text( "✏️ Редактирование рефлексии\n\n" "Отправьте новый текст рефлексии:" ) return REFLECTION_WAITING_CONTENT async def button_reflection_save(update: Update, context: ContextTypes.DEFAULT_TYPE): """Сохранение рефлексии из превью""" query = update.callback_query await query.answer() await save_reflection_after_button(query, context) return ConversationHandler.END async def save_reflection(update: Update, context: ContextTypes.DEFAULT_TYPE, message=None): """Сохранение рефлексии в БД""" user_id = update.effective_user.id temp_note = context.user_data.get('temp_note', {}) content = temp_note.get('content', '') raw_content = temp_note.get('raw_content') reflection_date = temp_note.get('reflection_date') # Сохраняем в БД note_id = notes_database.add_note( user_id=user_id, content=content, raw_content=raw_content, category='reflection', reflection_date=reflection_date ) if note_id: if message: await message.edit_text( f"✅ Рефлексия сохранена!\n\n" f"Посмотреть все рефлексии: /reflections" ) else: await update.message.reply_text( f"✅ Рефлексия сохранена!\n\n" f"Посмотреть все рефлексии: /reflections" ) else: error_msg = "❌ Ошибка при сохранении рефлексии" if message: await message.edit_text(error_msg) else: await update.message.reply_text(error_msg) # Очищаем temp_note context.user_data.pop('temp_note', None) async def save_reflection_after_button(query, context): """Сохранение рефлексии после нажатия кнопки""" user_id = query.from_user.id temp_note = context.user_data.get('temp_note', {}) content = temp_note.get('content', '') raw_content = temp_note.get('raw_content') reflection_date = temp_note.get('reflection_date') note_id = notes_database.add_note( user_id=user_id, content=content, raw_content=raw_content, category='reflection', reflection_date=reflection_date ) if note_id: # Добавляем кнопки для просмотра и возврата к списку keyboard = [ [InlineKeyboardButton("👁 Просмотреть", callback_data=f"view_note_{note_id}")], [InlineKeyboardButton("« К списку рефлексий", callback_data="view_reflections")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( f"✅ Рефлексия сохранена!\n\n" f"ID: #{note_id}\n" f"Дата: {reflection_date}", reply_markup=reply_markup ) else: await query.edit_message_text("❌ Ошибка при сохранении рефлексии") context.user_data.pop('temp_note', None) async def view_reflections_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик кнопки просмотра рефлексий (callback)""" query = update.callback_query await query.answer() user_id = update.effective_user.id # Получаем все рефлексии пользователя conn = notes_database.sqlite3.connect('tasks.db') conn.row_factory = notes_database.sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT id, content, reflection_date, created_at FROM notes WHERE user_id = ? AND category = 'reflection' ORDER BY reflection_date DESC LIMIT 10 ''', (user_id,)) reflections = cursor.fetchall() conn.close() if not reflections: await query.edit_message_text( "У вас пока нет рефлексий.\n\n" "Создайте первую: /reflect" ) return text = f"🌙 Ваши рефлексии (последние {len(reflections)}):\n\n" keyboard = [] for refl in reflections: date_str = refl['reflection_date'] or refl['created_at'].split()[0] preview = refl['content'][:100] if len(refl['content']) > 100: preview += "..." text += f"📅 {date_str}\n{preview}\n\n" keyboard.append([ InlineKeyboardButton(f"👁 {date_str}", callback_data=f"view_note_{refl['id']}") ]) keyboard.append([InlineKeyboardButton("« Главное меню заметок", callback_data="notes_main_menu")]) reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(text, reply_markup=reply_markup) async def reflections_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Команда /reflections - просмотр всех рефлексий""" user_id = update.effective_user.id # Получаем все рефлексии пользователя conn = notes_database.sqlite3.connect('tasks.db') conn.row_factory = notes_database.sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT id, content, reflection_date, created_at FROM notes WHERE user_id = ? AND category = 'reflection' ORDER BY reflection_date DESC LIMIT 10 ''', (user_id,)) reflections = cursor.fetchall() conn.close() if not reflections: await update.message.reply_text( "У вас пока нет рефлексий.\n\n" "Создайте первую: /reflect" ) return text = f"🌙 Ваши рефлексии (последние {len(reflections)}):\n\n" keyboard = [] for refl in reflections: date_str = refl['reflection_date'] or refl['created_at'].split()[0] preview = refl['content'][:100] if len(refl['content']) > 100: preview += "..." text += f"📅 {date_str}\n{preview}\n\n" keyboard.append([ InlineKeyboardButton(f"👁 {date_str}", callback_data=f"view_note_{refl['id']}") ]) keyboard.append([InlineKeyboardButton("« Главное меню заметок", callback_data="notes_main_menu")]) reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(text, reply_markup=reply_markup) async def button_start_reflection(update: Update, context: ContextTypes.DEFAULT_TYPE): """Кнопка создания рефлексии из напоминания""" query = update.callback_query await query.answer() from datetime import datetime context.user_data['temp_note'] = { 'category': 'reflection', 'reflection_date': datetime.now().strftime('%Y-%m-%d') } await query.edit_message_text( "🌙 Вечерняя рефлексия\n\n" "Как прошёл ваш день?\n" "Поделитесь своими мыслями (текст или голос):" ) return REFLECTION_WAITING_CONTENT # ============================================================================ # ConversationHandler для рефлексий # ============================================================================ reflection_handler = ConversationHandler( entry_points=[ CommandHandler('reflect', reflect_command), CallbackQueryHandler(button_start_reflection, pattern="^start_reflection$"), CallbackQueryHandler(button_reflection_edit, pattern="^reflection_edit$"), MessageHandler(reflection_trigger_filter & ~filters.COMMAND, handle_reflection_trigger) ], states={ REFLECTION_WAITING_CONTENT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_reflection_text), MessageHandler(filters.VOICE, receive_reflection_voice), CallbackQueryHandler(button_reflection_structure_ai, pattern="^reflection_structure_ai$"), CallbackQueryHandler(button_reflection_save_as_is, pattern="^reflection_save_as_is$"), CallbackQueryHandler(button_reflection_save, pattern="^reflection_save$"), CallbackQueryHandler(button_reflection_edit, pattern="^reflection_edit$") ] }, fallbacks=[CommandHandler('cancel', cancel_note)], per_message=False, allow_reentry=True )