From 69748f7b585252b9bdd79499479a87359234a508 Mon Sep 17 00:00:00 2001 From: Mattia Date: Sun, 8 Sep 2019 12:45:11 +0100 Subject: [PATCH 1/5] Implemented support for normal keyboards --- botogram/__init__.py | 1 + botogram/keyboards.py | 86 +++++++++++++++++++++++++++++++++ botogram/objects/mixins.py | 97 +++++++++++++++++++++++++++----------- 3 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 botogram/keyboards.py diff --git a/botogram/__init__.py b/botogram/__init__.py index 9c10eba..c373c62 100644 --- a/botogram/__init__.py +++ b/botogram/__init__.py @@ -35,6 +35,7 @@ from .objects import * from .utils import usernames_in from .callbacks import Buttons, ButtonsRow +from .keyboards import Keyboard, KeyboardRow # This code will simulate the Windows' multiprocessing behavior if the diff --git a/botogram/keyboards.py b/botogram/keyboards.py new file mode 100644 index 0000000..7de49bd --- /dev/null +++ b/botogram/keyboards.py @@ -0,0 +1,86 @@ +# Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + + + +class KeyboardRow: + """A row of a keyboard""" + + def __init__(self): + self._content = [] + + def text(self, text): + """Sends a message when the button is pressed""" + self._content.append({"text": text}) + + def request_contact(self, label): + """Ask the user if he wants to share his contact""" + + self._content.append({ + "text": label, + "request_contact": True, + }) + + def request_location(self, label): + """Ask the user if he wants to share his location""" + + self._content.append({ + "text": label, + "request_location": True, + }) + + def _get_content(self, chat): + """Get the content of this row""" + for item in self._content: + new = item.copy() + + # Replace any callable with its value + # This allows to dynamically generate field values + for key, value in new.items(): + if callable(value): + new[key] = value(chat) + + yield new + + +class Keyboard: + """Factory for keyboards""" + + def __init__(self, resize=False, one_time=False, selective=False): + self.resize_keyboard = resize + self.one_time_keyboard = one_time + self.selective = selective + self._rows = {} + + def __getitem__(self, index): + if index not in self._rows: + self._rows[index] = KeyboardRow() + return self._rows[index] + + def _serialize_attachment(self, chat): + rows = [ + list(row._get_content(chat)) for i, row in sorted( + tuple(self._rows.items()), key=lambda i: i[0] + ) + ] + + return {"keyboard": rows, "resize_keyboard": self.resize_keyboard, + "one_time_keyboard": self.one_time_keyboard, + "selective": self.selective} + diff --git a/botogram/objects/mixins.py b/botogram/objects/mixins.py index 10a9c4f..98e2147 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -52,7 +52,8 @@ def __(self, *args, **kwargs): class ChatMixin: """Add some methods for chats""" - def _get_call_args(self, reply_to, extra, attach, notify): + def _get_call_args(self, reply_to, extra, attach, notify, remove_keyboard, + force_reply, selective): """Get default API call arguments""" # Convert instance of Message to ids in reply_to if hasattr(reply_to, "id"): @@ -75,6 +76,23 @@ def _get_call_args(self, reply_to, extra, attach, notify): if not notify: args["disable_notification"] = True + if (remove_keyboard is None or force_reply is None) \ + and selective is not None: + raise ValueError("The selective attribute is only usable" + + "when remove_keyboard or force_reply is True") + + if remove_keyboard is not None: + args["reply_markup"] = json.dumps( + {"remove_keyboard": remove_keyboard} + ) + if selective is not None: + args["reply_markup"] = json.dumps({"selective": selective}) + + if force_reply is not None: + args["reply_markup"] = json.dumps({"force_reply": force_reply}) + if selective is not None: + args["reply_markup"] = json.dumps({"selective": selective}) + return args @staticmethod @@ -97,9 +115,11 @@ def _get_file_args(path, file_id, url): @_require_api def send(self, message, preview=True, reply_to=None, syntax=None, - extra=None, attach=None, notify=True): + extra=None, attach=None, notify=True, remove_keyboard=None, + force_reply=None, selective=None): """Send a message""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) args["text"] = message args["disable_web_page_preview"] = not preview @@ -112,9 +132,11 @@ def send(self, message, preview=True, reply_to=None, syntax=None, @_require_api def send_photo(self, path=None, file_id=None, url=None, caption=None, syntax=None, reply_to=None, extra=None, attach=None, - notify=True): + notify=True, remove_keyboard=None, force_reply=None, + selective=None): """Send a photo""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) if caption is not None: args["caption"] = caption if syntax is not None: @@ -133,10 +155,12 @@ def send_photo(self, path=None, file_id=None, url=None, caption=None, @_require_api def send_audio(self, path=None, file_id=None, url=None, duration=None, thumb=None, performer=None, title=None, reply_to=None, - extra=None, attach=None, notify=True, caption=None, *, + extra=None, attach=None, notify=True, remove_keyboard=None, + force_reply=None, selective=None, caption=None, *, syntax=None): """Send an audio track""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) if caption is not None: args["caption"] = caption if syntax is not None: @@ -164,9 +188,11 @@ def send_audio(self, path=None, file_id=None, url=None, duration=None, @_require_api def send_voice(self, path=None, file_id=None, url=None, duration=None, title=None, reply_to=None, extra=None, attach=None, - notify=True, caption=None, *, syntax=None): + notify=True, remove_keyboard=None, force_reply=None, + selective=None, caption=None, *, syntax=None): """Send a voice message""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) if caption is not None: args["caption"] = caption if syntax is not None: @@ -194,9 +220,11 @@ def send_voice(self, path=None, file_id=None, url=None, duration=None, def send_video(self, path=None, file_id=None, url=None, duration=None, caption=None, streaming=True, thumb=None, reply_to=None, extra=None, attach=None, - notify=True, *, syntax=None): + notify=True, remove_keyboard=None, force_reply=None, + selective=None, *, syntax=None): """Send a video""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) args["supports_streaming"] = streaming if duration is not None: args["duration"] = duration @@ -221,9 +249,11 @@ def send_video(self, path=None, file_id=None, url=None, @_require_api def send_video_note(self, path=None, file_id=None, duration=None, diameter=None, thumb=None, reply_to=None, extra=None, - attach=None, notify=True): + attach=None, notify=True, remove_keyboard=None, + force_reply=None, selective=None): """Send a video note""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) if duration is not None: args["duration"] = duration if diameter is not None: @@ -245,9 +275,11 @@ def send_video_note(self, path=None, file_id=None, duration=None, def send_gif(self, path=None, file_id=None, url=None, duration=None, width=None, height=None, caption=None, thumb=None, reply_to=None, extra=None, attach=None, - notify=True, syntax=None): + notify=True, remove_keyboard=None, force_reply=None, + selective=None, syntax=None): """Send an animation""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) if duration is not None: args["duration"] = duration if caption is not None: @@ -275,9 +307,11 @@ def send_gif(self, path=None, file_id=None, url=None, duration=None, @_require_api def send_file(self, path=None, file_id=None, url=None, thumb=None, reply_to=None, extra=None, attach=None, - notify=True, caption=None, *, syntax=None): + notify=True, remove_keyboard=None, force_reply=None, + selective=None, caption=None, *, syntax=None): """Send a generic file""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) if caption is not None: args["caption"] = caption if syntax is not None: @@ -298,10 +332,12 @@ def send_file(self, path=None, file_id=None, url=None, thumb=None, @_require_api def send_location(self, latitude, longitude, live_period=None, - reply_to=None, extra=None, attach=None, notify=True): + reply_to=None, extra=None, attach=None, notify=True, + remove_keyboard=None, force_reply=None, selective=None): """Send a geographic location, set live_period to a number between 60 and 86400 if it's a live location""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) args["latitude"] = latitude args["longitude"] = longitude @@ -316,9 +352,10 @@ def send_location(self, latitude, longitude, live_period=None, @_require_api def send_venue(self, latitude, longitude, title, address, foursquare=None, - reply_to=None, extra=None, attach=None, notify=True): + reply_to=None, extra=None, attach=None, notify=True, remove_keyboard=None, force_reply=None, selective=None): """Send a venue""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) args["latitude"] = latitude args["longitude"] = longitude args["title"] = title @@ -330,7 +367,8 @@ def send_venue(self, latitude, longitude, title, address, foursquare=None, @_require_api def send_sticker(self, sticker=None, reply_to=None, extra=None, - attach=None, notify=True, *, + attach=None, notify=True, remove_keyboard=None, + force_reply=None, selective=None, *, path=None, file_id=None, url=None): """Send a sticker""" if sticker is not None: @@ -342,7 +380,8 @@ def send_sticker(self, sticker=None, reply_to=None, extra=None, "The sticker parameter", "1.0", "use the path parameter", -3 ) - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) files = dict() args["sticker"], files["sticker"] = self._get_file_args(path, @@ -356,9 +395,11 @@ def send_sticker(self, sticker=None, reply_to=None, extra=None, @_require_api def send_contact(self, phone, first_name, last_name=None, *, reply_to=None, - extra=None, attach=None, notify=True): + extra=None, attach=None, notify=True, + remove_keyboard=None, force_reply=None, selective=None): """Send a contact""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) args["phone_number"] = phone args["first_name"] = first_name @@ -369,9 +410,11 @@ def send_contact(self, phone, first_name, last_name=None, *, reply_to=None, @_require_api def send_poll(self, question, *kargs, reply_to=None, extra=None, - attach=None, notify=True): + attach=None, notify=True, remove_keyboard=None, + force_reply=None, selective=None): """Send a poll""" - args = self._get_call_args(reply_to, extra, attach, notify) + args = self._get_call_args(reply_to, extra, attach, notify, + remove_keyboard, force_reply, selective) args["question"] = question args["options"] = json.dumps(list(kargs)) From d39b62de18b3cc5116f9f8186e8cda1d8fab7a49 Mon Sep 17 00:00:00 2001 From: Mattia Date: Sun, 8 Sep 2019 18:02:01 +0100 Subject: [PATCH 2/5] Fixing Travis CI errors --- botogram/keyboards.py | 2 -- botogram/objects/mixins.py | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/botogram/keyboards.py b/botogram/keyboards.py index 7de49bd..e191420 100644 --- a/botogram/keyboards.py +++ b/botogram/keyboards.py @@ -18,7 +18,6 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - class KeyboardRow: """A row of a keyboard""" @@ -83,4 +82,3 @@ def _serialize_attachment(self, chat): return {"keyboard": rows, "resize_keyboard": self.resize_keyboard, "one_time_keyboard": self.one_time_keyboard, "selective": self.selective} - diff --git a/botogram/objects/mixins.py b/botogram/objects/mixins.py index 98e2147..b1b2ac3 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -52,7 +52,7 @@ def __(self, *args, **kwargs): class ChatMixin: """Add some methods for chats""" - def _get_call_args(self, reply_to, extra, attach, notify, remove_keyboard, + def _get_call_args(self, reply_to, extra, attach, notify, remove_keyboard, force_reply, selective): """Get default API call arguments""" # Convert instance of Message to ids in reply_to @@ -84,14 +84,14 @@ def _get_call_args(self, reply_to, extra, attach, notify, remove_keyboard, if remove_keyboard is not None: args["reply_markup"] = json.dumps( {"remove_keyboard": remove_keyboard} - ) + ) if selective is not None: - args["reply_markup"] = json.dumps({"selective": selective}) + args["reply_markup"] = json.dumps({"selective": selective}) if force_reply is not None: - args["reply_markup"] = json.dumps({"force_reply": force_reply}) + args["reply_markup"] = json.dumps({"force_reply": force_reply}) if selective is not None: - args["reply_markup"] = json.dumps({"selective": selective}) + args["reply_markup"] = json.dumps({"selective": selective}) return args @@ -278,7 +278,7 @@ def send_gif(self, path=None, file_id=None, url=None, duration=None, notify=True, remove_keyboard=None, force_reply=None, selective=None, syntax=None): """Send an animation""" - args = self._get_call_args(reply_to, extra, attach, notify, + args = self._get_call_args(reply_to, extra, attach, notify, remove_keyboard, force_reply, selective) if duration is not None: args["duration"] = duration @@ -352,7 +352,8 @@ def send_location(self, latitude, longitude, live_period=None, @_require_api def send_venue(self, latitude, longitude, title, address, foursquare=None, - reply_to=None, extra=None, attach=None, notify=True, remove_keyboard=None, force_reply=None, selective=None): + reply_to=None, extra=None, attach=None, notify=True, + remove_keyboard=None, force_reply=None, selective=None): """Send a venue""" args = self._get_call_args(reply_to, extra, attach, notify, remove_keyboard, force_reply, selective) @@ -395,10 +396,10 @@ def send_sticker(self, sticker=None, reply_to=None, extra=None, @_require_api def send_contact(self, phone, first_name, last_name=None, *, reply_to=None, - extra=None, attach=None, notify=True, + extra=None, attach=None, notify=True, remove_keyboard=None, force_reply=None, selective=None): """Send a contact""" - args = self._get_call_args(reply_to, extra, attach, notify, + args = self._get_call_args(reply_to, extra, attach, notify, remove_keyboard, force_reply, selective) args["phone_number"] = phone args["first_name"] = first_name @@ -410,10 +411,10 @@ def send_contact(self, phone, first_name, last_name=None, *, reply_to=None, @_require_api def send_poll(self, question, *kargs, reply_to=None, extra=None, - attach=None, notify=True, remove_keyboard=None, + attach=None, notify=True, remove_keyboard=None, force_reply=None, selective=None): """Send a poll""" - args = self._get_call_args(reply_to, extra, attach, notify, + args = self._get_call_args(reply_to, extra, attach, notify, remove_keyboard, force_reply, selective) args["question"] = question args["options"] = json.dumps(list(kargs)) From 965f35e30d44e12d667b1f85532c9a402a487604 Mon Sep 17 00:00:00 2001 From: Mattia Date: Sun, 8 Sep 2019 18:10:12 +0100 Subject: [PATCH 3/5] Added myself to the contributors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 822efa5..b4e7a12 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ Contributors: Ilya Otyutskiy Stefano Teodorani Francesco Zimbolo + Mattia Effendi Original author: From 2d192c25120cc24e31c7c1b7a2f51061c9ae266e Mon Sep 17 00:00:00 2001 From: MattiaEffendi Date: Fri, 5 Jun 2020 11:53:33 +0200 Subject: [PATCH 4/5] Fixed replacing reply_markup arguments --- botogram/objects/mixins.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/botogram/objects/mixins.py b/botogram/objects/mixins.py index b1b2ac3..8655df2 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -76,22 +76,18 @@ def _get_call_args(self, reply_to, extra, attach, notify, remove_keyboard, if not notify: args["disable_notification"] = True - if (remove_keyboard is None or force_reply is None) \ - and selective is not None: - raise ValueError("The selective attribute is only usable" + - "when remove_keyboard or force_reply is True") - if remove_keyboard is not None: - args["reply_markup"] = json.dumps( - {"remove_keyboard": remove_keyboard} - ) + reply_markup = {} + reply_markup['remove_keyboard'] = remove_keyboard if selective is not None: - args["reply_markup"] = json.dumps({"selective": selective}) + reply_markup['selective'] = selective + args["reply_markup"] = json.dumps(reply_markup) if force_reply is not None: - args["reply_markup"] = json.dumps({"force_reply": force_reply}) + reply_markup = {} + reply_markup['force_reply'] = force_reply if selective is not None: - args["reply_markup"] = json.dumps({"selective": selective}) + reply_markup['selective'] = selective return args @@ -782,4 +778,4 @@ def __del__(self): if not self._used: utils.warn(1, "error_with_album", "you should use `with` to use send_album\ - -- check the documentation") + -- check the documentation") \ No newline at end of file From b2947cc36c319476acf339a67ce5cc7a7c074650 Mon Sep 17 00:00:00 2001 From: MattiaEffendi Date: Fri, 5 Jun 2020 12:01:32 +0200 Subject: [PATCH 5/5] Forgot a line --- botogram/objects/mixins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/botogram/objects/mixins.py b/botogram/objects/mixins.py index 8655df2..cb7e00f 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -88,6 +88,7 @@ def _get_call_args(self, reply_to, extra, attach, notify, remove_keyboard, reply_markup['force_reply'] = force_reply if selective is not None: reply_markup['selective'] = selective + args["reply_markup"] = json.dumps(reply_markup) return args