Skip to content

Clean and modify interface of functions in zulip_bots/lib.py to work for Embedded bots. #81

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
99 changes: 77 additions & 22 deletions zulip_bots/zulip_bots/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
#
Expand All @@ -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()

Expand All @@ -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,
Expand Down