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.