diff --git a/telegram/README_BOT.md b/telegram/README_BOT.md new file mode 100644 index 0000000..d11eb48 --- /dev/null +++ b/telegram/README_BOT.md @@ -0,0 +1,162 @@ +# Telegram Bot для создания постов и планирования путешествий + +## Функции бота + +### 📝 Создание постов +- Создание заголовка и основного текста поста +- Загрузка фото в лучшем качестве или для быстрой загрузки в `public/images/` +- Добавление видео +- Интеграция YouTube видео через Hugo shortcodes +- Локации с Яндекс.Карт +- Автоматическое создание Hugo markdown файлов в `content/post/` +- Генерация правильного front matter для Hugo + +### 🌍 Календарь поездок +- Создание названия и описания поездки +- Добавление фото к поездкам +- Автоматическое обновление файла `content/plan.md` + +### 📅 Управление календарём поездок +- Добавление месячных поездок (системные ежемесячные варианты) +- Добавление специальных поездок (праздничные и особенные события) +- Удаление существующих поездок из списка +- Просмотр всех доступных вариантов поездок +- Автоматическое обновление выпадающего списка в форме на сайте + +### 🔄 Git интеграция +- Автоматический commit всех изменений после создания поста +- Автоматический commit при обновлении календаря поездок +- Автоматический push в репозиторий Gitea/GitHub +- Осмысленные commit сообщения с указанием источника (Telegram бот) + +## Установка и запуск + +1. **Установите зависимости:** + ```bash + pip3 install -r requirements.txt + ``` + +2. **Настройте токен бота:** + ```bash + export TELEGRAM_BOT_TOKEN="ваш_токен_здесь" + ``` + + Или создайте файл `.env`: + ``` + TELEGRAM_BOT_TOKEN=ваш_токен_здесь + ``` + +3. **Запустите бота:** + ```bash + ./start_bot.sh + ``` + + Или напрямую: + ```bash + python3 telegram_bot.py + ``` + +## Использование + +1. Запустите бота командой `/start` +2. Выберите действие из главного меню: + - **📝 Создать пост** - для создания новых постов + - **🌍 Хочу поехать** - для добавления записей в календарь поездок + - **📅 Управление календарём** - для управления вариантами поездок в форме + +### Создание поста +1. Введите заголовок поста +2. Введите основной текст поста +3. Используйте кнопки для добавления: + - 📸 Фото (с выбором качества) + - 🎥 Видео + - 🔗 YouTube ссылки + - 📍 Локации с Яндекс.Карт +4. Нажмите "✅ Опубликовать пост" +5. Бот автоматически создаст Hugo markdown файл и сделает Git commit + +### Календарь поездок +1. Введите название поездки +2. Опишите детали поездки +3. При желании добавьте фото +4. Сохраните запись +5. Бот автоматически обновит `plan.md` и сделает Git commit + +### Управление календарём поездок +1. **Добавить месячную поездку:** + - Выберите "➕ Добавить месячную поездку" + - Введите название в формате "Полёты в [месяц] [год] года" + - Новая опция автоматически добавится в форму на сайте + +2. **Добавить специальную поездку:** + - Выберите "✨ Добавить специальную поездку" + - Введите название события (например: "Новогодние каникулы в горах") + - Опция будет добавлена в выпадающий список + +3. **Удалить поездку:** + - Выберите "❌ Удалить поездку" + - Выберите поездку из списка для удаления + - Опция будет удалена из формы на сайте + +4. **Просмотреть список:** + - Выберите "📋 Просмотреть список" + - Увидите все текущие варианты поездок + +## Интеграция с Hugo и Git + +### Структура файлов +- `content/post/` - markdown файлы постов +- `public/images/` - загруженные изображения и видео +- `content/plan.md` - файл календаря поездок + +### Создаваемые файлы + +#### Hugo пост (content/post/название-поста-20250802.md): +```markdown ++++ +title = 'Название поста' +slug = 'название-поста-20250802' +date = "2025-08-02T14:30:00" +image = 'images/post_20250802_143000_1.jpg' ++++ + +Основной текст поста + +## Фотографии + +{{< gallery dir="/images/" />}} + +## Видео + +{{< youtube dQw4w9WgXcQ >}} + +## Локации + +📍 [Посмотреть на карте](https://yandex.ru/maps/...) + +{{< rawhtml >}} +{{< back-to-top >}} +{{< /rawhtml >}} +``` + +### Git коммиты + +#### Пример коммита для поста: +``` +Добавлен новый пост: Название поста + +🤖 Создано через Telegram бота +``` + +#### Пример коммита для календаря: +``` +Обновлен календарь поездок: Поездка в горы + +🤖 Создано через Telegram бота +``` + +## Требования + +- Git репозиторий должен быть настроен с правами на push +- Бот должен запускаться из корня Hugo проекта +- Необходимые Python библиотеки: `python-telegram-bot`, `requests` \ No newline at end of file diff --git a/telegram/TODO.md b/telegram/TODO.md new file mode 100644 index 0000000..208dd42 --- /dev/null +++ b/telegram/TODO.md @@ -0,0 +1,106 @@ +# TODO для Telegram бота + +## ✅ Выполненные задачи + +- [x] **Add location input for post creation** + - ✅ Добавлен запрос направления/города при создании поста + - ✅ Новый шаг между заголовком и текстом поста + +- [x] **Update image naming to match existing pattern (City-YYYYMMDD-N.jpg)** + - ✅ Изменено именование файлов с `post_timestamp_N.jpg` на `City-YYYYMMDD-N.jpg` + - ✅ Используется введенное направление и текущая дата + +- [x] **Update Hugo post generation to use correct image paths** + - ✅ Обновлена генерация Hugo постов для использования правильных путей к изображениям + - ✅ Учтена новая схема именования файлов + +- [x] **Create function to transliterate city names to Latin** + - ✅ Функция транслитерации с русского на латиницу + - ✅ Обработка популярных городов/направлений (30+ городов) + - ✅ Fallback для неизвестных названий + +- [x] **Update post workflow to include location step** + - ✅ Изменена последовательность создания поста: + 1. Заголовок + 2. **Направление/город** + 3. Основной текст + 4. Медиафайлы + 5. Публикация + +## 🔄 Обновленный workflow бота + +### 📝 Создание постов: +1. **Заголовок поста** - пользователь вводит название +2. **Описание для превью** - краткое описание (1-2 слова) +3. **Направление/город** - пользователь указывает локацию (Москва, Алтай, и т.д.) +4. **Основной текст** - описание поста +5. **Медиафайлы** (опционально): + - Фото с автоматическим именованием `Город-YYYYMMDD-N.jpg` + - Видео с тем же принципом именования + - YouTube ссылки + - Локации Яндекс.Карт +6. **Публикация** - создание Hugo файла и Git commit + +### 📅 Управление календарём поездок: +1. **Добавить месячную поездку** - системные ежемесячные поездки (например: "Полёты в октябре 2025 года") +2. **Добавить специальную поездку** - особенные события (например: "Новогодние каникулы в горах") +3. **Удалить поездку** - удаление существующих опций из выпадающего списка +4. **Просмотреть список** - показать все текущие варианты поездок +5. **Автоматическое обновление** - изменения сразу отражаются в form на сайте + +## 📝 Обновления + +### Новое приветственное сообщение: +``` +🤖 Добро пожаловать в бот предназначенный для загрузки контента на сайт "Пока ты спал"! + +Выберите действие: +``` + +### Добавлено поле description: +- Отображается в Hugo front matter как `description = 'Поход'` +- Используется для превью постов +- Запрашивается после заголовка, до локации + +### Новая последовательность создания поста: +1. Заголовок: "Поездка в горы Алтая" +2. **Описание: "Поход"** ← НОВОЕ +3. Локация: "Алтай" → Altai (с пояснением про организацию фото) +4. Основной текст: "Невероятная поездка..." +5. Медиафайлы и публикация + +## 📸 Обновления по фото + +### Упрощенная загрузка фото: +- Убран выбор качества +- Только загрузка через файл/документ для максимального качества +- Понятные сообщения об ошибках при неправильной загрузке + +### Логика фото: +- **Первое фото** = главное для превью (в front matter как `image`) +- **Остальные фото** = дополнительные для галереи +- Статус указывается при добавлении каждого фото + +### Текст при запросе направления: +``` +Теперь введите направление/город поездки (на русском): +Это нужно для организации фото по названиям файлов. +Например: Москва, Питер, Алтай, Кавказ, Тула... +``` + +### Текст при загрузке фото: +``` +📸 Отправьте фото как файл через Telegram: + +Для сохранения качества обязательно отправляйте фото как документ/файл! +Первое фото будет главным (в превью), остальные добавятся в галерею. +``` + +## Текущая схема именования +Существующие файлы: `Aleksin-20210515-1.jpg`, `Altai-20220912-1.jpg`, etc. +Нужно: `Направление-YYYYMMDD-номер.jpg` + +## Примеры направлений в проекте +- Aleksin, Altai, Moscow, Piter, Tula, Tver, Yaroslavl, Vladimir, Serpuhov, Dmitrov, Kalyazin +- Kavkaz, Murmansk, Kaliningrad, KBR (Кабардино-Балкария) +- Pokrov, Sergiev, Rostov, Ryazan, Spirovo \ No newline at end of file diff --git a/telegram/env_example.txt b/telegram/env_example.txt new file mode 100644 index 0000000..09320d4 --- /dev/null +++ b/telegram/env_example.txt @@ -0,0 +1,2 @@ +# Telegram Bot Configuration +TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE \ No newline at end of file diff --git a/telegram/requirements.txt b/telegram/requirements.txt new file mode 100644 index 0000000..5050803 --- /dev/null +++ b/telegram/requirements.txt @@ -0,0 +1 @@ +python-telegram-bot==20.7 \ No newline at end of file diff --git a/telegram/start_bot.sh b/telegram/start_bot.sh new file mode 100755 index 0000000..4337723 --- /dev/null +++ b/telegram/start_bot.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Скрипт запуска Telegram бота + +echo "🤖 Запуск Telegram бота..." + +# Проверка наличия Python +if ! command -v python3 &> /dev/null; then + echo "❌ Python3 не найден. Установите Python3." + exit 1 +fi + +# Проверка наличия pip +if ! command -v pip3 &> /dev/null; then + echo "❌ pip3 не найден. Установите pip3." + exit 1 +fi + +# Установка зависимостей +echo "📦 Установка зависимостей..." +pip3 install -r requirements.txt + +# Проверка переменной окружения +if [ -z "$TELEGRAM_BOT_TOKEN" ]; then + echo "⚠️ Не установлен TELEGRAM_BOT_TOKEN" + echo "Установите токен бота:" + echo "export TELEGRAM_BOT_TOKEN='ваш_токен_здесь'" + echo "" + echo "Или создайте файл .env и добавьте туда:" + echo "TELEGRAM_BOT_TOKEN=ваш_токен_здесь" + echo "" + read -p "Введите токен бота: " token + export TELEGRAM_BOT_TOKEN="$token" +fi + +# Запуск бота +echo "🚀 Запуск бота..." +python3 telegram_bot.py \ No newline at end of file diff --git a/telegram/telegram_bot.py b/telegram/telegram_bot.py new file mode 100644 index 0000000..0a69944 --- /dev/null +++ b/telegram/telegram_bot.py @@ -0,0 +1,1188 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import logging +import requests +import re +import subprocess +from datetime import datetime +from pathlib import Path +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, ContextTypes, filters +import json + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +# Токен бота (замените на ваш токен) +BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN_HERE') + +# Пути к Hugo проекту +HUGO_PROJECT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__))) +CONTENT_POST_PATH = os.path.join(HUGO_PROJECT_PATH, 'content', 'post') +PUBLIC_IMAGES_PATH = os.path.join(HUGO_PROJECT_PATH, 'public', 'images') +PLAN_FILE_PATH = os.path.join(HUGO_PROJECT_PATH, 'content', 'plan.md') + +# Создаем папки если их нет +os.makedirs(CONTENT_POST_PATH, exist_ok=True) +os.makedirs(PUBLIC_IMAGES_PATH, exist_ok=True) + +# Состояния бота +class BotStates: + MAIN_MENU = 'main_menu' + CREATE_POST = 'create_post' + POST_TEXT = 'post_text' + POST_PHOTO = 'post_photo' + POST_VIDEO = 'post_video' + POST_YOUTUBE = 'post_youtube' + POST_LOCATION = 'post_location' + TRAVEL_CALENDAR = 'travel_calendar' + TRAVEL_TEXT = 'travel_text' + TRAVEL_PHOTO = 'travel_photo' + POST_LOCATION_NAME = 'post_location_name' + POST_DESCRIPTION = 'post_description' + CALENDAR_MANAGE = 'calendar_manage' + CALENDAR_ADD_MONTH = 'calendar_add_month' + CALENDAR_ADD_SPECIAL = 'calendar_add_special' + CALENDAR_REMOVE = 'calendar_remove' + +# Хранилище состояний пользователей +user_states = {} +user_data = {} + +def load_data(filename): + """Загрузка данных из JSON файла""" + try: + with open(filename, 'r', encoding='utf-8') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return [] + +def save_data(filename, data): + """Сохранение данных в JSON файл""" + with open(filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + +def generate_post_slug(title, date): + """Генерация slug для поста в формате Hugo""" + # Убираем специальные символы и заменяем пробелы на дефисы + slug = re.sub(r'[^\w\s-]', '', title.lower()) + slug = re.sub(r'[-\s]+', '-', slug).strip('-') + + # Добавляем дату в формате YYYYMMDD + date_str = date.strftime('%Y%m%d') + + # Если slug пустой, используем дату + if not slug: + slug = f"post-{date_str}" + else: + slug = f"{slug}-{date_str}" + + return slug + +def download_telegram_file(file_id, file_name, context): + """Скачивание файла из Telegram""" + try: + file = context.bot.get_file(file_id) + file_path = os.path.join(PUBLIC_IMAGES_PATH, file_name) + + # Скачиваем файл + response = requests.get(file.file_path) + with open(file_path, 'wb') as f: + f.write(response.content) + + return f"images/{file_name}" + except Exception as e: + logger.error(f"Ошибка при скачивании файла: {e}") + return None + +def create_hugo_post(post_data): + """Создание поста в формате Hugo""" + try: + # Создаем slug и имя файла + post_date = datetime.now() + post_slug = generate_post_slug(post_data.get('title', 'Новый пост'), post_date) + file_name = f"{post_slug}.md" + file_path = os.path.join(CONTENT_POST_PATH, file_name) + + # Генерируем front matter + front_matter = f"""+++ +title = '{post_data.get('title', 'Новый пост')}' +slug = '{post_slug}' +date = "{post_date.strftime('%Y-%m-%dT%H:%M:%S')}" +""" + + # Добавляем описание если есть + if post_data.get('description'): + front_matter += f"description = '{post_data['description']}'\n" + + # Добавляем изображение если есть + if post_data.get('main_image'): + front_matter += f"image = '{post_data['main_image']}'\n" + + front_matter += "+++\n\n" + + # Основной текст поста + content = post_data.get('text', '') + + # Добавляем фотографии в галерею + if post_data.get('photos'): + if len(post_data['photos']) > 1: + content += "\n\n## Фотографии\n\n" + content += "{{< gallery dir=\"/images/\" />}}\n" + elif len(post_data['photos']) == 1: + # Если одна фотография, то она уже указана как main_image в front matter + pass + + # Добавляем YouTube видео + if post_data.get('youtube_links'): + content += "\n\n## Видео\n\n" + for link in post_data['youtube_links']: + video_id = extract_youtube_id(link) + if video_id: + content += f'{{< youtube {video_id} >}}\n\n' + + # Добавляем локации + if post_data.get('locations'): + content += "\n\n## Локации\n\n" + for location in post_data['locations']: + content += f"📍 [Посмотреть на карте]({location})\n\n" + + # Добавляем кнопку "Назад наверх" + content += "\n{{< rawhtml >}}\n{{< back-to-top >}}\n{{< /rawhtml >}}\n" + + # Записываем файл + with open(file_path, 'w', encoding='utf-8') as f: + f.write(front_matter + content) + + return file_path + + except Exception as e: + logger.error(f"Ошибка при создании поста: {e}") + return None + +def extract_youtube_id(url): + """Извлечение ID видео из YouTube URL""" + patterns = [ + r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([^&\n?#]+)', + r'youtube\.com/watch\?.*v=([^&\n?#]+)' + ] + + for pattern in patterns: + match = re.search(pattern, url) + if match: + return match.group(1) + return None + +def add_travel_calendar_entry(entry_data): + """Добавление записи в календарь поездок""" + try: + # Читаем существующий файл plan.md + with open(PLAN_FILE_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + # Находим место для вставки новой записи (после календаря) + calendar_end = content.find('') + if calendar_end == -1: + return False + + # Создаем запись + new_entry = f"\n\n---\n\n### 📅 {datetime.now().strftime('%d.%m.%Y')}\n\n" + new_entry += f"**{entry_data.get('title', 'Новая поездка')}**\n\n" + new_entry += entry_data.get('text', '') + "\n\n" + + if entry_data.get('photo'): + new_entry += f"![Фото поездки]({entry_data['photo']})\n\n" + + # Вставляем запись + insert_position = content.find('\n\nЖелаете отправиться в путешествие?') + if insert_position != -1: + content = content[:insert_position] + new_entry + content[insert_position:] + else: + content += new_entry + + # Записываем обновленный файл + with open(PLAN_FILE_PATH, 'w', encoding='utf-8') as f: + f.write(content) + + return True + + except Exception as e: + logger.error(f"Ошибка при добавлении записи в календарь: {e}") + return False + +def get_current_travel_options(): + """Получение текущих опций поездок из plan.md""" + try: + with open(PLAN_FILE_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + # Ищем секцию select с опциями + select_start = content.find('') + + if select_start == -1 or select_end == -1: + return [] + + select_content = content[select_start:select_end] + + # Извлекаем опции + import re + options = re.findall(r'', select_content) + # Фильтруем пустые опции и исключаем "Свой вариант без БВС" + filtered_options = [(value, text) for value, text in options + if value and text and value != "Свой вариант без БВС"] + + return filtered_options + + except Exception as e: + logger.error(f"Ошибка при чтении опций календаря: {e}") + return [] + +def add_travel_option(option_text): + """Добавление новой опции поездки в plan.md""" + try: + with open(PLAN_FILE_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + # Ищем место для вставки новой опции (перед "Свой вариант без БВС") + target = '' + target_pos = content.find(target) + + if target_pos == -1: + return False + + # Создаем новую опцию + new_option = f' \n ' + + # Вставляем новую опцию + new_content = content[:target_pos] + new_option + content[target_pos:] + + # Записываем обновленный файл + with open(PLAN_FILE_PATH, 'w', encoding='utf-8') as f: + f.write(new_content) + + return True + + except Exception as e: + logger.error(f"Ошибка при добавлении опции поездки: {e}") + return False + +def remove_travel_option(option_text): + """Удаление опции поездки из plan.md""" + try: + with open(PLAN_FILE_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + # Ищем и удаляем опцию + pattern = f' \n' + new_content = re.sub(pattern, '', content) + + # Если не нашли точно такую строку, попробуем без лишних пробелов + if new_content == content: + pattern = f'' + new_content = re.sub(pattern + r'\s*', '', content) + + if new_content != content: + with open(PLAN_FILE_PATH, 'w', encoding='utf-8') as f: + f.write(new_content) + return True + else: + return False + + except Exception as e: + logger.error(f"Ошибка при удалении опции поездки: {e}") + return False + +def run_git_command(command, cwd=None): + """Выполнение Git команды""" + try: + if cwd is None: + cwd = HUGO_PROJECT_PATH + + result = subprocess.run( + command, + shell=True, + cwd=cwd, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode == 0: + logger.info(f"Git команда выполнена: {command}") + return True, result.stdout + else: + logger.error(f"Ошибка Git команды: {command}, {result.stderr}") + return False, result.stderr + + except subprocess.TimeoutExpired: + logger.error(f"Таймаут Git команды: {command}") + return False, "Timeout" + except Exception as e: + logger.error(f"Исключение при выполнении Git команды: {e}") + return False, str(e) + +def git_add_commit_push(files, commit_message): + """Добавление, коммит и пуш файлов в Git""" + try: + # Добавляем файлы + for file_path in files: + success, output = run_git_command(f"git add \"{file_path}\"") + if not success: + return False, f"Ошибка при добавлении файла {file_path}: {output}" + + # Создаем коммит + success, output = run_git_command(f"git commit -m \"{commit_message}\"") + if not success: + return False, f"Ошибка при создании коммита: {output}" + + # Пушим изменения + success, output = run_git_command("git push") + if not success: + return False, f"Ошибка при пуше: {output}" + + return True, "Изменения успешно отправлены в репозиторий" + + except Exception as e: + logger.error(f"Ошибка Git операций: {e}") + return False, str(e) + +def transliterate_city_name(city_name): + """Транслитерация названия города с русского на латиницу""" + # Словарь для известных городов и направлений + known_cities = { + 'москва': 'Moscow', + 'питер': 'Piter', + 'санкт-петербург': 'Piter', + 'спб': 'Piter', + 'тула': 'Tula', + 'тверь': 'Tver', + 'ярославль': 'Yaroslavl', + 'владимир': 'Vladimir', + 'серпухов': 'Serpuhov', + 'дмитров': 'Dmitrov', + 'калязин': 'Kalyazin', + 'кавказ': 'Kavkaz', + 'мурманск': 'Murmansk', + 'калининград': 'Kaliningrad', + 'кбр': 'KBR', + 'кабардино-балкария': 'KBR', + 'покров': 'Pokrov', + 'сергиев': 'Sergiev', + 'сергиев посад': 'Sergiev', + 'ростов': 'Rostov', + 'рязань': 'Ryazan', + 'спирово': 'Spirovo', + 'алексин': 'Aleksin', + 'алтай': 'Altai', + 'клин': 'Klin', + 'коломна': 'Kolomna', + 'можайск': 'Mojaisk', + 'дубна': 'Dubna', + 'кашира': 'Kashira', + 'волоколамск': 'Volok', + 'елки-палки': 'Elki-palki', + 'беломорск': 'Belayagora', + 'ржев': 'Rzhev', + 'ржев': 'Rzhev' + } + + # Приводим к нижнему регистру + city_lower = city_name.lower().strip() + + # Проверяем известные города + if city_lower in known_cities: + return known_cities[city_lower] + + # Простая транслитерация для неизвестных названий + translit_dict = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', + 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', + 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', + 'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', + 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', + ' ': '', '-': '', '_': '' + } + + result = '' + for char in city_lower: + if char in translit_dict: + result += translit_dict[char] + elif char.isalpha(): + result += char + + # Делаем первую букву заглавной + return result.capitalize() if result else 'Unknown' + +def generate_image_filename(location, date, image_number, extension='jpg'): + """Генерация имени файла изображения по схеме Location-YYYYMMDD-N.ext""" + date_str = date.strftime('%Y%m%d') + location_latin = transliterate_city_name(location) + return f"{location_latin}-{date_str}-{image_number}.{extension}" + +def get_main_keyboard(): + """Главная клавиатура""" + keyboard = [ + [KeyboardButton("📝 Создать пост")], + [KeyboardButton("🌍 Хочу поехать")], + [KeyboardButton("📅 Управление календарём")] + ] + return ReplyKeyboardMarkup(keyboard, resize_keyboard=True) + +def get_post_creation_keyboard(): + """Клавиатура для создания поста""" + keyboard = [ + [InlineKeyboardButton("📸 Добавить фото", callback_data="post_photo")], + [InlineKeyboardButton("🎥 Добавить видео", callback_data="post_video")], + [InlineKeyboardButton("🔗 YouTube ссылка", callback_data="post_youtube")], + [InlineKeyboardButton("📍 Локация (Яндекс.Карты)", callback_data="post_location")], + [InlineKeyboardButton("✅ Опубликовать пост", callback_data="publish_post")], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + return InlineKeyboardMarkup(keyboard) + +def get_photo_quality_keyboard(): + """Клавиатура для выбора качества фото (не используется, оставлена для совместимости)""" + keyboard = [ + [InlineKeyboardButton("🔙 Назад", callback_data="back_to_post")] + ] + return InlineKeyboardMarkup(keyboard) + +def get_calendar_management_keyboard(): + """Клавиатура для управления календарём""" + keyboard = [ + [InlineKeyboardButton("➕ Добавить месячную поездку", callback_data="calendar_add_monthly")], + [InlineKeyboardButton("✨ Добавить специальную поездку", callback_data="calendar_add_special")], + [InlineKeyboardButton("❌ Удалить поездку", callback_data="calendar_remove")], + [InlineKeyboardButton("📋 Просмотреть список", callback_data="calendar_list")], + [InlineKeyboardButton("🔙 Назад", callback_data="back_to_main")] + ] + return InlineKeyboardMarkup(keyboard) + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда /start""" + user_id = update.effective_user.id + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + + welcome_text = ( + "🤖 Добро пожаловать в бот предназначенный для загрузки контента на сайт \"Пока ты спал\"!\n\n" + "Выберите действие:" + ) + + await update.message.reply_text( + welcome_text, + reply_markup=get_main_keyboard() + ) + +async def handle_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка главного меню""" + user_id = update.effective_user.id + text = update.message.text + + if text == "📝 Создать пост": + user_states[user_id] = BotStates.CREATE_POST + user_data[user_id] = { + 'type': 'post', + 'title': '', + 'description': '', + 'location': '', + 'text': '', + 'photos': [], + 'videos': [], + 'youtube_links': [], + 'locations': [], + 'main_image': None + } + + await update.message.reply_text( + "📝 Создание нового поста для Hugo сайта\n\n" + "Сначала введите заголовок поста:", + reply_markup=None + ) + user_states[user_id] = BotStates.POST_DESCRIPTION + + elif text == "🌍 Хочу поехать": + user_states[user_id] = BotStates.TRAVEL_CALENDAR + user_data[user_id] = { + 'type': 'travel', + 'title': '', + 'text': '', + 'photo': None + } + + await update.message.reply_text( + "🌍 Добавление записи в календарь поездок\n\n" + "Введите название поездки:", + reply_markup=None + ) + user_states[user_id] = BotStates.TRAVEL_TEXT + + elif text == "📅 Управление календарём": + user_states[user_id] = BotStates.CALENDAR_MANAGE + await update.message.reply_text( + "📅 **Управление календарём поездок**\n\n" + "Выберите действие:", + reply_markup=get_calendar_management_keyboard() + ) + +async def handle_post_description(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка описания поста""" + user_id = update.effective_user.id + text = update.message.text + + if not user_data[user_id].get('title'): + # Первое сообщение - заголовок + user_data[user_id]['title'] = text + await update.message.reply_text( + f"✅ Заголовок сохранен: {text}\n\n" + "Теперь введите краткое описание для превью (1-2 слова):\n" + "Например: Поход, Экскурсия, Отдых, Приключение..." + ) + elif not user_data[user_id].get('description'): + # Второе сообщение - описание + user_data[user_id]['description'] = text + await update.message.reply_text( + f"✅ Описание сохранено: {text}\n\n" + "Теперь введите направление/город поездки (на русском):\n" + "Это нужно для организации фото по названиям файлов.\n" + "Например: Москва, Питер, Алтай, Кавказ, Тула..." + ) + user_states[user_id] = BotStates.POST_LOCATION_NAME + +async def handle_post_location_name(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка названия локации для поста""" + user_id = update.effective_user.id + text = update.message.text + + # Третье сообщение - локация + user_data[user_id]['location'] = text + location_latin = transliterate_city_name(text) + await update.message.reply_text( + f"✅ Направление сохранено: {text} → {location_latin}\n\n" + "Теперь введите основной текст поста:" + ) + user_states[user_id] = BotStates.POST_TEXT + +async def handle_post_text(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка основного текста поста""" + user_id = update.effective_user.id + text = update.message.text + + # Четвертое сообщение - основной текст + user_data[user_id]['text'] = text + await update.message.reply_text( + f"✅ Текст поста сохранен!\n\n" + f"📝 Заголовок: {user_data[user_id]['title']}\n" + f"📋 Описание: {user_data[user_id]['description']}\n" + f"📍 Направление: {user_data[user_id]['location']}\n\n" + "Теперь выберите дополнительные опции:", + reply_markup=get_post_creation_keyboard() + ) + +async def handle_travel_text(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка текста для календаря поездок""" + user_id = update.effective_user.id + text = update.message.text + + if not user_data[user_id].get('title'): + # Первое сообщение - название + user_data[user_id]['title'] = text + await update.message.reply_text( + f"✅ Название сохранено: {text}\n\n" + "Теперь опишите детали поездки:" + ) + else: + # Второе сообщение - описание + user_data[user_id]['text'] = text + + keyboard = [ + [InlineKeyboardButton("📸 Добавить фото", callback_data="travel_photo")], + [InlineKeyboardButton("✅ Сохранить", callback_data="save_travel")], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + await update.message.reply_text( + f"✅ Описание сохранено!\n\n" + f"Название: {user_data[user_id]['title']}\n" + f"Описание: {text}\n\n" + "Хотите добавить фото?", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + +async def handle_callback_query(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка callback запросов""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + data = query.data + + if data == "post_photo": + user_states[user_id] = BotStates.POST_PHOTO + await query.edit_message_text( + "📸 Отправьте фото как файл через Telegram:\n\n" + "Для сохранения качества обязательно отправляйте фото как документ/файл!\n" + "Первое фото будет главным (в превью), остальные добавятся в галерею." + ) + + elif data == "post_video": + user_states[user_id] = BotStates.POST_VIDEO + await query.edit_message_text( + "🎥 Отправьте видео:" + ) + + elif data == "post_youtube": + user_states[user_id] = BotStates.POST_YOUTUBE + await query.edit_message_text( + "🔗 Отправьте ссылку на YouTube видео:" + ) + + elif data == "post_location": + user_states[user_id] = BotStates.POST_LOCATION + await query.edit_message_text( + "📍 Отправьте ссылку на Яндекс.Карты:" + ) + + elif data == "travel_photo": + user_states[user_id] = BotStates.TRAVEL_PHOTO + await query.edit_message_text( + "📸 Отправьте фото как файл для календаря поездок:" + ) + + elif data == "publish_post": + await publish_hugo_post(query, user_id, context) + + elif data == "save_travel": + await save_travel_entry(query, user_id, context) + + elif data == "calendar_add_monthly": + user_states[user_id] = BotStates.CALENDAR_ADD_MONTH + await query.edit_message_text( + "➕ **Добавление месячной поездки**\n\n" + "Введите название поездки в формате:\n" + "`Полёты в [месяц] [год] года`\n\n" + "Например:\n" + "• Полёты в октябре 2025 года\n" + "• Полёты в ноябре 2025 года\n" + "• Полёты в декабре 2025 года" + ) + + elif data == "calendar_add_special": + user_states[user_id] = BotStates.CALENDAR_ADD_SPECIAL + await query.edit_message_text( + "✨ **Добавление специальной поездки**\n\n" + "Введите название специальной поездки:\n\n" + "Например:\n" + "• Новогодние каникулы в горах\n" + "• Майские праздники на природе\n" + "• Летний фестиваль" + ) + + elif data == "calendar_remove": + await handle_calendar_remove_callback(query, user_id, context) + + elif data == "calendar_list": + await handle_calendar_list_callback(query, user_id, context) + + elif data == "back_to_main": + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + await query.edit_message_text("Выберите действие:") + await context.bot.send_message( + chat_id=user_id, + text="Главное меню:", + reply_markup=get_main_keyboard() + ) + + elif data == "cancel": + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + await query.edit_message_text("❌ Операция отменена") + await context.bot.send_message( + chat_id=user_id, + text="Выберите действие:", + reply_markup=get_main_keyboard() + ) + + elif data.startswith("remove_trip_"): + trip_value = data.replace("remove_trip_", "") + success = remove_travel_option(trip_value) + + if success: + # Создаем Git коммит + commit_message = f"Удалена поездка: {trip_value}\n\n🤖 Создано через Telegram бота" + git_success, git_message = git_add_commit_push(["content/plan.md"], commit_message) + + message = f"✅ Поездка удалена!\n\n" + message += f"❌ Удалено: {trip_value}\n" + message += f"💡 Обновлен файл: content/plan.md\n" + + # Добавляем информацию о Git операции + if git_success: + message += f"🔄 {git_message}" + else: + message += f"⚠️ Git ошибка: {git_message}" + else: + message = "❌ Ошибка при удалении поездки" + + await query.edit_message_text( + message, + reply_markup=InlineKeyboardMarkup([[ + InlineKeyboardButton("🔙 Назад", callback_data="back_to_main") + ]]) + ) + + elif data == "back_to_post": + await query.edit_message_text( + "📝 Создание поста\n\n" + "Выберите дополнительные опции:", + reply_markup=get_post_creation_keyboard() + ) + +async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка фотографий""" + user_id = update.effective_user.id + state = user_states.get(user_id) + + if state == BotStates.POST_PHOTO: + # Проверяем, что фото отправлено как документ + if update.message.document: + photo = update.message.document + + # Генерируем имя файла по схеме Location-YYYYMMDD-N.ext + current_date = datetime.now() + file_extension = 'jpg' + if hasattr(photo, 'file_name') and photo.file_name: + file_extension = photo.file_name.split('.')[-1].lower() + + location = user_data[user_id].get('location', 'Unknown') + image_number = len(user_data[user_id]['photos']) + 1 + file_name = generate_image_filename(location, current_date, image_number, file_extension) + + # Скачиваем файл + image_path = download_telegram_file(photo.file_id, file_name, context) + + if image_path: + user_data[user_id]['photos'].append(image_path) + + # Первое фото делаем главным + if not user_data[user_id].get('main_image'): + user_data[user_id]['main_image'] = image_path + status = "главное фото для превью" + else: + status = "дополнительное фото для галереи" + + await update.message.reply_text( + f"✅ Фото добавлено как {status}!\n" + f"📸 Всего фото: {len(user_data[user_id]['photos'])}\n" + f"💾 Сохранено: {image_path}", + reply_markup=get_post_creation_keyboard() + ) + else: + await update.message.reply_text( + "❌ Ошибка при загрузке фото", + reply_markup=get_post_creation_keyboard() + ) + else: + # Если фото отправлено обычным способом + await update.message.reply_text( + "⚠️ Пожалуйста, отправьте фото как файл/документ!\n\n" + "Это необходимо для сохранения качества.\n" + "Нажмите на скрепку и выберите 'Файл'.", + reply_markup=get_post_creation_keyboard() + ) + + elif state == BotStates.TRAVEL_PHOTO: + # Проверяем, что фото отправлено как документ + if update.message.document: + photo = update.message.document + + # Генерируем имя файла + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + file_extension = 'jpg' + if hasattr(photo, 'file_name') and photo.file_name: + file_extension = photo.file_name.split('.')[-1].lower() + + file_name = f"travel_{timestamp}.{file_extension}" + + # Скачиваем файл + image_path = download_telegram_file(photo.file_id, file_name, context) + + if image_path: + user_data[user_id]['photo'] = image_path + + keyboard = [ + [InlineKeyboardButton("✅ Сохранить", callback_data="save_travel")], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + await update.message.reply_text( + f"✅ Фото добавлено!\n" + f"💾 Сохранено: {image_path}", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + else: + await update.message.reply_text("❌ Ошибка при загрузке фото") + else: + # Если фото отправлено обычным способом + await update.message.reply_text( + "⚠️ Пожалуйста, отправьте фото как файл/документ!\n\n" + "Это необходимо для сохранения качества.\n" + "Нажмите на скрепку и выберите 'Файл'." + ) + +async def handle_video(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка видео""" + user_id = update.effective_user.id + state = user_states.get(user_id) + + if state == BotStates.POST_VIDEO: + video = update.message.video + + # Генерируем имя файла по схеме Location-YYYYMMDD-N.mp4 + current_date = datetime.now() + location = user_data[user_id].get('location', 'Unknown') + video_number = len(user_data[user_id]['videos']) + 1 + file_name = generate_image_filename(location, current_date, video_number, 'mp4') + + # Скачиваем файл + video_path = download_telegram_file(video.file_id, file_name, context) + + if video_path: + user_data[user_id]['videos'].append(video_path) + await update.message.reply_text( + f"✅ Видео добавлено к посту!\n" + f"Сохранено: {video_path}", + reply_markup=get_post_creation_keyboard() + ) + else: + await update.message.reply_text( + "❌ Ошибка при загрузке видео", + reply_markup=get_post_creation_keyboard() + ) + +async def handle_text_input(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка текстового ввода""" + user_id = update.effective_user.id + state = user_states.get(user_id) + text = update.message.text + + if state == BotStates.POST_YOUTUBE: + user_data[user_id]['youtube_links'].append(text) + await update.message.reply_text( + "✅ YouTube ссылка добавлена!", + reply_markup=get_post_creation_keyboard() + ) + + elif state == BotStates.POST_LOCATION: + user_data[user_id]['locations'].append(text) + await update.message.reply_text( + "✅ Локация добавлена!", + reply_markup=get_post_creation_keyboard() + ) + +async def publish_hugo_post(query, user_id, context): + """Публикация поста в Hugo""" + post_data = user_data[user_id].copy() + + # Создаем Hugo пост + file_path = create_hugo_post(post_data) + + if file_path: + # Подготавливаем файлы для Git коммита + files_to_commit = [] + + # Добавляем markdown файл поста + relative_post_path = os.path.relpath(file_path, HUGO_PROJECT_PATH) + files_to_commit.append(relative_post_path) + + # Добавляем все загруженные изображения и видео + for photo_path in post_data.get('photos', []): + if photo_path.startswith('images/'): + # Конвертируем относительный путь в путь от корня проекта + full_image_path = f"public/{photo_path}" + files_to_commit.append(full_image_path) + + for video_path in post_data.get('videos', []): + if video_path.startswith('images/'): + # Конвертируем относительный путь в путь от корня проекта + full_video_path = f"public/{video_path}" + files_to_commit.append(full_video_path) + + # Создаем Git коммит + commit_message = f"Добавлен новый пост: {post_data['title']}\n\n🤖 Создано через Telegram бота" + git_success, git_message = git_add_commit_push(files_to_commit, commit_message) + + # Формирование сообщения о публикации + message = f"✅ Пост опубликован в Hugo!\n\n" + message += f"📝 Заголовок: {post_data['title']}\n" + message += f"📋 Описание: {post_data['description']}\n" + message += f"📄 Файл: {os.path.basename(file_path)}\n" + + if post_data['photos']: + message += f"📸 Фото: {len(post_data['photos'])} шт.\n" + if post_data['videos']: + message += f"🎥 Видео: {len(post_data['videos'])} шт.\n" + if post_data['youtube_links']: + message += f"🔗 YouTube: {len(post_data['youtube_links'])} ссылок\n" + if post_data['locations']: + message += f"📍 Локации: {len(post_data['locations'])} шт.\n" + + message += f"\n💡 Файл сохранен: content/post/{os.path.basename(file_path)}\n" + + # Добавляем информацию о Git операции + if git_success: + message += f"🔄 {git_message}" + else: + message += f"⚠️ Git ошибка: {git_message}" + + await query.edit_message_text(message) + else: + await query.edit_message_text("❌ Ошибка при создании поста") + + # Возврат в главное меню + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + + await query.message.reply_text( + "Выберите действие:", + reply_markup=get_main_keyboard() + ) + +async def save_travel_entry(query, user_id, context): + """Сохранение записи в календарь поездок""" + travel_data = user_data[user_id].copy() + + # Добавляем запись в план + success = add_travel_calendar_entry(travel_data) + + if success: + # Подготавливаем файлы для Git коммита + files_to_commit = ["content/plan.md"] + + # Добавляем фото если есть + if travel_data.get('photo') and travel_data['photo'].startswith('images/'): + full_image_path = f"public/{travel_data['photo']}" + files_to_commit.append(full_image_path) + + # Создаем Git коммит + commit_message = f"Обновлен календарь поездок: {travel_data['title']}\n\n🤖 Создано через Telegram бота" + git_success, git_message = git_add_commit_push(files_to_commit, commit_message) + + message = f"✅ Запись добавлена в календарь поездок!\n\n" + message += f"📝 Название: {travel_data['title']}\n" + message += f"📋 Описание: {travel_data['text']}\n" + + if travel_data.get('photo'): + message += f"📸 Фото: {travel_data['photo']}\n" + + message += f"\n💡 Обновлен файл: content/plan.md\n" + + # Добавляем информацию о Git операции + if git_success: + message += f"🔄 {git_message}" + else: + message += f"⚠️ Git ошибка: {git_message}" + + await query.edit_message_text(message) + else: + await query.edit_message_text("❌ Ошибка при добавлении записи") + + # Возврат в главное меню + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + + await query.message.reply_text( + "Выберите действие:", + reply_markup=get_main_keyboard() + ) + +async def handle_calendar_add_monthly(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка добавления месячной поездки""" + user_id = update.effective_user.id + text = update.message.text.strip() + + # Проверяем формат ввода (должен содержать месяц и год) + if not text: + await update.message.reply_text( + "❌ Пожалуйста, введите название поездки.\n" + "Например: Полёты в октябре 2025 года" + ) + return + + # Добавляем опцию в план + success = add_travel_option(text) + + if success: + # Создаем Git коммит + commit_message = f"Добавлена месячная поездка: {text}\n\n🤖 Создано через Telegram бота" + git_success, git_message = git_add_commit_push(["content/plan.md"], commit_message) + + message = f"✅ Месячная поездка добавлена!\n\n" + message += f"📅 Название: {text}\n" + message += f"💡 Обновлен файл: content/plan.md\n" + + # Добавляем информацию о Git операции + if git_success: + message += f"🔄 {git_message}" + else: + message += f"⚠️ Git ошибка: {git_message}" + + await update.message.reply_text(message, reply_markup=get_main_keyboard()) + else: + await update.message.reply_text( + "❌ Ошибка при добавлении поездки", + reply_markup=get_main_keyboard() + ) + + # Возврат в главное меню + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + +async def handle_calendar_add_special(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка добавления специальной поездки""" + user_id = update.effective_user.id + text = update.message.text.strip() + + if not text: + await update.message.reply_text( + "❌ Пожалуйста, введите название специальной поездки.\n" + "Например: Новогодние каникулы в горах" + ) + return + + # Добавляем опцию в план + success = add_travel_option(text) + + if success: + # Создаем Git коммит + commit_message = f"Добавлена специальная поездка: {text}\n\n🤖 Создано через Telegram бота" + git_success, git_message = git_add_commit_push(["content/plan.md"], commit_message) + + message = f"✅ Специальная поездка добавлена!\n\n" + message += f"✨ Название: {text}\n" + message += f"💡 Обновлен файл: content/plan.md\n" + + # Добавляем информацию о Git операции + if git_success: + message += f"🔄 {git_message}" + else: + message += f"⚠️ Git ошибка: {git_message}" + + await update.message.reply_text(message, reply_markup=get_main_keyboard()) + else: + await update.message.reply_text( + "❌ Ошибка при добавлении поездки", + reply_markup=get_main_keyboard() + ) + + # Возврат в главное меню + user_states[user_id] = BotStates.MAIN_MENU + user_data[user_id] = {} + +async def handle_calendar_remove_callback(query, user_id, context): + """Обработка удаления поездки из календаря""" + options = get_current_travel_options() + + if not options: + await query.edit_message_text( + "❌ Нет доступных поездок для удаления", + reply_markup=InlineKeyboardMarkup([[ + InlineKeyboardButton("🔙 Назад", callback_data="back_to_main") + ]]) + ) + return + + # Создаем клавиатуру с опциями для удаления + keyboard = [] + for value, text in options: + keyboard.append([InlineKeyboardButton( + f"❌ {text}", + callback_data=f"remove_trip_{value}" + )]) + keyboard.append([InlineKeyboardButton("🔙 Назад", callback_data="back_to_main")]) + + await query.edit_message_text( + "❌ **Удаление поездки**\n\n" + "Выберите поездку для удаления:", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + +async def handle_calendar_list_callback(query, user_id, context): + """Показать список текущих поездок""" + options = get_current_travel_options() + + if not options: + message = "📋 **Список поездок**\n\n❌ Пока нет добавленных поездок" + else: + message = "📋 **Список поездок**\n\n" + for i, (value, text) in enumerate(options, 1): + message += f"{i}. {text}\n" + + await query.edit_message_text( + message, + reply_markup=InlineKeyboardMarkup([[ + InlineKeyboardButton("🔙 Назад", callback_data="back_to_main") + ]]) + ) + +def main(): + """Основная функция запуска бота""" + # Создание приложения + application = Application.builder().token(BOT_TOKEN).build() + + # Добавление обработчиков + application.add_handler(CommandHandler("start", start)) + application.add_handler(CallbackQueryHandler(handle_callback_query)) + + # Обработчики для разных типов контента + application.add_handler(MessageHandler(filters.PHOTO | filters.Document.IMAGE, handle_photo)) + application.add_handler(MessageHandler(filters.VIDEO, handle_video)) + + # Обработчик текстовых сообщений + application.add_handler(MessageHandler( + filters.TEXT & ~filters.COMMAND, + lambda update, context: handle_message_by_state(update, context) + )) + + print("🤖 Hugo Bot запущен!") + print(f"📁 Посты сохраняются в: {CONTENT_POST_PATH}") + print(f"📸 Изображения сохраняются в: {PUBLIC_IMAGES_PATH}") + print(f"🌍 Календарь поездок: {PLAN_FILE_PATH}") + application.run_polling() + +async def handle_message_by_state(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Маршрутизация сообщений по состояниям""" + user_id = update.effective_user.id + state = user_states.get(user_id, BotStates.MAIN_MENU) + + if state == BotStates.MAIN_MENU: + await handle_main_menu(update, context) + elif state == BotStates.POST_DESCRIPTION: + await handle_post_description(update, context) + elif state == BotStates.POST_LOCATION_NAME: + await handle_post_location_name(update, context) + elif state == BotStates.POST_TEXT: + await handle_post_text(update, context) + elif state == BotStates.TRAVEL_TEXT: + await handle_travel_text(update, context) + elif state == BotStates.CALENDAR_ADD_MONTH: + await handle_calendar_add_monthly(update, context) + elif state == BotStates.CALENDAR_ADD_SPECIAL: + await handle_calendar_add_special(update, context) + elif state in [BotStates.POST_YOUTUBE, BotStates.POST_LOCATION]: + await handle_text_input(update, context) + else: + await update.message.reply_text( + "Используйте /start для начала работы с ботом." + ) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/temp_aug/Bogolubovo_20250419_0.md b/temp_aug/Bogolubovo_20250419_0.md deleted file mode 100644 index 7768854..0000000 --- a/temp_aug/Bogolubovo_20250419_0.md +++ /dev/null @@ -1,29 +0,0 @@ -+++ -title = 'Боголюбово' -slug = 'Bogolubovo0425' -image = 'images/Bogolubovo_20250419_1.jpg' -date = "2025-04-19T00:00:00" -description = 'Покров на Нерли' -disqus_identifier = '128' -+++ -В этот раз тоже не без приключений - сразу после рассвета пошёл дождь. -Но полёты всё же состоялись. - -![Bogolubovo0425](/images/Bogolubovo_20250419_2.jpg) - -![Bogolubovo0425](/images/Bogolubovo_20250419_3.jpg) - -![Bogolubovo0425](/images/Bogolubovo_20250419_4.jpg) - -![Bogolubovo0425](/images/Bogolubovo_20250419_5.jpg) - -![Bogolubovo0425](/images/Bogolubovo_20250419_6.jpg) - -Локация -{{< rawhtml >}} - -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} \ No newline at end of file diff --git a/temp_aug/Bogolubovo_20250419_1.jpg b/temp_aug/Bogolubovo_20250419_1.jpg deleted file mode 100644 index 96657b4..0000000 Binary files a/temp_aug/Bogolubovo_20250419_1.jpg and /dev/null differ diff --git a/temp_aug/Bogolubovo_20250419_2.jpg b/temp_aug/Bogolubovo_20250419_2.jpg deleted file mode 100644 index 13c8877..0000000 Binary files a/temp_aug/Bogolubovo_20250419_2.jpg and /dev/null differ diff --git a/temp_aug/Bogolubovo_20250419_3.jpg b/temp_aug/Bogolubovo_20250419_3.jpg deleted file mode 100644 index f96b7c7..0000000 Binary files a/temp_aug/Bogolubovo_20250419_3.jpg and /dev/null differ diff --git a/temp_aug/Bogolubovo_20250419_4.jpg b/temp_aug/Bogolubovo_20250419_4.jpg deleted file mode 100644 index 19cae23..0000000 Binary files a/temp_aug/Bogolubovo_20250419_4.jpg and /dev/null differ diff --git a/temp_aug/Bogolubovo_20250419_5.jpg b/temp_aug/Bogolubovo_20250419_5.jpg deleted file mode 100644 index dd283e6..0000000 Binary files a/temp_aug/Bogolubovo_20250419_5.jpg and /dev/null differ diff --git a/temp_aug/Bogolubovo_20250419_6.jpg b/temp_aug/Bogolubovo_20250419_6.jpg deleted file mode 100644 index b85df4c..0000000 Binary files a/temp_aug/Bogolubovo_20250419_6.jpg and /dev/null differ diff --git a/temp_aug/Dronoslet_20240705_0.md b/temp_aug/Dronoslet_20240705_0.md deleted file mode 100644 index 4b352c6..0000000 --- a/temp_aug/Dronoslet_20240705_0.md +++ /dev/null @@ -1,34 +0,0 @@ -+++ -title = 'Дронослёт' -slug = 'Dronoslet' -image = 'images/Dronoslet_20240705_1.jpg' -date = "2024-06-05T00:00:00" -description = 'Дюкинский заказник' -disqus_identifier = '104' -+++ -В отпуск удалось вырваться в это место - уголок Карелии во Владимирской области. Красивое место, о котором я рассказывал чуть раньше. Собиралось ехать много людей, но кто-то не смог, кто-то развернулся из-за непогоды, остались самые стойкие. И это было вознаграждено - непогода практически обошла нас стороной, полёты прошли отлично. Ну и у костра удалось посидеть не раз. - -![Dronoslet](/images/Dronoslet_20240705_2.jpg) - -![Dronoslet](/images/Dronoslet_20240705_3.jpg) - -![Dronoslet](/images/Dronoslet_20240705_4.jpg) - -![Dronoslet](/images/Dronoslet_20240705_5.jpg) - -![Dronoslet](/images/Dronoslet_20240705_6.jpg) - -![Dronoslet](/images/Dronoslet_20240705_7.jpg) - -![Dronoslet](/images/Dronoslet_20240705_8.jpg) - -{{< youtube id="53475UN_nDo" >}} - -Локация -{{< rawhtml >}} - -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} \ No newline at end of file diff --git a/temp_aug/Dukyn_20250425_0.md b/temp_aug/Dukyn_20250425_0.md deleted file mode 100644 index 4808f99..0000000 --- a/temp_aug/Dukyn_20250425_0.md +++ /dev/null @@ -1,24 +0,0 @@ -+++ -title = 'Дюкинский заказник' -slug = 'Dukyn2504' -image = 'images/Dukyn_20250425_1.jpg' -date = "2025-04-25T00:00:00" -description = 'скалы' -disqus_identifier = '129' -+++ -Очередной выезд в маленькую Карелию, Владимирская область. Скалы, лес, отличная погода и вот это вот всё. Да, ещё запускал одновременно два дрона с двух рук. Планируется выезд туда же с палатками, но можно и одним днём. Нужна погода. - -![Dukyn2504](/images/Dukyn_20250425_2.jpg) - -![Dukyn2504](/images/Dukyn_20250425_3.jpg) - -{{< youtube id="jXGd3HZl3io" >}} - -Локация -{{< rawhtml >}} - -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} \ No newline at end of file diff --git a/temp_aug/Kanyon_20250510_0.md b/temp_aug/Kanyon_20250510_0.md deleted file mode 100644 index a6619e4..0000000 --- a/temp_aug/Kanyon_20250510_0.md +++ /dev/null @@ -1,34 +0,0 @@ -+++ -title = 'Река Держа' -slug = 'Kanyon2505' -image = 'images/Kanyon_20250510_1.jpg' -date = "2025-05-10T00:00:00" -description = 'Тверская область' -disqus_identifier = '132' -+++ -Очередной выезд выходного дня. -И снова мы находим уголки с природой Карелии, но неподалёку от Москвы. -Здесь скалы не такие величественные, как в Дюкинском заказнике, но зато есть красивые спуски к реке. -Виды по пути тоже довольно живописные. - -![Kanyon2505](/images/Kanyon_20250510_2.jpg) - -![Kanyon2505](/images/Kanyon_20250510_3.jpg) - -![Kanyon2505](/images/Kanyon_20250510_4.jpg) - -![Kanyon2505](/images/Kanyon_20250510_5.jpg) - -![Kanyon2505](/images/Kanyon_20250510_6.jpg) - -Видео: https://youtu.be/3dQxelsU6mg - -{{< youtube id="3dQxelsU6mg" >}} - -Локация -{{< rawhtml >}} -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} diff --git a/temp_aug/Kanyon_20250510_2.jpg b/temp_aug/Kanyon_20250510_2.jpg deleted file mode 100644 index 08a8440..0000000 Binary files a/temp_aug/Kanyon_20250510_2.jpg and /dev/null differ diff --git a/temp_aug/Kanyon_20250510_4.jpg b/temp_aug/Kanyon_20250510_4.jpg deleted file mode 100644 index 7b557fd..0000000 Binary files a/temp_aug/Kanyon_20250510_4.jpg and /dev/null differ diff --git a/temp_aug/Kanyon_20250510_6.jpg b/temp_aug/Kanyon_20250510_6.jpg deleted file mode 100644 index af87a01..0000000 Binary files a/temp_aug/Kanyon_20250510_6.jpg and /dev/null differ diff --git a/temp_aug/Petushki_20250517_0.md b/temp_aug/Petushki_20250517_0.md deleted file mode 100644 index d88df74..0000000 --- a/temp_aug/Petushki_20250517_0.md +++ /dev/null @@ -1,28 +0,0 @@ -+++ -title = 'Дронослёт в Петушках' -slug = 'Petushki2505' -image = 'images/Petushki_20250517_1.jpg' -date = "2025-05-17T00:00:00" -description = 'у реки' -disqus_identifier = '131' -+++ -Очередной выезд выходного дня. И снова мы находим уголки с красивой природой. - -![Petushki2505](/images/Petushki_20250517_2.jpg) - -![Petushki2505](/images/Petushki_20250517_3.jpg) - -![Petushki2505](/images/Petushki_20250517_4.jpg) - -![Petushki2505](/images/Petushki_20250517_5.jpg) - -Видео https://youtu.be/4EHtC6LAdc4 -{{< youtube id="4EHtC6LAdc4" >}} - -Локация -{{< rawhtml >}} -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} diff --git a/temp_aug/Petushki_20250517_2.jpg b/temp_aug/Petushki_20250517_2.jpg deleted file mode 100644 index 6032afc..0000000 Binary files a/temp_aug/Petushki_20250517_2.jpg and /dev/null differ diff --git a/temp_aug/Petushki_20250517_3.jpg b/temp_aug/Petushki_20250517_3.jpg deleted file mode 100644 index 89f06b0..0000000 Binary files a/temp_aug/Petushki_20250517_3.jpg and /dev/null differ diff --git a/temp_aug/Radiotele_20250406_1.jpg b/temp_aug/Radiotele_20250406_1.jpg deleted file mode 100644 index d92bd1e..0000000 Binary files a/temp_aug/Radiotele_20250406_1.jpg and /dev/null differ diff --git a/temp_aug/Radiotele_20250406_2.jpg b/temp_aug/Radiotele_20250406_2.jpg deleted file mode 100644 index cea229a..0000000 Binary files a/temp_aug/Radiotele_20250406_2.jpg and /dev/null differ diff --git a/temp_aug/Radiotele_20250406_3.jpg b/temp_aug/Radiotele_20250406_3.jpg deleted file mode 100644 index 588552e..0000000 Binary files a/temp_aug/Radiotele_20250406_3.jpg and /dev/null differ diff --git a/temp_aug/Radiotele_20250406_4.jpg b/temp_aug/Radiotele_20250406_4.jpg deleted file mode 100644 index 81821b7..0000000 Binary files a/temp_aug/Radiotele_20250406_4.jpg and /dev/null differ diff --git a/temp_aug/Radiotele_20250406_5.jpg b/temp_aug/Radiotele_20250406_5.jpg deleted file mode 100644 index 10aabd5..0000000 Binary files a/temp_aug/Radiotele_20250406_5.jpg and /dev/null differ diff --git a/temp_aug/Rjev_20251005_0.md b/temp_aug/Rjev_20251005_0.md deleted file mode 100644 index 01f10eb..0000000 --- a/temp_aug/Rjev_20251005_0.md +++ /dev/null @@ -1,31 +0,0 @@ -+++ -title = 'Ржевский мемориал' -slug = 'Rjev2505' -image = 'images/Rjev_20251005_1.jpg' -date = "2025-05-10T00:00:00" -description = 'лето' -disqus_identifier = '133' -+++ -Мемориал оставляет неизгладимое впечатление: такой возвышенный, но эхо войны витает над этим местом. Что касается съёмок - у моих друзей получились прекрасные кадры на закате, но я поехать в тот раз не смог. Соответственно так и не смог заснять подсветку, видимо надо запланировать поездку осенью. - -![Rjev2505](/images/Rjev_20251005_2.jpg) - -![Rjev2505](/images/Rjev_20251005_3.jpg) - -![Rjev2505](/images/Rjev_20251005_4.jpg) - -![Rjev2505](/images/Rjev_20251005_5.jpg) - -![Rjev2505](/images/Rjev_20251005_6.jpg) - -Видео: https://youtu.be/Ro0iHYZnvW8 -{{< youtube id="Ro0iHYZnvW8" >}} - -Локация -{{< rawhtml >}} - -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} \ No newline at end of file diff --git a/temp_aug/Snazin_20250423_0.md b/temp_aug/Snazin_20250423_0.md deleted file mode 100644 index 752361f..0000000 --- a/temp_aug/Snazin_20250423_0.md +++ /dev/null @@ -1,39 +0,0 @@ -+++ -title = 'Усадьба Сназина' -slug = 'Snazin' -image = 'images/Snazin_20250423_1.jpg' -date = "2025-04-23T00:00:00" -description = 'на стыке времён' -disqus_identifier = '130' -+++ -Поездка выходного дня. -Усадьба основана генерал–майором Иваном Сназиным в 1797 году. -Возраст усадьбы ощущается уже на подъезде к ней: малые архитектурные формы поблекли, аллеи превратились в заросли. -Сама усадьба к сожалению в довольно плачевном состоянии - снаружи конструкции частично разрушены, а внутри находиться явно небезопасно. -Собственно поэтому на помощь пришёл дрон, позволив рассмотреть усадьбу изнутри. -Тем не менее готические контуры усадьбы в сочетании со львами и памятником вождю пролетариата намекают на необычную историю. - -Усадьба после смерти перешла по наследству внучке Ивана Сназина. Перестройкой усадьбы в готический замок занимался Владимир фон Гаслер, муж наследницы. -Позже в парке построили пионерский лагерь, во время Великой Отечественной войны здесь был госпиталь. - -Говорят, что в усадьбе обитает привидение - якобы в пруду утопилась местная крестьянка. -Лично мы привидение не встретили, но архитектура усадьбы, окружающая природа и атмосфера места точно не оставят вас равнодушными. - -![Snazin](/images/Snazin_20250423_2.jpg) - -![Snazin](/images/Snazin_20250423_3.jpg) - -![Snazin](/images/Snazin_20250423_4.jpg) - -![Snazin](/images/Snazin_20250423_5.jpg) - -{{< youtube id="PZOuu1K0zy4" >}} - -Локация -{{< rawhtml >}} - -{{< /rawhtml >}} - -{{< rawhtml >}} -Вернуться в начало страницы -{{< /rawhtml >}} \ No newline at end of file