Skip to content

Commit 5e7eb93

Browse files
committed
Latest version
1 parent f117428 commit 5e7eb93

File tree

8 files changed

+148
-85
lines changed

8 files changed

+148
-85
lines changed

telegram/bot.py

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,17 @@
2525
import logging
2626
import warnings
2727
from datetime import datetime
28-
from itertools import chain
29-
from pprint import pprint
3028

3129
from future.utils import string_types
3230

3331
from telegram import (Audio, Chat, ChatMember, Contact, Document, File, GameHighScore,
34-
InlineKeyboardMarkup, Location,
35-
Message, PhotoSize, ReplyKeyboardMarkup, ReplyMarkup, Sticker, StickerSet,
32+
Location,
33+
Message, PhotoSize, ReplyMarkup, Sticker, StickerSet,
3634
TelegramObject,
3735
Update, User, UserProfilePhotos, Venue, Video, VideoNote, Voice, WebhookInfo)
3836
from telegram.constants import NOTSET
3937
from telegram.error import InvalidToken, TelegramError
40-
from telegram.flow.action import Action, get_action_id
38+
from telegram.flow.actionmarkup import resolve_action_markup
4139
from telegram.utils.helpers import to_timestamp
4240
from telegram.utils.request import Request
4341

@@ -82,37 +80,12 @@ def decorator(self, *args, **kwargs):
8280

8381
callbacks = []
8482

85-
if kwargs.get('reply_markup'):
86-
reply_markup = kwargs.get('reply_markup')
83+
reply_markup = kwargs.get('reply_markup')
84+
if reply_markup:
8785
if isinstance(reply_markup, ReplyMarkup):
88-
buttons = []
89-
if isinstance(reply_markup, InlineKeyboardMarkup):
90-
buttons = reply_markup.inline_keyboard
91-
is_inline = True
92-
elif isinstance(reply_markup, ReplyKeyboardMarkup):
93-
buttons = reply_markup.keyboard
94-
is_inline = False
95-
96-
from telegram.ext import ActionButton
97-
buttons = [b for b
98-
in chain.from_iterable(buttons)
99-
if isinstance(b, ActionButton)]
100-
101-
for n, button in enumerate(buttons):
102-
# noinspection PyUnboundLocalVariable
103-
button.is_inline = is_inline
104-
callback = button.insert_callback(self.callback_manager)
105-
106-
chat_id = data.get("chat_id")
107-
try:
108-
chat_id = int(chat_id)
109-
except TypeError:
110-
pass
111-
else:
112-
callback.chat_id = chat_id
113-
114-
callbacks.append(callback)
115-
86+
callbacks.extend(
87+
resolve_action_markup(self.callback_manager, reply_markup, data.get("chat_id"))
88+
)
11689
data['reply_markup'] = reply_markup.to_json()
11790
else:
11891
data['reply_markup'] = reply_markup
@@ -1332,6 +1305,9 @@ def answer_inline_query(self,
13321305
if res.id is NOTSET:
13331306
res.insert_callback(self.callback_manager)
13341307

1308+
if hasattr(res, 'reply_markup'):
1309+
resolve_action_markup(self.callback_manager, res.reply_markup)
1310+
13351311
results = [res.to_dict() for res in results]
13361312

13371313
data = {'inline_query_id': inline_query_id, 'results': results}

telegram/ext/callbackmanager.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from abc import abstractmethod, ABCMeta
1+
from abc import ABCMeta, abstractmethod
22
from uuid import uuid4
33

44
from telegram.flow.action import get_action_id
5-
from telegram.utils.binaryencoder import obfuscate_id_binary, resolve_obfuscated_id
5+
from telegram.utils.binaryencoder import callback_id_from_query, _obfuscate_id_binary
66

77

88
class CallbackNotFound(Exception):
@@ -28,7 +28,7 @@ def __init__(self, id, action, data, one_time_callback=False):
2828

2929
def __repr__(self):
3030
return "CallbackItem({}, {}, {}, data={}, one_time_callback={})".format(
31-
resolve_obfuscated_id(self.id),
31+
callback_id_from_query(self.id),
3232
self.chat_id,
3333
self.action_id,
3434
type(self.model_data),
@@ -73,11 +73,8 @@ def __init__(self, persistence=None):
7373
self.persistence = persistence
7474

7575
def create_callback(self, action, data, random_id=True):
76-
if random_id:
77-
cb_id = self.create_unique_uuid()
78-
else:
79-
cb_id = self.get_next_id()
80-
cb_id = obfuscate_id_binary(cb_id)
76+
77+
cb_id = self.create_unique_uuid() if random_id else self.get_next_id()
8178

8279
callback = CallbackItem(cb_id, action, data)
8380
self._data[cb_id] = callback
@@ -125,7 +122,6 @@ def create_callback(self, action_id, data, random_id=True):
125122
cb_id = self.create_unique_uuid()
126123
else:
127124
cb_id = self.get_next_id()
128-
cb_id = obfuscate_id_binary(cb_id)
129125

130126
callback = CallbackItem(cb_id, action_id, data)
131127
self._data[cb_id] = callback

telegram/flow/actionbutton.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
# You should have received a copy of the GNU Lesser Public License
1818
# along with this program. If not, see [http://www.gnu.org/licenses/].
1919
"""This module contains an object that represents a Telegram InlineKeyboardButton."""
20-
from telegram import KeyboardButton, InlineKeyboardButton, TelegramObject
21-
from telegram.utils.binaryencoder import ZERO_CHAR3
20+
from telegram import InlineKeyboardButton, KeyboardButton, TelegramObject
21+
from telegram.flow.action import Action
22+
from telegram.utils.binaryencoder import insert_callback_id
2223

2324

2425
class ActionButton(TelegramObject):
@@ -69,40 +70,52 @@ class ActionButton(TelegramObject):
6970
"""
7071

7172
def __init__(self,
72-
caption,
7373
action,
74-
view_data=None):
74+
caption=None, # TODO DOCS: "will override..."
75+
view_data=None,
76+
switch_inline=None,
77+
switch_inline_current_chat=None):
78+
7579
if caption is None:
76-
raise ValueError("Buttons without a text caption are not possible.")
80+
if isinstance(action, Action):
81+
# noinspection PyProtectedMember
82+
if action._caption:
83+
caption = action.get_caption(view_data)
84+
else:
85+
raise ValueError("Neither this button nor its action have a caption. Pass the caption argument or "
86+
"set up your Action to have one.")
87+
else:
88+
raise ValueError("If the action is {} and not an Action object, a caption must be supplied as "
89+
"argument to this ActionButton.".format(type(action)))
90+
else:
91+
caption = action.get_caption(view_data)
92+
7793
if len(caption) > 108:
7894
# We have a maximum button caption length of 128 characters until they are truncated
7995
# server-side. We will use the remaining 20 characters to encode a random ID.
8096
raise ValueError("Button caption must not be longer than 108 characters.")
97+
if caption in (None, ''):
98+
raise ValueError("Buttons without a text caption are not possible.")
8199

82100
self.text = caption
101+
102+
if switch_inline and switch_inline_current_chat:
103+
raise ValueError("Cannot set both switch_inline and switch_inline_current_chat.")
104+
self.switch_inline_query = switch_inline
105+
self.switch_inline_query_current_chat = switch_inline_current_chat
106+
83107
self._action_id = action
84108
self._callback = None
85109
self.callback_data = None
86110
self._is_inline = None
87111

88112
self._view_data = view_data
89113

90-
@classmethod
91-
def from_action(cls, action, view_data=None):
92-
caption = action.get_caption(view_data)
93-
if caption in (None, ''):
94-
raise ValueError("Buttons without a text caption are not possible.")
95-
return cls(
96-
caption=caption,
97-
action=action.id,
98-
view_data=view_data
99-
)
100-
101114
def insert_callback(self, callback_manager):
102115
callback = callback_manager.create_callback(
103116
action_id=self._action_id,
104117
data=self._view_data,
105-
random_id=self._is_inline
118+
random_id=self._is_inline and not (self.switch_inline_query or self.switch_inline_query_current_chat)
106119
)
107120

108121
self._callback = callback
@@ -125,11 +138,31 @@ def to_dict(self):
125138
if not self._callback:
126139
raise ValueError("You need to call the insert_callback method before usage.")
127140

141+
if self.switch_inline_query or self.switch_inline_query_current_chat:
142+
if not self._is_inline:
143+
raise ValueError("switch_inline will not work with regular KeyboardButtons, "
144+
"use an InlineKeyboardMarkup instead.")
145+
128146
if self._is_inline:
129147
self.__bases__ = (InlineKeyboardButton,)
130-
self.callback_data = self._callback.id
131148
else:
132149
self.__bases__ = (KeyboardButton,)
133-
self.text = self.text + self._callback.id + ZERO_CHAR3
150+
151+
if not self._is_inline:
152+
# Regular KeyboardButton
153+
self.text = insert_callback_id(self.text, self._callback.id)
154+
elif self.switch_inline_query:
155+
# InlineKeyboardButton with switch_inline
156+
self.switch_inline_query = insert_callback_id(self.switch_inline_query, self._callback.id)
157+
elif self.switch_inline_query_current_chat:
158+
# InlineKeyboardButton with switch_inline_current_chat
159+
self.switch_inline_query_current_chat = insert_callback_id(self.switch_inline_query_current_chat,
160+
self._callback.id)
161+
else:
162+
# InlineKeyboardButton with callback_data
163+
self.callback_data = self._callback.id
164+
165+
print(self.text, type(self), ': ',
166+
self.switch_inline_query or self.switch_inline_query_current_chat or self.callback_data)
134167

135168
return super(ActionButton, self).to_dict()

telegram/flow/actionmarkup.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from itertools import chain
2+
3+
from telegram import InlineKeyboardMarkup, ReplyKeyboardMarkup
4+
5+
6+
# TODO REFACTOR: move into ReplyMarkup class
7+
def resolve_action_markup(callback_manager, reply_markup, chat_id=None):
8+
if chat_id:
9+
# Make a best effort to use the provided chat_id, but it will be inserted from the outside after the update
10+
# has been sent. This only needs to happen when the markup is bound to a specific chat, i.e. when
11+
# `callback_manager.lookup_chat_bound_callback` is used.
12+
try:
13+
chat_id = int(chat_id)
14+
except TypeError:
15+
chat_id = None
16+
17+
callbacks = []
18+
buttons = []
19+
if isinstance(reply_markup, InlineKeyboardMarkup):
20+
buttons = reply_markup.inline_keyboard
21+
is_inline = True
22+
elif isinstance(reply_markup, ReplyKeyboardMarkup):
23+
buttons = reply_markup.keyboard
24+
is_inline = False
25+
26+
from telegram.ext import ActionButton
27+
buttons = [b for b
28+
in chain.from_iterable(buttons)
29+
if isinstance(b, ActionButton)]
30+
31+
for n, button in enumerate(buttons):
32+
# noinspection PyUnboundLocalVariable
33+
button.is_inline = is_inline
34+
callback = button.insert_callback(callback_manager)
35+
36+
callback.chat_id = chat_id
37+
38+
callbacks.append(callback)
39+
40+
return callbacks

telegram/flow/inlinequeryactionhandler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ def check_update(self, update, dispatcher):
9494
9595
"""
9696
if isinstance(update, Update) and update.inline_query:
97+
98+
# TODO: check the obfuscated encoded thingy
99+
97100
if update.inline_query.id != self.action_id:
98101
return False
99102
if self.pattern:

telegram/flow/replyactionhandler.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
#
1717
# You should have received a copy of the GNU Lesser Public License
1818
# along with this program. If not, see [http://www.gnu.org/licenses/].
19-
import re
2019

21-
from telegram.flow.action import Action
2220
from telegram.ext.callbackmanager import CallbackNotFound
23-
from telegram.utils.binaryencoder import ZERO_CHAR1, ZERO_CHAR2, ZERO_CHAR3, resolve_obfuscated_id
2421
from telegram.ext.handler import Handler
22+
from telegram.flow.action import Action
23+
from telegram.utils.binaryencoder import callback_id_from_query
2524

2625

2726
class ReplyActionHandler(Handler):
@@ -112,21 +111,14 @@ def check_update(self, update, dispatcher):
112111
113112
"""
114113
if update.message and update.message.text:
115-
match = re.match(r'^.+?([{}{}]+){}$'.format(
116-
ZERO_CHAR1, ZERO_CHAR2, ZERO_CHAR3
117-
), update.message.text)
118-
119-
if not match:
120-
return
121-
122-
callback_id = match.group(1)
114+
callback_id = callback_id_from_query(update.message.text)
123115

124116
try:
125117
action = dispatcher.callback_manager.peek_action(callback_id)
126118
except CallbackNotFound:
127119
raise CallbackNotFound(
128120
"The callback {} is not present in this CallbackManager.".format(
129-
resolve_obfuscated_id(callback_id)
121+
callback_id_from_query(callback_id)
130122
))
131123

132124
if action == self.action_id:

telegram/inline/inlinequeryresult.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,16 @@ def __init__(self, type, id, view_data=None, **kwargs):
4646
self.id = NOTSET
4747
self._callback = None
4848
else:
49-
self.id = id
49+
self.id = str(id)
5050

5151
self._view_data = view_data
5252
self._id_attrs = (self.id,)
5353

5454
def insert_callback(self, callback_manager):
5555
callback = callback_manager.create_callback(
5656
action_id=self._action_id,
57-
data=self._view_data
57+
data=self._view_data,
58+
random_id=True
5859
)
5960

6061
self._callback = callback

telegram/utils/binaryencoder.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
1-
ZERO_CHAR1 = u"\u200C" # ZERO-WIDTH-NON-JOINER
2-
ZERO_CHAR2 = u"\u200B" # ZERO-WIDTH-SPACE
3-
ZERO_CHAR3 = u"\u200D" # ZERO-WIDTH-SPACE
1+
import re
42

3+
ZERO_CHAR_0 = u"\u200C" # ZERO-WIDTH-NON-JOINER
4+
ZERO_CHAR_1 = u"\u200B" # ZERO-WIDTH-SPACE
5+
ZERO_CHAR_DENOMINATOR = u"\u200D" # ZERO-WIDTH-SPACE
56

6-
def obfuscate_id_binary(id_):
7-
binary = "{0:b}".format(id_)
8-
return binary.replace('0', ZERO_CHAR1).replace('1', ZERO_CHAR2)
97

8+
def _obfuscate_id_binary(id_):
9+
binary = "{0:b}".format(int(id_))
10+
return binary.replace('0', ZERO_CHAR_0).replace('1', ZERO_CHAR_1)
1011

11-
def resolve_obfuscated_id(encoded):
12-
binary = encoded.replace(ZERO_CHAR1, '0').replace(ZERO_CHAR2, '1').replace(ZERO_CHAR3, '')
12+
13+
def _resolve_obfuscated_id(encoded):
14+
binary = encoded.replace(ZERO_CHAR_0, '0').replace(ZERO_CHAR_1, '1').replace(ZERO_CHAR_DENOMINATOR, '')
1315
try:
1416
return int(binary, 2) # convert to decimal
1517
except ValueError:
1618
return binary
19+
20+
21+
def insert_callback_id(query, callback_id):
22+
return ZERO_CHAR_DENOMINATOR + _obfuscate_id_binary(callback_id) + query
23+
24+
25+
def strip_obfuscation(obfuscated):
26+
return obfuscated.lstrip([ZERO_CHAR_0, ZERO_CHAR_1, ZERO_CHAR_DENOMINATOR])
27+
28+
29+
def callback_id_from_query(text):
30+
match = re.match(r'^{}([{}{}]+).+?$'.format(
31+
ZERO_CHAR_DENOMINATOR, ZERO_CHAR_0, ZERO_CHAR_1
32+
), text)
33+
34+
if not match:
35+
return None
36+
37+
callback_id = match.group(1)
38+
return _resolve_obfuscated_id(callback_id)

0 commit comments

Comments
 (0)