diff --git a/botogram/bot.py b/botogram/bot.py index ffb4a32..b5eb09c 100644 --- a/botogram/bot.py +++ b/botogram/bot.py @@ -22,6 +22,7 @@ from . import shared from . import tasks from . import messages +from . import callbacks class Bot(frozenbot.FrozenBot): @@ -75,6 +76,8 @@ def __init__(self, api_connection): messages.process_channel_post) self.register_update_processor("edited_channel_post", messages.process_channel_post_edited) + self.register_update_processor("callback_query", + callbacks.process_callback) self._bot_id = str(uuid.uuid4()) @@ -166,6 +169,11 @@ def __(func): return func return __ + def callback(self, func): + """Add a callback query hook""" + self._main_component.add_callback_query_hook(func) + return func + def timer(self, interval): """Register a new timer""" def __(func): diff --git a/botogram/callbacks.py b/botogram/callbacks.py new file mode 100644 index 0000000..02954d6 --- /dev/null +++ b/botogram/callbacks.py @@ -0,0 +1,22 @@ +""" + botogram.messages + Logic for callbacks sent to your bot + + Copyright (c) 2016 Pietro Albini + Released under the MIT license +""" + + +def process_callback(bot, chains, update): + for hook in chains["callback_query"]: + bot.logger.debug("Processing callback query in update #%s with the " + "hook %s..." % (update.update_id, hook.name)) + + result = hook.call(bot, update) + if result is True: + bot.logger.debug("Update %s was just processed by the %s hook." % + (update.update_id, hook.name)) + return + + bot.logger.debug("No hook actually processed the #%s update." % + update.update_id) diff --git a/botogram/components.py b/botogram/components.py index acef3d4..4289171 100644 --- a/botogram/components.py +++ b/botogram/components.py @@ -34,6 +34,7 @@ def __new__(cls, *args, **kwargs): self.__messages_edited_hooks = [] self.__channel_post_hooks = [] self.__channel_post_edited_hooks = [] + self.__callback_query_hooks = [] self._component_id = str(uuid.uuid4()) @@ -171,11 +172,19 @@ def add_channel_post_hook(self, func): def add_channel_post_edited_hook(self, func): """Add an edited channel post hook""" if not callable(func): - raise ValueError("A edited channel post hook must be callable") + raise ValueError("An edited channel post hook must be callable") hook = hooks.EditedChannelPostHook(func, self) self.__channel_post_edited_hooks.append(hook) + def add_callback_query_hook(self, func): + """Add a callback query hook""" + if not callable(func): + return ValueError("A callback query hook must be callable") + + hook = hooks.CallbackQueryHook(func, self) + self.__callback_query_hooks.append(hook) + def _add_no_commands_hook(self, func): """Register an hook which will be executed when no commands matches""" if not callable(func): @@ -200,7 +209,8 @@ def _get_chains(self): "chat_unavalable_hooks": [self.__chat_unavailable_hooks], "messages_edited": [self.__messages_edited_hooks], "channel_post": [self.__channel_post_hooks], - "channel_post_edited": [self.__channel_post_edited_hooks] + "channel_post_edited": [self.__channel_post_edited_hooks], + "callback_query": [self.__callback_query_hooks], } def _get_commands(self): diff --git a/botogram/hooks.py b/botogram/hooks.py index d337e8c..f2202da 100644 --- a/botogram/hooks.py +++ b/botogram/hooks.py @@ -230,6 +230,15 @@ def _call(self, bot, update): message=message) +class CallbackQueryHook(Hook): + """Underlying hook for @bot.callback""" + + def _call(self, bot, update): + query = update.callback_query + return bot._call(self.func, self.component_id, message=query.message, + user=query.sender, callback=query) + + class TimerHook(Hook): """Underlying hook for a timer""" diff --git a/botogram/objects/callback_query.py b/botogram/objects/callback_query.py new file mode 100644 index 0000000..fab2ad0 --- /dev/null +++ b/botogram/objects/callback_query.py @@ -0,0 +1,37 @@ +""" + botogram.objects.callback_query + Representation of the callback query-related upstream API objects + + Copyright (c) 2015 Pietro Albini + Released under the MIT license +""" + +from .base import BaseObject + +from .messages import User, Message +from . import mixins + + +class CallbackQuery(BaseObject, mixins.CallbackMixin): + """Telegram API representation of a callback query + + https://core.telegram.org/bots/api#callbackquery + """ + + required = { + "id": str, + "from": User, + "message": Message, + "chat_instance": str, + } + optional = { + "inline_message_id": str, + "data": str, + "game_short_name": str, + } + replace_keys = { + "from": "sender", + } + + def __init__(self, data, api=None): + super().__init__(data, api) diff --git a/botogram/objects/mixins.py b/botogram/objects/mixins.py index b2826cb..dca3a3b 100644 --- a/botogram/objects/mixins.py +++ b/botogram/objects/mixins.py @@ -297,3 +297,15 @@ def save(self, path): downloaded = self._api.file_content(response["result"]["file_path"]) with open(path, 'wb') as f: f.write(downloaded) + + +class CallbackMixin: + """Add some methods for callbacks""" + + @_require_api + def notify(self, text, alert=False, cache_time=0): + self._api.call("answerCallbackQuery", + {"callback_query_id": self.id, + "text": text, + "show_alert": alert, + "cache_time": cache_time}) diff --git a/botogram/objects/updates.py b/botogram/objects/updates.py index 1abce79..aa2629d 100644 --- a/botogram/objects/updates.py +++ b/botogram/objects/updates.py @@ -9,6 +9,7 @@ from .base import BaseObject, multiple from .messages import Message +from .callback_query import CallbackQuery class Update(BaseObject): @@ -24,7 +25,8 @@ class Update(BaseObject): "message": Message, "edited_message": Message, "channel_post": Message, - "edited_channel_post": Message + "edited_channel_post": Message, + "callback_query": CallbackQuery, } _check_equality_ = "update_id" diff --git a/docs/api/telegram.rst b/docs/api/telegram.rst index 88c2acc..3868d9b 100644 --- a/docs/api/telegram.rst +++ b/docs/api/telegram.rst @@ -34,6 +34,7 @@ about its business. * :py:class:`~botogram.ReplyKeyboardMarkup` * :py:class:`~botogram.ReplyKeyboardHide` * :py:class:`~botogram.ForceReply` +* :py:class:`~botogram.CallbackQuery` .. py:class:: botogram.User @@ -2077,6 +2078,47 @@ about its business. *This attribute can be None if it's not provided by Telegram.* +.. py:class:: botogram.CallbackQuery + + This class represents a callback query received by the bot. + + .. py:attribute:: id + + The id of the callback. + + .. py:attribute:: sender + + The :py:class:`~botogram.User` that sent the callback query. + + .. py:attribute:: message + + The :py:class:`~botogram.Message` that was attached to the button that originated the query. + + *This attribute can be None if it's not provided by Telegram.* + + .. py:attribute:: inline_message_id + + The id of the message sent via the bot in inline mode that was attached to the button that originated the query. + + *This attribute can be None if it's not provided by Telegram.* + + .. py:attribute:: chat_instance + + The chat to which the message with the callback button was sent. + + .. py:attribute:: data + + Data associated with the callback button. + + *This attribute can be None if it's not provided by Telegram.* + + .. py:attribute:: game_short_name + + Short name of a Game to be returned, serves as the unique identifier for the game. + + *This attribute can be None if it's not provided by Telegram.* + + .. _Telegram's Bot API: https://core.telegram.org/bots/api .. _API methods: https://core.telegram.org/bots/api#available-methods .. _API types: https://core.telegram.org/bots/api#available-types