Skip to content

bots: Enable 'run_bot_message_handler_for_bot' to be used by Embedded… #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 90 additions & 37 deletions zulip_bots/zulip_bots/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,91 @@ def state(self, default):
yield new_state
self.set_state(new_state)

def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name):
# type: (Any, bool, str) -> Any
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(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, at_mention_bot_name):
# type: (Dict[str, Any], str) -> bool
"""
This function is to ensure that the bot doesn't go into infinite loop if the sender name is
the same as the bot name. This function makes the bot not reply to such a sender.

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 at_mention_bot_name != message['sender_full_name']
return False

def initialize_config_bot(message_handler, bot_handler):
# type: (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):
# 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_name=at_mention_bot_name)

# 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, config_file, quiet, bot_name):
# type: (Any, str, bool, str) -> Any
#
# lib_module is of type Any, since it can contain any bot's
# handler class. Eventually, we want bot's handler classes to
Expand All @@ -173,53 +256,23 @@ 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()

if not quiet:
print(message_handler.usage())

def extract_query_without_mention(message, client):
# type: (Dict[str, Any], ExternalBotHandler) -> 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.
"""
bot_mention = r'^@(\*\*{0}\*\*)'.format(client.full_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
if message['type'] == 'private':
return client.full_name != message['sender_full_name']
return False

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)

# 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 is not None:
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,
Expand Down