Proper way to build menus with python-telegram-bot
Asked Answered
E

4

31

I work with python-telegram-bot and try to build a system of nested menus as BotFather bot does. For instance, you have a general bot menu

pic.1

where you can choose "Edit Bot" and get the new corresponding menu

pic.2

with an option to get back to the previous menu.

I try to achieve that with code:

# main menu
def start(bot, update):
    menu_main = [[InlineKeyboardButton('Option 1', callback_data='m1')],
                 [InlineKeyboardButton('Option 2', callback_data='m2')],
                 [InlineKeyboardButton('Option 3', callback_data='m3')]]
    reply_markup = InlineKeyboardMarkup(menu_main)
    update.message.reply_text('Choose the option:', reply_markup=reply_markup)

# all other menus
def menu_actions(bot, update):
    query = update.callback_query

    if query.data == 'm1':
        # first submenu
        menu_1 = [[InlineKeyboardButton('Submenu 1-1', callback_data='m1_1')],
                  [InlineKeyboardButton('Submenu 1-2', callback_data='m1_2')]]
        reply_markup = InlineKeyboardMarkup(menu_1)
        bot.edit_message_text(chat_id=query.message.chat_id,
                              message_id=query.message.message_id,
                              text='Choose the option:',
                              reply_markup=reply_markup)
    elif query.data == 'm2':
        # second submenu
        # first submenu
        menu_2 = [[InlineKeyboardButton('Submenu 2-1', callback_data='m2_1')],
                  [InlineKeyboardButton('Submenu 2-2', callback_data='m2_2')]]
        reply_markup = InlineKeyboardMarkup(menu_2)
        bot.edit_message_text(chat_id=query.message.chat_id,
                              message_id=query.message.message_id,
                              text='Choose the option:',
                              reply_markup=reply_markup)
    elif query.data == 'm1_1':
        ...
    elif query.data == 'm1_2':
        ...
    # and so on for every callback_data option

...

# handlers
dispatcher.add_handler(CommandHandler('start', start))
dispatcher.add_handler(CallbackQueryHandler(menu_actions))

This code works but I have a feeling that it is kind of irrational — to build a long elif tree.

Moreover, I can't figure out how to give to the user an option to get back to the main menu from second level menus (since the main menu is located in another handler and I can't catch it with a callback from CallbackQueryHandler).

So the question is — what is the best practice to build that kind of menu systems?

Ellie answered 1/7, 2018 at 17:12 Comment(0)
E
17

great answer by @dzNET. But it won't work in V12 so I changed a little bit

from telegram.ext import CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
############################### Bot ############################################
def start(update, context):
  update.message.reply_text(main_menu_message(),
                            reply_markup=main_menu_keyboard())

def main_menu(update,context):
  query = update.callback_query
  query.answer()
  query.edit_message_text(
                        text=main_menu_message(),
                        reply_markup=main_menu_keyboard())

def first_menu(update,context):
  query = update.callback_query
  query.answer()
  query.edit_message_text(
                        text=first_menu_message(),
                        reply_markup=first_menu_keyboard())

def second_menu(update,context):
  query = update.callback_query
  query.answer()
  query.edit_message_text(
                        text=second_menu_message(),
                        reply_markup=second_menu_keyboard())

# and so on for every callback_data option
def first_submenu(bot, update):
  pass

def second_submenu(bot, update):
  pass

############################ Keyboards #########################################
def main_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Option 1', callback_data='m1')],
              [InlineKeyboardButton('Option 2', callback_data='m2')],
              [InlineKeyboardButton('Option 3', callback_data='m3')]]
  return InlineKeyboardMarkup(keyboard)

def first_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 1-1', callback_data='m1_1')],
              [InlineKeyboardButton('Submenu 1-2', callback_data='m1_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

def second_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 2-1', callback_data='m2_1')],
              [InlineKeyboardButton('Submenu 2-2', callback_data='m2_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

############################# Messages #########################################
def main_menu_message():
  return 'Choose the option in main menu:'

def first_menu_message():
  return 'Choose the submenu in first menu:'

def second_menu_message():
  return 'Choose the submenu in second menu:'

############################# Handlers #########################################
updater = Updater('YOUR_TOKEN_HERE', use_context=True)

updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(main_menu, pattern='main'))
updater.dispatcher.add_handler(CallbackQueryHandler(first_menu, pattern='m1'))
updater.dispatcher.add_handler(CallbackQueryHandler(second_menu, pattern='m2'))
updater.dispatcher.add_handler(CallbackQueryHandler(first_submenu,
                                                    pattern='m1_1'))
updater.dispatcher.add_handler(CallbackQueryHandler(second_submenu,
                                                    pattern='m2_1'))

updater.start_polling()

Again thanks to @dzNET

Exchange answered 6/4, 2020 at 7:30 Comment(1)
For submenu's you should use query = bot.callback_query instead of query = update.callback_query - otherwise it throws the error query = update.callback_query AttributeError: 'CallbackContext' object has no attribute 'callback_query'Eruptive
Q
31

You should use an argument pattern in CallbackQueryHandler. Also is a good thing use a classes or functions for keyboards and messages.
To return to main menu add return button to submenu with specific callback pattern.

Please note: you use edit_message_text in menu. It's mean nothing will happen if you will call start function with reply_text method from any menu.

Full working example with functions:

#!/usr/bin/env python3.8
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
############################### Bot ############################################
def start(bot, update):
  bot.message.reply_text(main_menu_message(),
                         reply_markup=main_menu_keyboard())

def main_menu(bot, update):
  bot.callback_query.message.edit_text(main_menu_message(),
                          reply_markup=main_menu_keyboard())

def first_menu(bot, update):
  bot.callback_query.message.edit_text(first_menu_message(),
                          reply_markup=first_menu_keyboard())

def second_menu(bot, update):
  bot.callback_query.message.edit_text(second_menu_message(),
                          reply_markup=second_menu_keyboard())

def first_submenu(bot, update):
  pass

def second_submenu(bot, update):
  pass

def error(update, context):
    print(f'Update {update} caused error {context.error}')

############################ Keyboards #########################################
def main_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Menu 1', callback_data='m1')],
              [InlineKeyboardButton('Menu 2', callback_data='m2')],
              [InlineKeyboardButton('Menu 3', callback_data='m3')]]
  return InlineKeyboardMarkup(keyboard)

def first_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 1-1', callback_data='m1_1')],
              [InlineKeyboardButton('Submenu 1-2', callback_data='m1_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

def second_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 2-1', callback_data='m2_1')],
              [InlineKeyboardButton('Submenu 2-2', callback_data='m2_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

############################# Messages #########################################
def main_menu_message():
  return 'Choose the option in main menu:'

def first_menu_message():
  return 'Choose the submenu in first menu:'

def second_menu_message():
  return 'Choose the submenu in second menu:'

############################# Handlers #########################################
updater = Updater('XXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', use_context=True)
updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(main_menu, pattern='main'))
updater.dispatcher.add_handler(CallbackQueryHandler(first_menu, pattern='m1'))
updater.dispatcher.add_handler(CallbackQueryHandler(second_menu, pattern='m2'))
updater.dispatcher.add_handler(CallbackQueryHandler(first_submenu, pattern='m1_1'))
updater.dispatcher.add_handler(CallbackQueryHandler(second_submenu, pattern='m2_1'))
updater.dispatcher.add_error_handler(error)

updater.start_polling()
################################################################################

Sorry, i have two spaces in tab. :)

UPD: Fix submenu object.

Quadrangle answered 1/7, 2018 at 19:31 Comment(5)
Great answer! Thanks a lot for help! One notice I have: when adding callback handlers to dispatcher, we need to define the pattern attribute more clear: pattern='^m1$' or pattern='^m1_1$' — so that callback data m1 doesn't trigger 'm1_1'.Ellie
m1 and m1_1 is a different string. also pattern support regex and you can use this syntax if you want. glad to help you! enjoy!Quadrangle
I am sorry, but when I am trying to handle callback data m1_1 it for some reason triggers the handler for m1 callback data until I use more strong pattern with ^ and $ regex syntaxEllie
@Quadrangle I try your code, but don't understand how to handle pressing buttons on first or second menu. Could you help me?Habergeon
Before using this approch, this is deprecated since V12. It won't work with Updater(key, use_context=True). It can be force to work using use_context=False. A warning will trigger: menu.py:94: TelegramDeprecationWarning: Old Handler API is deprecated - see git.io/fxJuV for details updater = Updater(cfg['api'], use_context=False)Headpin
E
17

great answer by @dzNET. But it won't work in V12 so I changed a little bit

from telegram.ext import CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
############################### Bot ############################################
def start(update, context):
  update.message.reply_text(main_menu_message(),
                            reply_markup=main_menu_keyboard())

def main_menu(update,context):
  query = update.callback_query
  query.answer()
  query.edit_message_text(
                        text=main_menu_message(),
                        reply_markup=main_menu_keyboard())

def first_menu(update,context):
  query = update.callback_query
  query.answer()
  query.edit_message_text(
                        text=first_menu_message(),
                        reply_markup=first_menu_keyboard())

def second_menu(update,context):
  query = update.callback_query
  query.answer()
  query.edit_message_text(
                        text=second_menu_message(),
                        reply_markup=second_menu_keyboard())

# and so on for every callback_data option
def first_submenu(bot, update):
  pass

def second_submenu(bot, update):
  pass

############################ Keyboards #########################################
def main_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Option 1', callback_data='m1')],
              [InlineKeyboardButton('Option 2', callback_data='m2')],
              [InlineKeyboardButton('Option 3', callback_data='m3')]]
  return InlineKeyboardMarkup(keyboard)

def first_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 1-1', callback_data='m1_1')],
              [InlineKeyboardButton('Submenu 1-2', callback_data='m1_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

def second_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 2-1', callback_data='m2_1')],
              [InlineKeyboardButton('Submenu 2-2', callback_data='m2_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

############################# Messages #########################################
def main_menu_message():
  return 'Choose the option in main menu:'

def first_menu_message():
  return 'Choose the submenu in first menu:'

def second_menu_message():
  return 'Choose the submenu in second menu:'

############################# Handlers #########################################
updater = Updater('YOUR_TOKEN_HERE', use_context=True)

updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(main_menu, pattern='main'))
updater.dispatcher.add_handler(CallbackQueryHandler(first_menu, pattern='m1'))
updater.dispatcher.add_handler(CallbackQueryHandler(second_menu, pattern='m2'))
updater.dispatcher.add_handler(CallbackQueryHandler(first_submenu,
                                                    pattern='m1_1'))
updater.dispatcher.add_handler(CallbackQueryHandler(second_submenu,
                                                    pattern='m2_1'))

updater.start_polling()

Again thanks to @dzNET

Exchange answered 6/4, 2020 at 7:30 Comment(1)
For submenu's you should use query = bot.callback_query instead of query = update.callback_query - otherwise it throws the error query = update.callback_query AttributeError: 'CallbackContext' object has no attribute 'callback_query'Eruptive
F
2

I wrote a wrapper on top of python-telegram-bot to send keyboards and inlined messages: https://github.com/mevellea/telegram_menu


from telegram_menu import TelegramMenuSession, BaseMessage, NavigationHandler, ButtonType


class BotMessage(BaseMessage):

    def __init__(self, navigation_handler: NavigationHandler, bot_name: str):
        super().__init__(navigation_handler, label="BotMessage", inlined=True, home_after=True)
        self.bot_name = bot_name

    def update(self):
        self.keyboard = []
        self.add_button("API token", callback=self.do_something, btype=ButtonType.MESSAGE)
        self.add_button(":door:", callback=self.do_something, btype=ButtonType.MESSAGE)
        return f"What do you want to do with {self.bot_name}?"

    def do_something(self) -> str:
        return f"[{self.bot_name}] do something"


class EntryPointMessage(BaseMessage):

    def __init__(self, navigation: NavigationHandler) -> None:
        super().__init__(navigation, label="StartMessage")

    def update(self):
        self.add_button(label="First bot", callback=BotMessage(self.navigation, bot_name="Bot1"))
        self.add_button(label="Second bot", callback=BotMessage(self.navigation, bot_name="Bot2"))
        return "App entry point"


session = TelegramMenuSession(API_TOKEN)
session.start(EntryPointMessage, idle=True)

I hope this helps.

Fluorescein answered 27/3, 2022 at 19:9 Comment(1)
Can you provide an example of how to use the wrapper in your answer?Scully
G
1

Just to share a working version I have made with all the help provided by other users in this posts, as original code dont work in current versions, but this one, does :)

from telegram.ext import CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import ForceReply, Update
from telegram.ext import Updater
import configparser
from telegram.ext import *


config = configparser.ConfigParser()
config.read("config/config.ini")

bot_token=config["TelegramAPI"]["bot_token"]
admins=config["Users"]["admins"]

############################### Bot ############################################
async def start(update, context):
  await update.message.reply_text(await main_menu_message(),
                            reply_markup=await main_menu_keyboard())

async def main_menu(update,context):
  query = update.callback_query
  await query.answer()
  await query.edit_message_text(
                        text=await main_menu_message(),
                        reply_markup=await main_menu_keyboard())

async def first_menu(update,context):
  query = update.callback_query
  await query.answer()
  await query.edit_message_text(
                        text=await first_menu_message(),
                        reply_markup=await first_menu_keyboard())

async def second_menu(update,context):
  query = update.callback_query
  await query.answer()
  await query.edit_message_text(
                        text=await second_menu_message(),
                        reply_markup=await second_menu_keyboard())
  
async def third_menu(update,context):
  query = update.callback_query
  await query.answer()
  await query.edit_message_text(
                        text=await third_menu_message(),
                        reply_markup=await third_menu_keyboard())  

# and so on for every callback_data option
async def first_submenu(bot, update):
  pass

async def second_submenu(bot, update):
  pass

############################ Keyboards #########################################
async def main_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Option 1', callback_data='m1')],
              [InlineKeyboardButton('Option 2', callback_data='m2')],
              [InlineKeyboardButton('Option 3', callback_data='m3')]]
  return InlineKeyboardMarkup(keyboard)

async def first_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 1-1', callback_data='m1_1')],
              [InlineKeyboardButton('Submenu 1-2', callback_data='m1_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

async def second_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 2-1', callback_data='m2_1')],
              [InlineKeyboardButton('Submenu 2-2', callback_data='m2_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

async def third_menu_keyboard():
  keyboard = [[InlineKeyboardButton('Submenu 3-1', callback_data='m2_1')],
              [InlineKeyboardButton('Submenu 3-2', callback_data='m2_2')],
              [InlineKeyboardButton('Main menu', callback_data='main')]]
  return InlineKeyboardMarkup(keyboard)

############################# Messages #########################################
async def main_menu_message():
  return 'Choose the option in main menu:'

async def first_menu_message():
  return 'Choose the submenu in first menu:'

async def second_menu_message():
  return 'Choose the submenu in second menu:'

async def third_menu_message():
  return 'Choose the submenu in second menu:'

############################# Handlers #########################################
# Create the Application and pass it your bot's token.
def main() -> None:
    application = Application.builder().token(bot_token).build()

    application.add_handler(CommandHandler('start', start))
    application.add_handler(CallbackQueryHandler(main_menu, pattern='main'))
    application.add_handler(CallbackQueryHandler(first_menu, pattern='m1'))
    application.add_handler(CallbackQueryHandler(second_menu, pattern='m2'))
    application.add_handler(CallbackQueryHandler(third_menu, pattern='m3'))

    application.add_handler(CallbackQueryHandler(first_submenu,
                                                        pattern='m1_1'))
    application.add_handler(CallbackQueryHandler(second_submenu,
                                                        pattern='m2_1'))

    application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
    main()

Groscr answered 21/8, 2023 at 23:34 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.