arbitrarycallbackdatabot.py

  1#!/usr/bin/env python
  2# pylint: disable=unused-argument, wrong-import-position
  3# This program is dedicated to the public domain under the CC0 license.
  4
  5"""This example showcases how PTBs "arbitrary callback data" feature can be used.
  6
  7For detailed info on arbitrary callback data, see the wiki page at
  8https://github.com/python-telegram-bot/python-telegram-bot/wiki/Arbitrary-callback_data
  9
 10Note:
 11To use arbitrary callback data, you must install PTB via
 12`pip install "python-telegram-bot[callback-data]"`
 13"""
 14import logging
 15from typing import List, Tuple, cast
 16
 17from telegram import __version__ as TG_VER
 18
 19try:
 20    from telegram import __version_info__
 21except ImportError:
 22    __version_info__ = (0, 0, 0, 0, 0)  # type: ignore[assignment]
 23
 24if __version_info__ < (20, 0, 0, "alpha", 1):
 25    raise RuntimeError(
 26        f"This example is not compatible with your current PTB version {TG_VER}. To view the "
 27        f"{TG_VER} version of this example, "
 28        f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html"
 29    )
 30from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
 31from telegram.ext import (
 32    Application,
 33    CallbackQueryHandler,
 34    CommandHandler,
 35    ContextTypes,
 36    InvalidCallbackData,
 37    PicklePersistence,
 38)
 39
 40# Enable logging
 41logging.basicConfig(
 42    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
 43)
 44# set higher logging level for httpx to avoid all GET and POST requests being logged
 45logging.getLogger("httpx").setLevel(logging.WARNING)
 46
 47logger = logging.getLogger(__name__)
 48
 49
 50async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
 51    """Sends a message with 5 inline buttons attached."""
 52    number_list: List[int] = []
 53    await update.message.reply_text("Please choose:", reply_markup=build_keyboard(number_list))
 54
 55
 56async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
 57    """Displays info on how to use the bot."""
 58    await update.message.reply_text(
 59        "Use /start to test this bot. Use /clear to clear the stored data so that you can see "
 60        "what happens, if the button data is not available. "
 61    )
 62
 63
 64async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
 65    """Clears the callback data cache"""
 66    context.bot.callback_data_cache.clear_callback_data()
 67    context.bot.callback_data_cache.clear_callback_queries()
 68    await update.effective_message.reply_text("All clear!")
 69
 70
 71def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup:
 72    """Helper function to build the next inline keyboard."""
 73    return InlineKeyboardMarkup.from_column(
 74        [InlineKeyboardButton(str(i), callback_data=(i, current_list)) for i in range(1, 6)]
 75    )
 76
 77
 78async def list_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
 79    """Parses the CallbackQuery and updates the message text."""
 80    query = update.callback_query
 81    await query.answer()
 82    # Get the data from the callback_data.
 83    # If you're using a type checker like MyPy, you'll have to use typing.cast
 84    # to make the checker get the expected type of the callback_data
 85    number, number_list = cast(Tuple[int, List[int]], query.data)
 86    # append the number to the list
 87    number_list.append(number)
 88
 89    await query.edit_message_text(
 90        text=f"So far you've selected {number_list}. Choose the next item:",
 91        reply_markup=build_keyboard(number_list),
 92    )
 93
 94    # we can delete the data stored for the query, because we've replaced the buttons
 95    context.drop_callback_data(query)
 96
 97
 98async def handle_invalid_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
 99    """Informs the user that the button is no longer available."""
100    await update.callback_query.answer()
101    await update.effective_message.edit_text(
102        "Sorry, I could not process this button click 😕 Please send /start to get a new keyboard."
103    )
104
105
106def main() -> None:
107    """Run the bot."""
108    # We use persistence to demonstrate how buttons can still work after the bot was restarted
109    persistence = PicklePersistence(filepath="arbitrarycallbackdatabot")
110    # Create the Application and pass it your bot's token.
111    application = (
112        Application.builder()
113        .token("TOKEN")
114        .persistence(persistence)
115        .arbitrary_callback_data(True)
116        .build()
117    )
118
119    application.add_handler(CommandHandler("start", start))
120    application.add_handler(CommandHandler("help", help_command))
121    application.add_handler(CommandHandler("clear", clear))
122    application.add_handler(
123        CallbackQueryHandler(handle_invalid_button, pattern=InvalidCallbackData)
124    )
125    application.add_handler(CallbackQueryHandler(list_button))
126
127    # Run the bot until the user presses Ctrl-C
128    application.run_polling(allowed_updates=Update.ALL_TYPES)
129
130
131if __name__ == "__main__":
132    main()