Telegram-бота с нуля
1. Описание бота
Название: PhygitalSportBot
Описание:
PhygitalSportBot — это Telegram-бот, созданный для помощи пользователям в регистрации команд на турниры по фиджитал-спорту. Бот взаимодействует с пользователями через диалог, собирая данные о команде (дисциплина, название команды, ФИО капитана, контактный номер, дата рождения, город, организация, информация о дополнительных игроках и комментарий), и записывает их в Google Sheets. Бот также поддерживает беседу на тему фиджитал-спорта, используя OpenAI API (gpt-4o
) для генерации ответов. Промт для беседы загружается из Google Sheets.
2. Описание функционала
- Приветствие: Бот начинает диалог с приветственного сообщения и предлагает зарегистрировать команду.
- Регистрация команды:
- Запрашивает название дисциплины (например, Фиджитал-Футбол) и подтверждает выбор.
- Запрашивает название команды и подтверждает.
- Запрашивает ФИО капитана, контактный номер, дату рождения, город, организацию — с подтверждением на каждом шаге.
- Запрашивает информацию о дополнительных игроках (опционально).
- Запрашивает комментарий (опционально).
- Записывает данные в Google Sheets.
- Беседа: Если пользователь не хочет регистрироваться, бот продолжает беседу на тему фиджитал-спорта, используя OpenAI API (gpt-4o) и промт из Google Sheets.
- Проверка статуса: Команда /status <название_команды> позволяет проверить, зарегистрирована ли команда.
- Отмена: Команда /cancel позволяет отменить регистрацию и вернуться к беседе.
3. Требования к серверу
- Операционная система: Ubuntu 20.04 или выше (рекомендуется Ubuntu 22.04).
- Процессор: Минимум 1 ядро.
- Оперативная память: Минимум 2 ГБ (рекомендуется 4 ГБ).
- Дисковое пространство: Минимум 10 ГБ (для установки зависимостей и хранения логов).
- Интернет: Доступ к интернету с возможностью подключения к api.telegram.org и api.openai.com.
- Порты: Открытые исходящие порты 80 и 443 для HTTP/HTTPS-запросов.
- Локация сервера: Сервер должен находиться в стране, поддерживаемой OpenAI (например, Германия, США). Если сервер в неподдерживаемой стране (например, Россия), потребуется VPN.
4. Подготовка сервера
4.1. Обновление системы
Обновите систему, чтобы установить последние пакеты и исправления безопасности.
apt update && apt upgrade -y
- apt update — обновляет список доступных пакетов.
- apt upgrade -y — обновляет установленные пакеты, -y автоматически подтверждает действия.
4.2. Установка необходимых пакетов
Установите базовые утилиты, Python и зависимости:
apt install python3 python3-pip python3-venv nano curl git -y
- python3 — интерпретатор Python.
- python3-pip — менеджер пакетов для Python.
- python3-venv — для создания виртуального окружения.
- nano — текстовый редактор для редактирования файлов.
- curl — для проверки подключения к API.
- git — для работы с репозиториями (опционально).
- -y — автоматически подтверждает установку.
4.3. Создание рабочей директории
mkdir /root/bot_env cd /root/bot_env
- mkdir /root/bot_env — создаёт директорию /root/bot_env.
- cd /root/bot_env — переходит в созданную директорию.
4.4. Создание и активация виртуального окружения
Создайте виртуальное окружение, чтобы изолировать зависимости бота:
python3 -m venv /root/bot_env/venv source /root/bot_env/venv/bin/activate
- python3 -m venv /root/bot_env/venv — создаёт виртуальное окружение в директории /root/bot_env/venv.
- source /root/bot_env/venv/bin/activate — активирует виртуальное окружение (вы увидите (venv) в начале строки терминала).
4.5. Установка Python-библиотек
Установите все необходимые библиотеки для бота:
pip install telegram python-telegram-bot openai gspread oauth2client tenacity
- telegram и python-telegram-bot — для работы с Telegram API.
- openai — для работы с OpenAI API (gpt-4o).
- gspread — для работы с Google Sheets.
- oauth2client — для аутентификации в Google Sheets API.
- tenacity — для повторных попыток при записи в Google Sheets.
Successfully installed telegram-0.0.1 python-telegram-bot-21.11.1 openai-1.65.2
gspread-6.2.0 oauth2client-4.1.3 tenacity-9.0.0
5. Подготовка API-ключей и доступов
5.1. Получение API-ключа OpenAI
- Создайте учётную запись OpenAI:
- Перейдите на https://platform.openai.com/.
- Зарегистрируйтесь или войдите в существующую учётную запись.
- Получите API-ключ:
- Перейдите в раздел API Keys.
- Нажмите "Create new secret key", дайте ключу имя (например, PhygitalBotKey).
- Скопируйте ключ (например, sk-abc123...). Сохраните его в безопасное место.
- Добавьте API-ключ в переменную окружения:
На сервере добавьте ключ в /etc/environment для глобального доступа:
nano /etc/environment
Замените sk-abc123... на ваш API-ключ.
Сохраните файл (Ctrl + O, Enter, Ctrl + X) и примените изменения:
source /etc/environment
echo $OPENAI_API_KEY
Вывод должен быть вашим ключом: sk-abc123....
5.2. Получение токена Telegram
- Создайте бота в Telegram:
- Откройте Telegram и найдите @BotFather.
- Отправьте /start.
- Отправьте /newbot.
- Следуйте инструкциям: задайте имя бота (например, PhygitalSportBot) и username (например, @PhygitalSportBot).
- После создания бота вы получите токен, например: 7717227973:AAGwsD0GetKrPtn-OuMyoV_uBMRIZXdiS7U.
- Скопируйте токен и сохраните его в безопасное место.
5.3. Настройка доступа к Google Sheets
- Создайте файл credentials.json:
- Перейдите в Google Cloud Console: https://console.cloud.google.com.
- Создайте новый проект (например, PhygitalBot).
- Включите API:
- Создайте Service Account:
- Перейдите в "IAM & Admin" → "Service Accounts".
- Нажмите "Create Service Account".
- Укажите имя (например, phygital-bot-service-account), нажмите "Create and Continue", пропустите шаг с ролями, нажмите "Done".
- Создайте ключ:
- Перенесите credentials.json на сервер: Создайте директорию для файла:
mkdir -p /root/bot_env/credentials
scp /path/to/phygital-bot-XXXXX-XXXXXXXXXXXX.json root@your-server-ip:/root/bot_env/credentials/credentials.json
Замените /path/to/phygital-bot-XXXXX-XXXXXXXXXXXX.json на путь к файлу на вашем компьютере и your-server-ip на IP-адрес сервера.
ls -l /root/bot_env/credentials/credentials.json
6. Пошаговая инструкция установки
6.1. Создание файла bot.py
nano /root/bot_env/bot.py
import telegram from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove from telegram.ext import Application, CommandHandler, MessageHandler, ContextTypes, ConversationHandler from telegram.ext import filters import gspread from oauth2client.service_account import ServiceAccountCredentials import logging from openai import AsyncOpenAI from datetime import datetime import re from tenacity import retry, stop_after_attempt, wait_fixed import os # Настройка логирования logging.basicConfig(filename='/root/bot_env/bot.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # Токен бота TOKEN = '7717227973:AAGwsD0GetKrPtn-OuMyoV_uBMRIZXdiS7U' # Замените на ваш токен Telegram # API-ключ OpenAI из переменной окружения OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') # Инициализация асинхронного клиента OpenAI client = AsyncOpenAI(api_key=OPENAI_API_KEY) # Настройка Google Sheets scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] creds = ServiceAccountCredentials.from_json_keyfile_name('/root/bot_env/credentials/credentials.json', scope) client_gspread = gspread.authorize(creds) sheet = client_gspread.open_by_key('1whGZ1RefscCSiVNnVUGaXWiISnXa59EhXqxwmTXG3Vo') prompt_sheet = sheet.worksheet('promt') registration_sheet = sheet.worksheet('registration') # Кэшируем данные из Google Sheets при запуске PROMPT_CONTEXT = "\n".join(prompt_sheet.col_values(1)[1:]) if len(prompt_sheet.col_values(1)) > 1 else "Нет данных для контекста" logging.info(f"Loaded PROMPT_CONTEXT: {PROMPT_CONTEXT}") EXISTING_TEAMS = set(registration_sheet.col_values(3)[1:]) # Кэшируем названия команд # Кэш для ответов ChatGPT CHATGPT_RESPONSE_CACHE = {} # Асинхронная функция для запросов к ChatGPT async def get_chatgpt_response(prompt): # Проверяем, есть ли ответ в кэше if prompt in CHATGPT_RESPONSE_CACHE: return CHATGPT_RESPONSE_CACHE[prompt] try: response = await client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "Ты виртуальный помощник по фиджитал-спорту. Будь дружелюбным и энергичным!"}, {"role": "user", "content": prompt} ], temperature=0.7, max_tokens=500 ) answer = response.choices[0].message.content.strip() # Сохраняем в кэш CHATGPT_RESPONSE_CACHE[prompt] = answer return answer except Exception as e: logging.error(f"Ошибка при запросе к ChatGPT: {str(e)}") return "Извините, произошла ошибка при получении ответа от ИИ." # Состояния для ConversationHandler CHAT, DISCIPLINE, DISCIPLINE_CONFIRM, TEAM_NAME, TEAM_NAME_CONFIRM, FULL_NAME, FULL_NAME_CONFIRM, PHONE, PHONE_CONFIRM, BIRTH_DATE, BIRTH_DATE_CONFIRM, CITY, CITY_CONFIRM, ORGANIZATION, ORGANIZATION_CONFIRM, ADD_PLAYER, COMMENT = range(17) # Обработчик текстовых сообщений (без команды /talk) async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: user_message = update.message.text.strip().lower() if not user_message: await update.message.reply_text("Пожалуйста, напиши свой вопрос или сообщение.") return # Если пользователь в состоянии CHAT, продолжаем беседу if context.user_data.get('state') == 'CHAT': prompt = f"Опираясь на следующий контекст:\n{PROMPT_CONTEXT}\nПродолжи беседу на тему фиджитал-спорта. Пользователь написал: {user_message}" response = await get_chatgpt_response(prompt) await update.message.reply_text(response) return # Проверяем, не хочет ли пользователь зарегистрироваться if "регистр" in user_message or "записаться" in user_message: context.user_data['state'] = 'REGISTRATION' await update.message.reply_text( "Отлично! Давай зарегистрируем твою команду. Сначала укажи название дисциплины (например, Фиджитал-Футбол).", reply_markup=ReplyKeyboardRemove() ) return DISCIPLINE # Обрабатываем обычные сообщения через ChatGPT prompt = f"Опираясь на следующий контекст:\n{PROMPT_CONTEXT}\nОтветь на вопрос: {user_message}" response = await get_chatgpt_response(prompt) await update.message.reply_text(response) # Команда /start для начала диалога async def start_registration(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: logging.info("Received /start command") context.user_data.clear() # Очищаем старые данные context.user_data['state'] = 'CHAT' # Устанавливаем состояние CHAT context.user_data['registration'] = {} context.user_data['players'] = [] # Список для хранения игроков # Приветствие (без обращения к ChatGPT для скорости) await update.message.reply_text("Привет! Я твой помощник по фиджитал-спорту! 🏅 Фиджитал-спорт — это крутое сочетание физической активности и цифровых технологий. " "Давай поговорим об этом! Знаешь, какие дисциплины есть в фиджитал-спорте? Например, фиджитал-футбол или фиджитал-гонки!") # Предложение регистрации reply_keyboard = [['да', 'нет']] await update.message.reply_text( "Кстати, хочешь зарегистрировать свою команду на турнир? (да/нет)", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return CHAT # Обработчик состояния CHAT async def chat_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: user_message = update.message.text.strip().lower() logging.info(f"Received message in CHAT state: {user_message}") if user_message in ['да', 'yes', 'y']: logging.info("User agreed to registration, moving to DISCIPLINE state") context.user_data['state'] = 'REGISTRATION' await update.message.reply_text( "Отлично! Давай зарегистрируем твою команду. Сначала укажи название дисциплины (например, Фиджитал-Футбол).", reply_markup=ReplyKeyboardRemove() ) return DISCIPLINE elif user_message in ['нет', 'no', 'n']: await update.message.reply_text( "Хорошо, давай продолжим говорить о фиджитал-спорте! Что ты думаешь о сочетании физической активности и цифровых технологий?", reply_markup=ReplyKeyboardRemove() ) context.user_data['state'] = 'CHAT' return CHAT else: # Продолжаем беседу prompt = f"Опираясь на следующий контекст:\n{PROMPT_CONTEXT}\nПродолжи беседу на тему фиджитал-спорта. Пользователь написал: {user_message}" response = await get_chatgpt_response(prompt) await update.message.reply_text(response) # Повторно предлагаем регистрацию reply_keyboard = [['да', 'нет']] await update.message.reply_text( "Кстати, хочешь зарегистрировать свою команду? (да/нет)", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return CHAT # Обработчик названия дисциплины async def discipline(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: discipline = update.message.text.strip().capitalize() logging.info(f"Received discipline: {discipline}") context.user_data['registration']['discipline'] = discipline reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Дисциплина: {discipline}. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return DISCIPLINE_CONFIRM async def discipline_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in DISCIPLINE_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Отлично! Теперь укажи название команды (например, Молния).", reply_markup=ReplyKeyboardRemove() ) return TEAM_NAME elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи название дисциплины (например, Фиджитал-Футбол).", reply_markup=ReplyKeyboardRemove() ) return DISCIPLINE else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return DISCIPLINE_CONFIRM # Обработчик названия команды async def team_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: team_name = update.message.text.strip().capitalize() logging.info(f"Received team name: {team_name}") context.user_data['registration']['team_name'] = team_name reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Команда: {team_name}. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return TEAM_NAME_CONFIRM async def team_name_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in TEAM_NAME_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Супер! Теперь укажи ФИО капитана (например, Иванов Иван Иванович).", reply_markup=ReplyKeyboardRemove() ) return FULL_NAME elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи название команды (например, Молния).", reply_markup=ReplyKeyboardRemove() ) return TEAM_NAME else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return TEAM_NAME_CONFIRM # Обработчик ФИО async def full_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: full_name = update.message.text.strip() name_parts = full_name.split() logging.info(f"Received full name: {full_name}") if len(name_parts) != 3: await update.message.reply_text("Пожалуйста, укажи полное ФИО (Имя Фамилия Отчество, например, Иванов Иван Иванович).") return FULL_NAME formatted_name = " ".join(part.capitalize() for part in name_parts) context.user_data['registration']['full_name'] = formatted_name reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"ФИО капитана: {formatted_name}. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return FULL_NAME_CONFIRM async def full_name_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in FULL_NAME_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Отлично! Теперь укажи контактный телефон (например, 5555555). Формат будет преобразован в 555-55-55.", reply_markup=ReplyKeyboardRemove() ) return PHONE elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи ФИО заново (например, Иванов Иван Иванович).", reply_markup=ReplyKeyboardRemove() ) return FULL_NAME else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return FULL_NAME_CONFIRM # Обработчик телефона async def phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: phone = update.message.text.strip() phone = re.sub(r'\D', '', phone) logging.info(f"Received phone: {phone}") if len(phone) != 7: await update.message.reply_text("Пожалуйста, укажи 7 цифр для номера телефона (например, 5555555). Формат будет преобразован в 555-55-55.") return PHONE formatted_phone = f"{phone[:3]}-{phone[3:5]}-{phone[5:7]}" context.user_data['registration']['phone'] = formatted_phone reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Телефон: {formatted_phone}. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return PHONE_CONFIRM async def phone_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in PHONE_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Спасибо! Теперь укажи дату рождения капитана (в формате дд.мм.гг, например, 01.01.00). " "Можно также вводить в формате дд,мм,гг (например, 01,01,00) или ддммгггг (например, 01012000).", reply_markup=ReplyKeyboardRemove() ) return BIRTH_DATE elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи контактный телефон заново (например, 5555555).", reply_markup=ReplyKeyboardRemove() ) return PHONE else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return PHONE_CONFIRM # Обработчик даты рождения async def birth_date(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: birth_date = update.message.text.strip() logging.info(f"Received birth date: {birth_date}") if re.match(r'^\d{2},\d{2},\d{2}#39;, birth_date): birth_date = birth_date.replace(',', '.') elif re.match(r'^\d{8}#39;, birth_date): birth_date = f"{birth_date[:2]}.{birth_date[2:4]}.{birth_date[4:6]}" elif not re.match(r'^\d{2}\.\d{2}\.\d{2}#39;, birth_date): await update.message.reply_text("Пожалуйста, укажи дату рождения в формате дд.мм.гг (например, 01.01.00). " "Можно также вводить в формате дд,мм,гг (например, 01,01,00) или ддммгггг (например, 01012000).") return BIRTH_DATE try: birth_date_obj = datetime.strptime(birth_date, '%d.%m.%y') today = datetime.now() age = today.year - birth_date_obj.year - ((today.month, today.day) < (birth_date_obj.month, birth_date_obj.day)) context.user_data['registration']['birth_date'] = birth_date context.user_data['registration']['age'] = age reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Дата рождения: {birth_date}, возраст: {age} лет. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) except ValueError: await update.message.reply_text("Неверный формат даты. Укажи дату в формате дд.мм.гг (например, 01.01.00).") return BIRTH_DATE return BIRTH_DATE_CONFIRM async def birth_date_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in BIRTH_DATE_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Спасибо! Теперь укажи город проживания (например, Москва).", reply_markup=ReplyKeyboardRemove() ) return CITY elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи дату рождения заново (в формате дд.мм.гг, например, 01.01.00).", reply_markup=ReplyKeyboardRemove() ) return BIRTH_DATE else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return BIRTH_DATE_CONFIRM # Обработчик города async def city(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: city = update.message.text.strip().capitalize() logging.info(f"Received city: {city}") context.user_data['registration']['city'] = city reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Город: {city}. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return CITY_CONFIRM async def city_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in CITY_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Укажи организацию (цех) или учебное заведение, которое вы представляете (например, Цех №5 или МГУ).", reply_markup=ReplyKeyboardRemove() ) return ORGANIZATION elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи город проживания заново (например, Москва).", reply_markup=ReplyKeyboardRemove() ) return CITY else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return CITY_CONFIRM # Обработчик организации async def organization(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: organization = update.message.text.strip() logging.info(f"Received organization: {organization}") context.user_data['registration']['organization'] = organization reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Организация: {organization}. Всё верно?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return ORGANIZATION_CONFIRM async def organization_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in ORGANIZATION_CONFIRM state: {response}") if response == 'да': await update.message.reply_text( "Хочешь добавить других игроков в команду? (да/нет)", reply_markup=ReplyKeyboardMarkup([['да', 'нет']], one_time_keyboard=True) ) return ADD_PLAYER elif response == 'нет': await update.message.reply_text( "Давай исправим. Укажи организацию заново (например, Цех №5 или МГУ).", reply_markup=ReplyKeyboardRemove() ) return ORGANIZATION else: await update.message.reply_text("Пожалуйста, выбери 'да' или 'нет'.") return ORGANIZATION_CONFIRM # Обработчик добавления игроков async def add_player(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: response = update.message.text.strip().lower() logging.info(f"Received response in ADD_PLAYER state: {response}") if response == 'да': await update.message.reply_text("Укажи ФИО и дату рождения игрока (например, Петров Петр Петрович 02.02.02).") return ADD_PLAYER elif response == 'нет': players = context.user_data.get('players', []) await update.message.reply_text( "Хочешь добавить комментарий? (Если нет, напиши 'нет'). Максимальная длина — 500 символов.", reply_markup=ReplyKeyboardRemove() ) return COMMENT else: player_data = update.message.text.strip() context.user_data['players'].append(player_data) reply_keyboard = [['да', 'нет']] await update.message.reply_text( f"Игрок добавлен: {player_data}. Хочешь добавить ещё одного игрока?", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) return ADD_PLAYER # Обработчик комментария async def comment(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: comment = update.message.text.strip() logging.info(f"Received comment: {comment}") if comment.lower() == 'нет': comment = '' elif len(comment) > 500: await update.message.reply_text("Комментарий слишком длинный! Пожалуйста, укороти его до 500 символов.") return COMMENT context.user_data['registration']['comment'] = comment # Проверка на дублирование команды (используем кэш) team_name = context.user_data['registration']['team_name'] if team_name in EXISTING_TEAMS: await update.message.reply_text(f"Команда '{team_name}' уже зарегистрирована! Если это другая команда, укажи другое название. Напиши новое название команды.") return TEAM_NAME # Собираем данные для записи registration_data = context.user_data['registration'] players = context.user_data.get('players', []) date_registered = datetime.now().strftime("%H:%M %d.%m.%y") row = [ date_registered, registration_data['discipline'], registration_data['team_name'], registration_data['full_name'], registration_data['phone'], f"{registration_data['birth_date']} ({registration_data['age']})", registration_data['city'], registration_data['organization'], f"{comment}; Игроки: {'; '.join(players) if players else ''}" # Добавляем игроков в комментарий ] # Записываем данные в Google Sheets с повторными попытками @retry(stop=stop_after_attempt(3), wait=wait_fixed(2)) def append_to_sheet(row): registration_sheet.append_row(row) try: append_to_sheet(row) EXISTING_TEAMS.add(team_name) # Обновляем кэш await update.message.reply_text("Регистрация завершена! 🎉 Данные вашей команды записаны. Что ещё могу сделать для тебя? " "Давай продолжим говорить о фиджитал-спорте! Как думаешь, какая дисциплина самая интересная?") context.user_data['state'] = 'CHAT' # Переходим в режим беседы return CHAT except Exception as e: logging.error(f"Ошибка при записи в Google Sheets: {str(e)}") await update.message.reply_text("Произошла ошибка при записи данных. Пожалуйста, попробуй снова позже.") return ConversationHandler.END # Обработчик отмены async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: await update.message.reply_text("Регистрация отменена. Давай продолжим говорить о фиджитал-спорте! " "Какие у тебя планы на предстоящий турнир?") context.user_data['state'] = 'CHAT' return CHAT # Команда /status для проверки статуса регистрации async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not context.args: await update.message.reply_text("Пожалуйста, укажи название команды после команды /status, например: /status Молния") return team_name = " ".join(context.args).capitalize() try: all_rows = registration_sheet.get_all_values() found = False for row in all_rows[1:]: # Пропускаем заголовок if row[2] == team_name: # Колонка 3 - название команды await update.message.reply_text( f"Команда '{team_name}' зарегистрирована!\n" f"Дисциплина: {row[1]}\n" f"Капитан: {row[3]}\n" f"Телефон: {row[4]}\n" f"Дата рождения: {row[5]}\n" f"Город: {row[6]}\n" f"Организация: {row[7]}\n" f"Комментарий: {row[8]}" ) found = True break if not found: await update.message.reply_text(f"Команда '{team_name}' не найдена. Зарегистрируй её с помощью /start!") except Exception as e: logging.error(f"Ошибка при проверке статуса: {str(e)}") await update.message.reply_text("Произошла ошибка при проверке статуса. Попробуй снова позже.") # Основная функция для запуска бота def main() -> None: logging.info("Starting bot...") application = Application.builder().token(TOKEN).build() # Добавляем ConversationHandler для регистрации conv_handler = ConversationHandler( entry_points=[CommandHandler('start', start_registration)], states={ CHAT: [MessageHandler(filters.TEXT & ~filters.COMMAND, chat_handler)], DISCIPLINE: [MessageHandler(filters.TEXT & ~filters.COMMAND, discipline)], DISCIPLINE_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, discipline_confirm)], TEAM_NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, team_name)], TEAM_NAME_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, team_name_confirm')], FULL_NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, full_name)], FULL_NAME_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, full_name_confirm)], PHONE: [MessageHandler(filters.TEXT & ~filters.COMMAND, phone)], PHONE_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, phone_confirm)], BIRTH_DATE: [MessageHandler(filters.TEXT & ~filters.COMMAND, birth_date)], BIRTH_DATE_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, birth_date_confirm)], CITY: [MessageHandler(filters.TEXT & ~filters.COMMAND, city)], CITY_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, city_confirm)], ORGANIZATION: [MessageHandler(filters.TEXT & ~filters.COMMAND, organization)], ORGANIZATION_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, organization_confirm)], ADD_PLAYER: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_player)], COMMENT: [MessageHandler(filters.TEXT & ~filters.COMMAND, comment)], }, fallbacks=[CommandHandler('cancel', cancel)] ) application.add_handler(conv_handler) application.add_handler(CommandHandler('status', status)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text)) application.run_polling() if __name__ == '__main__': main()
Сохраните файл (Ctrl + O, Enter, Ctrl + X).
6.2. Настройка автозапуска
Чтобы бот запускался автоматически после перезагрузки сервера:
crontab -e
Выберите редактор (например, 1 для nano), затем добавьте:
@reboot /bin/bash -c "source /root/bot_env/venv/bin/activate && nohup /root/bot_env/venv/bin/python3 /root/bot_env/bot.py &"
Сохраните (Ctrl + O, Enter, Ctrl + X).
7. Процедуры запуска бота
7.1. Первый запуск
Запустите бота вручную для проверки:
source /root/bot_env/venv/bin/activate cd /root/bot_env nohup /root/bot_env/venv/bin/python3 bot.py &
- source /root/bot_env/venv/bin/activate — активирует виртуальное окружение.
- cd /root/bot_env — переходит в директорию бота.
- nohup /root/bot_env/venv/bin/python3 bot.py & — запускает бота в фоновом режиме, игнорируя закрытие терминала.
7.2. Проверка логов
tail -f /root/bot_env/bot.log
2025-03-03 20:00:00,123 - INFO - Starting bot... 2025-03-03 20:00:00,456 - INFO - HTTP Request: POST https://api.telegram.org/bot7717227973:AAGwsD0GetKrPtn-OuMyoV_uBMRIZXdiS7U/getUpdates "HTTP/1.1 200 OK"
7.3. Проверка процесса
ps aux | grep python
root 12345 0.1 0.2 123456 7890 ? S 20:00 0:01 /root/bot_env/venv/bin/python3 bot.py
8. Процедуры внесения изменений в промт в Google Sheets
8.1. Изменение промта
- Откройте Google Sheet: https://docs.google.com/spreadsheets/d/1whGZ1RefscCSiVNnVUGaXWiISnXa59EhXqxwmTXG3Vo/edit.
- Перейдите на лист promt.
- Измените текст в первом столбце (A) — это промт, который бот использует для беседы.
- Сохраните изменения (Google Sheets делает это автоматически).
8.2. Полный рестарт бота
Бот загружает промт при запуске, поэтому нужно перезапустить его:
ps aux | grep python kill -9 <PID>
source /root/bot_env/venv/bin/activate cd /root/bot_env nohup /root/bot_env/venv/bin/python3 bot.py &
8.3. Полный повторный запуск сервера
Если вы хотите перезапустить сервер:
ps aux | grep python kill -9 <PID>
reboot
Проверьте, что бот запустился: После перезагрузки подключитесь к серверу и проверьте:
ps aux | grep python tail -f /root/bot_env/bot.log
Бот должен автоматически запуститься благодаря crontab.
9. Полезные команды для тестирования и пояснения результатов
9.1. Проверка процесса
ps aux | grep python
root 12345 0.1 0.2 123456 7890 ? S 20:00 0:01 /root/bot_env/venv/bin/python3 bot.py
9.2. Проверка логов
tail -f /root/bot_env/bot.log
2025-03-03 20:00:00,123 - INFO - Starting bot... 2025-03-03 20:00:00,456 - INFO - HTTP Request: POST https://api.telegram.org/... "HTTP/1.1 200 OK"
Пояснение: Логи показывают, что бот запустился и успешно подключается к Telegram API. Если есть ошибки (например, 401 Unauthorized), проверьте токен Telegram.
9.3. Проверка подключения
curl https://api.telegram.org curl https://api.openai.com
- Ожидаемый результат: Оба запроса должны вернуть HTTP-ответ (например, 200 OK или HTML-код).
- Пояснение: Если запросы не проходят, проверьте фаервол или интернет-соединение.
9.4. Тестирование бота в Telegram
Привет! Я твой помощник по фиджитал-спорту! 🏅 Фиджитал-спорт — это крутое сочетание физической активности и цифровых технологий. Давай поговорим об этом! Знаешь, какие дисциплины есть в фиджитал-спорте? Например, фиджитал-футбол или фиджитал-гонки! Кстати, хочешь зарегистрировать свою команду на турнир? (да/нет)
Пояснение: Бот успешно запустился и отправил приветственное сообщение.
Отлично! Давай зарегистрируем твою команду. Сначала укажи название дисциплины (например, Фиджитал-Футбол).
Пояснение: Бот начал процесс регистрации.
- Введите дисциплину (например, "Фиджитал-Футбол"), подтвердите ("да").
- Укажите название команды (например, "Молния"), подтвердите.
- Укажите ФИО (например, "Иванов Иван Иванович"), подтвердите.
- Укажите телефон (например, "5555555"), подтвердите.
- Укажите дату рождения (например, "01.01.00"), подтвердите.
- Укажите город (например, "Москва"), подтвердите.
- Укажите организацию (например, "МГУ"), подтвердите.
- Ответьте "нет" на вопрос о дополнительных игроках.
- Ответьте "нет" на вопрос о комментарии.
- Ожидаемый результат:
Регистрация завершена! 🎉 Данные вашей команды записаны. Что ещё могу сделать для тебя? Давай продолжим говорить о фиджитал-спорте! Как думаешь, какая дисциплина самая интересная?
Пояснение: Бот успешно записал данные в Google Sheets.
/status Молния
Пояснение: Бот нашёл данные команды в Google Sheets и вывел их.
- Проверьте беседу: После регистрации спросите что-то о фиджитал-спорте, например: "Какие дисциплины самые популярные?"
Итог
Инструкция включает все шаги для установки бота с нуля: от подготовки сервера до тестирования. Бот будет работать, если все API-ключи и доступы настроены корректно. Если возникнут проблемы, проверьте логи (tail -f /root/bot_env/bot.log) и убедитесь, что сервер может подключиться к Telegram и OpenAI API.