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