Skip to content

Commit c21d99c

Browse files
committed
Add Menu proof of concept
1 parent ff897ce commit c21d99c

File tree

3 files changed

+249
-3
lines changed

3 files changed

+249
-3
lines changed

examples/menubot.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Simple Bot to reply to Telegram messages
5+
# This program is dedicated to the public domain under the CC0 license.
6+
7+
import logging
8+
9+
import telegram.ext
10+
from telegram.ext import CommandHandler
11+
from telegram.ext.menu import Menu, MenuHandler, Button
12+
13+
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
14+
level=logging.DEBUG)
15+
16+
logger = logging.getLogger(__name__)
17+
18+
updater = telegram.ext.Updater('TOKEN')
19+
dp = updater.dispatcher
20+
21+
22+
def error(bot, update, e):
23+
logger.warning('Update "%s" caused error "%s"' % (update, e))
24+
25+
26+
def start(bot, update):
27+
update.callback_query.answer()
28+
update.effective_message.reply_text('Hello!')
29+
30+
31+
class MainMenu(Menu):
32+
def text(self, update):
33+
return 'Hello {}. This is the main menu!'.format(update.effective_user.first_name)
34+
35+
def buttons(self):
36+
return [
37+
[Button('Test', menu=SubMenu1), Button('Test2', menu=SubMenu2)],
38+
[Button('Exit', callback=start)]
39+
]
40+
41+
42+
class SubMenu1(Menu):
43+
text = 'This is sub menu 1'
44+
45+
def buttons(self):
46+
return [
47+
# [ToggleButton('Toggleable', 'on'), ToggleButton('Toggleable', 'on')],
48+
[Button('Recursion', menu=SubMenu1), Button('Other menu!', menu=SubMenu2)],
49+
[Button('Back to main menu', menu=MainMenu)] # [BackButton('Back')]
50+
]
51+
52+
53+
class SubMenu2(Menu):
54+
text = 'This is sub menu 2'
55+
56+
buttons = [
57+
[Button('Start', start), Button('URL', url='https://google.com')],
58+
# [RadioButton('Option1', 1, group=1), RadioButton('Option2', 2, group=1)],
59+
[Button('Back to main menu', menu=MainMenu)] # [BackButton('Back')]
60+
]
61+
62+
dp.add_handler(MenuHandler(MainMenu))
63+
dp.add_handler(CommandHandler('menu', MainMenu.start, pass_user_data=True, pass_chat_data=True))
64+
65+
# Or maybe?
66+
# dp.add_handler(MenuHandler(MainMenu, entry=CommandHandler('menu'))
67+
68+
dp.add_error_handler(error)
69+
70+
updater.start_polling()
71+
updater.idle()

telegram/ext/handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Handler(object):
2626
by inheriting from this class.
2727
2828
Args:
29-
callback (function): A function that takes ``bot, update`` as
29+
callback (optional[function]): A function that takes ``bot, update`` as
3030
positional arguments. It will be called when the ``check_update``
3131
has determined that an update should be processed by this handler.
3232
pass_update_queue (optional[bool]): If set to ``True``, a keyword argument called
@@ -48,12 +48,13 @@ class Handler(object):
4848
"""
4949

5050
def __init__(self,
51-
callback,
51+
callback=None,
5252
pass_update_queue=False,
5353
pass_job_queue=False,
5454
pass_user_data=False,
5555
pass_chat_data=False):
56-
self.callback = callback
56+
if callback is not None:
57+
self.callback = callback
5758
self.pass_update_queue = pass_update_queue
5859
self.pass_job_queue = pass_job_queue
5960
self.pass_user_data = pass_user_data

telegram/ext/menu.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python
2+
#
3+
# A library that provides a Python interface to the Telegram Bot API
4+
# Copyright (C) 2015-2017
5+
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser Public License
18+
# along with this program. If not, see [http://www.gnu.org/licenses/].
19+
"""This module contains objects helps with creating menus for telegram bots."""
20+
21+
# pylint: disable=undefined-variable, not-callable
22+
23+
import uuid
24+
from itertools import chain
25+
26+
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
27+
from telegram.error import BadRequest
28+
from telegram.ext import Handler
29+
30+
try:
31+
str_type = str
32+
except NameError:
33+
str_type = basestring # noqa
34+
35+
36+
class Menu(object):
37+
_instance = None
38+
_buttons = None
39+
text = ''
40+
buttons = None
41+
data = {}
42+
43+
def __new__(cls):
44+
if not cls._instance:
45+
cls._instance = super(Menu, cls).__new__(cls)
46+
return cls._instance
47+
48+
def callback(self, bot, update, user_data, chat_data):
49+
try:
50+
return update.callback_query.edit_message_text(self.get_text(update),
51+
reply_markup=self.keyboard(user_data,
52+
chat_data))
53+
except BadRequest as e:
54+
if 'Message is not modified' in e.message:
55+
update.callback_query.answer()
56+
else:
57+
raise
58+
59+
@classmethod
60+
def start(cls, bot, update, user_data=None, chat_data=None):
61+
# user_ and chat_data is only needed if we wanna do stuff that need state (ie.
62+
# ToggleButtons)
63+
return update.message.reply_text(cls().get_text(update), reply_markup=cls().keyboard(
64+
user_data, chat_data))
65+
66+
def keyboard(self, user_data, chat_data):
67+
return InlineKeyboardMarkup([[x.keyboard_button(user_data, chat_data) for x in y] for y in
68+
self.get_buttons()]) # noqa
69+
70+
def get_buttons(self):
71+
if self._buttons is None:
72+
if callable(self.buttons):
73+
self._buttons = self.buttons()
74+
else:
75+
self._buttons = self.buttons
76+
return self._buttons
77+
78+
def get_text(self, update):
79+
if callable(self.text):
80+
return self.text(update) # noqa
81+
else:
82+
return self.text
83+
84+
85+
class Button(Handler):
86+
def __init__(self,
87+
text,
88+
callback=None,
89+
menu=None,
90+
url=None,
91+
callback_data=None,
92+
switch_inline_query=None,
93+
switch_inline_query_current_chat=None,
94+
callback_game=None,
95+
pass_update_queue=False,
96+
pass_job_queue=False,
97+
pass_user_data=False,
98+
pass_chat_data=False,
99+
name=None):
100+
self._text = text
101+
102+
if callback is not None and menu is not None:
103+
raise RuntimeError
104+
self.callback = callback
105+
if menu is not None:
106+
self.callback = menu().callback
107+
pass_user_data = True
108+
pass_chat_data = True
109+
self.menu = menu
110+
111+
self.url = url
112+
self.callback_data = callback_data
113+
self.switch_inline_query = switch_inline_query
114+
self.switch_inline_query_current_chat = switch_inline_query_current_chat
115+
self.callback_game = callback_game
116+
117+
self.name = name
118+
if self.name is None:
119+
self.name = str(uuid.uuid4())
120+
121+
self.parent_menu = None
122+
123+
super(Button, self).__init__(
124+
pass_update_queue=pass_update_queue,
125+
pass_job_queue=pass_job_queue,
126+
pass_user_data=pass_user_data,
127+
pass_chat_data=pass_chat_data)
128+
129+
def check_update(self, update):
130+
# Since it's not registered using Dispatcher.add_handler this really doesn't matter
131+
return None
132+
133+
def handle_update(self, update, dispatcher):
134+
optional_args = self.collect_optional_args(dispatcher, update)
135+
136+
return self.callback(dispatcher.bot, update, **optional_args)
137+
138+
def keyboard_button(self, user_data, chat_data):
139+
return InlineKeyboardButton(self.text(user_data, chat_data), self.url, self.name,
140+
self.switch_inline_query,
141+
self.switch_inline_query_current_chat,
142+
self.callback_game)
143+
144+
def text(self, user_data, chat_data):
145+
return self._text
146+
147+
148+
class MenuHandler(Handler):
149+
def __init__(self, menu):
150+
self.menu = menu
151+
self.buttons = {}
152+
self.collect_buttons(self.menu)
153+
154+
super(MenuHandler, self).__init__(
155+
pass_update_queue=None,
156+
pass_job_queue=None,
157+
pass_user_data=None,
158+
pass_chat_data=None)
159+
160+
def collect_buttons(self, menu):
161+
for button in chain.from_iterable(menu().get_buttons()):
162+
button.parent_menu = menu
163+
if button.name not in self.buttons and (button.callback is not None or
164+
button.menu is not None):
165+
self.buttons[button.name] = button
166+
if button.menu is not None:
167+
self.collect_buttons(button.menu)
168+
169+
def check_update(self, update):
170+
return update.callback_query and update.callback_query.data in self.buttons
171+
172+
def handle_update(self, update, dispatcher):
173+
# Let the button handle it
174+
return self.buttons[update.callback_query.data].handle_update(update, dispatcher)

0 commit comments

Comments
 (0)