Repository: opsdroid/opsdroid

Issue:
Add a matcher which always matches
It is conceivable that you might want a skill to match on every message which passes through opsdroid. An example would be updating the last seen time of a user in the [seen skill](https://github.com/opsdroid/skill-seen). 

Currently this is achieved by using the `match_regex` decorator with an expression which matches anything. However once #154 has been implemented this will cease to work.

Therefore it would be good to have a matcher which matches every single time and can easily be made exempt of #154. For example it could be called `@match_always` or `@match_everything`.


Discussion:

Resolving patch:
diff --git a/opsdroid/core.py b/opsdroid/core.py
--- a/opsdroid/core.py
+++ b/opsdroid/core.py
@@ -11,6 +11,7 @@
 from opsdroid.connector import Connector
 from opsdroid.database import Database
 from opsdroid.loader import Loader
+from opsdroid.parsers.always import parse_always
 from opsdroid.parsers.regex import parse_regex
 from opsdroid.parsers.apiai import parse_apiai
 from opsdroid.parsers.luisai import parse_luisai
@@ -187,6 +188,8 @@ async def parse(self, message):
 
             tasks.append(
                 self.eventloop.create_task(parse_regex(self, message)))
+            tasks.append(
+                self.eventloop.create_task(parse_always(self, message)))
 
             if "parsers" in self.config:
                 _LOGGER.debug("Processing parsers")
diff --git a/opsdroid/matchers.py b/opsdroid/matchers.py
--- a/opsdroid/matchers.py
+++ b/opsdroid/matchers.py
@@ -107,3 +107,19 @@ async def wrapper(req, opsdroid=opsdroid, config=config):
 
         return func
     return matcher
+
+
+def match_always(func=None):
+    """Return always match decorator."""
+    def matcher(func):
+        """Add decorated function to skills list for always matching."""
+        opsdroid = get_opsdroid()
+        config = opsdroid.loader.current_import_config
+        opsdroid.skills.append({"always": True, "skill": func,
+                                "config": config})
+        return func
+
+    # Allow for decorator with or without parenthesis as there are no args.
+    if callable(func):
+        return matcher(func)
+    return matcher
diff --git a/opsdroid/parsers/always.py b/opsdroid/parsers/always.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/parsers/always.py
@@ -0,0 +1,25 @@
+"""A helper function for parsing and executing always skills."""
+
+import logging
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def parse_always(opsdroid, message):
+    """Parse a message always."""
+    # 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.
+    for skill in opsdroid.skills:
+        if "always" in skill and skill["always"]:
+            try:
+                await skill["skill"](opsdroid, skill["config"], message)
+            except Exception:
+                await message.respond(
+                    "Whoops there has been an error")
+                await message.respond(
+                    "Check the log for details")
+                _LOGGER.exception("Exception when parsing '" +
+                                  message.text + "'")


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -178,7 +178,7 @@ async def test_parse_apiai(self):
             message = Message("Hello world", "user", "default", mock_connector)
             with amock.patch('opsdroid.parsers.apiai.parse_apiai'):
                 tasks = await opsdroid.parse(message)
-                self.assertEqual(len(tasks), 2)  # apiai and regex
+                self.assertEqual(len(tasks), 3)  # apiai, regex and always
                 for task in tasks:
                     await task
 
@@ -193,7 +193,7 @@ async def test_parse_luisai(self):
             message = Message("Hello world", "user", "default", mock_connector)
             with amock.patch('opsdroid.parsers.luisai.parse_luisai'):
                 tasks = await opsdroid.parse(message)
-                self.assertEqual(len(tasks), 2)  # luisai and regex
+                self.assertEqual(len(tasks), 3)  # luisai, regex and always
                 for task in tasks:
                     await task
 
@@ -208,6 +208,6 @@ async def test_parse_witai(self):
             message = Message("Hello world", "user", "default", mock_connector)
             with amock.patch('opsdroid.parsers.witai.parse_witai'):
                 tasks = await opsdroid.parse(message)
-                self.assertEqual(len(tasks), 2)  # witai and regex
+                self.assertEqual(len(tasks), 3)  # witai, regex and always
                 for task in tasks:
                     await task
diff --git a/tests/test_parser_always.py b/tests/test_parser_always.py
new file mode 100644
--- /dev/null
+++ b/tests/test_parser_always.py
@@ -0,0 +1,51 @@
+
+import asynctest
+import asynctest.mock as amock
+
+from opsdroid.core import OpsDroid
+from opsdroid.matchers import match_always
+from opsdroid.message import Message
+from opsdroid.parsers.always import parse_always
+
+
+class TestParserAlways(asynctest.TestCase):
+    """Test the opsdroid always parser."""
+
+    async def test_parse_always_decorator_parens(self):
+        with OpsDroid() as opsdroid:
+            mock_skill = amock.CoroutineMock()
+            match_always()(mock_skill)
+
+            mock_connector = amock.CoroutineMock()
+            message = Message("Hello world", "user", "default", mock_connector)
+
+            await parse_always(opsdroid, message)
+
+            self.assertTrue(mock_skill.called)
+
+    async def test_parse_always_decorate_no_parens(self):
+        with OpsDroid() as opsdroid:
+            mock_skill = amock.CoroutineMock()
+            match_always(mock_skill)
+
+            mock_connector = amock.CoroutineMock()
+            message = Message("Hello world", "user", "default", mock_connector)
+
+            await parse_always(opsdroid, message)
+
+            self.assertTrue(mock_skill.called)
+
+    async def test_parse_always_raises(self):
+        with OpsDroid() as opsdroid:
+            mock_skill = amock.CoroutineMock()
+            mock_skill.side_effect = Exception()
+            match_always()(mock_skill)
+            self.assertEqual(len(opsdroid.skills), 1)
+
+            mock_connector = amock.CoroutineMock()
+            message = Message("Hello world", "user",
+                              "default", mock_connector)
+
+            await parse_always(opsdroid, message)
+
+            self.assertTrue(mock_skill.called)