예제 #1
0
 def test_generate_config_if_none_exist(self):
     cdf_backup = Loader.create_default_config
     Loader.create_default_config = mock.Mock(
         return_value=os.path.abspath("tests/configs/minimal.yaml"))
     Loader.load_config_file(["file_which_does_not_exist"])
     self.assertTrue(Loader.create_default_config.called)
     Loader.create_default_config = cdf_backup
예제 #2
0
 def test_load_non_existant_config_file(self):
     cdf_backup = Loader.create_default_config
     Loader.create_default_config = mock.Mock(
         return_value=os.path.abspath("/tmp/my_nonexistant_config"))
     with mock.patch('sys.exit') as mock_sysexit:
         Loader.load_config_file(["file_which_does_not_exist"])
         self.assertTrue(Loader.create_default_config.called)
         self.assertTrue(mock_sysexit.called)
     Loader.create_default_config = cdf_backup
예제 #3
0
 def test_yaml_load_exploit(self):
     with mock.patch('sys.exit'):
         config = Loader.load_config_file(
             [os.path.abspath("tests/configs/include_exploit.yaml")])
         self.assertIsNone(config)
         # If the command in exploit.yaml is echoed it will return 0
         self.assertNotEqual(config, 0)
예제 #4
0
파일: core.py 프로젝트: opsdroid/opsdroid
 def __init__(self, config=None):
     """Start opsdroid."""
     self.bot_name = 'opsdroid'
     self._running = False
     self.sys_status = 0
     self.connectors = []
     self.connector_tasks = []
     self.eventloop = asyncio.get_event_loop()
     if os.name != 'nt':
         for sig in (signal.SIGINT, signal.SIGTERM):
             self.eventloop.add_signal_handler(
                 sig,
                 lambda: asyncio.ensure_future(self.handle_signal()))
     self.eventloop.set_exception_handler(self.handle_async_exception)
     self.skills = []
     self.memory = Memory()
     self.modules = {}
     self.cron_task = None
     self.loader = Loader(self)
     if config is None:
         self.config = {}
     else:
         self.config = config
     self.stats = {
         "messages_parsed": 0,
         "webhooks_called": 0,
         "total_response_time": 0,
         "total_responses": 0,
     }
     self.web_server = None
     self.stored_path = []
예제 #5
0
파일: core.py 프로젝트: opsdroid/opsdroid
 async def reload(self):
     """Reload opsdroid."""
     await self.unload()
     self.config = Loader.load_config_file([
         "configuration.yaml",
         DEFAULT_CONFIG_PATH,
         "/etc/opsdroid/configuration.yaml"
     ])
     self.load()
예제 #6
0
 def test_load_minimal_config_file_2(self):
     opsdroid, loader = self.setup()
     loader._install_module = mock.MagicMock()
     loader.import_module = mock.MagicMock()
     config = Loader.load_config_file(
         [os.path.abspath("tests/configs/minimal_2.yaml")])
     modules = loader.load_modules_from_config(config)
     self.assertIsNotNone(modules["connectors"])
     self.assertIsNone(modules["databases"])
     self.assertIsNotNone(modules["skills"])
     self.assertIsNotNone(config)
예제 #7
0
def main():
    """Opsdroid is a chat bot framework written in Python.

    It is designed to be extendable, scalable and simple.
    See https://opsdroid.github.io/ for more information.
    """
    check_dependencies()

    config = Loader.load_config_file([
        "configuration.yaml",
        DEFAULT_CONFIG_PATH,
        "/etc/opsdroid/configuration.yaml"
        ])
    configure_lang(config)
    configure_logging(config)
    welcome_message(config)

    with OpsDroid(config=config) as opsdroid:
        opsdroid.load()
        opsdroid.run()
예제 #8
0
 def test_load_broken_config_file(self):
     with mock.patch("sys.exit") as patched_sysexit:
         Loader.load_config_file(
             [os.path.abspath("tests/configs/broken.yaml")])
         self.assertTrue(patched_sysexit.called)
예제 #9
0
파일: core.py 프로젝트: RonitRay/opsdroid
class OpsDroid():
    """Root object for opsdroid."""

    # pylint: disable=too-many-instance-attributes
    # All are reasonable in this case.

    instances = []

    def __init__(self):
        """Start opsdroid."""
        self.bot_name = 'opsdroid'
        self.sys_status = 0
        self.connectors = []
        self.connector_tasks = []
        self.eventloop = asyncio.get_event_loop()
        if os.name != 'nt':
            for sig in (signal.SIGINT, signal.SIGTERM):
                self.eventloop.add_signal_handler(sig, self.call_stop)
        self.skills = []
        self.memory = Memory()
        self.loader = Loader(self)
        self.config = {}
        self.stats = {
            "messages_parsed": 0,
            "webhooks_called": 0,
            "total_response_time": 0,
            "total_responses": 0,
        }
        self.web_server = None
        self.stored_path = []

    def __enter__(self):
        """Add self to existing instances."""
        self.stored_path = copy.copy(sys.path)
        if not self.__class__.instances:
            self.__class__.instances.append(weakref.proxy(self))
        else:
            self.critical("opsdroid has already been started", 1)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Remove self from existing instances."""
        sys.path = self.stored_path
        self.__class__.instances = []
        asyncio.set_event_loop(asyncio.new_event_loop())

    @property
    def default_connector(self):
        """Return the default connector."""
        default_connector = None
        for connector in self.connectors:
            if "default" in connector.config and connector.config["default"]:
                default_connector = connector
                break
        if default_connector is None:
            default_connector = self.connectors[0]
        return default_connector

    def exit(self):
        """Exit application."""
        _LOGGER.info(_("Exiting application with return code %s"),
                     str(self.sys_status))
        sys.exit(self.sys_status)

    def critical(self, error, code):
        """Exit due to unrecoverable error."""
        self.sys_status = code
        _LOGGER.critical(error)
        self.exit()

    def call_stop(self):
        """Signal handler to call disconnect and stop."""
        future = asyncio.ensure_future(self.disconnect())
        future.add_done_callback(self.stop)
        return future

    async def disconnect(self):
        """Disconnect all the connectors."""
        for connector in self.connectors:
            await connector.disconnect(self)

    def stop(self, future=None):
        """Stop the event loop."""
        pending = asyncio.Task.all_tasks()
        for task in pending:
            task.cancel()
        self.eventloop.stop()
        print('')  # Prints a character return for return to shell
        _LOGGER.info(_("Keyboard interrupt, exiting."))

    def load(self):
        """Load configuration."""
        self.config = self.loader.load_config_file([
            "configuration.yaml", DEFAULT_CONFIG_PATH,
            "/etc/opsdroid/configuration.yaml"
        ])

    def start_loop(self):
        """Start the event loop."""
        connectors, databases, skills = \
            self.loader.load_modules_from_config(self.config)
        _LOGGER.debug(_("Loaded %i skills"), len(skills))
        if databases is not None:
            self.start_databases(databases)
        self.setup_skills(skills)
        self.train_parsers(skills)
        self.start_connector_tasks(connectors)
        self.eventloop.create_task(parse_crontab(self))
        self.web_server.start()
        try:
            pending = asyncio.Task.all_tasks()
            self.eventloop.run_until_complete(asyncio.gather(*pending))
        except RuntimeError as error:
            if str(error) != 'Event loop is closed':
                raise error
        finally:
            self.eventloop.close()

    def setup_skills(self, skills):
        """Call the setup function on the passed in skills."""
        with contextlib.suppress(AttributeError):
            for skill in skills:
                skill["module"].setup(self, self.config)

    def train_parsers(self, skills):
        """Train the parsers."""
        if "parsers" in self.config:
            parsers = self.config["parsers"] or []
            tasks = []
            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and \
                    ("enabled" not in rasanlu[0] or
                     rasanlu[0]["enabled"] is not False):
                tasks.append(
                    asyncio.ensure_future(train_rasanlu(rasanlu[0], skills),
                                          loop=self.eventloop))
            self.eventloop.run_until_complete(
                asyncio.gather(*tasks, loop=self.eventloop))

    def start_connector_tasks(self, connectors):
        """Start the connectors."""
        for connector_module in connectors:
            for _, cls in connector_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Connector) and\
                   cls is not Connector:
                    connector = cls(connector_module["config"])
                    self.connectors.append(connector)

        if connectors:
            for connector in self.connectors:
                self.eventloop.run_until_complete(connector.connect(self))
            for connector in self.connectors:
                task = self.eventloop.create_task(connector.listen(self))
                self.connector_tasks.append(task)
        else:
            self.critical("All connectors failed to load", 1)

    def start_databases(self, databases):
        """Start the databases."""
        if not databases:
            _LOGGER.debug(databases)
            _LOGGER.warning(_("All databases failed to load"))
        for database_module in databases:
            for name, cls in database_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Database) and \
                   cls is not Database:
                    _LOGGER.debug(_("Adding database: %s"), name)
                    database = cls(database_module["config"])
                    self.memory.databases.append(database)
                    self.eventloop.run_until_complete(database.connect(self))

    async def run_skill(self, skill, config, message):
        """Execute a skill."""
        # pylint: disable=broad-except
        # We want to catch all exceptions coming from a skill module and not
        # halt the application. If a skill throws an exception it just doesn't
        # give a response to the user, so an error response should be given.
        try:
            await skill(self, config, message)
        except Exception:
            if message:
                await message.respond(_("Whoops there has been an error"))
                await message.respond(_("Check the log for details"))
            _LOGGER.exception(_("Exception when running skill '%s' "),
                              str(config["name"]))

    async def get_ranked_skills(self, message):
        """Take a message and return a ranked list of matching skills."""
        skills = []
        skills = skills + await parse_regex(self, message)

        if "parsers" in self.config:
            _LOGGER.debug(_("Processing parsers..."))
            parsers = self.config["parsers"] or []

            dialogflow = [
                p for p in parsers
                if p["name"] == "dialogflow" or p["name"] == "apiai"
            ]

            # Show deprecation message but  parse message
            # Once it stops working remove this bit
            apiai = [p for p in parsers if p["name"] == "apiai"]
            if apiai:
                _LOGGER.warning(
                    _("Api.ai is now called Dialogflow. This "
                      "parser will stop working in the future "
                      "please swap: 'name: apiai' for "
                      "'name: dialogflow' in configuration.yaml"))

            if len(dialogflow) == 1 and \
                    ("enabled" not in dialogflow[0] or
                     dialogflow[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking dialogflow..."))
                skills = skills + \
                    await parse_dialogflow(self, message, dialogflow[0])

            luisai = [p for p in parsers if p["name"] == "luisai"]
            if len(luisai) == 1 and \
                    ("enabled" not in luisai[0] or
                     luisai[0]["enabled"] is not False):
                _LOGGER.debug("Checking luisai...")
                skills = skills + \
                    await parse_luisai(self, message, luisai[0])

            recastai = [p for p in parsers if p["name"] == "recastai"]
            if len(recastai) == 1 and \
                    ("enabled" not in recastai[0] or
                     recastai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Recast.AI..."))
                skills = skills + \
                    await parse_recastai(self, message, recastai[0])

            witai = [p for p in parsers if p["name"] == "witai"]
            if len(witai) == 1 and \
                    ("enabled" not in witai[0] or
                     witai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking wit.ai..."))
                skills = skills + \
                    await parse_witai(self, message, witai[0])

            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and \
                    ("enabled" not in rasanlu[0] or
                     rasanlu[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Rasa NLU..."))
                skills = skills + \
                    await parse_rasanlu(self, message, rasanlu[0])

        return sorted(skills, key=lambda k: k["score"], reverse=True)

    async def parse(self, message):
        """Parse a string against all skills."""
        self.stats["messages_parsed"] = self.stats["messages_parsed"] + 1
        tasks = []
        if message.text.strip() != "":
            _LOGGER.debug(_("Parsing input: %s"), message.text)

            tasks.append(
                self.eventloop.create_task(parse_always(self, message)))

            ranked_skills = await self.get_ranked_skills(message)
            if ranked_skills:
                tasks.append(
                    self.eventloop.create_task(
                        self.run_skill(ranked_skills[0]["skill"],
                                       ranked_skills[0]["config"], message)))

        return tasks
예제 #10
0
class OpsDroid:
    """Root object for opsdroid."""

    # pylint: disable=too-many-instance-attributes
    # All are reasonable in this case.

    instances = []

    def __init__(self, config=None):
        """Start opsdroid."""
        self.bot_name = "opsdroid"
        self._running = False
        self.sys_status = 0
        self.connectors = []
        self.connector_tasks = []
        self.eventloop = asyncio.get_event_loop()
        if os.name != "nt":
            for sig in (signal.SIGINT, signal.SIGTERM):
                self.eventloop.add_signal_handler(
                    sig, lambda: asyncio.ensure_future(self.handle_signal()))
        self.eventloop.set_exception_handler(self.handle_async_exception)
        self.skills = []
        self.memory = Memory()
        self.modules = {}
        self.cron_task = None
        self.loader = Loader(self)
        if config is None:
            self.config = {}
        else:
            self.config = config
        self.stats = {
            "messages_parsed": 0,
            "webhooks_called": 0,
            "total_response_time": 0,
            "total_responses": 0,
        }
        self.web_server = None
        self.stored_path = []

    def __enter__(self):
        """Add self to existing instances."""
        self.stored_path = copy.copy(sys.path)
        if not self.__class__.instances:
            self.__class__.instances.append(weakref.proxy(self))
        else:
            self.critical("opsdroid has already been started", 1)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Remove self from existing instances."""
        sys.path = self.stored_path
        self.__class__.instances = []
        asyncio.set_event_loop(asyncio.new_event_loop())

    @property
    def default_connector(self):
        """Return the default connector."""
        default_connector = None
        for connector in self.connectors:
            if "default" in connector.config and connector.config["default"]:
                default_connector = connector
                break
        if default_connector is None:
            default_connector = self.connectors[0]
        return default_connector

    def exit(self):
        """Exit application."""
        _LOGGER.info(_("Exiting application with return code %s"),
                     str(self.sys_status))
        sys.exit(self.sys_status)

    def critical(self, error, code):
        """Exit due to unrecoverable error."""
        self.sys_status = code
        _LOGGER.critical(error)
        self.exit()

    @staticmethod
    def handle_async_exception(loop, context):
        """Handle exceptions from async coroutines."""
        if "future" in context:
            try:  # pragma: nocover
                context["future"].result()
            # pylint: disable=broad-except
            except Exception:  # pragma: nocover
                _LOGGER.exception(_("Caught exception"))
        else:
            _LOGGER.error(_("Caught exception"))
        _LOGGER.error(context)

    def is_running(self):
        """Check whether opsdroid is running."""
        return self._running

    async def handle_signal(self):
        """Handle signals."""
        self._running = False
        await self.unload()

    def run(self):
        """Start the event loop."""
        _LOGGER.info(_("Opsdroid is now running, press ctrl+c to exit."))
        if not self.is_running():
            self._running = True
            while self.is_running():
                pending = asyncio.Task.all_tasks()
                with contextlib.suppress(asyncio.CancelledError):
                    self.eventloop.run_until_complete(asyncio.gather(*pending))

            self.eventloop.stop()
            self.eventloop.close()

            _LOGGER.info(_("Bye!"))
            self.exit()
        else:
            _LOGGER.error(_("Oops! Opsdroid is already running."))

    def load(self):
        """Load modules."""
        self.modules = self.loader.load_modules_from_config(self.config)
        _LOGGER.debug(_("Loaded %i skills"), len(self.modules["skills"]))
        self.setup_skills(self.modules["skills"])
        self.web_server = Web(self)
        self.web_server.setup_webhooks(self.skills)
        self.train_parsers(self.modules["skills"])
        if self.modules["databases"] is not None:
            self.start_databases(self.modules["databases"])
        self.start_connectors(self.modules["connectors"])
        self.cron_task = self.eventloop.create_task(parse_crontab(self))
        self.eventloop.create_task(self.web_server.start())

    async def unload(self, future=None):
        """Stop the event loop."""
        _LOGGER.info(_("Received stop signal, exiting."))

        _LOGGER.info(_("Removing skills..."))
        for skill in self.skills:
            _LOGGER.info(_("Removed %s"), skill.config["name"])
            self.skills.remove(skill)

        for connector in self.connectors:
            _LOGGER.info(_("Stopping connector %s..."), connector.name)
            await connector.disconnect()
            self.connectors.remove(connector)
            _LOGGER.info(_("Stopped connector %s"), connector.name)

        for database in self.memory.databases:
            _LOGGER.info(_("Stopping database %s..."), database.name)
            await database.disconnect()
            self.memory.databases.remove(database)
            _LOGGER.info(_("Stopped database %s"), database.name)

        _LOGGER.info(_("Stopping web server..."))
        await self.web_server.stop()
        self.web_server = None
        _LOGGER.info(_("Stopped web server"))

        _LOGGER.info(_("Stopping cron..."))
        self.cron_task.cancel()
        self.cron_task = None
        _LOGGER.info(_("Stopped cron"))

        _LOGGER.info(_("Stopping pending tasks..."))
        tasks = asyncio.Task.all_tasks()
        for task in list(tasks):
            if not task.done() and task is not asyncio.Task.current_task():
                task.cancel()
        _LOGGER.info(_("Stopped pending tasks"))

    async def reload(self):
        """Reload opsdroid."""
        await self.unload()
        self.config = Loader.load_config_file([
            "configuration.yaml",
            DEFAULT_CONFIG_PATH,
            "/etc/opsdroid/configuration.yaml",
        ])
        self.load()

    def setup_skills(self, skills):
        """Call the setup function on the loaded skills.

        Iterates through all the skills which have been loaded and runs
        any setup functions which have been defined in the skill.

        Args:
            skills (list): A list of all the loaded skills.

        """
        for skill in skills:
            for func in skill["module"].__dict__.values():
                if isinstance(func, type) and issubclass(
                        func, Skill) and func != Skill:
                    skill_obj = func(self, skill["config"])

                    for name in skill_obj.__dir__():
                        # pylint: disable=broad-except
                        # Getting an attribute of
                        # an object might raise any type of exceptions, for
                        # example within an external library called from an
                        # object property.  Since we are only interested in
                        # skill methods, we can safely ignore these.
                        try:
                            method = getattr(skill_obj, name)
                        except Exception:
                            continue

                        if hasattr(method, "skill"):
                            self.skills.append(method)

                    continue

                if hasattr(func, "skill"):
                    _LOGGER.warning(
                        _("Function based skills are deprecated "
                          "and will be removed in a future "
                          "release. Please use class-based skills "
                          "instead."))
                    func.config = skill["config"]
                    self.skills.append(func)

        with contextlib.suppress(AttributeError):
            for skill in skills:
                skill["module"].setup(self, self.config)
                _LOGGER.warning(
                    _("<skill module>.setup() is deprecated and "
                      "will be removed in a future release. "
                      "Please use class-based skills instead."))

    def train_parsers(self, skills):
        """Train the parsers."""
        if "parsers" in self.config:
            parsers = self.config["parsers"] or []
            tasks = []
            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and ("enabled" not in rasanlu[0]
                                      or rasanlu[0]["enabled"] is not False):
                tasks.append(
                    asyncio.ensure_future(train_rasanlu(rasanlu[0], skills),
                                          loop=self.eventloop))
            self.eventloop.run_until_complete(
                asyncio.gather(*tasks, loop=self.eventloop))

    def start_connectors(self, connectors):
        """Start the connectors."""
        for connector_module in connectors:
            for _, cls in connector_module["module"].__dict__.items():
                if (isinstance(cls, type) and issubclass(cls, Connector)
                        and cls is not Connector):
                    connector = cls(connector_module["config"], self)
                    self.connectors.append(connector)

        if connectors:
            for connector in self.connectors:
                if self.eventloop.is_running():
                    self.eventloop.create_task(connector.connect())
                else:
                    self.eventloop.run_until_complete(connector.connect())
            for connector in self.connectors:
                task = self.eventloop.create_task(connector.listen())
                self.connector_tasks.append(task)
        else:
            self.critical("All connectors failed to load", 1)

    # pylint: disable=W0640
    @property
    def _connector_names(self):  # noqa: D401
        """Mapping of names to connector instances."""
        if not self.connectors:
            raise ValueError("No connectors have been started")

        names = {}
        for connector in self.connectors:
            name = connector.config.get("name", connector.name)
            # Deduplicate any names
            if name in names:
                # Calculate the number of keys in names which start with name.
                n_key = len(list(filter(lambda x: x.startswith(name), names)))
                name += "_{}".format(n_key)
            names[name] = connector

        return names

    def start_databases(self, databases):
        """Start the databases."""
        if not databases:
            _LOGGER.debug(databases)
            _LOGGER.warning(_("All databases failed to load"))
        for database_module in databases:
            for name, cls in database_module["module"].__dict__.items():
                if (isinstance(cls, type) and issubclass(cls, Database)
                        and cls is not Database):
                    _LOGGER.debug(_("Adding database: %s"), name)
                    database = cls(database_module["config"])
                    self.memory.databases.append(database)
                    self.eventloop.run_until_complete(database.connect())

    async def run_skill(self, skill, config, message):
        """Execute a skill."""
        # pylint: disable=broad-except
        # We want to catch all exceptions coming from a skill module and not
        # halt the application. If a skill throws an exception it just doesn't
        # give a response to the user, so an error response should be given.
        try:
            if len(inspect.signature(skill).parameters.keys()) > 1:
                await skill(self, config, message)
            else:
                await skill(message)
        except Exception:
            if message:
                await message.respond(
                    events.Message(_("Whoops there has been an error")))
                await message.respond(
                    events.Message(_("Check the log for details")))

            _LOGGER.exception(_("Exception when running skill '%s' "),
                              str(config["name"]))

    async def get_ranked_skills(self, skills, message):
        """Take a message and return a ranked list of matching skills."""
        ranked_skills = []
        if isinstance(message, events.Message):
            ranked_skills += await parse_regex(self, skills, message)
            ranked_skills += await parse_format(self, skills, message)
        ranked_skills += await parse_event_type(self, message)

        if "parsers" in self.config:
            _LOGGER.debug(_("Processing parsers..."))
            parsers = self.config["parsers"] or []

            dialogflow = [
                p for p in parsers
                if p["name"] == "dialogflow" or p["name"] == "apiai"
            ]

            # Show deprecation message but  parse message
            # Once it stops working remove this bit
            apiai = [p for p in parsers if p["name"] == "apiai"]
            if apiai:
                _LOGGER.warning(
                    _("Api.ai is now called Dialogflow. This "
                      "parser will stop working in the future "
                      "please swap: 'name: apiai' for "
                      "'name: dialogflow' in configuration.yaml"))

            if len(dialogflow) == 1 and ("enabled" not in dialogflow[0]
                                         or dialogflow[0]["enabled"]
                                         is not False):
                _LOGGER.debug(_("Checking dialogflow..."))
                ranked_skills += await parse_dialogflow(
                    self, skills, message, dialogflow[0])

            luisai = [p for p in parsers if p["name"] == "luisai"]
            if len(luisai) == 1 and ("enabled" not in luisai[0]
                                     or luisai[0]["enabled"] is not False):
                _LOGGER.debug("Checking luisai...")
                ranked_skills += await parse_luisai(self, skills, message,
                                                    luisai[0])

            sapcai = [p for p in parsers if p["name"] == "sapcai"]
            if len(sapcai) == 1 and ("enabled" not in sapcai[0]
                                     or sapcai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Recast.AI..."))
                ranked_skills += await parse_sapcai(self, skills, message,
                                                    sapcai[0])

            witai = [p for p in parsers if p["name"] == "witai"]
            if len(witai) == 1 and ("enabled" not in witai[0]
                                    or witai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking wit.ai..."))
                ranked_skills += await parse_witai(self, skills, message,
                                                   witai[0])

            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and ("enabled" not in rasanlu[0]
                                      or rasanlu[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Rasa NLU..."))
                ranked_skills += await parse_rasanlu(self, skills, message,
                                                     rasanlu[0])

        return sorted(ranked_skills, key=lambda k: k["score"], reverse=True)

    async def _constrain_skills(self, skills, message):
        """Remove skills with contraints which prohibit them from running.

        Args:
            skills (list): A list of skills to be checked for constraints.
            message (opsdroid.events.Message): The message currently being
                parsed.

        Returns:
            list: A list of the skills which were not constrained.

        """
        return [
            skill for skill in skills if all(
                constraint(message) for constraint in skill.constraints)
        ]

    async def parse(self, event):
        """Parse a string against all skills."""
        self.stats["messages_parsed"] = self.stats["messages_parsed"] + 1
        tasks = []
        if isinstance(event, events.Event):
            _LOGGER.debug(_("Parsing input: %s"), event)

            tasks.append(self.eventloop.create_task(parse_always(self, event)))

            unconstrained_skills = await self._constrain_skills(
                self.skills, event)
            ranked_skills = await self.get_ranked_skills(
                unconstrained_skills, event)
            if ranked_skills:
                tasks.append(
                    self.eventloop.create_task(
                        self.run_skill(
                            ranked_skills[0]["skill"],
                            ranked_skills[0]["config"],
                            ranked_skills[0]["message"],
                        )))

        return tasks

    async def send(self, event):
        """Send an event.

        If ``event.connector`` is not set this method will use
        `OpsDroid.default_connector`. If ``event.connector`` is a string, it
        will be resolved to the name of the connectors configured in this
        instance.

        Args:
            event (opsdroid.events.Event): The event to send.

        """
        if isinstance(event.connector, str):
            event.connector = self._connector_names[event.connector]

        if not event.connector:
            event.connector = self.default_connector

        return await event.connector.send(event)
예제 #11
0
class OpsDroid:
    """Root object for opsdroid."""

    # pylint: disable=too-many-instance-attributes
    # All are reasonable in this case.

    instances = []

    def __init__(self, config=None, config_path=None):
        """Start opsdroid."""
        self.bot_name = "opsdroid"
        self._running = False
        self.sys_status = 0
        self.connectors = []
        self.eventloop = asyncio.get_event_loop()
        if os.name != "nt":
            for sig in (signal.SIGINT, signal.SIGTERM):
                self.eventloop.add_signal_handler(
                    sig,
                    lambda: asyncio.ensure_future(self.handle_stop_signal()))
            self.eventloop.add_signal_handler(
                signal.SIGHUP, lambda: asyncio.ensure_future(self.reload()))
        self.eventloop.set_exception_handler(self.handle_async_exception)
        self.skills = []
        self.memory = Memory()
        self.modules = {}
        self.loader = Loader(self)
        self.config_path = config_path if config_path else DEFAULT_CONFIG_LOCATIONS
        if config is None:
            self.config = {}
        else:
            self.config = config
        self.stats = {
            "messages_parsed": 0,
            "webhooks_called": 0,
            "total_response_time": 0,
            "total_responses": 0,
        }
        self.web_server = None
        self.stored_path = []
        self.reload_paths = []
        self.tasks = []

    def __enter__(self):
        """Add self to existing instances."""
        self.stored_path = copy.copy(sys.path)
        if not self.__class__.instances:
            self.__class__.instances.append(weakref.proxy(self))
        else:
            self.critical("opsdroid has already been started", 1)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Remove self from existing instances."""
        sys.path = self.stored_path
        self.__class__.instances = []
        asyncio.set_event_loop(asyncio.new_event_loop())

    @property
    def default_connector(self):
        """Return the default connector.

        Returns:
            default_connector (connector object): A connector that was configured as default.

        """
        default_connector = None
        for connector in self.connectors:
            if "default" in connector.config and connector.config["default"]:
                default_connector = connector
                break
        if default_connector is None:
            default_connector = self.connectors[0]
        return default_connector

    def exit(self):
        """Exit application."""
        _LOGGER.info(_("Exiting application with return code %s."),
                     str(self.sys_status))
        sys.exit(self.sys_status)

    def critical(self, error, code):
        """Exit due to unrecoverable error.

        Args:
            error (String): Describes the error encountered.
            code (Integer): Error code to exit with.

        """
        self.sys_status = code
        _LOGGER.critical(error)
        self.exit()

    @staticmethod
    def handle_async_exception(loop, context):
        """Handle exceptions from async coroutines.

        Args:
            loop (asyncio.loop): Running loop that raised the exception.
            context (String): Describes the exception encountered.

        """
        print("ERROR: Unhandled exception in opsdroid, exiting...")
        if "future" in context:
            try:  # pragma: nocover
                context["future"].result()
            # pylint: disable=broad-except
            except Exception:  # pragma: nocover
                print("Caught exception")
                print(context)

    def is_running(self):
        """Check whether opsdroid is running."""
        return self._running

    async def handle_stop_signal(self):
        """Handle signals."""
        self._running = False
        await self.stop()
        await self.unload()

    def run(self):
        """Start the event loop."""
        self.sync_load()
        if not self.is_running():
            _LOGGER.info(_("Opsdroid is now running, press ctrl+c to exit."))
            self._running = True
            while self.is_running():
                with contextlib.suppress(asyncio.CancelledError):
                    self.eventloop.run_until_complete(self.start())

            self.eventloop.stop()
            self.eventloop.close()

            _LOGGER.info(_("Bye!"))
            self.exit()
        else:
            _LOGGER.error(_("Oops! Opsdroid is already running."))

    async def start(self):
        """Create tasks and then run all created tasks concurrently."""
        if len(self.skills) == 0:
            self.critical(_("No skills in configuration, at least 1 required"),
                          1)

        await self.start_connectors()
        self.create_task(self.start_databases())
        self.create_task(self.watch_paths())
        self.create_task(parse_crontab(self))
        self.create_task(self.web_server.start())

        self.create_task(self.parse(events.OpsdroidStarted()))

        await self._run_tasks()

    async def _run_tasks(self):
        """
        Run all created tasks concurrently.

        This is separate from start() so that tests can run the loop without
        creating any of the tasks.
        """
        self._running = True
        with contextlib.suppress(asyncio.CancelledError):
            await asyncio.gather(*self.tasks)
        self._running = False

    def create_task(self, task):
        """Create an async task and add it to the list of tasks."""
        self.tasks.append(self.eventloop.create_task(task))

    def sync_load(self):
        """Run the load modules method synchronously."""
        self.eventloop.run_until_complete(self.load())

    async def load(self, config=None):
        """Load modules."""
        if config is not None:
            self.config = config
        self.modules = self.loader.load_modules_from_config(self.config)
        _LOGGER.debug(_("Loaded %i skills."), len(self.modules["skills"]
                                                  or []))
        self.web_server = Web(self)
        self.setup_skills(self.modules["skills"])
        await self.setup_databases(self.modules["databases"])
        await self.setup_connectors(self.modules["connectors"])
        self.web_server.setup_webhooks(self.skills)
        await self.train_parsers(self.modules["skills"])

    async def stop(self):
        """Stop all tasks running in opsdroid."""
        _LOGGER.info(_("Received stop signal, exiting."))

        for connector in self.connectors:
            _LOGGER.info(_("Stopping connector %s..."), connector.name)
            await connector.disconnect()
            _LOGGER.info(_("Stopped connector %s."), connector.name)

        for database in self.memory.databases[:]:
            _LOGGER.info(_("Stopping database %s..."), database.name)
            await database.disconnect()
            _LOGGER.info(_("Stopped database %s."), database.name)

        _LOGGER.info(_("Stopping web server..."))
        await self.web_server.stop()
        _LOGGER.info(_("Stopped web server."))

        _LOGGER.info(_("Stopping pending tasks..."))
        for task in self.tasks:
            if not task.done() and task is not asyncio.current_task():
                task.cancel()
        _LOGGER.info(_("Stopped pending tasks."))

    async def unload(self, future=None):
        """Stop the event loop."""
        self.skills = []
        self.connectors = []
        self.memory.databases = []
        self.web_server = None
        self.modules = {}

    async def reload(self):
        """Reload opsdroid."""
        await self.stop()
        await self.unload()
        self.config = load_config_file(self.config_path)
        await self.load()
        await self.start()

    def setup_skills(self, skills):
        """Call the setup function on the loaded skills.

        Iterates through all the skills which have been loaded and runs
        any setup functions which have been defined in the skill.

        Args:
            skills (list): A list of all the loaded skills.

        """
        if not skills:
            return

        for skill in skills:
            for func in skill["module"].__dict__.values():
                if isinstance(func, type) and issubclass(
                        func, Skill) and func != Skill:
                    skill_obj = func(self, skill["config"])

                    for name in skill_obj.__dir__():
                        # pylint: disable=broad-except
                        # Getting an attribute of
                        # an object might raise any type of exceptions, for
                        # example within an external library called from an
                        # object property.  Since we are only interested in
                        # skill methods, we can safely ignore these.
                        try:
                            method = getattr(skill_obj, name)
                        except Exception:
                            continue

                        if hasattr(method, "skill"):
                            self.register_skill(method)

                    continue

                if hasattr(func, "skill"):
                    self.register_skill(func, skill["config"])

        with contextlib.suppress(AttributeError):
            for skill in skills:
                skill["module"].setup(self, self.config)

    def register_skill(self, skill, config=None):
        """Register a skill callable."""
        if config is not None:
            skill.config = config
        self.skills.append(skill)

    async def watch_paths(self):
        """Watch locally installed skill paths for file changes and reload on change.

        If a file within a locally installed skill is modified then opsdroid should be
        reloaded to pick up this change.

        When skills are loaded all local skills have their paths registered in
        ``self.reload_paths`` so we will watch those paths for changes.

        """
        async def watch_and_reload(opsdroid, path):
            async for _ in awatch(path, watcher_cls=PythonWatcher):
                await opsdroid.reload()

        if self.config.get("autoreload", False):
            _LOGGER.warning(
                _("Watching module files for changes. "
                  "Warning autoreload is an experimental feature."))
            await asyncio.gather(
                *[watch_and_reload(self, path) for path in self.reload_paths])

    async def train_parsers(self, skills):
        """Train the parsers.

        Args:
            skills (list): A list of all the loaded skills.

        """
        if "parsers" in self.modules:
            parsers = self.modules.get("parsers", {})
            rasanlu = get_parser_config("rasanlu", parsers)
            if rasanlu and rasanlu["enabled"]:
                await train_rasanlu(rasanlu, skills)

    async def setup_connectors(self, connectors):
        """Extract connectors from modules and register them in opsdroid.

        Args:
            connectors (list): A list of all the loaded connector modules.

        """
        for connector_module in connectors:
            for _, cls in connector_module["module"].__dict__.items():
                if (isinstance(cls, type) and issubclass(cls, Connector)
                        and cls is not Connector):
                    connector = cls(connector_module["config"], self)
                    self.connectors.append(connector)

        if not self.connectors:
            self.critical("All connectors failed to load.", 1)

    async def start_connectors(self):
        """Start the connectors.

        Iterates through all the connectors parsed in the argument,
        spawns all that can be loaded, and keeps them open (listening).

        """
        await asyncio.gather(
            *[connector.connect() for connector in self.connectors])
        for connector in self.connectors:
            self.create_task(connector.listen())

    # pylint: disable=W0640
    @property
    def _connector_names(self):  # noqa: D401
        """Mapping of names to connector instances.

        Returns:
            names (list): A list of the names of connectors that are running.

        """
        if not self.connectors:
            raise ValueError("No connectors have been started")

        names = {}
        for connector in self.connectors:
            name = connector.config.get("name", connector.name)
            # Deduplicate any names
            if name in names:
                # Calculate the number of keys in names which start with name.
                n_key = len(list(filter(lambda x: x.startswith(name), names)))
                name += "_{}".format(n_key)
            names[name] = connector

        return names

    async def setup_databases(self, databases):
        """Extract database from modules and register them in opsdroid.

        Args:
            databases (list): A list of all the loaded database modules.

        """
        if not databases:
            self.memory.databases = [InMemoryDatabase()]
            return

        for database_module in databases:
            for name, cls in database_module["module"].__dict__.items():
                if (isinstance(cls, type) and issubclass(cls, Database)
                        and cls is not Database):
                    _LOGGER.debug(_("Adding database: %s."), name)
                    database = cls(database_module["config"], opsdroid=self)
                    self.memory.databases.append(database)

    async def start_databases(self):
        """Start the databases.

        Iterates through all the database modules parsed
        in the argument, connects and starts them.

        """
        await asyncio.gather(
            *[database.connect() for database in self.memory.databases])

    async def run_skill(self, skill, config, event):
        """Execute a skill.

        Attempts to run the skill parsed and provides other arguments to the skill if necessary.
        Also handles the exception encountered if th e

        Args:
            skill: name of the skill to be run.
            config: The configuration the skill must be loaded in.
            event: Message/event to be parsed to the chat service.

        """
        # pylint: disable=broad-except
        # We want to catch all exceptions coming from a skill module and not
        # halt the application. If a skill throws an exception it just doesn't
        # give a response to the user, so an error response should be given.
        try:
            if len(inspect.signature(skill).parameters.keys()) > 1:
                return await skill(self, config, event)
            else:
                return await skill(event)
        except Exception:
            _LOGGER.exception(_("Exception when running skill '%s'."),
                              str(config["name"]))
            if event:
                await event.respond(
                    events.Message(_("Whoops there has been an error.")))
                await event.respond(
                    events.Message(_("Check the log for details.")))

    async def get_ranked_skills(self, skills, message):
        """Take a message and return a ranked list of matching skills.

        Args:
            skills (list): List of all available skills.
            message (string): Context message to base the ranking of skills on.

        Returns:
            ranked_skills (list): List of all available skills sorted and ranked based on the score they muster when matched against the message parsed.

        """
        ranked_skills = []
        if isinstance(message, events.Message):
            ranked_skills += await parse_regex(self, skills, message)
            ranked_skills += await parse_format(self, skills, message)

        if "parsers" in self.modules:
            _LOGGER.debug(_("Processing parsers..."))
            parsers = self.modules.get("parsers", {})

            dialogflow = get_parser_config("dialogflow", parsers)
            if dialogflow and dialogflow["enabled"]:
                _LOGGER.debug(_("Checking dialogflow..."))
                ranked_skills += await parse_dialogflow(
                    self, skills, message, dialogflow)

            luisai = get_parser_config("luisai", parsers)
            if luisai and luisai["enabled"]:
                _LOGGER.debug(_("Checking luisai..."))
                ranked_skills += await parse_luisai(self, skills, message,
                                                    luisai)

            sapcai = get_parser_config("sapcai", parsers)
            if sapcai and sapcai["enabled"]:
                _LOGGER.debug(_("Checking SAPCAI..."))
                ranked_skills += await parse_sapcai(self, skills, message,
                                                    sapcai)

            witai = get_parser_config("witai", parsers)
            if witai and witai["enabled"]:
                _LOGGER.debug(_("Checking wit.ai..."))
                ranked_skills += await parse_witai(self, skills, message,
                                                   witai)

            watson = get_parser_config("watson", parsers)
            if watson and watson["enabled"]:
                _LOGGER.debug(_("Checking IBM Watson..."))
                ranked_skills += await parse_watson(self, skills, message,
                                                    watson)

            rasanlu = get_parser_config("rasanlu", parsers)
            if rasanlu and rasanlu["enabled"]:
                _LOGGER.debug(_("Checking Rasa NLU..."))
                ranked_skills += await parse_rasanlu(self, skills, message,
                                                     rasanlu)

        return sorted(ranked_skills, key=lambda k: k["score"], reverse=True)

    def get_connector(self, name):
        """Get a connector object.

        Get a specific connector by name from the list of active connectors.

        Args:
            name (string): Name of the connector we want to access.

        Returns:
            connector (opsdroid.connector.Connector): An opsdroid connector.

        """
        try:
            [connector] = [
                connector for connector in self.connectors
                if connector.name == name
            ]
            return connector
        except ValueError:
            return None

    def get_database(self, name):
        """Get a database object.

        Get a specific database by name from the list of active databases.

        Args:
            name (string): Name of the database we want to access.

        Returns:
            database (opsdroid.database.Database): An opsdroid database.

        """
        try:
            [database] = [
                database for database in self.memory.databases
                if database.name == name
            ]
            return database
        except ValueError:
            return None

    def get_skill_instance(self, skill):
        """Get the parent instance of a skill.

        Skills in opsdroid are functions or bound methods of a class instance. For class based skills
        sometimes you may want to get the instance of the class the method is bound to. This helper
        will retrieve the instance for a method skill.

        Args:
            skill: The skill we want to get the instance for.

        Returns:
            The instance of the class or ``None`` if the skill is a function skill

        """
        while hasattr(skill, "__wrapped__"):
            skill = skill.__wrapped__
        if hasattr(skill, "__self__"):
            return skill.__self__
        else:
            return None

    async def _constrain_skills(self, skills, message):
        """Remove skills with contraints which prohibit them from running.

        Args:
            skills (list): A list of skills to be checked for constraints.
            message (opsdroid.events.Message): The message currently being
                parsed.

        Returns:
            list: A list of the skills which were not constrained.

        """
        return [
            skill for skill in skills if all(
                constraint(message) for constraint in skill.constraints)
        ]

    async def parse(self, event):
        """Parse a string against all skills.

        Args:
            event (String): The string to parsed against all available skills.

        Returns:
            tasks (list): Task that tells the skill which best matches the parsed event.

        """
        self.stats["messages_parsed"] = self.stats["messages_parsed"] + 1
        tasks = []
        tasks.append(self.eventloop.create_task(parse_always(self, event)))
        tasks.append(self.eventloop.create_task(parse_event_type(self, event)))
        if isinstance(event, events.Message):
            _LOGGER.debug(_("Parsing input: %s."), event)

            unconstrained_skills = await self._constrain_skills(
                self.skills, event)
            ranked_skills = await self.get_ranked_skills(
                unconstrained_skills, event)
            if ranked_skills:
                tasks.append(
                    self.eventloop.create_task(
                        self.run_skill(
                            ranked_skills[0]["skill"],
                            ranked_skills[0]["config"],
                            ranked_skills[0]["message"],
                        )))
        if len(tasks) == 2:  # no other skills ran other than 2 default ones
            tasks.append(
                self.eventloop.create_task(parse_catchall(self, event)))
        await asyncio.gather(*tasks)

        return tasks

    async def send(self, event):
        """Send an event.

        If ``event.connector`` is not set this method will use
        `OpsDroid.default_connector`. If ``event.connector`` is a string, it
        will be resolved to the name of the connectors configured in this
        instance.

        Args:
            event (opsdroid.events.Event): The event to send.

        """
        if isinstance(event.connector, str):
            event.connector = self._connector_names[event.connector]

        if not event.connector:
            event.connector = self.default_connector

        return await event.connector.send(event)
예제 #12
0
class OpsDroid():
    """Root object for opsdroid."""

    # pylint: disable=too-many-instance-attributes
    # All are reasonable in this case.

    instances = []

    def __init__(self):
        """Start opsdroid."""
        self.bot_name = 'opsdroid'
        self.sys_status = 0
        self.connectors = []
        self.connector_tasks = []
        self.eventloop = asyncio.get_event_loop()
        for sig in (signal.SIGINT, signal.SIGTERM):
            self.eventloop.add_signal_handler(sig, self.stop)
        self.skills = []
        self.memory = Memory()
        self.loader = Loader(self)
        self.config = {}
        self.stats = {
            "messages_parsed": 0,
            "webhooks_called": 0,
            "total_response_time": 0,
            "total_responses": 0,
        }
        self.web_server = None
        self.should_restart = False
        self.stored_path = []
        _LOGGER.info("Created main opsdroid object")

    def __enter__(self):
        """Add self to existing instances."""
        self.stored_path = copy.copy(sys.path)
        if not self.__class__.instances:
            self.__class__.instances.append(weakref.proxy(self))
        else:
            self.critical("opsdroid has already been started", 1)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Remove self from existing instances."""
        sys.path = self.stored_path
        self.__class__.instances = []
        asyncio.set_event_loop(asyncio.new_event_loop())

    @property
    def default_connector(self):
        """Return the default connector."""
        default_connector = None
        for connector in self.connectors:
            if "default" in connector.config and connector.config["default"]:
                default_connector = connector
                break
        if default_connector is None:
            default_connector = self.connectors[0]
        return default_connector

    def exit(self):
        """Exit application."""
        _LOGGER.info("Exiting application with return code " +
                     str(self.sys_status))
        sys.exit(self.sys_status)

    def critical(self, error, code):
        """Exit due to unrecoverable error."""
        self.sys_status = code
        _LOGGER.critical(error)
        print("Error: " + error)
        self.exit()

    def restart(self):
        """Restart opsdroid."""
        self.should_restart = True
        self.stop()

    def stop(self):
        """Stop the event loop."""
        pending = asyncio.Task.all_tasks()
        for task in pending:
            task.cancel()
        self.eventloop.stop()
        print('')  # Prints a character return for return to shell
        _LOGGER.info("Keyboard interrupt, exiting.")

    def load(self):
        """Load configuration."""
        self.config = self.loader.load_config_file([
            "configuration.yaml", DEFAULT_CONFIG_PATH,
            "/etc/opsdroid/configuration.yaml"
        ])

    def start_loop(self):
        """Start the event loop."""
        connectors, databases, skills = \
            self.loader.load_modules_from_config(self.config)
        _LOGGER.debug("Loaded %i skills", len(skills))
        if databases is not None:
            self.start_databases(databases)
        self.setup_skills(skills)
        self.start_connector_tasks(connectors)
        self.eventloop.create_task(parse_crontab(self))
        self.web_server.start()
        try:
            pending = asyncio.Task.all_tasks()
            self.eventloop.run_until_complete(asyncio.gather(*pending))
        except RuntimeError as error:
            if str(error) != 'Event loop is closed':
                raise error
        finally:
            self.eventloop.close()

    def setup_skills(self, skills):
        """Call the setup function on the passed in skills."""
        for skill in skills:
            try:
                skill["module"].setup(self)
            except AttributeError:
                pass

    def start_connector_tasks(self, connectors):
        """Start the connectors."""
        for connector_module in connectors:
            for _, cls in connector_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Connector) and\
                   cls is not Connector:
                    connector = cls(connector_module["config"])
                    self.connectors.append(connector)

        if connectors:
            for connector in self.connectors:
                self.eventloop.run_until_complete(connector.connect(self))
            for connector in self.connectors:
                task = self.eventloop.create_task(connector.listen(self))
                self.connector_tasks.append(task)
        else:
            self.critical("All connectors failed to load", 1)

    def start_databases(self, databases):
        """Start the databases."""
        if not databases:
            _LOGGER.debug(databases)
            _LOGGER.warning("All databases failed to load")
        for database_module in databases:
            for name, cls in database_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Database) and \
                   cls is not Database:
                    _LOGGER.debug("Adding database: " + name)
                    database = cls(database_module["config"])
                    self.memory.databases.append(database)
                    self.eventloop.run_until_complete(database.connect(self))

    async def parse(self, message):
        """Parse a string against all skills."""
        self.stats["messages_parsed"] = self.stats["messages_parsed"] + 1
        tasks = []
        if message.text.strip() != "":
            _LOGGER.debug("Parsing input: " + message.text)

            tasks.append(self.eventloop.create_task(parse_regex(self,
                                                                message)))

            if "parsers" in self.config:
                _LOGGER.debug("Processing parsers")
                parsers = self.config["parsers"]

                apiai = [p for p in parsers if p["name"] == "apiai"]
                _LOGGER.debug("Checking apiai")
                if len(apiai) == 1 and \
                        ("enabled" not in apiai[0] or
                         apiai[0]["enabled"] is not False):
                    _LOGGER.debug("Parsing with apiai")
                    tasks.append(
                        self.eventloop.create_task(
                            parse_apiai(self, message, apiai[0])))

                luisai = [p for p in parsers if p["name"] == "luisai"]
                _LOGGER.debug("Checking luisai")
                if len(luisai) == 1 and \
                        ("enabled" not in luisai[0] or
                         luisai[0]["enabled"] is not False):
                    _LOGGER.debug("Parsing with luisai")
                    tasks.append(
                        self.eventloop.create_task(
                            parse_luisai(self, message, luisai[0])))
        return tasks
예제 #13
0
 def test_load_broken_config_file(self):
     with mock.patch('sys.exit') as patched_sysexit:
         Loader.load_config_file(
             [os.path.abspath("tests/configs/broken.yaml")])
         self.assertTrue(patched_sysexit.called)
예제 #14
0
파일: core.py 프로젝트: opsdroid/opsdroid
class OpsDroid():
    """Root object for opsdroid."""

    # pylint: disable=too-many-instance-attributes
    # All are reasonable in this case.

    instances = []

    def __init__(self, config=None):
        """Start opsdroid."""
        self.bot_name = 'opsdroid'
        self._running = False
        self.sys_status = 0
        self.connectors = []
        self.connector_tasks = []
        self.eventloop = asyncio.get_event_loop()
        if os.name != 'nt':
            for sig in (signal.SIGINT, signal.SIGTERM):
                self.eventloop.add_signal_handler(
                    sig,
                    lambda: asyncio.ensure_future(self.handle_signal()))
        self.eventloop.set_exception_handler(self.handle_async_exception)
        self.skills = []
        self.memory = Memory()
        self.modules = {}
        self.cron_task = None
        self.loader = Loader(self)
        if config is None:
            self.config = {}
        else:
            self.config = config
        self.stats = {
            "messages_parsed": 0,
            "webhooks_called": 0,
            "total_response_time": 0,
            "total_responses": 0,
        }
        self.web_server = None
        self.stored_path = []

    def __enter__(self):
        """Add self to existing instances."""
        self.stored_path = copy.copy(sys.path)
        if not self.__class__.instances:
            self.__class__.instances.append(weakref.proxy(self))
        else:
            self.critical("opsdroid has already been started", 1)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Remove self from existing instances."""
        sys.path = self.stored_path
        self.__class__.instances = []
        asyncio.set_event_loop(asyncio.new_event_loop())

    @property
    def default_connector(self):
        """Return the default connector."""
        default_connector = None
        for connector in self.connectors:
            if "default" in connector.config and connector.config["default"]:
                default_connector = connector
                break
        if default_connector is None:
            default_connector = self.connectors[0]
        return default_connector

    def exit(self):
        """Exit application."""
        _LOGGER.info(_("Exiting application with return code %s"),
                     str(self.sys_status))
        sys.exit(self.sys_status)

    def critical(self, error, code):
        """Exit due to unrecoverable error."""
        self.sys_status = code
        _LOGGER.critical(error)
        self.exit()

    @staticmethod
    def handle_async_exception(loop, context):
        """Handle exceptions from async coroutines."""
        _LOGGER.error(_("Caught exception"))
        _LOGGER.error(context)

    def is_running(self):
        """Check whether opsdroid is running."""
        return self._running

    async def handle_signal(self):
        """Handle signals."""
        self._running = False
        await self.unload()

    def run(self):
        """Start the event loop."""
        _LOGGER.info(_("Opsdroid is now running, press ctrl+c to exit."))
        if not self.is_running():
            self._running = True
            while self.is_running():
                pending = asyncio.Task.all_tasks()
                with contextlib.suppress(asyncio.CancelledError):
                    self.eventloop.run_until_complete(asyncio.gather(*pending))

            self.eventloop.stop()
            self.eventloop.close()

            _LOGGER.info(_("Bye!"))
            self.exit()
        else:
            _LOGGER.error(_("Oops! Opsdroid is already running."))

    def load(self):
        """Load modules."""
        self.modules = self.loader.load_modules_from_config(self.config)
        _LOGGER.debug(_("Loaded %i skills"), len(self.modules["skills"]))
        self.setup_skills(self.modules["skills"])
        self.web_server = Web(self)
        self.web_server.setup_webhooks(self.skills)
        self.train_parsers(self.modules["skills"])
        if self.modules["databases"] is not None:
            self.start_databases(self.modules["databases"])
        self.start_connectors(self.modules["connectors"])
        self.cron_task = self.eventloop.create_task(parse_crontab(self))
        self.eventloop.create_task(self.web_server.start())

    async def unload(self, future=None):
        """Stop the event loop."""
        _LOGGER.info(_("Received stop signal, exiting."))

        _LOGGER.info(_("Removing skills..."))
        for skill in self.skills:
            _LOGGER.info(_("Removed %s"), skill.config['name'])
            self.skills.remove(skill)

        for connector in self.connectors:
            _LOGGER.info(_("Stopping connector %s..."), connector.name)
            await connector.disconnect()
            self.connectors.remove(connector)
            _LOGGER.info(_("Stopped connector %s"), connector.name)

        for database in self.memory.databases:
            _LOGGER.info(_("Stopping database %s..."), database.name)
            await database.disconnect()
            self.memory.databases.remove(database)
            _LOGGER.info(_("Stopped database %s"), database.name)

        _LOGGER.info(_("Stopping web server..."))
        await self.web_server.stop()
        self.web_server = None
        _LOGGER.info(_("Stopped web server"))

        _LOGGER.info(_("Stopping cron..."))
        self.cron_task.cancel()
        self.cron_task = None
        _LOGGER.info(_("Stopped cron"))

        _LOGGER.info(_("Stopping pending tasks..."))
        tasks = asyncio.Task.all_tasks()
        for task in list(tasks):
            if not task.done() and task is not asyncio.Task.current_task():
                task.cancel()
        _LOGGER.info(_("Stopped pending tasks"))

    async def reload(self):
        """Reload opsdroid."""
        await self.unload()
        self.config = Loader.load_config_file([
            "configuration.yaml",
            DEFAULT_CONFIG_PATH,
            "/etc/opsdroid/configuration.yaml"
        ])
        self.load()

    def setup_skills(self, skills):
        """Call the setup function on the loaded skills.

        Iterates through all the skills which have been loaded and runs
        any setup functions which have been defined in the skill.

        Args:
            skills (list): A list of all the loaded skills.

        """
        for skill in skills:
            for func in skill["module"].__dict__.values():
                if (isinstance(func, type) and
                        issubclass(func, Skill) and
                        func != Skill):
                    skill_obj = func(self, skill['config'])

                    for name in skill_obj.__dir__():
                        # pylint: disable=broad-except
                        # Getting an attribute of
                        # an object might raise any type of exceptions, for
                        # example within an external library called from an
                        # object property.  Since we are only interested in
                        # skill methods, we can safely ignore these.
                        try:
                            method = getattr(skill_obj, name)
                        except Exception:
                            continue

                        if hasattr(method, 'skill'):
                            self.skills.append(method)

                    continue

                if hasattr(func, "skill"):
                    _LOGGER.warning(_("Function based skills are deprecated "
                                      "and will be removed in a future "
                                      "release. Please use class-based skills "
                                      "instead."))
                    func.config = skill['config']
                    self.skills.append(func)

        with contextlib.suppress(AttributeError):
            for skill in skills:
                skill["module"].setup(self, self.config)
                _LOGGER.warning(_("<skill module>.setup() is deprecated and "
                                  "will be removed in a future release. "
                                  "Please use class-based skills instead."))

    def train_parsers(self, skills):
        """Train the parsers."""
        if "parsers" in self.config:
            parsers = self.config["parsers"] or []
            tasks = []
            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and \
                    ("enabled" not in rasanlu[0] or
                     rasanlu[0]["enabled"] is not False):
                tasks.append(
                    asyncio.ensure_future(
                        train_rasanlu(rasanlu[0], skills),
                        loop=self.eventloop))
            self.eventloop.run_until_complete(
                asyncio.gather(*tasks, loop=self.eventloop))

    def start_connectors(self, connectors):
        """Start the connectors."""
        for connector_module in connectors:
            for _, cls in connector_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Connector) and\
                   cls is not Connector:
                    connector = cls(connector_module["config"], self)
                    self.connectors.append(connector)

        if connectors:
            for connector in self.connectors:
                if self.eventloop.is_running():
                    self.eventloop.create_task(connector.connect())
                else:
                    self.eventloop.run_until_complete(connector.connect())
            for connector in self.connectors:
                task = self.eventloop.create_task(connector.listen())
                self.connector_tasks.append(task)
        else:
            self.critical("All connectors failed to load", 1)

    # pylint: disable=W0640
    @property
    def _connector_names(self):  # noqa: D401
        """Mapping of names to connector instances."""
        if not self.connectors:
            raise ValueError("No connectors have been started")

        names = {}
        for connector in self.connectors:
            name = connector.config.get("name", connector.name)
            # Deduplicate any names
            if name in names:
                # Calculate the number of keys in names which start with name.
                n_key = len(list(filter(lambda x: x.startswith(name), names)))
                name += "_{}".format(n_key)
            names[name] = connector

        return names

    def start_databases(self, databases):
        """Start the databases."""
        if not databases:
            _LOGGER.debug(databases)
            _LOGGER.warning(_("All databases failed to load"))
        for database_module in databases:
            for name, cls in database_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Database) and \
                   cls is not Database:
                    _LOGGER.debug(_("Adding database: %s"), name)
                    database = cls(database_module["config"])
                    self.memory.databases.append(database)
                    self.eventloop.run_until_complete(database.connect())

    async def run_skill(self, skill, config, message):
        """Execute a skill."""
        # pylint: disable=broad-except
        # We want to catch all exceptions coming from a skill module and not
        # halt the application. If a skill throws an exception it just doesn't
        # give a response to the user, so an error response should be given.
        try:
            if len(inspect.signature(skill).parameters.keys()) > 1:
                await skill(self, config, message)
            else:
                await skill(message)
        except Exception:
            if message:
                await message.respond(_("Whoops there has been an error"))
                await message.respond(_("Check the log for details"))
            _LOGGER.exception(_("Exception when running skill '%s' "),
                              str(config["name"]))

    async def get_ranked_skills(self, skills, message):
        """Take a message and return a ranked list of matching skills."""
        ranked_skills = []
        ranked_skills += await parse_regex(self, skills, message)

        if "parsers" in self.config:
            _LOGGER.debug(_("Processing parsers..."))
            parsers = self.config["parsers"] or []

            dialogflow = [p for p in parsers if p["name"] == "dialogflow"
                          or p["name"] == "apiai"]

            # Show deprecation message but  parse message
            # Once it stops working remove this bit
            apiai = [p for p in parsers if p["name"] == "apiai"]
            if apiai:
                _LOGGER.warning(_("Api.ai is now called Dialogflow. This "
                                  "parser will stop working in the future "
                                  "please swap: 'name: apiai' for "
                                  "'name: dialogflow' in configuration.yaml"))

            if len(dialogflow) == 1 and \
                    ("enabled" not in dialogflow[0] or
                     dialogflow[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking dialogflow..."))
                ranked_skills += \
                    await parse_dialogflow(self, skills,
                                           message, dialogflow[0])

            luisai = [p for p in parsers if p["name"] == "luisai"]
            if len(luisai) == 1 and \
                    ("enabled" not in luisai[0] or
                     luisai[0]["enabled"] is not False):
                _LOGGER.debug("Checking luisai...")
                ranked_skills += \
                    await parse_luisai(self, skills,
                                       message, luisai[0])

            sapcai = [p for p in parsers if p["name"] == "sapcai"]
            if len(sapcai) == 1 and \
                    ("enabled" not in sapcai[0] or
                     sapcai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Recast.AI..."))
                ranked_skills += \
                    await parse_sapcai(self, skills,
                                       message, sapcai[0])

            witai = [p for p in parsers if p["name"] == "witai"]
            if len(witai) == 1 and \
                    ("enabled" not in witai[0] or
                     witai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking wit.ai..."))
                ranked_skills += \
                    await parse_witai(self, skills,
                                      message, witai[0])

            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and \
                    ("enabled" not in rasanlu[0] or
                     rasanlu[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Rasa NLU..."))
                ranked_skills += \
                    await parse_rasanlu(self, skills,
                                        message, rasanlu[0])

        return sorted(ranked_skills, key=lambda k: k["score"], reverse=True)

    async def _constrain_skills(self, skills, message):
        """Remove skills with contraints which prohibit them from running.

        Args:
            skills (list): A list of skills to be checked for constraints.
            message (opsdroid.events.Message): The message currently being
                parsed.

        Returns:
            list: A list of the skills which were not constrained.

        """
        return [
            skill for skill in skills if all(
                constraint(message)
                for constraint in skill.constraints
            )
        ]

    async def parse(self, message):
        """Parse a string against all skills."""
        self.stats["messages_parsed"] = self.stats["messages_parsed"] + 1
        tasks = []
        if message is not None:
            if str(message.text).strip():
                _LOGGER.debug(_("Parsing input: %s"), message.text)

                tasks.append(
                    self.eventloop.create_task(parse_always(self, message)))

                unconstrained_skills = await self._constrain_skills(
                    self.skills, message)
                ranked_skills = await self.get_ranked_skills(
                    unconstrained_skills, message)
                if ranked_skills:
                    tasks.append(
                        self.eventloop.create_task(
                            self.run_skill(ranked_skills[0]["skill"],
                                           ranked_skills[0]["config"],
                                           ranked_skills[0]["message"])))

        return tasks

    async def send(self, event):
        """Send an event.

        If ``event.connector`` is not set this method will use
        `OpsDroid.default_connector`. If ``event.connector`` is a string, it
        will be resolved to the name of the connectors configured in this
        instance.

        Args:
            event (opsdroid.events.Event): The event to send.

        """
        if isinstance(event.connector, str):
            event.connector = self._connector_names[event.connector]

        if not event.connector:
            event.connector = self.default_connector

        return await event.connector.send(event)
예제 #15
0
파일: core.py 프로젝트: drmorr0/opsdroid
class OpsDroid():
    """Root object for opsdroid."""

    # pylint: disable=too-many-instance-attributes
    # All are reasonable in this case.

    instances = []

    def __init__(self, config=None):
        """Start opsdroid."""
        self.bot_name = 'opsdroid'
        self._running = False
        self.sys_status = 0
        self.connectors = []
        self.connector_tasks = []
        self.eventloop = asyncio.get_event_loop()
        if os.name != 'nt':
            for sig in (signal.SIGINT, signal.SIGTERM):
                self.eventloop.add_signal_handler(
                    sig, lambda: asyncio.ensure_future(self.handle_signal()))
        self.eventloop.set_exception_handler(self.handle_async_exception)
        self.skills = []
        self.memory = Memory()
        self.modules = {}
        self.cron_task = None
        self.loader = Loader(self)
        if config is None:
            self.config = {}
        else:
            self.config = config
        self.stats = {
            "messages_parsed": 0,
            "webhooks_called": 0,
            "total_response_time": 0,
            "total_responses": 0,
        }
        self.web_server = None
        self.stored_path = []

    def __enter__(self):
        """Add self to existing instances."""
        self.stored_path = copy.copy(sys.path)
        if not self.__class__.instances:
            self.__class__.instances.append(weakref.proxy(self))
        else:
            self.critical("opsdroid has already been started", 1)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Remove self from existing instances."""
        sys.path = self.stored_path
        self.__class__.instances = []
        asyncio.set_event_loop(asyncio.new_event_loop())

    @property
    def default_connector(self):
        """Return the default connector."""
        default_connector = None
        for connector in self.connectors:
            if "default" in connector.config and connector.config["default"]:
                default_connector = connector
                break
        if default_connector is None:
            default_connector = self.connectors[0]
        return default_connector

    def exit(self):
        """Exit application."""
        _LOGGER.info(_("Exiting application with return code %s"),
                     str(self.sys_status))
        sys.exit(self.sys_status)

    def critical(self, error, code):
        """Exit due to unrecoverable error."""
        self.sys_status = code
        _LOGGER.critical(error)
        self.exit()

    @staticmethod
    def handle_async_exception(loop, context):
        """Handle exceptions from async coroutines."""
        _LOGGER.error(_("Caught exception"))
        _LOGGER.error(context)

    def is_running(self):
        """Check whether opsdroid is running."""
        return self._running

    async def handle_signal(self):
        """Handle signals."""
        self._running = False
        await self.unload()

    def run(self):
        """Start the event loop."""
        _LOGGER.info(_("Opsdroid is now running, press ctrl+c to exit."))
        if not self.is_running():
            self._running = True
            while self.is_running():
                pending = asyncio.Task.all_tasks()
                with contextlib.suppress(asyncio.CancelledError):
                    self.eventloop.run_until_complete(asyncio.gather(*pending))

            self.eventloop.stop()
            self.eventloop.close()

            _LOGGER.info(_("Bye!"))
            self.exit()
        else:
            _LOGGER.error(_("Oops! Opsdroid is already running."))

    def load(self):
        """Load modules."""
        self.modules = self.loader.load_modules_from_config(self.config)
        _LOGGER.debug(_("Loaded %i skills"), len(self.modules["skills"]))
        self.setup_skills(self.modules["skills"])
        self.web_server = Web(self)
        self.web_server.setup_webhooks(self.skills)
        self.train_parsers(self.modules["skills"])
        if self.modules["databases"] is not None:
            self.start_databases(self.modules["databases"])
        self.start_connectors(self.modules["connectors"])
        self.cron_task = self.eventloop.create_task(parse_crontab(self))
        self.eventloop.create_task(self.web_server.start())

    async def unload(self, future=None):
        """Stop the event loop."""
        _LOGGER.info(_("Received stop signal, exiting."))

        _LOGGER.info(_("Removing skills..."))
        for skill in self.skills:
            _LOGGER.info(_("Removed %s"), skill.config['name'])
            self.skills.remove(skill)

        for connector in self.connectors:
            _LOGGER.info(_("Stopping connector %s..."), connector.name)
            await connector.disconnect(self)
            self.connectors.remove(connector)
            _LOGGER.info(_("Stopped connector %s"), connector.name)

        for database in self.memory.databases:
            _LOGGER.info(_("Stopping database %s..."), database.name)
            await database.disconnect(self)
            self.memory.databases.remove(database)
            _LOGGER.info(_("Stopped database %s"), database.name)

        _LOGGER.info(_("Stopping web server..."))
        await self.web_server.stop()
        self.web_server = None
        _LOGGER.info(_("Stopped web server"))

        _LOGGER.info(_("Stopping cron..."))
        self.cron_task.cancel()
        self.cron_task = None
        _LOGGER.info(_("Stopped cron"))

    async def reload(self):
        """Reload opsdroid."""
        await self.unload()
        self.config = Loader.load_config_file([
            "configuration.yaml", DEFAULT_CONFIG_PATH,
            "/etc/opsdroid/configuration.yaml"
        ])
        self.load()

    def setup_skills(self, skills):
        """Call the setup function on the loaded skills.

        Iterates through all the skills which have been loaded and runs
        any setup functions which have been defined in the skill.

        Args:
            skills (list): A list of all the loaded skills.

        """
        for skill in skills:
            for _, func in skill["module"].__dict__.items():
                if hasattr(func, "skill"):
                    func.config = skill['config']
                    self.skills.append(func)
        with contextlib.suppress(AttributeError):
            for skill in skills:
                skill["module"].setup(self, self.config)

    def train_parsers(self, skills):
        """Train the parsers."""
        if "parsers" in self.config:
            parsers = self.config["parsers"] or []
            tasks = []
            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and \
                    ("enabled" not in rasanlu[0] or
                     rasanlu[0]["enabled"] is not False):
                tasks.append(
                    asyncio.ensure_future(train_rasanlu(rasanlu[0], skills),
                                          loop=self.eventloop))
            self.eventloop.run_until_complete(
                asyncio.gather(*tasks, loop=self.eventloop))

    def start_connectors(self, connectors):
        """Start the connectors."""
        for connector_module in connectors:
            for _, cls in connector_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Connector) and\
                   cls is not Connector:
                    connector = cls(connector_module["config"])
                    self.connectors.append(connector)

        if connectors:
            for connector in self.connectors:
                if self.eventloop.is_running():
                    self.eventloop.create_task(connector.connect(self))
                else:
                    self.eventloop.run_until_complete(connector.connect(self))
            for connector in self.connectors:
                task = self.eventloop.create_task(connector.listen(self))
                self.connector_tasks.append(task)
        else:
            self.critical("All connectors failed to load", 1)

    def start_databases(self, databases):
        """Start the databases."""
        if not databases:
            _LOGGER.debug(databases)
            _LOGGER.warning(_("All databases failed to load"))
        for database_module in databases:
            for name, cls in database_module["module"].__dict__.items():
                if isinstance(cls, type) and \
                   issubclass(cls, Database) and \
                   cls is not Database:
                    _LOGGER.debug(_("Adding database: %s"), name)
                    database = cls(database_module["config"])
                    self.memory.databases.append(database)
                    self.eventloop.run_until_complete(database.connect(self))

    async def run_skill(self, skill, config, message):
        """Execute a skill."""
        # pylint: disable=broad-except
        # We want to catch all exceptions coming from a skill module and not
        # halt the application. If a skill throws an exception it just doesn't
        # give a response to the user, so an error response should be given.
        try:
            await skill(self, config, message)
        except Exception:
            if message:
                await message.respond(_("Whoops there has been an error"))
                await message.respond(_("Check the log for details"))
            _LOGGER.exception(_("Exception when running skill '%s' "),
                              str(config["name"]))

    async def get_ranked_skills(self, skills, message):
        """Take a message and return a ranked list of matching skills."""
        ranked_skills = []
        ranked_skills += await parse_regex(self, skills, message)

        if "parsers" in self.config:
            _LOGGER.debug(_("Processing parsers..."))
            parsers = self.config["parsers"] or []

            dialogflow = [
                p for p in parsers
                if p["name"] == "dialogflow" or p["name"] == "apiai"
            ]

            # Show deprecation message but  parse message
            # Once it stops working remove this bit
            apiai = [p for p in parsers if p["name"] == "apiai"]
            if apiai:
                _LOGGER.warning(
                    _("Api.ai is now called Dialogflow. This "
                      "parser will stop working in the future "
                      "please swap: 'name: apiai' for "
                      "'name: dialogflow' in configuration.yaml"))

            if len(dialogflow) == 1 and \
                    ("enabled" not in dialogflow[0] or
                     dialogflow[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking dialogflow..."))
                ranked_skills += \
                    await parse_dialogflow(self, skills,
                                           message, dialogflow[0])

            luisai = [p for p in parsers if p["name"] == "luisai"]
            if len(luisai) == 1 and \
                    ("enabled" not in luisai[0] or
                     luisai[0]["enabled"] is not False):
                _LOGGER.debug("Checking luisai...")
                ranked_skills += \
                    await parse_luisai(self, skills,
                                       message, luisai[0])

            recastai = [p for p in parsers if p["name"] == "recastai"]
            if len(recastai) == 1 and \
                    ("enabled" not in recastai[0] or
                     recastai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Recast.AI..."))
                ranked_skills += \
                    await parse_recastai(self, skills,
                                         message, recastai[0])

            witai = [p for p in parsers if p["name"] == "witai"]
            if len(witai) == 1 and \
                    ("enabled" not in witai[0] or
                     witai[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking wit.ai..."))
                ranked_skills += \
                    await parse_witai(self, skills,
                                      message, witai[0])

            rasanlu = [p for p in parsers if p["name"] == "rasanlu"]
            if len(rasanlu) == 1 and \
                    ("enabled" not in rasanlu[0] or
                     rasanlu[0]["enabled"] is not False):
                _LOGGER.debug(_("Checking Rasa NLU..."))
                ranked_skills += \
                    await parse_rasanlu(self, skills,
                                        message, rasanlu[0])

        return sorted(ranked_skills, key=lambda k: k["score"], reverse=True)

    async def _constrain_skills(self, skills, message):
        """Remove skills with contraints which prohibit them from running.

        Args:
            skills (list): A list of skills to be checked for constraints.
            message (opsdroid.message.Message): The message currently being
                parsed.

        Returns:
            list: A list of the skills which were not constrained.

        """
        for skill in skills:
            for constraint in skill.constraints:
                if not constraint(message):
                    skills.remove(skill)
        return skills

    async def parse(self, message):
        """Parse a string against all skills."""
        self.stats["messages_parsed"] = self.stats["messages_parsed"] + 1
        tasks = []
        if message.text.strip() != "":
            _LOGGER.debug(_("Parsing input: %s"), message.text)

            tasks.append(
                self.eventloop.create_task(parse_always(self, message)))

            unconstrained_skills = await self._constrain_skills(
                self.skills, message)
            ranked_skills = await self.get_ranked_skills(
                unconstrained_skills, message)
            if ranked_skills:
                tasks.append(
                    self.eventloop.create_task(
                        self.run_skill(ranked_skills[0]["skill"],
                                       ranked_skills[0]["config"], message)))

        return tasks