Создание Telegram-бота с помощью Python-Telegram-Bot: подробное руководство

Создание бота Telegram может значительно расширить функциональность вашего обмена сообщениями, позволяя использовать все, от автоматических ответов до сложных взаимодействий. Это подробное руководство проведет вас через процесс создания базового бота Telegram с помощью Python, включая пользовательские команды, обработку сообщений и пользовательские клавиатуры. Мы будем использовать библиотеку python-telegram-bot из-за ее простоты и мощных функций.

  Необходимые условия

  • Python 3.x установлен в вашей системе.
  •   Аккаунт в Telegram.
  • Менеджер пакетов pip для установки библиотек Python.

Введение в бот для объявления о продаже автомобилей

В этой статье я продемонстрирую очень простой бот для объявления о продаже автомобилей, который предназначен для упрощения процесса сбора некоторой необходимой информации от пользователей, желающих выставить свои автомобили на продажу. Участвуя в структурированном разговоре, бот собирает такие данные, как тип, цвет и пробег автомобиля, и даже позволяет загружать фотографии, в результате чего создается резюме, подтверждающее детали объявления.

  Ключевые особенности:

  • Интерактивный поток разговоров для сбора информации об автомобиле.
  • Встроенная клавиатура для удобного выбора параметров.
  • Возможность загрузить фото автомобиля.
  • Краткое изложение деталей объявления для подтверждения.

Шаг 1: Настройте своего бота Telegram

  1. Создайте своего бота: откройте Telegram и найдите аккаунт «BotFather». Начните беседу и используйте команду /newbot для создания нового бота. Следуйте инструкциям, чтобы настроить имя и имя пользователя бота. Затем BotFather предоставит вам токен, который имеет решающее значение для доступа к Telegram Bot API. Храните этот токен в надежном месте и не делитесь им.

2. Установите необходимые библиотеки: Установите python-telegram-bot с помощью pip:

python3 -m pip install python-telegram-bot

Шаг 2: Создание бота с помощью Python

Теперь давайте углубимся в программирование вашего бота. Пожалуйста, создайте новый файл Python, например, my_telegram_bot.pyи откройте его в вашем любимом текстовом редакторе. Затем выполните следующие действия, чтобы написать бота.

  Импорт библиотек:

Начните с импорта необходимых модулей и настройки логирования, чтобы помочь в отладке:

import logging
from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, InlineKeyboardButton, InlineKeyboardMarkup)
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters)

logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)

Определите состояния беседы:

Состояния в боте Telegram, особенно при использовании обработчика разговора, служат фреймворком для управления потоком взаимодействия между ботом и пользователем. По сути, они являются маркерами или контрольными точками, которые определяют, в какой части разговора пользователь участвует в данный момент, и определяют, что бот должен делать дальше, основываясь на вводе данных пользователем. Ниже приведен более общий обзор роли и функциональных возможностей состояний в управлении беседами ботов. Назначение и функционал состояний в Telegram-боте заключаются в следующем:

  1. Последовательное управление потоком: Состояния позволяют боту управлять последовательным потоком разговора. Переходя из одного состояния в другое, бот может провести пользователя через ряд шагов, вопросов или вариантов в логическом порядке.
  2. Контекстная осведомленность: Они помогают боту поддерживать контекст в разговоре. Зная текущее состояние, бот понимает, какая информация была предоставлена пользователем и какая информация еще необходима, что позволяет ему реагировать соответствующим образом.
  3. Обработка пользовательского ввода: В зависимости от текущего состояния бот может по-разному обрабатывать вводимые пользователем данные. Например, входные данные в состоянии «CAR_TYPE» будут пониматься как указание пользователем типа продаваемого автомобиля, в то время как те же входные данные в состоянии «CAR_COLOR» будут интерпретироваться как цвет автомобиля.
  4. Реализация условной логики: Состояния позволяют реализовать условную логику в диалоге. В зависимости от реакции или выбора пользователя бот может пропустить определенные состояния, повторить их или направить пользователя по другому пути общения.
  5. Обработка и повторение ошибок: Они облегчают обработку ошибок и повторение вопросов, если пользователь дает неожиданные или недействительные ответы. Отслеживая текущее состояние, бот может повторно запросить у пользователя информацию правильно.
  6. Постоянство состояния: В более сложных ботах состояния могут сохраняться и сохраняться между сеансами, что позволяет пользователям продолжить разговор с того места, на котором они остановились, даже если они временно покидают чат или если бот перезапускается.

Перечислим состояния, в которых наш бот будет управлять потоком:

CAR_TYPE, CAR_COLOR, CAR_MILEAGE_DECISION, CAR_MILEAGE, PHOTO, SUMMARY = range(6)

Реализуйте обработчики бесед:

Обработчики разговоров в ботах Telegram, особенно при использовании таких библиотек, как python-telegram-bot, являются мощными инструментами, которые управляют потоком разговоров на основе вводимых пользователем данных и предопределенных состояний. Они имеют решающее значение для разработки ботов, требующих последовательности взаимодействий, таких как сбор информации, навигация пользователей по меню или выполнение команд в определенном порядке. Вот более подробный обзор того, как работают обработчики разговоров и их роль в разработке ботов:

Назначение и функциональные возможности:

  1. Управление состояниями разговора: Обработчики бесед отслеживают текущее состояние диалога с каждым пользователем. Они определяют, что бот должен делать дальше, на основе вводимых пользователем данных и текущего состояния, обеспечивая плавное и логичное прохождение различных этапов взаимодействия.
  2. Маршрутизация вводимых пользователем данных: Они направляют вводимые пользователем данные в различные функции обратного вызова в зависимости от текущего состояния. Это означает, что один и тот же ввод может привести к разным результатам в зависимости от того, на каком этапе потока разговора находится пользователь.
  3. Работа с командами и текстом: Обработчики бесед могут различать команды (например, /start или /help) и обычные текстовые сообщения, что позволяет разработчикам указывать различные ответы или действия для каждого типа ввода.
  4. Интеграция с клавиатурами и кнопками: Они безупречно работают с пользовательскими клавиатурами и встроенными кнопками, позволяя разработчикам создавать интерактивные и удобные интерфейсы в рамках разговора. Пользователи могут выбирать параметры или перемещаться по функциям бота с помощью этих элементов пользовательского интерфейса.
  5. Резервные варианты и тайм-ауты: Обработчики диалогов поддерживают резервные функции, которые могут срабатывать, когда пользователь отправляет неожиданные данные или когда разговор необходимо сбросить. Они также могут обрабатывать тайм-ауты, автоматически завершая разговор после периода бездействия.

Реализация обработчиков диалогов:

Реализация обработчика беседы обычно включает в себя определение точек входа, состояний и резервных вариантов:

  • Точки входа: Это триггеры, с которых начинается разговор. Как правило, в качестве точки входа используется команда /start, но можно определить несколько точек входа для разных потоков беседы.
  • Государств: Как уже говорилось, состояния представляют разные точки разговора. Каждое состояние связано с одной или несколькими функциями обратного вызова, которые определяют поведение бота на этом этапе. Разработчики сопоставляют состояния с этими обратными вызовами, диктуя ход диалога.
  • Запасные варианты: Резервные функции определяются для обработки непредвиденных ситуаций или для обеспечения способа выхода из диалога или его сброса. Распространенным резервным вариантом является команда /cancel, которая позволяет пользователям остановить разговор в любой момент.

Далее следует функция обработчика запуска, которая инициирует диалог (точку входа), представляя пользователю выбор типов автомобилей:

def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Starts the conversation and asks the user about their preferred car type."""
reply_keyboard = [['Sedan', 'SUV', 'Sports', 'Electric']]

await update.message.reply_text(
'<b>Welcome to the Car Sales Listing Bot!\n'
'Let\'s get some details about the car you\'re selling.\n'
'What is your car type?</b>',
parse_mode='HTML',
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True, resize_keyboard=True),
)

return CAR_TYPE

Здесь вы можете ознакомиться с остальными обработчиками:


async def car_type(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the user's car type."""
user = update.message.from_user
context.user_data['car_type'] = update.message.text
cars = {"Sedan": "🚗", "SUV": "🚙", "Sport": "🏎️", "Electric": "⚡"}
logger.info('Car type of %s: %s', user.first_name, update.message.text)
await update.message.reply_text(
f'<b>You selected {update.message.text} car {cars[update.message.text]}.\n'
f'What color your car is?</b>',
parse_mode='HTML',
reply_markup=ReplyKeyboardRemove(),
)

# Define inline buttons for car color selection
keyboard = [
[InlineKeyboardButton('Red', callback_data='Red')],
[InlineKeyboardButton('Blue', callback_data='Blue')],
[InlineKeyboardButton('Black', callback_data='Black')],
[InlineKeyboardButton('White', callback_data='White')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('<b>Please choose:</b>', parse_mode='HTML', reply_markup=reply_markup)

return CAR_COLOR


async def car_color(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the user's car color."""
query = update.callback_query
await query.answer()
context.user_data['car_color'] = query.data
await query.edit_message_text(
text=f'<b>You selected {query.data} color.\n'
f'Would you like to fill in the mileage for your car?</b>',
parse_mode='HTML'
)

# Define inline buttons for mileage decision
keyboard = [
[InlineKeyboardButton('Fill', callback_data='Fill')],
[InlineKeyboardButton('Skip', callback_data='Skip')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.message.reply_text('<b>Choose an option:</b>', parse_mode='HTML', reply_markup=reply_markup)

return CAR_MILEAGE_DECISION


async def car_mileage_decision(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Asks the user to fill in the mileage or skip."""
query = update.callback_query
await query.answer()
decision = query.data

if decision == 'Fill':
await query.edit_message_text(text='<b>Please type in the mileage (e.g., 50000):</b>', parse_mode='HTML')
return CAR_MILEAGE
else:
await query.edit_message_text(text='<b>Mileage step skipped.</b>', parse_mode='HTML')
return await skip_mileage(update, context)


async def car_mileage(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the car mileage."""
context.user_data['car_mileage'] = update.message.text
await update.message.reply_text('<b>Mileage noted.\n'
'Please upload a photo of your car 📷, or send /skip.</b>',
parse_mode='HTML')
return PHOTO


async def skip_mileage(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Skips the mileage input."""
context.user_data['car_mileage'] = 'Not provided'

text = '<b>Please upload a photo of your car 📷, or send /skip.</b>'

# Determine the correct way to send a reply based on the update type
if update.callback_query:
# If called from a callback query, use the callback_query's message
chat_id = update.callback_query.message.chat_id
await context.bot.send_message(chat_id=chat_id, text=text, parse_mode='HTML')
# Optionally, you might want to acknowledge the callback query
await update.callback_query.answer()
elif update.message:
# If called from a direct message
await update.message.reply_text(text)
else:
# Handle other cases or log an error/warning
logger.warning('skip_mileage was called without a message or callback_query context.')

return PHOTO


async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the photo."""
photo_file = await update.message.photo[-1].get_file()
# Correctly store the file_id of the uploaded photo for later use
context.user_data['car_photo'] = photo_file.file_id # Preserve this line

# Inform user and transition to summary
await update.message.reply_text('<b>Photo uploaded successfully.\n'
'Let\'s summarize your selections.</b>',
parse_mode='HTML'
)
await summary(update, context) # Proceed to summary


async def skip_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Skips the photo upload."""
await update.message.reply_text('<b>No photo uploaded.\n'
'Let\'s summarize your selections.</b>',
parse_mode='HTML')
await summary(update, context)


async def summary(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Summarizes the user's selections and ends the conversation, including the uploaded image."""
selections = context.user_data
# Construct the summary text
summary_text = (f"<b>Here's what you told me about your car:\n</b>"
f"<b>Car Type:</b> {selections.get('car_type')}\n"
f"<b>Color:</b> {selections.get('car_color')}\n"
f"<b>Mileage:</b> {selections.get('car_mileage')}\n"
f"<b>Photo:</b> {'Uploaded' if 'car_photo' in selections else 'Not provided'}")

chat_id = update.effective_chat.id

# If a photo was uploaded, send it back with the summary as the caption
if 'car_photo' in selections and selections['car_photo'] != 'Not provided':
await context.bot.send_photo(chat_id=chat_id, photo=selections['car_photo'], caption=summary_text, parse_mode='HTML')
else:
# If no photo was uploaded, just send the summary text
await context.bot.send_message(chat_id=chat_id, text=summary_text, parse_mode='HTML')

return ConversationHandler.END


async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Cancels and ends the conversation."""
await update.message.reply_text('Bye! Hope to talk to you again soon.', reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END

Основная функция и опрос ботов

В функции main настройте Application и ConversationHandler, включая точки входа, состояния и резервные варианты. Запустите бота с опросом, чтобы прослушивать обновления:


def main() -> None:
"""Run the bot."""
application = Application.builder().token("YOUR TOKEN HERE").build()

conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
CAR_TYPE: [MessageHandler(filters.TEXT & ~filters.COMMAND, car_type)],
CAR_COLOR: [CallbackQueryHandler(car_color)],
CAR_MILEAGE_DECISION: [CallbackQueryHandler(car_mileage_decision)],
CAR_MILEAGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, car_mileage)],
PHOTO: [
MessageHandler(filters.PHOTO, photo),
CommandHandler('skip', skip_photo)
],
SUMMARY: [MessageHandler(filters.ALL, summary)]
},
fallbacks=[CommandHandler('cancel', cancel)],
)

application.add_handler(conv_handler)

# Handle the case when a user sends /start but they're not in a conversation
application.add_handler(CommandHandler('start', start))

application.run_polling()

  Запустите бота:

Завершите свой скрипт вызовом функции main. Запустите бота, выполнив скрипт Python в терминале.

Здесь вы можете найти весь код:

import logging
from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove, Update,
InlineKeyboardButton, InlineKeyboardMarkup)
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler,
ContextTypes, ConversationHandler, MessageHandler, filters)

# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)

logger = logging.getLogger(__name__)

# Define states
CAR_TYPE, CAR_COLOR, CAR_MILEAGE_DECISION, CAR_MILEAGE, PHOTO, SUMMARY = range(6)


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Starts the conversation and asks the user about their preferred car type."""
reply_keyboard = [['Sedan', 'SUV', 'Sports', 'Electric']]

await update.message.reply_text(
'<b>Welcome to the Car Sales Listing Bot!\n'
'Let\'s get some details about the car you\'re selling.\n'
'What is your car type?</b>',
parse_mode='HTML',
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True, resize_keyboard=True),
)

return CAR_TYPE


async def car_type(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the user's car type."""
user = update.message.from_user
context.user_data['car_type'] = update.message.text
cars = {"Sedan": "🚗", "SUV": "🚙", "Sports": "🏎️", "Electric": "⚡"}
logger.info('Car type of %s: %s', user.first_name, update.message.text)
await update.message.reply_text(
f'<b>You selected {update.message.text} car {cars[update.message.text]}.\n'
f'What color your car is?</b>',
parse_mode='HTML',
reply_markup=ReplyKeyboardRemove(),
)

# Define inline buttons for car color selection
keyboard = [
[InlineKeyboardButton('Red', callback_data='Red')],
[InlineKeyboardButton('Blue', callback_data='Blue')],
[InlineKeyboardButton('Black', callback_data='Black')],
[InlineKeyboardButton('White', callback_data='White')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('<b>Please choose:</b>', parse_mode='HTML', reply_markup=reply_markup)

return CAR_COLOR


async def car_color(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the user's car color."""
query = update.callback_query
await query.answer()
context.user_data['car_color'] = query.data
await query.edit_message_text(
text=f'<b>You selected {query.data} color.\n'
f'Would you like to fill in the mileage for your car?</b>',
parse_mode='HTML'
)

# Define inline buttons for mileage decision
keyboard = [
[InlineKeyboardButton('Fill', callback_data='Fill')],
[InlineKeyboardButton('Skip', callback_data='Skip')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.message.reply_text('<b>Choose an option:</b>', parse_mode='HTML', reply_markup=reply_markup)

return CAR_MILEAGE_DECISION


async def car_mileage_decision(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Asks the user to fill in the mileage or skip."""
query = update.callback_query
await query.answer()
decision = query.data

if decision == 'Fill':
await query.edit_message_text(text='<b>Please type in the mileage (e.g., 50000):</b>', parse_mode='HTML')
return CAR_MILEAGE
else:
await query.edit_message_text(text='<b>Mileage step skipped.</b>', parse_mode='HTML')
return await skip_mileage(update, context)


async def car_mileage(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the car mileage."""
context.user_data['car_mileage'] = update.message.text
await update.message.reply_text('<b>Mileage noted.\n'
'Please upload a photo of your car 📷, or send /skip.</b>',
parse_mode='HTML')
return PHOTO


async def skip_mileage(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Skips the mileage input."""
context.user_data['car_mileage'] = 'Not provided'

text = '<b>Please upload a photo of your car 📷, or send /skip.</b>'

# Determine the correct way to send a reply based on the update type
if update.callback_query:
# If called from a callback query, use the callback_query's message
chat_id = update.callback_query.message.chat_id
await context.bot.send_message(chat_id=chat_id, text=text, parse_mode='HTML')
# Optionally, you might want to acknowledge the callback query
await update.callback_query.answer()
elif update.message:
# If called from a direct message
await update.message.reply_text(text)
else:
# Handle other cases or log an error/warning
logger.warning('skip_mileage was called without a message or callback_query context.')

return PHOTO


async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Stores the photo."""
photo_file = await update.message.photo[-1].get_file()
# Correctly store the file_id of the uploaded photo for later use
context.user_data['car_photo'] = photo_file.file_id # Preserve this line

# Inform user and transition to summary
await update.message.reply_text('<b>Photo uploaded successfully.\n'
'Let\'s summarize your selections.</b>',
parse_mode='HTML'
)
await summary(update, context) # Proceed to summary


async def skip_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Skips the photo upload."""
await update.message.reply_text('<b>No photo uploaded.\n'
'Let\'s summarize your selections.</b>',
parse_mode='HTML')
await summary(update, context)


async def summary(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Summarizes the user's selections and ends the conversation, including the uploaded image."""
selections = context.user_data
# Construct the summary text
summary_text = (f"<b>Here's what you told me about your car:\n</b>"
f"<b>Car Type:</b> {selections.get('car_type')}\n"
f"<b>Color:</b> {selections.get('car_color')}\n"
f"<b>Mileage:</b> {selections.get('car_mileage')}\n"
f"<b>Photo:</b> {'Uploaded' if 'car_photo' in selections else 'Not provided'}")

chat_id = update.effective_chat.id

# If a photo was uploaded, send it back with the summary as the caption
if 'car_photo' in selections and selections['car_photo'] != 'Not provided':
await context.bot.send_photo(chat_id=chat_id, photo=selections['car_photo'], caption=summary_text, parse_mode='HTML')
else:
# If no photo was uploaded, just send the summary text
await context.bot.send_message(chat_id=chat_id, text=summary_text, parse_mode='HTML')

return ConversationHandler.END


async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Cancels and ends the conversation."""
await update.message.reply_text('Bye! Hope to talk to you again soon.', reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END


def main() -> None:
"""Run the bot."""
application = Application.builder().token("YOUR TOKEN HERE").build()

conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
CAR_TYPE: [MessageHandler(filters.TEXT & ~filters.COMMAND, car_type)],
CAR_COLOR: [CallbackQueryHandler(car_color)],
CAR_MILEAGE_DECISION: [CallbackQueryHandler(car_mileage_decision)],
CAR_MILEAGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, car_mileage)],
PHOTO: [
MessageHandler(filters.PHOTO, photo),
CommandHandler('skip', skip_photo)
],
SUMMARY: [MessageHandler(filters.ALL, summary)]
},
fallbacks=[CommandHandler('cancel', cancel)],
)

application.add_handler(conv_handler)

# Handle the case when a user sends /start but they're not in a conversation
application.add_handler(CommandHandler('start', start))

application.run_polling()


if __name__ == '__main__':
main()

Шаг 3: Тестирование и взаимодействие с ботом

После запуска скрипта найдите своего бота в Telegram и начните взаимодействовать с ним. Теперь вы должны иметь возможность использовать команду /start для начала разговора, который поможет вам выставить автомобиль на продажу.

  Заключение:

Вы только что расширили свой Telegram-бот, добавив в него обработку текстовых сообщений и интерактивные кнопки, что сделало его гораздо более привлекательным. Это лишь малая часть того, что возможно с библиотекой python-telegram-bot. По мере дальнейшего изучения вы найдете варианты обработки различных типов контента, интеграции с внешними API и многое другое. Погрузитесь в документацию библиотеки, чтобы узнать обо всех возможностях вашего нового Telegram-бота.

Удачного программирования и приятного воплощения в жизнь вашего Telegram-бота!

Ваша поддержка значит очень многое! 🙌

Если вам понравилась эта статья и вы нашли ее ценной, пожалуйста, подумайте о том, чтобы похлопать в ладоши, чтобы выразить свою поддержку. Не стесняйтесь изучать другие мои статьи, где я освещаю широкий спектр тем, связанных с программированием на Python и другими. Подписавшись на меня, вы будете в курсе моего последнего контента и идей. Я с нетерпением жду возможности поделиться с вами новыми знаниями и связаться с вами в будущих статьях. А пока продолжайте программировать, продолжайте учиться и, самое главное, наслаждайтесь путешествием!

  Удачного программирования!