Building Telegram Bot with Python-Telegram-Bot: A Comprehensive Guide

Moraneus
12 min readFeb 12, 2024

Creating a Telegram bot can significantly extend the functionality of your messaging experience, allowing for everything from automated responses to sophisticated interactions. This comprehensive guide will walk you through the process of building a basic Telegram bot with Python, incorporating user commands, message handling, and custom keyboards. We’ll use the python-telegram-bot library for its simplicity and powerful features.

Prerequisites

  • Python 3.x installed on your system.
  • A Telegram account.
  • The pip package manager to install Python libraries.

Introduction to the Car Sales Listing Bot

In this article, I will demonstrate a very simple Car Sales Listing Bot that is designed to streamline the process of gathering some necessary information from users wishing to list their cars for sale. By engaging in a structured conversation, the bot collects details such as the car’s type, color, and mileage, and even allows for photo uploads, culminating in a summary that confirms the listing’s details.

Key Features:

  • Interactive conversation flow to collect car details.
  • Inline keyboard for easy selection of options.
  • Ability to upload a photo of the car.
  • Summary of the listing details for confirmation.

Step 1: Set Up Your Telegram Bot

  1. Create Your Bot: Open Telegram and search for the “BotFather” account. Start a conversation and use the /newbot command to create a new bot. Follow the prompts to set up your bot's name and username. BotFather will then give you a token, which is crucial for accessing the Telegram Bot API. Keep this token secure and do not share it.

2. Install Required Libraries: Install python-telegram-bot using pip:

python3 -m pip install python-telegram-bot

Step 2: Creating Your Bot with Python

Now, let’s dive into coding your bot. Please create a new Python file, e.g., my_telegram_bot.pyand open it in your favorite text editor. Then, follow these steps to write your bot.

Import Libraries:

Start by importing necessary modules and setting up logging to help with debugging:

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__)

Define Conversation States:

States in a Telegram bot, especially when using a conversation handler, serve as a framework to manage the flow of interaction between the bot and the user. They are essentially markers or checkpoints that define what part of the conversation the user is currently engaged with and determine what the bot should do next based on the user’s input. Here’s a more general overview of the role and functionality of states in managing bot conversations. The purpose and functionality of states in the Telegram bot are:

  1. Sequential Flow Management: States allow the bot to manage a sequential flow of conversation. By moving from one state to another, the bot can guide the user through a series of steps, questions, or options in a logical order.
  2. Context Awareness: They help the bot maintain context in a conversation. By knowing the current state, the bot understands what information has been provided by the user and what information is still needed, enabling it to respond appropriately.
  3. User Input Processing: Based on the current state, the bot can process user inputs differently. For example, an input in the “CAR_TYPE” state would be understood as the user specifying the type of car they’re selling, while the same input in the “CAR_COLOR” state would be interpreted as the color of the car.
  4. Conditional Logic Implementation: States allow for the implementation of conditional logic in the conversation. Depending on user responses or choices, the bot can decide to skip certain states, repeat them, or take the user down a different conversational path.
  5. Error Handling and Repetition: They facilitate error handling and the repetition of questions if the user provides unexpected or invalid responses. By keeping track of the current state, the bot can re-prompt the user for information correctly.
  6. State Persistence: In more complex bots, states can be stored and persisted across sessions, allowing users to pick up the conversation where they left off, even if they temporarily leave the chat or if the bot restarts.

Let’s enumerate the states for our bot to manage the flow:

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

Implement The Conversation Handlers:

Conversation handlers in Telegram bots, particularly when using libraries like python-telegram-bot, are powerful tools that manage the flow of conversations based on user inputs and predefined states. They are crucial for developing bots that require a sequence of interactions, such as collecting information, guiding users through menus, or executing commands in a specific order. Here's a closer look at how conversation handlers work and their role in bot development:

Purpose and Functionality:

  1. Managing Conversational States: Conversation handlers keep track of the current state of the dialogue with each user. They determine what the bot should do next based on the user’s input and the current state, allowing for a smooth and logical progression through different stages of interaction.
  2. Routing User Inputs: They route user inputs to different callback functions based on the current state. This means that the same input can lead to different outcomes depending on where the user is in the conversation flow.
  3. Handling Commands and Text: Conversation handlers can differentiate between commands (like /start or /help) and regular text messages, allowing developers to specify distinct responses or actions for each type of input.
  4. Integrating with Keyboards and Buttons: They work seamlessly with custom keyboards and inline buttons, enabling developers to create interactive and user-friendly interfaces within the conversation. Users can select options or navigate through the bot’s features using these UI elements.
  5. Fallbacks and Timeouts: Conversation handlers support fallback functions, which can be triggered when the user sends unexpected input or when the conversation needs to be reset. They can also handle timeouts, ending a conversation automatically after a period of inactivity.

Implementing Conversation Handlers:

Implementing a conversation handler typically involves defining entry points, states, and fallbacks:

  • Entry Points: These are triggers that start the conversation. Commonly, the /start command is used as an entry point, but you can define multiple entry points for different conversation flows.
  • States: As discussed, states represent different points in the conversation. Each state is associated with one or more callback functions that define the bot’s behavior at that stage. Developers map states to these callbacks, dictating the flow of the conversation.
  • Fallbacks: Fallback functions are defined to handle unexpected situations or to provide a way to exit or reset the conversation. A common fallback is a /cancel command that allows users to stop the conversation at any point.

Following, is the start handler function initiates the conversation (entry point), presenting the user with a selection of car types:

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

Here you can find the rest of the handlers:


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 Function and Bot Polling

In the main function, set up the Application and ConversationHandler, including entry points, states, and fallbacks. Start the bot with polling to listen for updates:


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()

Run Your Bot:

Complete your script with a call to the main function. Run your bot by executing the Python script in your terminal.

Here you can find the whole code:

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()

Step 3: Testing and Interacting with Your Bot

After running your script, find your bot on Telegram and start interacting with it. You should now be able to use the /start command to start a conversation, which will guide you through listing a car for sale.

Conclusion:

You have just expanded your Telegram bot to include text message handling and interactive buttons, making it far more engaging. This is just scratching the surface of what’s possible with the python-telegram-bot library. As you explore further, you'll find options for handling different types of content, integrating with external APIs, and much more. Dive into the library's documentation to discover all the possibilities for your new Telegram bot.

Happy coding, and enjoy bringing your Telegram bot to life!

Your Support Means a Lot! 🙌

If you enjoyed this article and found it valuable, please consider giving it a clap to show your support. Feel free to explore my other articles, where I cover a wide range of topics related to Python programming and others. By following me, you’ll stay updated on my latest content and insights. I look forward to sharing more knowledge and connecting with you through future articles. Until then, keep coding, keep learning, and most importantly, enjoy the journey!

Happy programming!

References:

--

--