diff --git a/zulip_bots/zulip_bots/lib.py b/zulip_bots/zulip_bots/lib.py index 11c8dd0b3..d42f6d5cc 100644 --- a/zulip_bots/zulip_bots/lib.py +++ b/zulip_bots/zulip_bots/lib.py @@ -79,6 +79,7 @@ def __init__(self, client, root_dir): self._client = client self._root_dir = root_dir try: + self.user_id = user_profile['user_id'] self.full_name = user_profile['full_name'] self.email = user_profile['email'] except KeyError: @@ -161,27 +162,89 @@ def state(self, default): yield new_state self.set_state(new_state) -def extract_query_without_mention(message, client): - # type: (Dict[str, Any], ExternalBotHandler) -> str +def extract_query_without_mention(message, at_mention_bot_name): + # type: (Dict[str, Any], str) -> str """ If the bot is the first @mention in the message, then this function returns the message with the bot's @mention removed. Otherwise, it returns None. + This function is being leveraged by two systems; external bot system and embedded bot system. + This function is being called by: + 1. 'run_message_handler_for_bot' function (zulip_bots/lib.py file in zulip/python-zulip-api + repository) that executes/runs/calls external bots. + 2. 'consume' function in EmbeddedBotWorker class (zerver/worker/queue_processors.py + file in zulip/zulip repository) that executes/runs/calls embedded bots. + + Since, this is a general utility function for any working bot, it is planned to be an independent + function for now. Any refactoring should correctly be reflected in all the bot systems using this + function. """ - bot_mention = r'^@(\*\*{0}\*\*)'.format(client.full_name) + bot_mention = r'^@(\*\*{0}\*\*)'.format(at_mention_bot_name) start_with_mention = re.compile(bot_mention).match(message['content']) if start_with_mention is None: return None query_without_mention = message['content'][len(start_with_mention.group()):] return query_without_mention.lstrip() -def is_private(message, client): - # type: (Dict[str, Any], ExternalBotHandler) -> bool - # bot will not reply if the sender name is the same as the bot name - # to prevent infinite loop +def is_private(message, at_mention_bot_id): + # type: (Dict[str, Any], int) -> bool + """ + This function is to ensure that the bot doesn't go into infinite loop if the message sender id is + the same as the id of the bot which is called. This function makes the bot not reply to itself. + + This function is being leveraged by two systems; external bot system and embedded bot system, + any change/modification in the structure of this should be reflected at other places accordingly. + For details read "extract_query_without_mention" function docstring. + """ if message['type'] == 'private': - return client.full_name != message['sender_full_name'] + return at_mention_bot_id != message['sender_id'] return False +def initialize_config_bot(message_handler, bot_handler): + # type: (Any, Any) -> None + """ + If a bot has bot-specific configuration settings (both public or private) to be set, then this + function calls the 'initialize' function which in turn calls 'get_config_info' for bots. + + This function is being leveraged by two systems; external bot system and embedded bot system, + any change/modification in the structure of this should be reflected at other places accordingly. + For details read "extract_query_without_mention" function docstring. + """ + if hasattr(message_handler, 'initialize'): + message_handler.initialize(bot_handler=bot_handler) + +def get_message_content_if_bot_is_called(message, at_mention_bot_name, at_mention_bot_id): + # type: (Dict[str, Any], str) -> Any + """ + Check if the bot is called or not; a bot can be called by 2 ways: @mention-botname or private message + to the bot. Once it is confirmed if a bot is called or not, then we move to the second part of the + function. + If the bot is privately messaged, then the message content need not be modified and the bot can directly + process the entire message content. + If the bot is called by @mention-botname, then we need to remove @mention-botname for the bot to + process the rest of the message content. + + This function is being leveraged by two systems; external bot system and embedded bot system, + any change/modification in the structure of this should be reflected at other places accordingly. + For details read "extract_query_without_mention" function docstring. + """ + # is_mentioned is true if the bot is mentioned at ANY position (not necessarily + # the first @mention in the message). + is_mentioned = message['is_mentioned'] + is_private_message = is_private(message=message, at_mention_bot_id=at_mention_bot_id) + + # Strip at-mention botname from the message + if is_mentioned: + # message['content'] will be None when the bot's @-mention is not at the beginning. + # In that case, the message shall not be handled. + message['content'] = extract_query_without_mention(message=message, + at_mention_bot_name=at_mention_bot_name) + if message['content'] is None: + return + + if (is_private_message or is_mentioned): + return message['content'] + return None + def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name): # type: (Any, bool, str) -> Any # @@ -196,8 +259,7 @@ def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name): restricted_client = ExternalBotHandler(client, bot_dir) message_handler = lib_module.handler_class() - if hasattr(message_handler, 'initialize'): - message_handler.initialize(bot_handler=restricted_client) + initialize_config_bot(message_handler=message_handler, bot_handler=restricted_client) state_handler = StateHandler() @@ -208,20 +270,13 @@ def handle_message(message): # type: (Dict[str, Any]) -> None logging.info('waiting for next message') - # is_mentioned is true if the bot is mentioned at ANY position (not necessarily - # the first @mention in the message). - is_mentioned = message['is_mentioned'] - is_private_message = is_private(message, restricted_client) + message_content_if_bot_is_called = get_message_content_if_bot_is_called(message=message, + at_mention_bot_name=restricted_client.full_name, + at_mention_bot_id=restricted_client.user_id) - # Strip at-mention botname from the message - if is_mentioned: - # message['content'] will be None when the bot's @-mention is not at the beginning. - # In that case, the message shall not be handled. - message['content'] = extract_query_without_mention(message=message, client=restricted_client) - if message['content'] is None: - return + if message_content_if_bot_is_called: + message['content'] = message_content_if_bot_is_called - if is_private_message or is_mentioned: message_handler.handle_message( message=message, bot_handler=restricted_client,