Add a command line option to run.py for bot config files.
Before this change, we were looking for config files in default locations in source control, which is not a good place to look for them. Now `run.py` and friends have a command line argument where users can specify the config files. Note that the change to server.py is only a partial fix to make it so that bots that don't use third party config files won't crash. That program needs an overhaul, anyway.
This commit is contained in:
		
							parent
							
								
									57c90ddca0
								
							
						
					
					
						commit
						536ba1843a
					
				
					 5 changed files with 104 additions and 25 deletions
				
			
		| 
						 | 
					@ -19,6 +19,9 @@ from types import ModuleType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zulip import Client, ZulipError
 | 
					from zulip import Client, ZulipError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NoBotConfigException(Exception):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def exit_gracefully(signum, frame):
 | 
					def exit_gracefully(signum, frame):
 | 
				
			||||||
    # type: (int, Optional[Any]) -> None
 | 
					    # type: (int, Optional[Any]) -> None
 | 
				
			||||||
    sys.exit(0)
 | 
					    sys.exit(0)
 | 
				
			||||||
| 
						 | 
					@ -83,8 +86,8 @@ class StateHandler(object):
 | 
				
			||||||
        return key in self.state_
 | 
					        return key in self.state_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExternalBotHandler(object):
 | 
					class ExternalBotHandler(object):
 | 
				
			||||||
    def __init__(self, client, root_dir, bot_details):
 | 
					    def __init__(self, client, root_dir, bot_details, bot_config_file):
 | 
				
			||||||
        # type: (Client, str, Dict[str, Any]) -> None
 | 
					        # type: (Client, str, Dict[str, Any], str) -> None
 | 
				
			||||||
        # Only expose a subset of our Client's functionality
 | 
					        # Only expose a subset of our Client's functionality
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            user_profile = client.get_profile()
 | 
					            user_profile = client.get_profile()
 | 
				
			||||||
| 
						 | 
					@ -108,6 +111,7 @@ class ExternalBotHandler(object):
 | 
				
			||||||
        self._client = client
 | 
					        self._client = client
 | 
				
			||||||
        self._root_dir = root_dir
 | 
					        self._root_dir = root_dir
 | 
				
			||||||
        self.bot_details = bot_details
 | 
					        self.bot_details = bot_details
 | 
				
			||||||
 | 
					        self.bot_config_file = bot_config_file
 | 
				
			||||||
        self._storage = StateHandler(client)
 | 
					        self._storage = StateHandler(client)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.user_id = user_profile['user_id']
 | 
					            self.user_id = user_profile['user_id']
 | 
				
			||||||
| 
						 | 
					@ -155,15 +159,45 @@ class ExternalBotHandler(object):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_config_info(self, bot_name, optional=False):
 | 
					    def get_config_info(self, bot_name, optional=False):
 | 
				
			||||||
        # type: (str, Optional[bool]) -> Dict[str, Any]
 | 
					        # type: (str, Optional[bool]) -> Dict[str, Any]
 | 
				
			||||||
        conf_file_path = os.path.realpath(os.path.join(self._root_dir, bot_name + '.conf'))
 | 
					
 | 
				
			||||||
        config = configparser.ConfigParser()
 | 
					        if self.bot_config_file is None:
 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            with open(conf_file_path) as conf:
 | 
					 | 
				
			||||||
                config.readfp(conf)  # type: ignore # readfp->read_file in python 3, so not in stubs
 | 
					 | 
				
			||||||
        except IOError:
 | 
					 | 
				
			||||||
            if optional:
 | 
					            if optional:
 | 
				
			||||||
                return dict()
 | 
					                return dict()
 | 
				
			||||||
            raise
 | 
					
 | 
				
			||||||
 | 
					            # Well written bots should catch this exception
 | 
				
			||||||
 | 
					            # and provide nice error messages with instructions
 | 
				
			||||||
 | 
					            # on setting up the configuration specfic to this bot.
 | 
				
			||||||
 | 
					            # And then `run.py` should also catch exceptions on how
 | 
				
			||||||
 | 
					            # to specify the file in the command line.
 | 
				
			||||||
 | 
					            raise NoBotConfigException(bot_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if bot_name not in self.bot_config_file:
 | 
				
			||||||
 | 
					            print('''
 | 
				
			||||||
 | 
					                WARNING!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {} does not adhere to the
 | 
				
			||||||
 | 
					                file naming convention, and it could be a
 | 
				
			||||||
 | 
					                sign that you passed in the
 | 
				
			||||||
 | 
					                wrong third-party configuration file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                The suggested name is {}.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                We will proceed anyway.
 | 
				
			||||||
 | 
					                '''.format(self.bot_config_file, bot_name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We expect the caller to pass in None if the user does
 | 
				
			||||||
 | 
					        # not specify a bot_config_file.  If they pass in a bogus
 | 
				
			||||||
 | 
					        # filename, we'll let an IOError happen here.  Callers
 | 
				
			||||||
 | 
					        # like `run.py` will do the command line parsing and checking
 | 
				
			||||||
 | 
					        # for the existence of the file.
 | 
				
			||||||
 | 
					        config = configparser.ConfigParser()
 | 
				
			||||||
 | 
					        with open(self.bot_config_file) as conf:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                config.readfp(conf)  # type: ignore # readfp->read_file in python 3, so not in stubs
 | 
				
			||||||
 | 
					            except configparser.Error as e:
 | 
				
			||||||
 | 
					                display_config_file_errors(str(e), self.bot_config_file)
 | 
				
			||||||
 | 
					                sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return dict(config.items(bot_name))
 | 
					        return dict(config.items(bot_name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def open(self, filepath):
 | 
					    def open(self, filepath):
 | 
				
			||||||
| 
						 | 
					@ -206,8 +240,8 @@ def display_config_file_errors(error_msg, config_file):
 | 
				
			||||||
    print('\nERROR: {} seems to be broken:\n\n{}'.format(config_file, file_contents))
 | 
					    print('\nERROR: {} seems to be broken:\n\n{}'.format(config_file, file_contents))
 | 
				
			||||||
    print('\nMore details here:\n\n{}\n'.format(error_msg))
 | 
					    print('\nMore details here:\n\n{}\n'.format(error_msg))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name):
 | 
					def run_message_handler_for_bot(lib_module, quiet, config_file, bot_config_file, bot_name):
 | 
				
			||||||
    # type: (Any, bool, str, str) -> Any
 | 
					    # type: (Any, bool, str, str, str) -> Any
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
    # lib_module is of type Any, since it can contain any bot's
 | 
					    # lib_module is of type Any, since it can contain any bot's
 | 
				
			||||||
    # handler class. Eventually, we want bot's handler classes to
 | 
					    # handler class. Eventually, we want bot's handler classes to
 | 
				
			||||||
| 
						 | 
					@ -231,7 +265,7 @@ def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name):
 | 
				
			||||||
        sys.exit(1)
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bot_dir = os.path.dirname(lib_module.__file__)
 | 
					    bot_dir = os.path.dirname(lib_module.__file__)
 | 
				
			||||||
    restricted_client = ExternalBotHandler(client, bot_dir, bot_details)
 | 
					    restricted_client = ExternalBotHandler(client, bot_dir, bot_details, bot_config_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    message_handler = lib_module.handler_class()
 | 
					    message_handler = lib_module.handler_class()
 | 
				
			||||||
    if hasattr(message_handler, 'initialize'):
 | 
					    if hasattr(message_handler, 'initialize'):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,11 @@ from importlib import import_module
 | 
				
			||||||
from os.path import basename, splitext
 | 
					from os.path import basename, splitext
 | 
				
			||||||
from typing import Any, Optional, Text
 | 
					from typing import Any, Optional, Text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zulip_bots.lib import run_message_handler_for_bot
 | 
					from zulip_bots.lib import (
 | 
				
			||||||
 | 
					    run_message_handler_for_bot,
 | 
				
			||||||
 | 
					    NoBotConfigException,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zulip_bots.provision import provision_bot
 | 
					from zulip_bots.provision import provision_bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
 | 
					current_dir = os.path.dirname(os.path.abspath(__file__))
 | 
				
			||||||
| 
						 | 
					@ -63,6 +67,10 @@ def parse_args():
 | 
				
			||||||
                        required=True,
 | 
					                        required=True,
 | 
				
			||||||
                        help='zulip configuration file (e.g. ~/Downloads/zuliprc)')
 | 
					                        help='zulip configuration file (e.g. ~/Downloads/zuliprc)')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.add_argument('--bot-config-file', '-b',
 | 
				
			||||||
 | 
					                        action='store',
 | 
				
			||||||
 | 
					                        help='third party configuration file (e.g. ~/giphy.conf')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    parser.add_argument('--force',
 | 
					    parser.add_argument('--force',
 | 
				
			||||||
                        action='store_true',
 | 
					                        action='store_true',
 | 
				
			||||||
                        help='try running the bot even if dependencies install fails')
 | 
					                        help='try running the bot even if dependencies install fails')
 | 
				
			||||||
| 
						 | 
					@ -75,7 +83,7 @@ def parse_args():
 | 
				
			||||||
    return args
 | 
					    return args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def exit_gracefully_if_config_file_does_not_exist(config_file):
 | 
					def exit_gracefully_if_zulip_config_file_does_not_exist(config_file):
 | 
				
			||||||
    # type: (str) -> None
 | 
					    # type: (str) -> None
 | 
				
			||||||
    if not os.path.exists(config_file):
 | 
					    if not os.path.exists(config_file):
 | 
				
			||||||
        print('''
 | 
					        print('''
 | 
				
			||||||
| 
						 | 
					@ -87,6 +95,21 @@ def exit_gracefully_if_config_file_does_not_exist(config_file):
 | 
				
			||||||
            ''' % (config_file,))
 | 
					            ''' % (config_file,))
 | 
				
			||||||
        sys.exit(1)
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def exit_gracefully_if_bot_config_file_does_not_exist(bot_config_file):
 | 
				
			||||||
 | 
					    # type: (str) -> None
 | 
				
			||||||
 | 
					    if bot_config_file is None:
 | 
				
			||||||
 | 
					        # This is a common case, just so succeed quietly. (Some
 | 
				
			||||||
 | 
					        # bots don't have third party configuration.)
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not os.path.exists(bot_config_file):
 | 
				
			||||||
 | 
					        print('''
 | 
				
			||||||
 | 
					            ERROR: %s does not exist.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            You probably just specified the wrong file location here.
 | 
				
			||||||
 | 
					            ''' % (bot_config_file,))
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					def main():
 | 
				
			||||||
    # type: () -> None
 | 
					    # type: () -> None
 | 
				
			||||||
    args = parse_args()
 | 
					    args = parse_args()
 | 
				
			||||||
| 
						 | 
					@ -103,14 +126,28 @@ def main():
 | 
				
			||||||
    if not args.quiet:
 | 
					    if not args.quiet:
 | 
				
			||||||
        logging.basicConfig(stream=sys.stdout, level=logging.INFO)
 | 
					        logging.basicConfig(stream=sys.stdout, level=logging.INFO)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    exit_gracefully_if_config_file_does_not_exist(args.config_file)
 | 
					    # It's a bit unfortunate that we have two config files, but the
 | 
				
			||||||
 | 
					    # alternative would be way worse for people running multiple bots
 | 
				
			||||||
 | 
					    # or testing against multiple Zulip servers.
 | 
				
			||||||
 | 
					    exit_gracefully_if_zulip_config_file_does_not_exist(args.config_file)
 | 
				
			||||||
 | 
					    exit_gracefully_if_bot_config_file_does_not_exist(args.bot_config_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    run_message_handler_for_bot(
 | 
					    try:
 | 
				
			||||||
        lib_module=lib_module,
 | 
					        run_message_handler_for_bot(
 | 
				
			||||||
        config_file=args.config_file,
 | 
					            lib_module=lib_module,
 | 
				
			||||||
        quiet=args.quiet,
 | 
					            config_file=args.config_file,
 | 
				
			||||||
        bot_name=bot_name
 | 
					            bot_config_file=args.bot_config_file,
 | 
				
			||||||
    )
 | 
					            quiet=args.quiet,
 | 
				
			||||||
 | 
					            bot_name=bot_name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    except NoBotConfigException:
 | 
				
			||||||
 | 
					        print('''
 | 
				
			||||||
 | 
					            ERROR: Your bot requires you to specify a third party
 | 
				
			||||||
 | 
					            config file with the --bot-config-file option.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Exiting now.
 | 
				
			||||||
 | 
					            ''')
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
    main()
 | 
					    main()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ class BotTestCaseBase(TestCase):
 | 
				
			||||||
        # Mocking ExternalBotHandler
 | 
					        # Mocking ExternalBotHandler
 | 
				
			||||||
        self.patcher = patch('zulip_bots.lib.ExternalBotHandler', autospec=True)
 | 
					        self.patcher = patch('zulip_bots.lib.ExternalBotHandler', autospec=True)
 | 
				
			||||||
        self.MockClass = self.patcher.start()
 | 
					        self.MockClass = self.patcher.start()
 | 
				
			||||||
        self.mock_bot_handler = self.MockClass(None, None, None)
 | 
					        self.mock_bot_handler = self.MockClass(None, None, None, None)
 | 
				
			||||||
        self.mock_client = MagicMock()
 | 
					        self.mock_client = MagicMock()
 | 
				
			||||||
        self.mock_client.get_storage.return_value = {'result': 'success', 'state': {}}
 | 
					        self.mock_client.get_storage.return_value = {'result': 'success', 'state': {}}
 | 
				
			||||||
        self.mock_client.update_storage.return_value = {'result': 'success'}
 | 
					        self.mock_client.update_storage.return_value = {'result': 'success'}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,11 +29,12 @@ class TestDefaultArguments(TestCase):
 | 
				
			||||||
    @patch('zulip_bots.run.run_message_handler_for_bot')
 | 
					    @patch('zulip_bots.run.run_message_handler_for_bot')
 | 
				
			||||||
    def test_argument_parsing_with_bot_name(self, mock_run_message_handler_for_bot):
 | 
					    def test_argument_parsing_with_bot_name(self, mock_run_message_handler_for_bot):
 | 
				
			||||||
        # type: (mock.Mock) -> None
 | 
					        # type: (mock.Mock) -> None
 | 
				
			||||||
        with patch('zulip_bots.run.exit_gracefully_if_config_file_does_not_exist'):
 | 
					        with patch('zulip_bots.run.exit_gracefully_if_zulip_config_file_does_not_exist'):
 | 
				
			||||||
            zulip_bots.run.main()
 | 
					            zulip_bots.run.main()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        mock_run_message_handler_for_bot.assert_called_with(bot_name='giphy',
 | 
					        mock_run_message_handler_for_bot.assert_called_with(bot_name='giphy',
 | 
				
			||||||
                                                            config_file='/foo/bar/baz.conf',
 | 
					                                                            config_file='/foo/bar/baz.conf',
 | 
				
			||||||
 | 
					                                                            bot_config_file=None,
 | 
				
			||||||
                                                            lib_module=mock.ANY,
 | 
					                                                            lib_module=mock.ANY,
 | 
				
			||||||
                                                            quiet=False)
 | 
					                                                            quiet=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,12 +42,13 @@ class TestDefaultArguments(TestCase):
 | 
				
			||||||
    @patch('zulip_bots.run.run_message_handler_for_bot')
 | 
					    @patch('zulip_bots.run.run_message_handler_for_bot')
 | 
				
			||||||
    def test_argument_parsing_with_bot_path(self, mock_run_message_handler_for_bot):
 | 
					    def test_argument_parsing_with_bot_path(self, mock_run_message_handler_for_bot):
 | 
				
			||||||
        # type: (mock.Mock) -> None
 | 
					        # type: (mock.Mock) -> None
 | 
				
			||||||
        with patch('zulip_bots.run.exit_gracefully_if_config_file_does_not_exist'):
 | 
					        with patch('zulip_bots.run.exit_gracefully_if_zulip_config_file_does_not_exist'):
 | 
				
			||||||
            zulip_bots.run.main()
 | 
					            zulip_bots.run.main()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        mock_run_message_handler_for_bot.assert_called_with(
 | 
					        mock_run_message_handler_for_bot.assert_called_with(
 | 
				
			||||||
            bot_name='giphy',
 | 
					            bot_name='giphy',
 | 
				
			||||||
            config_file='/foo/bar/baz.conf',
 | 
					            config_file='/foo/bar/baz.conf',
 | 
				
			||||||
 | 
					            bot_config_file=None,
 | 
				
			||||||
            lib_module=mock.ANY,
 | 
					            lib_module=mock.ANY,
 | 
				
			||||||
            quiet=False)
 | 
					            quiet=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,13 @@ def load_bot_handlers():
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            bot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
 | 
					            bot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
 | 
				
			||||||
                                   'bots', bot)
 | 
					                                   'bots', bot)
 | 
				
			||||||
            bot_handlers[bot] = ExternalBotHandler(client, bot_dir, bot_details={})
 | 
					            # TODO: Figure out how to pass in third party config info.
 | 
				
			||||||
 | 
					            bot_handlers[bot] = ExternalBotHandler(
 | 
				
			||||||
 | 
					                client,
 | 
				
			||||||
 | 
					                bot_dir,
 | 
				
			||||||
 | 
					                bot_details={},
 | 
				
			||||||
 | 
					                bot_config_file=None
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        except SystemExit:
 | 
					        except SystemExit:
 | 
				
			||||||
            return BadRequest("Cannot fetch user profile for bot {}, make sure you have set up the flaskbotrc "
 | 
					            return BadRequest("Cannot fetch user profile for bot {}, make sure you have set up the flaskbotrc "
 | 
				
			||||||
                              "file correctly.".format(bot))
 | 
					                              "file correctly.".format(bot))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue