diff --git a/AUTHORS b/AUTHORS index a6526a5..4e42fed 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Contributors: Ilya Otyutskiy Stefano Teodorani Francesco Zimbolo + Mattia Effendi Daniele Ceribelli Original author: diff --git a/botogram/__init__.py b/botogram/__init__.py index f0fd06e..477fb1b 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 from .inline import ( InlineInputMessage, InlineInputLocation, diff --git a/botogram/keyboards.py b/botogram/keyboards.py new file mode 100644 index 0000000..e191420 --- /dev/null +++ b/botogram/keyboards.py @@ -0,0 +1,84 @@ +# 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 61e9a6e..b79775f 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -53,7 +53,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"): @@ -76,6 +77,20 @@ def _get_call_args(self, reply_to, extra, attach, notify): if not notify: args["disable_notification"] = True + if remove_keyboard is not None: + reply_markup = {} + reply_markup['remove_keyboard'] = remove_keyboard + if selective is not None: + reply_markup['selective'] = selective + args["reply_markup"] = json.dumps(reply_markup) + + if force_reply is not None: + reply_markup = {} + reply_markup['force_reply'] = force_reply + if selective is not None: + reply_markup['selective'] = selective + args["reply_markup"] = json.dumps(reply_markup) + return args @staticmethod @@ -98,9 +113,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 @@ -113,9 +130,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: @@ -134,10 +153,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: @@ -165,9 +186,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: @@ -195,9 +218,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 @@ -222,9 +247,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: @@ -246,9 +273,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: @@ -276,9 +305,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: @@ -299,10 +330,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 @@ -317,9 +350,11 @@ 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 @@ -331,7 +366,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: @@ -343,7 +379,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, @@ -358,9 +395,10 @@ def send_sticker(self, sticker=None, reply_to=None, extra=None, @_require_api def send_contact(self, phone, first_name, last_name=None, vcard=None, *, reply_to=None, - extra=None, attach=None, notify=True): + extra=None, attach=None, notify=True, 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 @@ -373,9 +411,11 @@ def send_contact(self, phone, first_name, last_name=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))