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 = 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 = 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, 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() if not quiet: 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 run_message_handler_for_bot(lib_module, quiet): # Make sure you set up your ~/.zuliprc client = Client() 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 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 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)
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 message_instance.extras['url'] = ( '{site}/#narrow/stream/{stream}' '/subject/{subject}/near/{mid}').format( site=self.identity['site'], stream=quote(room.title), subject=quote(room.subject), mid=message['id'], ) 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 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 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)
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)