Skip to content

Commit f953bd8

Browse files
committed
Add basic support for sending inline keyboards
1 parent e1ed389 commit f953bd8

File tree

5 files changed

+146
-27
lines changed

5 files changed

+146
-27
lines changed

botogram/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .runner import run
3535
from .objects import *
3636
from .utils import usernames_in
37+
from .callbacks import buttons
3738

3839

3940
# This code will simulate the Windows' multiprocessing behavior if the

botogram/callbacks.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,65 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919

2020

21+
class ButtonsRow:
22+
"""A row of an inline keyboard"""
23+
24+
def __init__(self):
25+
self._content = []
26+
27+
def url(self, label, url):
28+
"""Open an URL when the button is pressed"""
29+
self._content.append({"text": label, "url": url})
30+
31+
def callback(self, label, callback, data=None):
32+
"""Trigger a callback when the button is pressed"""
33+
if data is not None:
34+
msg = "%s\0%s" % (callback, data)
35+
else:
36+
msg = callback
37+
38+
self._content.append({"text": label, "callback_data": msg})
39+
40+
def switch_inline_query(self, label, query="", current_chat=False):
41+
"""Switch the user to this bot's inline query"""
42+
if current_chat:
43+
self._content.append({
44+
"text": label,
45+
"switch_inline_query_current_chat": query,
46+
})
47+
else:
48+
self._content.append({
49+
"text": label,
50+
"switch_inline_query": query,
51+
})
52+
53+
54+
class Buttons:
55+
"""Factory for inline keyboards"""
56+
57+
def __init__(self):
58+
self._rows = {}
59+
60+
def __getitem__(self, index):
61+
if index not in self._rows:
62+
self._rows[index] = ButtonsRow()
63+
return self._rows[index]
64+
65+
def _serialize_attachment(self):
66+
rows = [
67+
row._content for i, row in sorted(
68+
tuple(self._rows.items()), key=lambda i: i[0]
69+
)
70+
]
71+
72+
return {"inline_keyboard": rows}
73+
74+
75+
def buttons():
76+
"""Create a new inline keyboard"""
77+
return Buttons()
78+
79+
2180
def parse_callback_data(data):
2281
"""Parse the callback data generated by botogram and return it"""
2382
if "\0" in data:

botogram/hooks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ def _call(self, bot, update):
220220
q = update.callback_query
221221

222222
name, data = parse_callback_data(q._data)
223-
print(name, self._name)
224223
if name != self._name:
225224
return
226225

botogram/objects/mixins.py

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from .. import utils
2525
from .. import syntaxes
26+
from ..utils.deprecations import _deprecated_message
2627

2728

2829
_objects_module = None
@@ -51,7 +52,7 @@ def __(self, *args, **kwargs):
5152
class ChatMixin:
5253
"""Add some methods for chats"""
5354

54-
def _get_call_args(self, reply_to, extra, notify=True):
55+
def _get_call_args(self, reply_to, extra, attach, notify):
5556
"""Get default API call arguments"""
5657
# Convert instance of Message to ids in reply_to
5758
if hasattr(reply_to, "message_id"):
@@ -61,17 +62,24 @@ def _get_call_args(self, reply_to, extra, notify=True):
6162
if reply_to is not None:
6263
args["reply_to_message_id"] = reply_to
6364
if extra is not None:
65+
_deprecated_message(
66+
"The extra parameter", "1.0", "use the attach parameter", -4
67+
)
6468
args["reply_markup"] = json.dumps(extra.serialize())
69+
if attach is not None:
70+
if not hasattr(attach, "_serialize_attachment"):
71+
raise ValueError("%s is not an attachment" % attach)
72+
args["reply_markup"] = json.dumps(attach._serialize_attachment())
6573
if not notify:
6674
args["disable_notification"] = True
6775

6876
return args
6977

7078
@_require_api
7179
def send(self, message, preview=True, reply_to=None, syntax=None,
72-
extra=None, notify=True):
80+
extra=None, attach=None, notify=True):
7381
"""Send a message"""
74-
args = self._get_call_args(reply_to, extra, notify)
82+
args = self._get_call_args(reply_to, extra, attach, notify)
7583
args["text"] = message
7684
args["disable_web_page_preview"] = not preview
7785

@@ -83,9 +91,9 @@ def send(self, message, preview=True, reply_to=None, syntax=None,
8391

8492
@_require_api
8593
def send_photo(self, path, caption=None, reply_to=None, extra=None,
86-
notify=True):
94+
attach=None, notify=True):
8795
"""Send a photo"""
88-
args = self._get_call_args(reply_to, extra, notify)
96+
args = self._get_call_args(reply_to, extra, attach, notify)
8997
if caption is not None:
9098
args["caption"] = caption
9199

@@ -96,9 +104,9 @@ def send_photo(self, path, caption=None, reply_to=None, extra=None,
96104

97105
@_require_api
98106
def send_audio(self, path, duration=None, performer=None, title=None,
99-
reply_to=None, extra=None, notify=True):
107+
reply_to=None, extra=None, attach=None, notify=True):
100108
"""Send an audio track"""
101-
args = self._get_call_args(reply_to, extra, notify)
109+
args = self._get_call_args(reply_to, extra, attach, notify)
102110
if duration is not None:
103111
args["duration"] = duration
104112
if performer is not None:
@@ -113,9 +121,9 @@ def send_audio(self, path, duration=None, performer=None, title=None,
113121

114122
@_require_api
115123
def send_voice(self, path, duration=None, title=None, reply_to=None,
116-
extra=None, notify=True):
124+
extra=None, attach=None, notify=True):
117125
"""Send a voice message"""
118-
args = self._get_call_args(reply_to, extra, notify)
126+
args = self._get_call_args(reply_to, extra, attach, notify)
119127
if duration is not None:
120128
args["duration"] = duration
121129

@@ -126,9 +134,9 @@ def send_voice(self, path, duration=None, title=None, reply_to=None,
126134

127135
@_require_api
128136
def send_video(self, path, duration=None, caption=None, reply_to=None,
129-
extra=None, notify=True):
137+
extra=None, attach=None, notify=True):
130138
"""Send a video"""
131-
args = self._get_call_args(reply_to, extra, notify)
139+
args = self._get_call_args(reply_to, extra, attach, notify)
132140
if duration is not None:
133141
args["duration"] = duration
134142
if caption is not None:
@@ -140,9 +148,10 @@ def send_video(self, path, duration=None, caption=None, reply_to=None,
140148
expect=_objects().Message)
141149

142150
@_require_api
143-
def send_file(self, path, reply_to=None, extra=None, notify=True):
151+
def send_file(self, path, reply_to=None, extra=None, attach=None,
152+
notify=True):
144153
"""Send a generic file"""
145-
args = self._get_call_args(reply_to, extra, notify)
154+
args = self._get_call_args(reply_to, extra, attach, notify)
146155

147156
files = {"document": open(path, "rb")}
148157

@@ -151,9 +160,9 @@ def send_file(self, path, reply_to=None, extra=None, notify=True):
151160

152161
@_require_api
153162
def send_location(self, latitude, longitude, reply_to=None, extra=None,
154-
notify=True):
163+
attach=None, notify=True):
155164
"""Send a geographic location"""
156-
args = self._get_call_args(reply_to, extra, notify)
165+
args = self._get_call_args(reply_to, extra, attach, notify)
157166
args["latitude"] = latitude
158167
args["longitude"] = longitude
159168

@@ -162,9 +171,9 @@ def send_location(self, latitude, longitude, reply_to=None, extra=None,
162171

163172
@_require_api
164173
def send_venue(self, latitude, longitude, title, address, foursquare=None,
165-
reply_to=None, extra=None, notify=True):
174+
reply_to=None, extra=None, attach=None, notify=True):
166175
"""Send a venue"""
167-
args = self._get_call_args(reply_to, extra, notify)
176+
args = self._get_call_args(reply_to, extra, attach, notify)
168177
args["latitude"] = latitude
169178
args["longitude"] = longitude
170179
args["title"] = title
@@ -175,19 +184,20 @@ def send_venue(self, latitude, longitude, title, address, foursquare=None,
175184
self._api.call("sendVenue", args, expect=_objects().Message)
176185

177186
@_require_api
178-
def send_sticker(self, sticker, reply_to=None, extra=None, notify=True):
187+
def send_sticker(self, sticker, reply_to=None, extra=None, attach=None,
188+
notify=True):
179189
"""Send a sticker"""
180-
args = self._get_call_args(reply_to, extra, notify)
190+
args = self._get_call_args(reply_to, extra, attach, notify)
181191

182192
files = {"sticker": open(sticker, "rb")}
183193
return self._api.call("sendSticker", args, files,
184194
expect=_objects().Message)
185195

186196
@_require_api
187197
def send_contact(self, phone, first_name, last_name=None, *, reply_to=None,
188-
extra=None, notify=True):
198+
extra=None, attach=None, notify=True):
189199
"""Send a contact"""
190-
args = self._get_call_args(reply_to, extra, notify)
200+
args = self._get_call_args(reply_to, extra, attach, notify)
191201
args["phone_number"] = phone
192202
args["first_name"] = first_name
193203

@@ -228,7 +238,7 @@ def forward_to(self, to, notify=True):
228238
expect=_objects().Message)
229239

230240
@_require_api
231-
def edit(self, text, syntax=None, preview=True, extra=None):
241+
def edit(self, text, syntax=None, preview=True, extra=None, attach=None):
232242
"""Edit this message"""
233243
args = {"message_id": self.message_id, "chat_id": self.chat.id}
234244
args["text"] = text
@@ -239,24 +249,47 @@ def edit(self, text, syntax=None, preview=True, extra=None):
239249

240250
if not preview:
241251
args["disable_web_page_preview"] = True
252+
242253
if extra is not None:
243-
args["reply_markup"] = extra.serialize()
254+
_deprecated_message(
255+
"The extra parameter", "1.0", "use the attach parameter", -3
256+
)
257+
args["reply_markup"] = json.dumps(extra.serialize())
258+
if attach is not None:
259+
if not hasattr(attach, "_serialize_attachment"):
260+
raise ValueError("%s is not an attachment" % attach)
261+
args["reply_markup"] = json.dumps(attach._serialize_attachment())
244262

245263
self._api.call("editMessageText", args)
246264
self.text = text
247265

248266
@_require_api
249-
def edit_caption(self, caption, extra=None):
267+
def edit_caption(self, caption, extra=None, attach=None):
250268
"""Edit this message's caption"""
251269
args = {"message_id": self.message_id, "chat_id": self.chat.id}
252270
args["caption"] = caption
253271

254272
if extra is not None:
255-
args["reply_markup"] = extra.serialize()
273+
_deprecated_message(
274+
"The extra parameter", "1.0", "use the attach parameter", -3
275+
)
276+
args["reply_markup"] = json.dumps(extra.serialize())
277+
if attach is not None:
278+
if not hasattr(attach, "_serialize_attachment"):
279+
raise ValueError("%s is not an attachment" % attach)
280+
args["reply_markup"] = json.dumps(attach._serialize_attachment())
256281

257282
self._api.call("editMessageCaption", args)
258283
self.caption = caption
259284

285+
@_require_api
286+
def edit_attachs(self, attach):
287+
"""Edit this message's attachments"""
288+
args = {"message_id": self.message_id, "chat_id": self.chat.id}
289+
args["reply_markup"] = attach
290+
291+
self._api.call("editMessageReplyMarkup", args)
292+
260293
@_require_api
261294
def reply(self, *args, **kwargs):
262295
"""Reply to the current message"""

tests/test_callbacks.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,34 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
# DEALINGS IN THE SOFTWARE.
2020

21-
from botogram.callbacks import parse_callback_data
21+
import json
22+
23+
from botogram.callbacks import Buttons, parse_callback_data
24+
25+
26+
def test_buttons():
27+
buttons = Buttons()
28+
buttons[0].url("test 1", "http://example.com")
29+
buttons[0].callback("test 2", "test_callback")
30+
buttons[3].callback("test 3", "another_callback", "data")
31+
buttons[2].switch_inline_query("test 4")
32+
buttons[2].switch_inline_query("test 5", "wow", current_chat=True)
33+
34+
assert buttons._serialize_attachment() == {
35+
"inline_keyboard": [
36+
[
37+
{"text": "test 1", "url": "http://example.com"},
38+
{"text": "test 2", "callback_data": "test_callback"},
39+
],
40+
[
41+
{"text": "test 4", "switch_inline_query": ""},
42+
{"text": "test 5", "switch_inline_query_current_chat": "wow"},
43+
],
44+
[
45+
{"text": "test 3", "callback_data": "another_callback\0data"},
46+
],
47+
],
48+
}
2249

2350

2451
def test_parse_callback_data():

0 commit comments

Comments
 (0)