Repository: opsdroid/opsdroid

Issue:
Opsdroid exits with a weird error when Rasa is not reachable
# Description

When using Opsdroid with Rasa (`rasanlu` parser) and Rasa is not reachable, then Opsdroid exits with an exception, which doesn't describe directly the cause of the error.

Also described here: https://github.com/opsdroid/opsdroid/pull/1785#pullrequestreview-701831106

## Steps to Reproduce

* Make sure Opsdroid cannot connect to Rasa (shut it down or use non-existent hostname)
* Run Opsdroid with a sample skill and `intents.yml` for Rasa

## Expected Functionality

* Opsdroid exits with a non-zero error code and a appropriate error message stating, what caused the error.


## Experienced Functionality

Opsdroid exits with a stack trace:

```
...
DEBUG opsdroid.core Loaded 1 skills.
DEBUG opsdroid.connector.websocket Starting Websocket connector.
DEBUG opsdroid.parsers.rasanlu Checking Rasa NLU version.
ERROR opsdroid.parsers.rasanlu Unable to connect to Rasa NLU.
DEBUG asyncio Using selector: KqueueSelector
Traceback (most recent call last):
  File "/usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Users/olegfiksel/opsdroid/opsdroid/__main__.py", line 12, in <module>
    init()
  File "/Users/olegfiksel/opsdroid/opsdroid/__main__.py", line 9, in init
    opsdroid.cli.cli()
  File "/Users/olegfiksel/opsdroid/venv/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/Users/olegfiksel/opsdroid/venv/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/Users/olegfiksel/opsdroid/venv/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/olegfiksel/opsdroid/venv/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/olegfiksel/opsdroid/venv/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/Users/olegfiksel/opsdroid/opsdroid/cli/start.py", line 41, in start
    opsdroid.run()
  File "/Users/olegfiksel/opsdroid/opsdroid/core.py", line 170, in run
    self.sync_load()
  File "/Users/olegfiksel/opsdroid/opsdroid/core.py", line 219, in sync_load
    self.eventloop.run_until_complete(self.load())
  File "/usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "/Users/olegfiksel/opsdroid/opsdroid/core.py", line 232, in load
    await self.train_parsers(self.modules["skills"])
  File "/Users/olegfiksel/opsdroid/opsdroid/core.py", line 360, in train_parsers
    rasa_version_is_compatible = await has_compatible_version_rasanlu(
  File "/Users/olegfiksel/opsdroid/opsdroid/parsers/rasanlu.py", line 103, in has_compatible_version_rasanlu
    version = json_object["version"]
TypeError: 'NoneType' object is not subscriptable
```

## Versions
- **Opsdroid version: master (1b392dd96d9d5ba16b0cd6d7e757dd1f061a4a9b)**
- **Python version: 3.9.9**
- **OS/Docker version: MacOS 12.6**

## Configuration File

```yaml
welcome-message: false
logging:
  level: debug
connectors:
  websocket:
parsers:
  rasanlu:
    url: http://non-existent-host:5005
    token: very-secure-rasa-auth-token
skills:
  rasa-test:
    path: /Users/olegfiksel/opsdroid/rasa_test_skill
```

Skill file and `intents.yml` are irrelevant because Opsdroid doesn't get to this point.




Discussion:

Resolving patch:
diff --git a/opsdroid/core.py b/opsdroid/core.py
--- a/opsdroid/core.py
+++ b/opsdroid/core.py
@@ -30,7 +30,7 @@
 from opsdroid.parsers.rasanlu import (
     parse_rasanlu,
     train_rasanlu,
-    has_compatible_version_rasanlu,
+    rasa_usable,
 )
 from opsdroid.parsers.regex import parse_regex
 from opsdroid.parsers.sapcai import parse_sapcai
@@ -357,11 +357,11 @@ async def train_parsers(self, skills):
             parsers = self.modules.get("parsers", {})
             rasanlu = get_parser_config("rasanlu", parsers)
             if rasanlu and rasanlu["enabled"]:
-                rasa_version_is_compatible = await has_compatible_version_rasanlu(
-                    rasanlu
-                )
-                if rasa_version_is_compatible is False:
-                    self.critical("Rasa version is not compatible", 5)
+                if await rasa_usable(rasanlu) is False:
+                    self.critical(
+                        "Cannot connect to Rasa or the Rasa version is not compatible.",
+                        5,
+                    )
                 await train_rasanlu(rasanlu, skills)
 
     async def setup_connectors(self, connectors):
diff --git a/opsdroid/parsers/rasanlu.py b/opsdroid/parsers/rasanlu.py
--- a/opsdroid/parsers/rasanlu.py
+++ b/opsdroid/parsers/rasanlu.py
@@ -94,10 +94,12 @@ async def _get_rasa_nlu_version(config):
         return result
 
 
-async def has_compatible_version_rasanlu(config):
-    """Check if Rasa NLU is compatible with the API we implement"""
+async def rasa_usable(config):
+    """Check if can connect to Rasa NLU and the version is compatible with the API we implement"""
     _LOGGER.debug(_("Checking Rasa NLU version."))
     json_object = await _get_rasa_nlu_version(config)
+    if json_object is None:
+        return False
     version = json_object["version"]
     minimum_compatible_version = json_object["minimum_compatible_version"]
     # Make sure we don't run against a 1.x.x Rasa NLU because it has a different API


Test patch:
diff --git a/tests/test_parser_rasanlu.py b/tests/test_parser_rasanlu.py
--- a/tests/test_parser_rasanlu.py
+++ b/tests/test_parser_rasanlu.py
@@ -499,25 +499,28 @@ async def test__get_rasa_nlu_version(self):
                 await rasanlu._get_rasa_nlu_version({}), result.text.return_value
             )
 
-    async def test_has_compatible_version_rasanlu(self):
+    async def test_rasa_usable(self):
         with amock.patch.object(rasanlu, "_get_rasa_nlu_version") as mock_crc:
             mock_crc.return_value = {
                 "version": "1.0.0",
                 "minimum_compatible_version": "1.0.0",
             }
-            self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), False)
+            self.assertEqual(await rasanlu.rasa_usable({}), False)
 
             mock_crc.return_value = {
                 "version": "2.6.2",
                 "minimum_compatible_version": "2.6.0",
             }
-            self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), True)
+            self.assertEqual(await rasanlu.rasa_usable({}), True)
 
             mock_crc.return_value = {
                 "version": "3.1.2",
                 "minimum_compatible_version": "3.0.0",
             }
-            self.assertEqual(await rasanlu.has_compatible_version_rasanlu({}), True)
+            self.assertEqual(await rasanlu.rasa_usable({}), True)
+
+            mock_crc.return_value = None
+            self.assertEqual(await rasanlu.rasa_usable({}), False)
 
     async def test__load_model(self):
         with amock.patch("aiohttp.ClientSession.put") as patched_request:
@@ -629,7 +632,7 @@ async def test_train_rasanlu_fails(self):
         ) as mock_btu, amock.patch.object(
             rasanlu, "_get_intents_fingerprint"
         ) as mock_gif, amock.patch.object(
-            rasanlu, "has_compatible_version_rasanlu"
+            rasanlu, "rasa_usable"
         ) as mock_crc, amock.patch.object(
             rasanlu, "_load_model"
         ) as mock_lmo, amock.patch.object(
@@ -703,7 +706,7 @@ async def test_train_rasanlu_succeeded(self):
         ) as mock_btu, amock.patch.object(
             rasanlu, "_get_intents_fingerprint"
         ) as mock_gif, amock.patch.object(
-            rasanlu, "has_compatible_version_rasanlu"
+            rasanlu, "rasa_usable"
         ) as mock_crc, amock.patch.object(
             rasanlu, "_load_model"
         ) as mock_lmo, amock.patch.object(