Repository: opsdroid/opsdroid

Issue:
Constraints remove skills "forever"
# Description
The new constraints feature (which by the way, are an awesome feature) remove a skill "forever" when are activated.

I write "forever" in quotes because the skills will be unavailable until Opsdroid is restarted.


## Steps to Reproduce
- Add any constraint to a skill (for example, `@constrain_users(['alice'])`)
- Send a message from the user `alice`. Opsdroid will answer as expected.
- Send a message from the user `bob`. Opsdroid will not answer, as expected.
- Send again a message from the user `alice`. Unexpectedly, opsdroid will not answer.

## Expected Functionality
Opsdroid should answer when `alice` sends the message after `bob`.

## Experienced Functionality
Opsdroid doesn't answer when `alice` sends the message after `bob`.

## Versions
- **Opsdroid version: 0.14.1**
- **Python version: 3.6.7**
- **OS/Docker version: Ubuntu 18.04**

## Additional Details
The problem seems to be in the methods `parse` and `_constrain_skills` of the `OpsDroid` class.

`parse` calls `_constrain_skills` with `self.skills` as a parameter. `self.skills` is a list, so is passed by reference. Inside `_constrain_skills`, that list is mutated in `skills.remove(skill)`. So we're mutating `self.skills` directly.


Discussion:

Resolving patch:
diff --git a/opsdroid/core.py b/opsdroid/core.py
--- a/opsdroid/core.py
+++ b/opsdroid/core.py
@@ -422,11 +422,12 @@ async def _constrain_skills(self, skills, message):
             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
+        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."""


Test patch:
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
--- a/tests/test_constraints.py
+++ b/tests/test_constraints.py
@@ -103,3 +103,26 @@ async def test_constrain_connectors_skips(self):
                 Message('Hello', 'user', '#general', connector)
             )
             self.assertEqual(len(tasks), 2) # match_always and the skill
+
+    async def test_constraint_can_be_called_after_skip(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.eventloop = mock.CoroutineMock()
+            skill = await self.getMockSkill()
+            skill = match_regex(r'.*')(skill)
+            skill = constraints.constrain_users(['user'])(skill)
+            opsdroid.skills.append(skill)
+
+            tasks = await opsdroid.parse(
+                Message('Hello', 'user', '#general', None)
+            )
+            self.assertEqual(len(tasks), 2) # match_always and the skill
+
+            tasks = await opsdroid.parse(
+                Message('Hello', 'otheruser', '#general', None)
+            )
+            self.assertEqual(len(tasks), 1) # Just match_always
+
+            tasks = await opsdroid.parse(
+                Message('Hello', 'user', '#general', None)
+            )
+            self.assertEqual(len(tasks), 2)  # match_always and the skill