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
2523from itertools import chain
2624
2725from telegram import InlineKeyboardButton , InlineKeyboardMarkup
2826from telegram .error import BadRequest
2927from telegram .ext import Handler
3028
29+ try :
30+ str_type = basestring # noqa
31+ except NameError :
32+ str_type = str
33+
3134
3235def 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
8387class 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
147163class 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
162249class 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