diff --git a/botogram/before_after.py b/botogram/before_after.py new file mode 100644 index 0000000..8981ad3 --- /dev/null +++ b/botogram/before_after.py @@ -0,0 +1,45 @@ +# 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 +# DEALINGS IN THE SOFTWARE. + + +def process_before(bot, chains, update): + """before process""" + for hook in chains["before_processors"]: + bot.logger.debug("Processing 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 True + return False + + +def process_after(bot, chains, update): + """after process""" + for hook in chains["after_processors"]: + bot.logger.debug("Processing 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 True + return False diff --git a/botogram/bot.py b/botogram/bot.py index dbcf01c..f754acd 100644 --- a/botogram/bot.py +++ b/botogram/bot.py @@ -145,6 +145,11 @@ def before_processing(self, func): self._main_component.add_before_processing_hook(func) return func + def after_processing(self, func): + """Register a after processing hook""" + self._main_component.add_after_processing_hook(func) + return func + def process_message(self, func): """Add a message processor hook""" self._main_component.add_process_message_hook(func) diff --git a/botogram/callbacks.py b/botogram/callbacks.py index 509dff0..c2d78bb 100644 --- a/botogram/callbacks.py +++ b/botogram/callbacks.py @@ -23,6 +23,7 @@ from . import crypto from .context import ctx +from .before_after import process_before, process_after DIGEST = hashlib.md5 @@ -172,7 +173,7 @@ def process(bot, chains, update): chat = update.callback_query.message.chat raw = update.callback_query._data - + result = False try: name, data = parse_callback_data(bot, chat, raw) except crypto.TamperedMessageError: @@ -181,7 +182,8 @@ def process(bot, chains, update): % update.update_id ) return - + if process_before(bot, chains, update): + return for hook in chains["callbacks"]: bot.logger.debug("Processing update #%s with the hook %s" % (update.update_id, hook.name)) @@ -190,7 +192,11 @@ def process(bot, chains, update): if result is True: bot.logger.debug("Update #%s was just processed by the %s hook" % (update.update_id, hook.name)) - return + break + if process_after(bot, chains, update): + return + if result: + 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 c453c43..e1b137e 100644 --- a/botogram/components.py +++ b/botogram/components.py @@ -48,6 +48,7 @@ def __new__(cls, *args, **kwargs): self.__processors = [] self.__no_commands = [] self.__before_processors = [] + self.__after_processors = [] self.__memory_preparers = [] self.__timers = [] self.__chat_unavailable_hooks = [] @@ -75,6 +76,14 @@ def add_before_processing_hook(self, func): hook = hooks.BeforeProcessingHook(func, self) self.__before_processors.append(hook) + def add_after_processing_hook(self, func): + """Register a before processing hook""" + if not callable(func): + raise ValueError("A after processing hook must be callable") + + hook = hooks.AfterProcessingHook(func, self) + self.__after_processors.append(hook) + def add_process_message_hook(self, func): """Add a message processor hook""" if not callable(func): @@ -255,13 +264,14 @@ def _add_no_commands_hook(self, func): def _get_chains(self): """Get the full hooks chain for this component""" messages = [ - self.__before_processors[:], [self.__commands[name]._hook for name in sorted(self.__commands.keys())], self.__no_commands[:], self.__processors[:], ] return { + "before_processors": [self.__before_processors[:]], + "after_processors": [self.__after_processors[:]], "messages": messages, "poll_updates": [self.__poll_update_hooks], "memory_preparers": [self.__memory_preparers], diff --git a/botogram/frozenbot.py b/botogram/frozenbot.py index 1f49fc5..875f70e 100644 --- a/botogram/frozenbot.py +++ b/botogram/frozenbot.py @@ -110,6 +110,10 @@ def before_processing(self, func): """Register a before processing hook""" raise FrozenBotError("Can't add hooks to a bot at runtime") + def after_processing(self, func): + """Register a after processing hook""" + raise FrozenBotError("Can't add hooks to a bot at runtime") + def process_message(self, func): """Register a message processor hook""" raise FrozenBotError("Can't add hooks to a bot at runtime") diff --git a/botogram/hooks.py b/botogram/hooks.py index d7d008b..1e19c7f 100644 --- a/botogram/hooks.py +++ b/botogram/hooks.py @@ -80,7 +80,39 @@ def rebuild(cls, func, component, args): class BeforeProcessingHook(Hook): - """Underlying hook for @bot.process_message""" + """before processing hook for all update""" + + def _call(self, bot, update): + """*Actually* call the hook""" + # add message and chat for backward compatibility + kwargs = {"update": update, "message": None, "chat": None, + "user": None} + if update.message is not None: + kwargs.update({"message": update.message, + "chat": update.message.chat, + "user": update.message.sender}) + elif update.edited_message is not None: + kwargs.update({"message": update.edited_message, + "chat": update.edited_message.chat, + "user": update.edited_message.sender}) + elif update.channel_post is not None: + kwargs.update({"message": update.channel_post, + "chat": update.channel_post.chat, + "user": update.channel_post.sender}) + elif update.edited_channel_post is not None: + kwargs.update({"message": update.edited_channel_post, + "chat": update.edited_channel_post.chat, + "user": update.edited_channel_post.sender}) + elif update.callback_query is not None: + kwargs.update({"message": update.callback_query.message, + "chat": update.callback_query.message.chat, + "user": update.callback_query.sender}) + + return bot._call(self.func, self.component_id, **kwargs) + + +class AfterProcessingHook(BeforeProcessingHook): + """after processing hook for all update""" pass diff --git a/botogram/messages.py b/botogram/messages.py index f00a169..8b37d64 100644 --- a/botogram/messages.py +++ b/botogram/messages.py @@ -17,10 +17,14 @@ # 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 # DEALINGS IN THE SOFTWARE. +from .before_after import process_before, process_after def process_message(bot, chains, update): """Process a message sent to the bot""" + result = False + if process_before(bot, chains, update): + return for hook in chains["messages"]: bot.logger.debug("Processing update #%s with the hook %s..." % (update.update_id, hook.name)) @@ -29,14 +33,20 @@ def process_message(bot, chains, update): if result is True: bot.logger.debug("Update #%s was just processed by the %s hook." % (update.update_id, hook.name)) - return - + break + if process_after(bot, chains, update): + return + if result: + return bot.logger.debug("No hook actually processed the #%s update." % update.update_id) def process_edited_message(bot, chains, update): """Process an edited message""" + result = False + if process_before(bot, chains, update): + return for hook in chains["messages_edited"]: bot.logger.debug("Processing edited message in update #%s with the " "hook %s..." % (update.update_id, hook.name)) @@ -45,14 +55,20 @@ def process_edited_message(bot, chains, update): if result is True: bot.logger.debug("Update %s was just processed by the %s hook." % (update.update_id, hook.name)) - return - + break + if process_after(bot, chains, update): + return + if result: + return bot.logger.debug("No hook actually processed the #%s update." % update.update_id) def process_channel_post(bot, chains, update): """Process a channel post""" + result = False + if process_before(bot, chains, update): + return for hook in chains["channel_post"]: bot.logger.debug("Processing channel post in update #%s with the " "hook %s..." % (update.update_id, hook.name)) @@ -61,14 +77,20 @@ def process_channel_post(bot, chains, update): if result is True: bot.logger.debug("Update %s was just processed by the %s hook." % (update.update_id, hook.name)) - return - + break + if process_after(bot, chains, update): + return + if result: + return bot.logger.debug("No hook actually processed the #%s update." % update.update_id) def process_channel_post_edited(bot, chains, update): """Process an edited channel post""" + result = False + if process_before(bot, chains, update): + return for hook in chains["channel_post_edited"]: bot.logger.debug("Processing edited channel post in update #%s with" "the hook %s..." % (update.update_id, hook.name)) @@ -77,7 +99,11 @@ def process_channel_post_edited(bot, chains, update): if result is True: bot.logger.debug("Update %s was just processed by the %s hook." % (update.update_id, hook.name)) - return + break + if process_after(bot, chains, update): + return + if result: + return bot.logger.debug("No hook actually processed the #%s update." % update.update_id) @@ -85,6 +111,9 @@ def process_channel_post_edited(bot, chains, update): def process_poll_update(bot, chains, update): """Process a poll update""" + result = False + if process_before(bot, chains, update): + return for hook in chains["poll_updates"]: bot.logger.debug("Processing poll update in update #%s with" "the hook %s..." % (update.update_id, hook.name)) @@ -93,7 +122,11 @@ def process_poll_update(bot, chains, update): if result is True: bot.logger.debug("Update %s was just processed by the %s hook." % (update.update_id, hook.name)) - return + break + if process_after(bot, chains, update): + return + if result: + return bot.logger.debug("No hook actually processed the #%s update." % update.update_id) diff --git a/docs/api/bot.rst b/docs/api/bot.rst index d6d0921..9b356ae 100644 --- a/docs/api/bot.rst +++ b/docs/api/bot.rst @@ -98,17 +98,45 @@ components. Functions decorated with this decorator will be called before an update is processed. This allows you, for example, to set up a filter on who can - send messages to the bot. Decorated functions will be called with two + send messages to the bot. Decorated functions will be called with four parameters: + * A ``update`` parameter with the representation + of the update (an istance of :py:class:`botogram.Update`) * A ``chat`` parameter with the representation of the chat in which the - message was sent (an instance of :py:class:`botogram.Chat`) + message was sent (an instance of :py:class:`botogram.Chat`) if provided by update * A ``message`` parameter with the representation of the received - message (an instance of :py:class:`botogram.Message`) + message (an instance of :py:class:`botogram.Message`) if provided by update + * A ``user`` parameter with the representation of the user send + update (an instance of :py:class:`botogram.User`) if provided by update + + .. versionchanged:: 0.7 + + Added update and user parameter If the function returns ``True``, then the message processing is stopped, and no more functions will be called for this update. + .. py:decoratormethod:: after_processing + + Functions decorated with this decorator will be called after an update + is processed. This allows you, for example, to send log. + Decorated functions will be called with four parameters: + + * A ``update`` parameter with the representation + of the update (an istance of :py:class:`botogram.Update`) + * A ``chat`` parameter with the representation of the chat in which the + message was sent (an instance of :py:class:`botogram.Chat`) if provided by update + * A ``message`` parameter with the representation of the received + message (an instance of :py:class:`botogram.Message`) if provided by update + * A ``user`` parameter with the representation of the user send + update (an instance of :py:class:`botogram.User`) if provided by update + + If the function returns ``True``, then the message processing is stopped, + and no more functions will be called for this update. + + .. versionadded:: 0.7 + .. py:decoratormethod:: process_message Functions decorated with this decorator will be called while processing diff --git a/docs/api/components.rst b/docs/api/components.rst index 80d6e44..59f2ce5 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -35,18 +35,47 @@ about how to create them in the ":ref:`custom-components`" chapter. The function provided to this method will be called before an update is processed by a bot which uses the component. This allows you, for example, to set up a filter on who can send messages to the bot. - Provided functions will be called with two parameters: + Provided functions will be called with four parameters: + * A ``update`` parameter with the representation + of the update (an istance of :py:class:`botogram.Update`) * A ``chat`` parameter with the representation of the chat in which the - message was sent (an instance of :py:class:`botogram.Chat`) + message was sent (an instance of :py:class:`botogram.Chat`) if provided by update * A ``message`` parameter with the representation of the received - message (an instance of :py:class:`botogram.Message`) + message (an instance of :py:class:`botogram.Message`) if provided by update + * A ``user`` parameter with the representation of the user send + update (an instance of :py:class:`botogram.User`) if provided by update If the function returns ``True``, then the message processing is stopped, and no more functions will be called for that update. :param callable func: The function you want to add. + .. versionchanged:: 0.7 + + Added update and user parameter + + .. py:method:: add_after_processing_hook(func) + + The function provided to this method will be called after an update is + processed by a bot which uses the component. This allows you, for + example, to send log. + Provided functions will be called with four parameters: + + * A ``update`` parameter with the representation + of the update (an istance of :py:class:`botogram.Update`) + * A ``chat`` parameter with the representation of the chat in which the + message was sent (an instance of :py:class:`botogram.Chat`) if provided by update + * A ``message`` parameter with the representation of the received + message (an instance of :py:class:`botogram.Message`) if provided by update + * A ``user`` parameter with the representation of the user send + update (an instance of :py:class:`botogram.User`) if provided by update + + :param callable func: The function you want to add. + + .. versionadded:: 0.7 + + .. py:method:: add_process_message_hook(func) The function provided to this method will be called while processing an diff --git a/docs/changelog/0.7.rst b/docs/changelog/0.7.rst index 6160a66..39eb2b4 100644 --- a/docs/changelog/0.7.rst +++ b/docs/changelog/0.7.rst @@ -18,7 +18,10 @@ Release description not yet written. New features ------------ +* Added after processing method + * New :py:meth:`botogram.Bot.after_processing` + * New :py:meth:`botogram.Component.add_after_processing_hook` * Added support for inline mode @@ -103,3 +106,6 @@ Bug fixes --------- * Fixed :py:meth:`botogram.Message.edit_attach` to work with inline callbacks +* Fixed before_processing now run with all update, before only for the message (command and process_message) + * :py:meth:`botogram.Bot.before_processing` + * :py:meth:`botogram.Component.add_before_processing_hook` diff --git a/docs/custom-components.rst b/docs/custom-components.rst index 49d6889..7ea92ea 100644 --- a/docs/custom-components.rst +++ b/docs/custom-components.rst @@ -94,13 +94,12 @@ the :py:meth:`botogram.Component.add_before_processing_hook` method: self.allowed = allowed self.add_before_processing_hook(self.filter) - def filter(self, chat, message): - if message.sender.id not in self.allowed: + def filter(self, chat, message, user): + if user.id not in self.allowed: return True # Stop processing the update -And the component is complete! The filter simply checks if the message -sender's ID is in the allowed list. If not, it tells botogram the message was -successfully processed, preventing the calls to all the other hooks. The full +And the component is complete! The filter simply checks if the user sender update ID is in the allowed list. +If not, it tells botogram the message was successfully processed, preventing the calls to all the other hooks. The full source code of the component is the following: .. code-block:: python @@ -116,12 +115,13 @@ source code of the component is the following: self.allowed = allowed self.add_before_processing_hook(self.filter) - def filter(self, chat, message): - if message.sender.id not in self.allowed: + def filter(self, chat, message, user): + if user.id not in self.allowed: return True # Stop processing the update .. _custom-components-use: - +.. versionchanged:: 0.7 + use user parameter instead of message parameter Using a custom component ========================