def run_message_handler_for_bot(lib_module, quiet, config_file): # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file) restricted_client = RestrictedClient(client) message_handler = lib_module.handler_class() class StateHandler(object): def __init__(self): self.state = None def set_state(self, state): self.state = state def get_state(self): return self.state state_handler = StateHandler() if not quiet: print(message_handler.usage()) def handle_message(message): logging.info('waiting for next message') if message_handler.triage_message(message=message): message_handler.handle_message( message=message, client=restricted_client, state_handler=state_handler ) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def run_message_handler_for_bot(lib_module, quiet, config_file): # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file) restricted_client = BotHandlerApi(client) message_handler = lib_module.handler_class() state_handler = StateHandler() if not quiet: print(message_handler.usage()) def extract_query_without_mention(message, client): """ 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): # 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): 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) # 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 is_private_message or is_mentioned: message_handler.handle_message( message=message, client=restricted_client, state_handler=state_handler ) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def run_message_handler_for_bot(lib_module, quiet, config_file): # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file) restricted_client = RestrictedClient(client) message_handler = lib_module.handler_class() if not quiet: print(message_handler.usage()) def handle_message(message): logging.info('waiting for next message') if message_handler.triage_message(message=message): message_handler.handle_message( message=message, client=restricted_client) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def __init__( self, client: Client, root_dir: str, bot_details: Dict[str, Any], bot_config_file: Optional[str]=None, bot_config_parser: Optional[configparser.ConfigParser]=None, ) -> None: # Only expose a subset of our Client's functionality try: user_profile = client.get_profile() except ZulipError as e: print(''' ERROR: {} Have you not started the server? Or did you mis-specify the URL? '''.format(e)) sys.exit(1) if user_profile.get('result') == 'error': msg = user_profile.get('msg', 'unknown') print(''' ERROR: {} '''.format(msg)) sys.exit(1) self._rate_limit = RateLimit(20, 5) self._client = client self._root_dir = root_dir self.bot_details = bot_details self.bot_config_file = bot_config_file self._bot_config_parser = bot_config_parser self._storage = StateHandler(client) try: self.user_id = user_profile['user_id'] self.full_name = user_profile['full_name'] self.email = user_profile['email'] except KeyError: logging.error('Cannot fetch user profile, make sure you have set' ' up the zuliprc file correctly.') sys.exit(1)
def add_alert_words(client: Client) -> None: word = ['foo', 'bar'] result = client.add_alert_words(word) assert result['result'] == 'success'
def get_user_agent(client: Client) -> None: result = client.get_user_agent() assert result.startswith("ZulipPython/")
def test_user_not_authorized_error(nonadmin_client: Client) -> None: result = nonadmin_client.get_streams(include_all_active=True) validate_against_openapi_schema(result, "/rest-error-handling", "post", "400_2")
def remove_alert_words(client: Client) -> None: word = ["foo"] result = client.remove_alert_words(word) assert result["result"] == "success"
def get_alert_words(client: Client) -> None: result = client.get_alert_words() assert result["result"] == "success"
def get_subscribers(client: Client) -> None: result = client.get_subscribers(stream='new stream') assert result['subscribers'] == ['*****@*****.**', '*****@*****.**']
def test_invalid_api_key(client_with_invalid_key: Client) -> None: result = client_with_invalid_key.get_subscriptions() validate_against_openapi_schema(result, "/rest-error-handling", "post", "400_0")
def get_bot_api_client(self, user_profile): # type: (UserProfile) -> BotHandlerApi raw_client = Client(email=str(user_profile.email), api_key=str(user_profile.api_key), site=str(user_profile.realm.uri)) return BotHandlerApi(raw_client)
def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name): # type: (Any, bool, str, 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 # inherit from a common prototype specifying the handle_message # function. # # Set default bot_details, then override from class, if provided bot_details = { 'name': bot_name.capitalize(), 'description': "", } bot_details.update(getattr(lib_module.handler_class, 'META', {})) # Make sure you set up your ~/.zuliprc client_name = "Zulip{}Bot".format(bot_name.capitalize()) try: client = Client(config_file=config_file, client=client_name) except configparser.Error as e: file_contents = open(config_file).read() print('\nERROR: {} seems to be broken:\n\n{}'.format( config_file, file_contents)) print('\nMore details here:\n\n' + str(e) + '\n') sys.exit(1) bot_dir = os.path.dirname(lib_module.__file__) restricted_client = ExternalBotHandler(client, bot_dir, bot_details) message_handler = lib_module.handler_class() if hasattr(message_handler, 'initialize'): message_handler.initialize(bot_handler=restricted_client) if not quiet: print("Running {} Bot:".format(bot_details['name'])) if bot_details['description'] != "": print("\n\t{}".format(bot_details['description'])) print(message_handler.usage()) def handle_message(message, flags): # type: (Dict[str, Any], List[str]) -> None logging.info('waiting for next message') # `mentioned` will be in `flags` if the bot is mentioned at ANY position # (not necessarily the first @mention in the message). is_mentioned = 'mentioned' in flags is_private_message = is_private_message_from_another_user( message, 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 is_private_message or is_mentioned: message_handler.handle_message(message=message, bot_handler=restricted_client) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') def event_callback(event): # type: (Dict[str, Any]) -> None if event['type'] == 'message': handle_message(event['message'], event['flags']) client.call_on_each_event(event_callback, ['message'])
def run_message_handler_for_bot( lib_module: Any, quiet: bool, config_file: str, bot_config_file: str, bot_name: str, bot_source: 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 inherit from a common prototype specifying the handle_message function. Set default bot_details, then override from class, if provided """ bot_details = { "name": bot_name.capitalize(), "description": "", } bot_details.update(getattr(lib_module.handler_class, "META", {})) # Make sure you set up your ~/.zuliprc client_name = f"Zulip{bot_name.capitalize()}Bot" try: client = Client(config_file=config_file, client=client_name) except configparser.Error as e: display_config_file_errors(str(e), config_file) sys.exit(1) bot_dir = os.path.dirname(lib_module.__file__) restricted_client = ExternalBotHandler(client, bot_dir, bot_details, bot_config_file) message_handler = prepare_message_handler(bot_name, restricted_client, lib_module) if not quiet: print("Running {} Bot (from {}):".format(bot_details["name"], bot_source)) if bot_details["description"] != "": print("\n\t{}".format(bot_details["description"])) if hasattr(message_handler, "usage"): print(message_handler.usage()) else: print( f"WARNING: {bot_name} is missing usage handler, please add one eventually" ) def handle_message(message: Dict[str, Any], flags: List[str]) -> None: logging.info("waiting for next message") # `mentioned` will be in `flags` if the bot is mentioned at ANY position # (not necessarily the first @mention in the message). is_mentioned = "mentioned" in flags is_private_message = is_private_message_but_not_group_pm( message, restricted_client) # Provide bots with a way to access the full, unstripped message message["full_content"] = message["content"] # 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 is_private_message or is_mentioned: message_handler.handle_message(message=message, bot_handler=restricted_client) signal.signal(signal.SIGINT, exit_gracefully) logging.info("starting message handling...") def event_callback(event: Dict[str, Any]) -> None: if event["type"] == "message": handle_message(event["message"], event["flags"]) client.call_on_each_event(event_callback, ["message"])
def test_invalid_api_key(client_with_invalid_key: Client) -> None: result = client_with_invalid_key.list_subscriptions() validate_against_openapi_schema(result, '/rest-error-handling', 'post', '400_0')
class ZulipBot: def __init__(self): self.client = Client(email = os.environ['ZULIP_USERNAME'], api_key = os.environ['ZULIP_API_KEY']) self.subscribe_streams() def subscribe_streams(self): response = get('https://api.zulip.com/v1/streams', auth=(os.environ['ZULIP_USERNAME'], os.environ['ZULIP_API_KEY'])) if response.status_code == 200: streams = [{'name': stream['name']} for stream in response.json()['streams']] self.client.add_subscriptions(streams) else: raise RuntimeError(response) ##Function to check for any messages def read_message(self, msg): content = msg['content'].split(',') sender_email = msg['sender_email'] if sender_email == os.environ['ZULIP_USERNAME']: return if content[0].upper() in ['RUNNING', 'RUNNINGBOT', '@**RUNNING**']: return_info = self.find_runs(content) if return_info is None: self.send_message("No results", msg) else: [self.send_message(run, msg) for run in return_info] else: return def find_runs(self, content): run_info = sorted(content[1:]) if len(run_info) == 2: run_info.append('min=1') elif len(run_info) == 1: run_info.extend(['max=5.5', 'min=1']) run_params = [r.split("=")[-1] for r in run_info] get_coords = GoogleRequests() lat, lon = get_coords.get_geocode(run_params[0]) new_req = MMFRouteAPI() json_data = new_req.get_routes(lat, lon, run_params[1], run_params[2]) list_runs = new_req.list_runs(json_data) if len(list_runs) < 1: return None return list_runs def send_message(self, return_content, msg): links = ["Run Name: ", return_content[0], "Distance (miles): ", return_content[1], "Link:", return_content[-1]] return_str = " ".join(links) if msg['type'] == 'stream': self.client.send_message({ 'type': 'stream', 'subject': 'RUNNINGBOT', 'to': msg['display_recipient'], 'content': return_str}) elif msg['type'] == 'private': self.client.send_message({ 'type': 'private', 'to': msg['sender_email'], 'content': return_str })
def set_typing_status(client: Client) -> None: ensure_users([10, 11], ["hamlet", "iago"]) # {code_example|start} # The user has started to type in the group PM with Iago and Polonius user_id1 = 10 user_id2 = 11 request = { "op": "start", "to": [user_id1, user_id2], } result = client.set_typing_status(request) # {code_example|end} validate_against_openapi_schema(result, "/typing", "post", "200") # {code_example|start} # The user has finished typing in the group PM with Iago and Polonius user_id1 = 10 user_id2 = 11 request = { "op": "stop", "to": [user_id1, user_id2], } result = client.set_typing_status(request) # {code_example|end} validate_against_openapi_schema(result, "/typing", "post", "200") # {code_example|start} # The user has started to type in topic "typing status" of stream "Denmark" stream_id = client.get_stream_id("Denmark")["stream_id"] topic = "typing status" request = { "type": "stream", "op": "start", "to": [stream_id], "topic": topic, } result = client.set_typing_status(request) # {code_example|end} validate_against_openapi_schema(result, "/typing", "post", "200") # {code_example|start} # The user has finished typing in topic "typing status" of stream "Denmark" stream_id = client.get_stream_id("Denmark")["stream_id"] topic = "typing status" request = { "type": "stream", "op": "stop", "to": [stream_id], "topic": topic, } result = client.set_typing_status(request) # {code_example|end} validate_against_openapi_schema(result, "/typing", "post", "200")
def update_presence(client: Client) -> None: request = {'status': 'active', 'ping_only': False, 'new_user_input': False} result = client.update_presence(request) assert result['result'] == 'success'
class ZulipBot(object): def __init__(self, name, stream): self._client = Client() self._name = name self._self_short_name = u'{name}-bot'.format(name=self._name) self._self_mention = self._format_mention(self._name) self._stream = stream self._topic_router = {} self._command_handlers = { 'hello': self._hello_command_handler, 'help': self._help_command_handler, } def run(self): self._client.call_on_each_message(self._message_handler) def send_public_message(self, content, topic, stream=None): message = { 'type': 'stream', 'to': stream or self._stream, 'content': content, 'topic': topic, } self._client.send_message(message) def send_private_message(self, content, address): message = {'type': 'private', 'to': address, 'content': content} self._client.send_message(message) def send_reply(self, content, message): """Send content as reply to message.""" if self._is_private_message(message): self.send_private_message(content, message.get('sender_email')) else: self.send_public_message(content, message.get('subject'), message.get('stream')) @staticmethod def _format_mention(name): return u'@**{name}**'.format(name=name) def _is_self_sent(self, message): return message.get('sender_short_name') == self._self_short_name def _is_self_mention(self, message): return message.get('content').find(self._self_mention) != -1 def _strip_self_mention(self, content): return content.replace(self._self_mention, '') @staticmethod def _is_private_message(message): return message.get('type') == 'private' def _message_handler(self, message): is_relevant_message = not self._is_self_sent(message) and ( self._is_self_mention(message) or self._is_private_message(message)) if is_relevant_message: topic = message.get('subject') self._topic_router.get(topic, self._default_router)(message) def _default_router(self, message): commands = [ cmd for cmd in self._strip_self_mention(message.get( 'content')).split(' ') if cmd ] self._command_handlers.get(commands[0], self._default_command_handler)(commands[1:], message) def _default_command_handler(self, subcommands, message): reply = (u'I did not understand the message:\n' u'```quote\n' u'{content}\n' u'```\n' u'For a list of recognized commands, send `help`.').format( content=message.get('content')) self.send_reply(reply, message) def _help_command_handler(self, subcommands, message): """Get help about recognized commands.""" if subcommands and subcommands[0] in self._command_handlers: command = subcommands[0] reply = u'*{command}*: {desc}'.format( command=command, desc=self._command_handlers[command].__doc__) else: reply = [(u'**Supported commands**\n' u'\n' u'Command|Description\n' u'-------|-----------')] reply.extend( u'{cmd}|{desc}'.format(cmd=k, desc=v.__doc__.split('\n')[0]) for (k, v) in iteritems(self._command_handlers)) reply.append(u'\nSend `help {command}` for more information.') reply = '\n'.join(reply) self.send_reply(reply, message) def _hello_command_handler(self, subcommands, message): """Say hello.""" sender = message.get('sender_short_name') reply = u'Hi {mention} :wave:'.format( mention=self._format_mention(sender)) self.send_reply(reply, message)
def run_message_handler_for_bot( lib_module: Any, quiet: bool, config_file: str, bot_config_file: str, bot_name: 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 inherit from a common prototype specifying the handle_message function. Set default bot_details, then override from class, if provided """ bot_details = { 'name': bot_name.capitalize(), 'description': "", } bot_details.update(getattr(lib_module.handler_class, 'META', {})) # Make sure you set up your ~/.zuliprc client_name = "Zulip{}Bot".format(bot_name.capitalize()) try: client = Client(config_file=config_file, client=client_name) except configparser.Error as e: display_config_file_errors(str(e), config_file) sys.exit(1) bot_dir = os.path.dirname(lib_module.__file__) restricted_client = ExternalBotHandler(client, bot_dir, bot_details, bot_config_file) message_handler = prepare_message_handler(bot_name, restricted_client, lib_module) if not quiet: print("Running {} Bot:".format(bot_details['name'])) if bot_details['description'] != "": print("\n\t{}".format(bot_details['description'])) print(message_handler.usage()) def handle_message(message: Dict[str, Any], flags: List[str]) -> None: logging.info('waiting for next message') # `mentioned` will be in `flags` if the bot is mentioned at ANY position # (not necessarily the first @mention in the message). is_mentioned = 'mentioned' in flags is_private_message = is_private_message_from_another_user( message, restricted_client.user_id) # Provide bots with a way to access the full, unstripped message message['full_content'] = message['content'] # 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 is_private_message or is_mentioned: message_handler.handle_message(message=message, bot_handler=restricted_client) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') def event_callback(event: Dict[str, Any]) -> None: if event['type'] == 'message': handle_message(event['message'], event['flags']) client.call_on_each_event(event_callback, ['message'])
def run_message_handler_for_bot(lib_module, quiet, config_file): # type: (Any, 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 # inherit from a common prototype specifying the handle_message # function. # # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file) restricted_client = ExternalBotHandler(client) message_handler = lib_module.handler_class() if hasattr(message_handler, 'initialize'): message_handler.initialize(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) # 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 is_private_message or is_mentioned: message_handler.handle_message( message=message, bot_handler=restricted_client, state_handler=state_handler ) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def add_alert_words(client: Client) -> None: word = ["foo", "bar"] result = client.add_alert_words(word) assert result["result"] == "success"
def test_generated_curl_examples_for_success(client: Client, owner_client: Client) -> None: authentication_line = f"{client.email}:{client.api_key}" # A limited Markdown engine that just processes the code example syntax. realm = get_realm("zulip") md_engine = markdown.Markdown(extensions=[ markdown_extension.makeExtension(api_url=realm.uri + "/api") ]) # We run our curl tests in alphabetical order (except that we # delay the deactivate-user test to the very end), since we depend # on "add" tests coming before "remove" tests in some cases. We # should try to either avoid ordering dependencies or make them # very explicit. for file_name in sorted(glob.glob("templates/zerver/api/*.md")): with open(file_name) as f: for line in f: # A typical example from the Markdown source looks like this: # {generate_code_example(curl, ...} if not line.startswith("{generate_code_example(curl"): continue # To do an end-to-end test on the documentation examples # that will be actually shown to users, we use the # Markdown rendering pipeline to compute the user-facing # example, and then run that to test it. curl_command_html = md_engine.convert(line.strip()) unescaped_html = html.unescape(curl_command_html) curl_command_text = unescaped_html[len("<p><code>curl\n" ):-len("</code></p>")] curl_command_text = curl_command_text.replace( "BOT_EMAIL_ADDRESS:BOT_API_KEY", authentication_line) # TODO: This needs_reactivation block is a hack. # However, it's awkward to test the "deactivate # myself" endpoint with how this system tries to use # the same account for all tests without some special # logic for that endpoint; and the hack is better than # just not documenting the endpoint. needs_reactivation = False user_id = 0 if file_name == "templates/zerver/api/deactivate-own-user.md": needs_reactivation = True user_id = client.get_profile()["user_id"] print("Testing {} ...".format( curl_command_text.split("\n")[0])) # Turn the text into an arguments list. generated_curl_command = [ x for x in shlex.split(curl_command_text) if x != "\n" ] response_json = None response = None try: # We split this across two lines so if curl fails and # returns non-JSON output, we'll still print it. response_json = subprocess.check_output( generated_curl_command, universal_newlines=True) response = json.loads(response_json) assert response["result"] == "success" if needs_reactivation: owner_client.reactivate_user_by_id(user_id) except (AssertionError, Exception): error_template = """ Error verifying the success of the API documentation curl example. File: {file_name} Line: {line} Curl command: {curl_command} Response: {response} This test is designed to check each generate_code_example(curl) instance in the API documentation for success. If this fails then it means that the curl example that was generated was faulty and when tried, it resulted in an unsuccessful response. Common reasons for why this could occur: 1. One or more example values in zerver/openapi/zulip.yaml for this endpoint do not line up with the values in the test database. 2. One or more mandatory parameters were included in the "exclude" list. To learn more about the test itself, see zerver/openapi/test_curl_examples.py. """ print( error_template.format( file_name=file_name, line=line, curl_command=generated_curl_command, response=response_json if response is None else json.dumps(response, indent=4), )) raise if REGISTERED_GENERATOR_FUNCTIONS != CALLED_GENERATOR_FUNCTIONS: raise Exception( "Some registered generator functions were not called:\n" " " + str(REGISTERED_GENERATOR_FUNCTIONS - CALLED_GENERATOR_FUNCTIONS))
def test_invalid_stream_error(client: Client) -> None: result = client.get_stream_id("nonexistent") validate_against_openapi_schema(result, "/get_stream_id", "get", "400")
def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name): # type: (Any, 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 # inherit from a common prototype specifying the handle_message # function. # # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file, client="Zulip{}Bot".format(bot_name.capitalize())) bot_dir = os.path.dirname(lib_module.__file__) restricted_client = ExternalBotHandler(client, bot_dir) message_handler = lib_module.handler_class() if hasattr(message_handler, 'initialize'): message_handler.initialize(bot_handler=restricted_client) state_handler = StateHandler() # Set default bot_details, then override from class, if provided bot_details = { 'name': bot_name.capitalize(), 'description': "", } bot_details.update(getattr(lib_module.handler_class, 'META', {})) if not quiet: print("Running {} Bot:".format(bot_details['name'])) if bot_details['description'] != "": print("\n\t{}".format(bot_details['description'])) print(message_handler.usage()) 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_from_another_user( message, 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 is_private_message or is_mentioned: message_handler.handle_message(message=message, bot_handler=restricted_client, state_handler=state_handler) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def get_subscribers(client: Client) -> None: result = client.get_subscribers(stream="new stream") assert result["subscribers"] == ["*****@*****.**", "*****@*****.**"]
class ZulipBackend(ErrBot): def __init__(self, config): super().__init__(config) config.MESSAGE_SIZE_LIMIT = ZULIP_MESSAGE_SIZE_LIMIT self.identity = config.BOT_IDENTITY for key in ('email', 'key', 'site'): if key not in self.identity: log.fatal( "You need to supply the key `{}` for me to use. `{key}` and its value " "can be found in your bot's `zuliprc` config file.".format( key)) sys.exit(1) compact = config.COMPACT_OUTPUT if hasattr(config, 'COMPACT_OUTPUT') else False enable_format('text', TEXT_CHRS, borders=not compact) self.client = Client(email=self.identity['email'], api_key=self.identity['key'], site=self.identity['site']) def serve_once(self): self.bot_identifier = self.build_identifier(self.client.email) log.info("Initializing connection") self.client.ensure_session() log.info("Connected") self.reset_reconnection_count() self.connect_callback() try: self.client.call_on_each_message(self._handle_message) except KeyboardInterrupt: log.info("Interrupt received, shutting down..") return True # True means shutdown was requested. except Exception: log.exception("Error reading from Zulip updates stream.") raise finally: log.debug("Triggering disconnect callback.") self.disconnect_callback() def _handle_message(self, message): """ Handles incoming messages. In Zulip, there are three types of messages: Private messages, Private group messages, and Stream messages. This plugin handles Group PMs as normal PMs between the bot and the user. Stream messages are handled as messages to rooms. """ if not message['content']: log.warning("Unhandled message type (not a text message) ignored") return message_instance = self.build_message(message['content']) if message['type'] == 'private': message_instance.frm = ZulipPerson( id=message['sender_email'], full_name=message['sender_full_name'], emails=[message['sender_email']], client=message['client']) message_instance.to = ZulipPerson( id=message['sender_email'], full_name=','.join([ recipient['full_name'] for recipient in message['display_recipient'] ]), emails=[ recipient['email'] for recipient in message['display_recipient'] ], client=None) elif message['type'] == 'stream': room = ZulipRoom(id=message['display_recipient'], title=message['display_recipient'], subject=message['subject']) message_instance.frm = ZulipRoomOccupant( id=message['sender_email'], full_name=message['sender_full_name'], emails=[message['sender_email']], client=message['client'], room=room) message_instance.to = room else: raise ValueError("Invalid message type `{}`.".format( message['type'])) self.callback_message(message_instance) def send_message(self, msg): super().send_message(msg) msg_data = { 'content': msg.body, } if isinstance(msg.to, ZulipRoom): msg_data['type'] = 'stream' msg_data['subject'] = msg.to.subject msg_data['to'] = msg.to.title elif isinstance(msg.to, ZulipPerson): if isinstance(msg.to, ZulipRoomOccupant): msg_data['type'] = 'stream' msg_data['subject'] = msg.to.room.subject msg_data['to'] = msg.to.room.title else: msg_data['type'] = 'private' msg_data['to'] = msg.to.emails else: raise ValueError("Invalid message recipient of type {}".format( type(msg.to).__name__)) try: self.client.send_message(msg_data) except Exception: log.exception( "An exception occurred while trying to send the following message " "to %s: %s" % (msg.to.id, msg.body)) raise def is_from_self(self, msg): return msg.frm.aclattr == self.client.email def change_presence(self, status: str = ONLINE, message: str = '') -> None: # At this time, Zulip doesn't support active presence change. pass def build_identifier(self, txtrep): return ZulipPerson(id=txtrep, full_name=txtrep, emails=[txtrep], client=self.client) def build_reply(self, msg, text=None, private=False, threaded=False): response = self.build_message(text) response.to = msg.to return response @property def mode(self): return 'zulip' def query_room(self, room): return ZulipRoom(title=room, client=self.client) def rooms(self): result = parse_query_result(self.client.list_subscriptions()) return [ ZulipRoom(title=subscription['name'], id=subscription['name']) for subscription in result['subscriptions'] ] def prefix_groupchat_reply(self, message, identifier): super().prefix_groupchat_reply(message, identifier) message.body = '@**{0}** {1}'.format(identifier.full_name, message.body) def _zulip_upload_stream(self, stream): """Perform upload defined in a stream.""" try: stream.accept() result = self.client.upload_file(stream.raw) if result['result'] == 'success': message_instance = self.build_message("[{}]({})".format( stream.name, result['uri'])) message_instance.to = stream.identifier self.send_message(message_instance) stream.success() else: stream.error() except Exception: log.exception("Upload of {0} to {1} failed.".format( stream.name, stream.identifier)) def send_stream_request(self, identifier, fsource, name='file', size=None, stream_type=None): """Starts a file transfer. :param identifier: ZulipPerson or ZulipRoom Identifier of the Person or Room to send the stream to. :param fsource: str, dict or binary data File URL or binary content from a local file. Optionally a dict with binary content plus metadata can be given. See `stream_type` for more details. :param name: str, optional Name of the file. Not sure if this works always. :param size: str, optional Size of the file obtained with os.path.getsize. This is only used for debug logging purposes. :param stream_type: str, optional Type of the stream. Choices: 'document', 'photo', 'audio', 'video', 'sticker', 'location'. Right now used for debug logging purposes only. :return stream: str or Stream If `fsource` is str will return str, else return Stream. """ def _metadata(fsource): if isinstance(fsource, dict): return fsource.pop('content'), fsource else: return fsource, None def _is_valid_url(url): try: from urlparse import urlparse except Exception: from urllib.parse import urlparse return bool(urlparse(url).scheme) content, meta = _metadata(fsource) if isinstance(content, str): if not _is_valid_url(content): raise ValueError("Not valid URL: {}".format(content)) else: raise NotImplementedError( "The Zulip backend does not yet support URL stream requests." ) else: stream = Stream(identifier, content, name, size, stream_type) log.debug( "Requesting upload of {0} to {1} (size hint: {2}, stream type: {3})" .format(name, identifier, size, stream_type)) self.thread_pool.apply_async(self._zulip_upload_stream, (stream, )) return stream
def get_alert_words(client: Client) -> None: result = client.get_alert_words() assert result['result'] == 'success'
def run_message_handler_for_bot(lib_module, quiet, config_file): # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file) restricted_client = BotHandlerApi(client) message_handler = lib_module.handler_class() class StateHandler(object): def __init__(self): self.state = None def set_state(self, state): self.state = state def get_state(self): return self.state state_handler = StateHandler() if not quiet: print(message_handler.usage()) def extract_message_if_mentioned(message, client): bot_mention = r'^@(\*\*{0}\*\*\s|{0}\s)(?=.*)'.format(client.full_name) start_with_mention = re.compile(bot_mention).match(message['content']) if start_with_mention: query = message['content'][len(start_with_mention.group()):] return query else: bot_response = 'Please mention me first, then type the query.' if message['type'] == 'private': client.send_message(dict( type='private', to=message['sender_email'], content=bot_response, )) else: client.send_message(dict( type='stream', to=message['display_recipient'], subject=message['subject'], content=bot_response, )) return None def is_private(message, client): # 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): logging.info('waiting for next message') is_mentioned = message['is_mentioned'] is_private_message = is_private(message, restricted_client) # Strip at-mention botname from the message if is_mentioned: message['content'] = extract_message_if_mentioned(message=message, client=restricted_client) if message['content'] is None: return if is_private_message or is_mentioned: message_handler.handle_message( message=message, client=restricted_client, state_handler=state_handler ) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def remove_alert_words(client: Client) -> None: word = ['foo'] result = client.remove_alert_words(word) assert result['result'] == 'success'
def run_message_handler_for_bot(lib_module, quiet, config_file): # type: (Any, 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 # inherit from a common prototype specifying the handle_message # function. # # Make sure you set up your ~/.zuliprc client = Client(config_file=config_file) restricted_client = BotHandlerApi(client) message_handler = lib_module.handler_class() state_handler = StateHandler() if not quiet: print(message_handler.usage()) def extract_query_without_mention(message, client): # type: (Dict[str, Any], BotHandlerApi) -> 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], BotHandlerApi) -> 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) # 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 is_private_message or is_mentioned: message_handler.handle_message(message=message, bot_handler=restricted_client, state_handler=state_handler) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') client.call_on_each_message(handle_message)
def test_missing_request_argument(client: Client) -> None: result = client.render_message({}) validate_against_openapi_schema(result, '/rest-error-handling', 'post', '400_1')
def test_invalid_stream_error(client: Client) -> None: result = client.get_stream_id('nonexistent') validate_against_openapi_schema(result, '/get_stream_id', 'get', '400')
def __init__(self): self.client = Client(email = os.environ['ZULIP_USERNAME'], api_key = os.environ['ZULIP_API_KEY']) self.subscribe_streams()
def run_message_handler_for_bot( lib_module: Any, quiet: bool, config_file: str, bot_config_file: str, bot_name: 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 inherit from a common prototype specifying the handle_message function. Set default bot_details, then override from class, if provided """ bot_details = { 'name': bot_name.capitalize(), 'description': "", } bot_details.update(getattr(lib_module.handler_class, 'META', {})) # Make sure you set up your ~/.zuliprc client_name = "Zulip{}Bot".format(bot_name.capitalize()) try: client = Client(config_file=config_file, client=client_name) except configparser.Error as e: display_config_file_errors(str(e), config_file) sys.exit(1) bot_dir = os.path.dirname(lib_module.__file__) restricted_client = ExternalBotHandler(client, bot_dir, bot_details, bot_config_file) message_handler = prepare_message_handler(bot_name, restricted_client, lib_module) if not quiet: print("Running {} Bot:".format(bot_details['name'])) if bot_details['description'] != "": print("\n\t{}".format(bot_details['description'])) print(message_handler.usage()) def handle_message(message: Dict[str, Any], flags: List[str]) -> None: logging.info('waiting for next message') # `mentioned` will be in `flags` if the bot is mentioned at ANY position # (not necessarily the first @mention in the message). is_mentioned = 'mentioned' in flags is_private_message = is_private_message_from_another_user(message, restricted_client.user_id) # Provide bots with a way to access the full, unstripped message message['full_content'] = message['content'] # 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 is_private_message or is_mentioned: message_handler.handle_message( message=message, bot_handler=restricted_client ) signal.signal(signal.SIGINT, exit_gracefully) logging.info('starting message handling...') def event_callback(event: Dict[str, Any]) -> None: if event['type'] == 'message': handle_message(event['message'], event['flags']) client.call_on_each_event(event_callback, ['message'])