Repository: opsdroid/opsdroid

Issue:
Invert constraints
I can imagine situations where it would be useful to be able to invert constraints so that they do the opposite of what they are designed to do.

```python
from opsdroid.skill import Skill
from opsdroid.matchers import match_regex
from opsdroid.constraints import constrain_users

class MySkill(Skill):

    @match_regex(r'hi')
    @constrain_users(['alice', 'bob'], invert=True)
    async def hello(self, message):
        """Says 'Hey' to anyone EXCEPT 'alice' and 'bob'."""
        await message.respond('Hey')
```


Discussion:
Hey @jacobtomlinson 
Would love to work on this one during `hacktoberfest` if you don't mind.
Please go ahead!

Resolving patch:
diff --git a/opsdroid/constraints.py b/opsdroid/constraints.py
--- a/opsdroid/constraints.py
+++ b/opsdroid/constraints.py
@@ -5,6 +5,7 @@
 """
 
 import logging
+from functools import wraps
 
 from opsdroid.helper import add_skill_attributes
 
@@ -12,7 +13,17 @@
 _LOGGER = logging.getLogger(__name__)
 
 
-def constrain_rooms(rooms):
+def invert_wrapper(func):
+    """Inverts the result of a function."""
+
+    @wraps(func)
+    def inverted_func(*args, **kwargs):
+        return not func(*args, **kwargs)
+
+    return inverted_func
+
+
+def constrain_rooms(rooms, invert=False):
     """Return room constraint decorator."""
 
     def constraint_decorator(func):
@@ -23,13 +34,15 @@ def constraint_callback(message, rooms=rooms):
             return message.target in rooms
 
         func = add_skill_attributes(func)
+        if invert:
+            constraint_callback = invert_wrapper(constraint_callback)
         func.constraints.append(constraint_callback)
         return func
 
     return constraint_decorator
 
 
-def constrain_users(users):
+def constrain_users(users, invert=False):
     """Return user constraint decorator."""
 
     def constraint_decorator(func):
@@ -40,13 +53,15 @@ def constraint_callback(message, users=users):
             return message.user in users
 
         func = add_skill_attributes(func)
+        if invert:
+            constraint_callback = invert_wrapper(constraint_callback)
         func.constraints.append(constraint_callback)
         return func
 
     return constraint_decorator
 
 
-def constrain_connectors(connectors):
+def constrain_connectors(connectors, invert=False):
     """Return connector constraint decorator."""
 
     def constraint_decorator(func):
@@ -57,6 +72,8 @@ def constraint_callback(message, connectors=connectors):
             return message.connector and (message.connector.name in connectors)
 
         func = add_skill_attributes(func)
+        if invert:
+            constraint_callback = invert_wrapper(constraint_callback)
         func.constraints.append(constraint_callback)
         return func
 


Test patch:
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
--- a/tests/test_constraints.py
+++ b/tests/test_constraints.py
@@ -43,6 +43,17 @@ async def test_constrain_rooms_skips(self):
             tasks = await opsdroid.parse(Message("Hello", "user", "#general", None))
             self.assertEqual(len(tasks), 2)  # match_always and the skill
 
+    async def test_constrain_rooms_inverted(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.eventloop = mock.CoroutineMock()
+            skill = await self.getMockSkill()
+            skill = match_regex(r".*")(skill)
+            skill = constraints.constrain_rooms(["#general"], invert=True)(skill)
+            opsdroid.skills.append(skill)
+
+            tasks = await opsdroid.parse(Message("Hello", "user", "#general", None))
+            self.assertEqual(len(tasks), 1)  # match_always only
+
     async def test_constrain_users_constrains(self):
         with OpsDroid() as opsdroid:
             opsdroid.eventloop = mock.CoroutineMock()
@@ -67,6 +78,17 @@ async def test_constrain_users_skips(self):
             tasks = await opsdroid.parse(Message("Hello", "user", "#general", None))
             self.assertEqual(len(tasks), 2)  # match_always and the skill
 
+    async def test_constrain_users_inverted(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.eventloop = mock.CoroutineMock()
+            skill = await self.getMockSkill()
+            skill = match_regex(r".*")(skill)
+            skill = constraints.constrain_users(["user"], invert=True)(skill)
+            opsdroid.skills.append(skill)
+
+            tasks = await opsdroid.parse(Message("Hello", "user", "#general", None))
+            self.assertEqual(len(tasks), 1)  # match_always only
+
     async def test_constrain_connectors_constrains(self):
         with OpsDroid() as opsdroid:
             opsdroid.eventloop = mock.CoroutineMock()
@@ -95,6 +117,21 @@ async def test_constrain_connectors_skips(self):
             )
             self.assertEqual(len(tasks), 2)  # match_always and the skill
 
+    async def test_constrain_connectors_inverted(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.eventloop = mock.CoroutineMock()
+            skill = await self.getMockSkill()
+            skill = match_regex(r".*")(skill)
+            skill = constraints.constrain_connectors(["slack"], invert=True)(skill)
+            opsdroid.skills.append(skill)
+            connector = mock.Mock()
+            connector.configure_mock(name="slack")
+
+            tasks = await opsdroid.parse(
+                Message("Hello", "user", "#general", connector)
+            )
+            self.assertEqual(len(tasks), 1)  # match_always only
+
     async def test_constraint_can_be_called_after_skip(self):
         with OpsDroid() as opsdroid:
             opsdroid.eventloop = mock.CoroutineMock()