Skip to content

Commit bf0a9e8

Browse files
committed
Add ToggleButton and RadioButton and remove the need for chat/user data
Also rename the name parameter of buttons to uuid to better detail what it does, and allow the toggle and radio buttons to have names seperate from their uuid Pass update around (unfortunately many properties aren't properties anymore) Add a post_init method to buttons that are called after they've gotten assigned their parent_menu attribute
1 parent 0373cee commit bf0a9e8

File tree

2 files changed

+151
-41
lines changed

2 files changed

+151
-41
lines changed

examples/menubot.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import telegram.ext
1010
from telegram.ext import CommandHandler
11-
from telegram.ext.menu import Menu, MenuHandler, Button, BackButton
11+
from telegram.ext.menu import Menu, MenuHandler, Button, BackButton, ToggleButton, RadioButton
1212

1313
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
1414
level=logging.DEBUG)
@@ -28,6 +28,11 @@ def start(bot, update):
2828
update.effective_message.reply_text('Hello!')
2929

3030

31+
def submit(bot, update, menu_data):
32+
update.callback_query.answer()
33+
update.effective_message.reply_text(str(menu_data))
34+
35+
3136
class MainMenu(Menu):
3237
def text(self, update):
3338
return 'Hello {}. This is the main menu!'.format(update.effective_user.first_name)
@@ -44,7 +49,7 @@ class SubMenu1(Menu):
4449

4550
def buttons(self):
4651
return [
47-
# [ToggleButton('Toggleable', 'on'), ToggleButton('Toggleable', 'on')],
52+
[Button('URL', url='https://google.com')],
4853
[Button('Recursion', menu=sub_menu1), Button('Other menu!', menu=sub_menu2)],
4954
[BackButton('Back')]
5055
]
@@ -54,9 +59,23 @@ class SubMenu2(Menu):
5459
text = 'This is sub menu 2'
5560

5661
buttons = [
57-
[Button('Start', start), Button('URL', url='https://google.com')],
58-
# [RadioButton('Option1', 1, group=1), RadioButton('Option2', 2, group=1)],
59-
[BackButton('Back')]
62+
[
63+
ToggleButton('test', 'Test'),
64+
ToggleButton('count', states=((1, '1'), (2, '2'), (3, '3')), default=2)
65+
],
66+
[
67+
RadioButton('options', 1, 'Option 1'),
68+
RadioButton('options', 2, 'Option 2', enabled=True),
69+
RadioButton('options', 3, 'Option 3')
70+
],
71+
[
72+
RadioButton('custom', 1, ('[ ] Custom', '[x] Custom')),
73+
RadioButton('custom', 2, ('[ ] Custom 2', '[x] Custom 2'), enabled=True)
74+
],
75+
[
76+
Button('Submit', submit, pass_menu_data=True),
77+
BackButton('Back')
78+
]
6079
]
6180

6281

@@ -65,7 +84,7 @@ class SubMenu2(Menu):
6584
sub_menu2 = SubMenu2()
6685

6786
dp.add_handler(MenuHandler(main_menu))
68-
dp.add_handler(CommandHandler('menu', main_menu.start, pass_user_data=True, pass_chat_data=True))
87+
dp.add_handler(CommandHandler('menu', main_menu.start))
6988

7089
# Or maybe?
7190
# dp.add_handler(MenuHandler(MainMenu, entry=CommandHandler('menu'))

telegram/ext/menu.py

Lines changed: 126 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,19 @@
1818
# along with this program. If not, see [http://www.gnu.org/licenses/].
1919
"""This module contains objects helps with creating menus for telegram bots."""
2020

21-
# pylint: disable=not-callable
22-
23-
import uuid
24-
from collections import defaultdict
21+
from uuid import uuid4
22+
from collections import defaultdict, OrderedDict
2523
from itertools import chain
2624

2725
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
2826
from telegram.error import BadRequest
2927
from telegram.ext import Handler
3028

29+
try:
30+
str_type = basestring # noqa
31+
except NameError:
32+
str_type = str
33+
3134

3235
def id_from_update(update):
3336
if update.callback_query.message:
@@ -39,50 +42,51 @@ class Menu(object):
3942
_buttons = None
4043
text = ''
4144
buttons = None
42-
data = {}
45+
data = None
46+
default_data = None
4347
root_menu = None # populated in menuhandler
4448
stack = None # Only used in root menu assigned in menuhandler
4549

46-
def callback(self, bot, update, user_data, chat_data):
47-
self.root_menu.stack[id_from_update(update)].append(self)
50+
def callback(self, bot, update, add_to_stack=True):
51+
if add_to_stack:
52+
self.root_menu.stack[id_from_update(update)].append(self)
53+
4854
try:
4955
return update.callback_query.edit_message_text(self.get_text(update),
50-
reply_markup=self.keyboard(user_data,
51-
chat_data))
56+
reply_markup=self.keyboard(update))
5257
except BadRequest as e:
5358
if 'Message is not modified' in e.message:
5459
update.callback_query.answer()
5560
else:
5661
raise
5762

58-
def start(self, bot, update, user_data=None, chat_data=None):
63+
def start(self, bot, update):
5964
# user_ and chat_data is only needed if we wanna do stuff that need state (ie.
6065
# ToggleButtons)
61-
return update.message.reply_text(self.get_text(update), reply_markup=self.keyboard(
62-
user_data, chat_data))
66+
return update.message.reply_text(self.get_text(update), reply_markup=self.keyboard(update))
6367

64-
def keyboard(self, user_data, chat_data):
65-
return InlineKeyboardMarkup([[x.keyboard_button(user_data, chat_data) for x in y] for y in
68+
def keyboard(self, update):
69+
return InlineKeyboardMarkup([[x.keyboard_button(update) for x in y] for y in
6670
self.get_buttons()]) # noqa
6771

6872
def get_buttons(self):
6973
if self._buttons is None:
7074
if callable(self.buttons):
71-
self._buttons = self.buttons() # noqa
75+
self._buttons = self.buttons() # noqa pylint: disable=not-callable
7276
else:
7377
self._buttons = self.buttons
7478
return self._buttons
7579

7680
def get_text(self, update):
7781
if callable(self.text):
78-
return self.text(update) # noqa
82+
return self.text(update) # noqa pylint: disable=not-callable
7983
else:
8084
return self.text
8185

8286

8387
class Button(Handler):
8488
def __init__(self,
85-
text,
89+
text=None,
8690
callback=None,
8791
menu=None,
8892
url=None,
@@ -94,16 +98,15 @@ def __init__(self,
9498
pass_job_queue=False,
9599
pass_user_data=False,
96100
pass_chat_data=False,
97-
name=None):
101+
pass_menu_data=False,
102+
uuid=None):
98103
self._text = text
99104

100105
if callback is not None and menu is not None:
101106
raise RuntimeError
102107
self.callback = callback
103108
if menu is not None:
104109
self.callback = menu.callback
105-
pass_user_data = True
106-
pass_chat_data = True
107110
self.menu = menu
108111

109112
self.url = url
@@ -112,12 +115,12 @@ def __init__(self,
112115
self.switch_inline_query_current_chat = switch_inline_query_current_chat
113116
self.callback_game = callback_game
114117

115-
self.name = name
116-
if self.name is None:
117-
self.name = str(uuid.uuid4())
118+
self.uuid = uuid
119+
if self.uuid is None:
120+
self.uuid = str(uuid4())
121+
self.pass_menu_data = pass_menu_data
118122

119123
self.parent_menu = None
120-
self.root_menu = None
121124

122125
super(Button, self).__init__(
123126
pass_update_queue=pass_update_queue,
@@ -129,44 +132,131 @@ def check_update(self, update):
129132
# Since it's not registered using Dispatcher.add_handler this really doesn't matter
130133
return None
131134

135+
def collect_optional_args(self, dispatcher, update=None):
136+
optional_args = super(Button, self).collect_optional_args(dispatcher, update)
137+
138+
if self.pass_menu_data:
139+
menu_data = self.parent_menu.root_menu.default_data.copy()
140+
menu_data.update(self.parent_menu.root_menu.data[id_from_update(update)])
141+
optional_args['menu_data'] = menu_data
142+
143+
return optional_args
144+
132145
def handle_update(self, update, dispatcher):
133146
optional_args = self.collect_optional_args(dispatcher, update)
134147

135148
return self.callback(dispatcher.bot, update, **optional_args)
136149

137-
def keyboard_button(self, user_data, chat_data):
138-
return InlineKeyboardButton(self.text(user_data, chat_data), self.url, self.name,
150+
def keyboard_button(self, update):
151+
return InlineKeyboardButton(self.text(update), self.url, self.uuid,
139152
self.switch_inline_query,
140153
self.switch_inline_query_current_chat,
141154
self.callback_game)
142155

143-
def text(self, user_data, chat_data):
156+
def text(self, update):
144157
return self._text
145158

159+
def post_init(self):
160+
pass
161+
146162

147163
class BackButton(Button):
148-
def __init__(self, text, name=None):
149-
super(BackButton, self).__init__(text, callback=self._callback,
150-
pass_user_data=True, pass_chat_data=True, name=name)
164+
def __init__(self, text, uuid=None):
165+
super(BackButton, self).__init__(text, callback=self._callback, uuid=uuid)
151166

152-
def _callback(self, bot, update, user_data, chat_data):
167+
def _callback(self, bot, update):
153168
stack = self.parent_menu.root_menu.stack[id_from_update(update)]
154169
try:
155170
stack.pop()
156171
last_menu = stack.pop()
157172
except IndexError:
158173
last_menu = self.parent_menu.root_menu
159-
last_menu.callback(bot, update, user_data, chat_data)
174+
last_menu.callback(bot, update)
175+
176+
177+
class ToggleButton(Button):
178+
def __init__(self, name, text=None, states=None, default=None, uuid=None):
179+
self.name = name
180+
if (text is None and states is None) or (text is not None and states is not None):
181+
raise RuntimeError
182+
if text is not None:
183+
states = (False, text), (True, '\u2714' + text)
184+
if default is None:
185+
default = states[0][0]
186+
self.default = default
187+
self.states = OrderedDict(states)
188+
189+
super(ToggleButton, self).__init__(callback=self._callback, uuid=uuid)
190+
191+
def post_init(self):
192+
self.parent_menu.root_menu.default_data[self.name] = self.default
193+
194+
def _callback(self, bot, update):
195+
data = self.parent_menu.root_menu.data[id_from_update(update)]
196+
197+
current = data.get(self.name, self.default)
198+
keys = list(self.states.keys())
199+
index = keys.index(current) + 1
200+
if index > len(keys) - 1:
201+
index = 0
202+
data[self.name] = keys[index]
203+
204+
return self.parent_menu.callback(bot, update, add_to_stack=False)
205+
206+
def text(self, update):
207+
data = self.parent_menu.root_menu.data[id_from_update(update)]
208+
return self.states[data.get(self.name, self.default)]
209+
210+
211+
class RadioButton(Button):
212+
def __init__(self, name, value, text, enabled=False, uuid=None):
213+
self.value = value
214+
self.name = name
215+
216+
super(RadioButton, self).__init__(callback=self._callback, uuid=uuid)
217+
218+
if isinstance(text, str_type):
219+
self._text = ('\u26aa' + text, '\U0001f518' + text)
220+
else:
221+
self._text = text
222+
223+
self.enabled = enabled
224+
225+
def post_init(self):
226+
default_data = self.parent_menu.root_menu.default_data
227+
if self.name in default_data:
228+
if default_data[self.name] is not None:
229+
return
230+
if self.enabled is True:
231+
default_data[self.name] = self.value
232+
else:
233+
default_data[self.name] = None
234+
235+
def _callback(self, bot, update):
236+
data = self.parent_menu.root_menu.data[id_from_update(update)]
237+
data[self.name] = self.value
238+
239+
return self.parent_menu.callback(bot, update, add_to_stack=False)
240+
241+
def text(self, update):
242+
data = self.parent_menu.root_menu.data[id_from_update(update)]
243+
value = data.get(self.name)
244+
if (value is None and self.enabled) or value == self.value:
245+
return self._text[1] # True
246+
return self._text[0] # False
160247

161248

162249
class MenuHandler(Handler):
163250
def __init__(self, menu):
164251
self.menu = menu
165252
self.buttons = {}
166-
self.collect_buttons(self.menu)
167253

254+
menu.data = defaultdict(dict)
255+
menu.default_data = dict()
168256
menu.stack = defaultdict(list)
169257

258+
self.collect_buttons(self.menu)
259+
170260
super(MenuHandler, self).__init__(
171261
pass_update_queue=None,
172262
pass_job_queue=None,
@@ -177,9 +267,10 @@ def collect_buttons(self, menu):
177267
menu.root_menu = self.menu
178268
for button in chain.from_iterable(menu.get_buttons()):
179269
button.parent_menu = menu
180-
if button.name not in self.buttons and (button.callback is not None or
270+
button.post_init()
271+
if button.uuid not in self.buttons and (button.callback is not None or
181272
button.menu is not None):
182-
self.buttons[button.name] = button
273+
self.buttons[button.uuid] = button
183274
if button.menu is not None:
184275
self.collect_buttons(button.menu)
185276

0 commit comments

Comments
 (0)