March 3

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

  1. Создайте учётную запись OpenAI:
    • Перейдите на https://platform.openai.com/.
    • Зарегистрируйтесь или войдите в существующую учётную запись.
  2. Получите API-ключ:
    • Перейдите в раздел API Keys.
    • Нажмите "Create new secret key", дайте ключу имя (например, PhygitalBotKey).
    • Скопируйте ключ (например, sk-abc123...). Сохраните его в безопасное место.
  3. Добавьте API-ключ в переменную окружения:

На сервере добавьте ключ в /etc/environment для глобального доступа:

nano /etc/environment

Добавьте строку:

OPENAI_API_KEY="sk-abc123..."

Замените sk-abc123... на ваш API-ключ.
Сохраните файл (Ctrl + O, Enter, Ctrl + X) и примените изменения:

source /etc/environment

Проверьте, что ключ доступен:

echo $OPENAI_API_KEY 

Вывод должен быть вашим ключом: sk-abc123....

5.2. Получение токена Telegram

  1. Создайте бота в Telegram:
    • Откройте Telegram и найдите @BotFather.
    • Отправьте /start.
    • Отправьте /newbot.
    • Следуйте инструкциям: задайте имя бота (например, PhygitalSportBot) и username (например, @PhygitalSportBot).
    • После создания бота вы получите токен, например: 7717227973:AAGwsD0GetKrPtn-OuMyoV_uBMRIZXdiS7U.
    • Скопируйте токен и сохраните его в безопасное место.

5.3. Настройка доступа к Google Sheets

  1. Создайте файл credentials.json:
    • Перейдите в Google Cloud Console: https://console.cloud.google.com.
    • Создайте новый проект (например, PhygitalBot).
    • Включите API:
      • В разделе "APIs & Services" → "Library" найдите и включите Google Sheets API и Google Drive API.
    • Создайте Service Account:
      • Перейдите в "IAM & Admin" → "Service Accounts".
      • Нажмите "Create Service Account".
      • Укажите имя (например, phygital-bot-service-account), нажмите "Create and Continue", пропустите шаг с ролями, нажмите "Done".
    • Создайте ключ:
      • В списке Service Accounts выберите созданный аккаунт, перейдите на вкладку "Keys".
      • Нажмите "Add Key" → "Create New Key", выберите "JSON", нажмите "Create".
      • Файл phygital-bot-XXXXX-XXXXXXXXXXXX.json скачается на ваш компьютер.
  2. Перенесите 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

  1. Дайте доступ к Google Sheet:
    • Откройте Google Sheet: https://docs.google.com/spreadsheets/d/1whGZ1RefscCSiVNnVUGaXWiISnXa59EhXqxwmTXG3Vo/edit.
    • Нажмите "Share" и добавьте client_email из credentials.json (например, phygital-bot-service-account@your-project-id.iam.gserviceaccount.com) с правами "Editor".

6. Пошаговая инструкция установки

6.1. Создание файла bot.py

Создайте файл 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. Изменение промта

  1. Откройте Google Sheet: https://docs.google.com/spreadsheets/d/1whGZ1RefscCSiVNnVUGaXWiISnXa59EhXqxwmTXG3Vo/edit.
  2. Перейдите на лист promt.
  3. Измените текст в первом столбце (A) — это промт, который бот использует для беседы.
  4. Сохраните изменения (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
  • Ожидаемый результат: Вы должны увидеть процесс bot.py, например:

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

  • Отправьте /start:
    • Ожидаемый результат:

Привет! Я твой помощник по фиджитал-спорту! 🏅 Фиджитал-спорт — это крутое сочетание физической активности и цифровых технологий. Давай поговорим об этом! Знаешь, какие дисциплины есть в фиджитал-спорте? Например, фиджитал-футбол или фиджитал-гонки! Кстати, хочешь зарегистрировать свою команду на турнир? (да/нет)

Пояснение: Бот успешно запустился и отправил приветственное сообщение.

  • Ответьте "да":
    • Ожидаемый результат:

Отлично! Давай зарегистрируем твою команду. Сначала укажи название дисциплины (например, Фиджитал-Футбол).

Пояснение: Бот начал процесс регистрации.

  • Пройдите регистрацию:
    • Введите дисциплину (например, "Фиджитал-Футбол"), подтвердите ("да").
    • Укажите название команды (например, "Молния"), подтвердите.
    • Укажите ФИО (например, "Иванов Иван Иванович"), подтвердите.
    • Укажите телефон (например, "5555555"), подтвердите.
    • Укажите дату рождения (например, "01.01.00"), подтвердите.
    • Укажите город (например, "Москва"), подтвердите.
    • Укажите организацию (например, "МГУ"), подтвердите.
    • Ответьте "нет" на вопрос о дополнительных игроках.
    • Ответьте "нет" на вопрос о комментарии.
    • Ожидаемый результат:

Регистрация завершена! 🎉 Данные вашей команды записаны. Что ещё могу сделать для тебя? Давай продолжим говорить о фиджитал-спорте! Как думаешь, какая дисциплина самая интересная?

Пояснение: Бот успешно записал данные в Google Sheets.

  • Проверьте статус:
/status Молния
    • Ожидаемый результат:

Команда 'Молния' зарегистрирована!

Дисциплина: Фиджитал-Футбол

Капитан: Иванов Иван Иванович

Телефон: 555-55-55

Дата рождения: 01.01.00 (24)

Город: Москва

Организация: МГУ

Комментарий: Игроки:

Пояснение: Бот нашёл данные команды в Google Sheets и вывел их.

  • Проверьте беседу: После регистрации спросите что-то о фиджитал-спорте, например: "Какие дисциплины самые популярные?"
    • Ожидаемый результат: Бот ответит, используя промт из Google Sheets и OpenAI API.
    • Пояснение: Бот успешно продолжает беседу.

Итог

Инструкция включает все шаги для установки бота с нуля: от подготовки сервера до тестирования. Бот будет работать, если все API-ключи и доступы настроены корректно. Если возникнут проблемы, проверьте логи (tail -f /root/bot_env/bot.log) и убедитесь, что сервер может подключиться к Telegram и OpenAI API.

Если понравилось пишите 👨‍🚀