Repository: opsdroid/opsdroid

Issue:
Parser regex.py does not support identically named group in regex_matcher
# Description

When using the same group name in **match_regex**  decorator **regex.py** issue an error because the python re module used to parse messages does not support identical named group


## Steps to Reproduce
- Create a skill like (the goal is to have a multiple patterns using the same group name)

```python
from opsdroid.matchers import match_regex
from opsdroid.skill import Skill

PATTERNS = '|'.join([
    "say (?P<word>\w+)",
    "repeat (?P<word>\w+)",
    "say",
    "repeat"
])


class SaySkill(Skill):

    @match_regex(PATTERNS, case_sensitive=False, matching_condition='match')
    async def say(self, message):
        word = message.entities['word']['value'] or None
        if word is None:
            word = 'No word to say'

        await message.respond(word)

```

- Add it to configuration file
- using opsdroid shell or opsdroid desktop test it 
- Issue occurs

```
DEBUG opsdroid.core: Parsing input: <opsdroid.events.Message(text=say hello)>.
ERROR aiohttp.server: Error handling request
Traceback (most recent call last):
  File "/usr/lib64/python3.6/site-packages/aiohttp/web_protocol.py", line 418, in start
    resp = await task
  File "/usr/lib64/python3.6/site-packages/aiohttp/web_app.py", line 458, in _handle
    resp = await handler(request)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/connector/websocket/__init__.py", line 101, in websocket_handler
    await self.opsdroid.parse(message)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/core.py", line 498, in parse
    ranked_skills = await self.get_ranked_skills(unconstrained_skills, event)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/core.py", line 421, in get_ranked_skills
    ranked_skills += await parse_regex(self, skills, message)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/parsers/regex.py", line 40, in parse_regex
    regex = await match_regex(message.text, opts)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/parsers/regex.py", line 29, in match_regex
    regex = re.match(opts["expression"], text, is_case_sensitive())
  File "/usr/lib64/python3.6/re.py", line 172, in match
    return _compile(pattern, flags).match(string)
  File "/usr/lib64/python3.6/re.py", line 301, in _compile
    p = sre_compile.compile(pattern, flags)
  File "/usr/lib64/python3.6/sre_compile.py", line 562, in compile
    p = sre_parse.parse(p, flags)
  File "/usr/lib64/python3.6/sre_parse.py", line 855, in parse
    p = _parse_sub(source, pattern, flags & SRE_FLAG_VERBOSE, 0)
  File "/usr/lib64/python3.6/sre_parse.py", line 416, in _parse_sub
    not nested and not items))
  File "/usr/lib64/python3.6/sre_parse.py", line 759, in _parse
    raise source.error(err.msg, len(name) + 1) from None
sre_constants.error: redefinition of group name 'city' as group 2; was group 1 at position 42  <-------
```

## Expected Functionality
Handling multiple patterns with the same name group

```
DEBUG opsdroid.parsers.crontab: Running crontab skills at Fri May  1 19:03:00 2020.
DEBUG opsdroid.core: Parsing input: <opsdroid.events.Message(text=say hello)>.
DEBUG opsdroid.core: Processing parsers...
DEBUG opsdroid.connector.websocket: Responding with: 'hello' in target 8d9c8b96-8bcd-11ea-b043-0050568f7a82    <----------
```
## Experienced Functionality

debug logs :

```
DEBUG opsdroid.core: Parsing input: <opsdroid.events.Message(text=say hello)>.
ERROR aiohttp.server: Error handling request
Traceback (most recent call last):
  File "/usr/lib64/python3.6/site-packages/aiohttp/web_protocol.py", line 418, in start
    resp = await task
  File "/usr/lib64/python3.6/site-packages/aiohttp/web_app.py", line 458, in _handle
    resp = await handler(request)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/connector/websocket/__init__.py", line 101, in websocket_handler
    await self.opsdroid.parse(message)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/core.py", line 498, in parse
    ranked_skills = await self.get_ranked_skills(unconstrained_skills, event)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/core.py", line 421, in get_ranked_skills
    ranked_skills += await parse_regex(self, skills, message)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/parsers/regex.py", line 40, in parse_regex
    regex = await match_regex(message.text, opts)
  File "/home/ibot/ibot-opsdroid/opsdroid/opsdroid/parsers/regex.py", line 29, in match_regex
    regex = re.match(opts["expression"], text, is_case_sensitive())
  File "/usr/lib64/python3.6/re.py", line 172, in match
    return _compile(pattern, flags).match(string)
  File "/usr/lib64/python3.6/re.py", line 301, in _compile
    p = sre_compile.compile(pattern, flags)
  File "/usr/lib64/python3.6/sre_compile.py", line 562, in compile
    p = sre_parse.parse(p, flags)
  File "/usr/lib64/python3.6/sre_parse.py", line 855, in parse
    p = _parse_sub(source, pattern, flags & SRE_FLAG_VERBOSE, 0)
  File "/usr/lib64/python3.6/sre_parse.py", line 416, in _parse_sub
    not nested and not items))
  File "/usr/lib64/python3.6/sre_parse.py", line 759, in _parse
    raise source.error(err.msg, len(name) + 1) from None
sre_constants.error: redefinition of group name 'city' as group 2; was group 1 at position 42  <-------
```

## Versions
- **Opsdroid version:*v0.18.0*
- **Python version:*3.6.7*

<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion:

Resolving patch:
diff --git a/opsdroid/parsers/regex.py b/opsdroid/parsers/regex.py
--- a/opsdroid/parsers/regex.py
+++ b/opsdroid/parsers/regex.py
@@ -1,16 +1,16 @@
 """A helper function for parsing and executing regex skills."""
 
 import logging
-import re
+import regex
 
 _LOGGER = logging.getLogger(__name__)
 
 
-async def calculate_score(regex, score_factor):
+async def calculate_score(expression, score_factor):
     """Calculate the score of a regex."""
     # The score asymptotically approaches the max score
     # based on the length of the expression.
-    return (1 - (1 / ((len(regex) + 1) ** 2))) * score_factor
+    return (1 - (1 / ((len(expression) + 1) ** 2))) * score_factor
 
 
 async def match_regex(text, opts):
@@ -19,15 +19,15 @@ async def match_regex(text, opts):
     def is_case_sensitive():
         if opts["case_sensitive"]:
             return False
-        return re.IGNORECASE
+        return regex.IGNORECASE
 
     if opts["matching_condition"].lower() == "search":
-        regex = re.search(opts["expression"], text, is_case_sensitive())
+        matched_regex = regex.search(opts["expression"], text, is_case_sensitive())
     elif opts["matching_condition"].lower() == "fullmatch":
-        regex = re.fullmatch(opts["expression"], text, is_case_sensitive())
+        matched_regex = regex.fullmatch(opts["expression"], text, is_case_sensitive())
     else:
-        regex = re.match(opts["expression"], text, is_case_sensitive())
-    return regex
+        matched_regex = regex.match(opts["expression"], text, is_case_sensitive())
+    return matched_regex
 
 
 async def parse_regex(opsdroid, skills, message):
@@ -37,10 +37,10 @@ async def parse_regex(opsdroid, skills, message):
         for matcher in skill.matchers:
             if "regex" in matcher:
                 opts = matcher["regex"]
-                regex = await match_regex(message.text, opts)
-                if regex:
-                    message.regex = regex
-                    for regroup, value in regex.groupdict().items():
+                matched_regex = await match_regex(message.text, opts)
+                if matched_regex:
+                    message.regex = matched_regex
+                    for regroup, value in matched_regex.groupdict().items():
                         message.update_entity(regroup, value, None)
                     matched_skills.append(
                         {


Test patch:
diff --git a/tests/test_parser_regex.py b/tests/test_parser_regex.py
--- a/tests/test_parser_regex.py
+++ b/tests/test_parser_regex.py
@@ -148,3 +148,19 @@ async def test_parse_regex_named_groups_entities(self):
             self.assertEqual(len(parsed_message.entities.keys()), 1)
             self.assertTrue("name" in parsed_message.entities.keys())
             self.assertEqual(parsed_message.entities["name"]["value"], "opsdroid")
+
+    async def test_parse_regex_identically_named_groups_entities(self):
+        with OpsDroid() as opsdroid:
+            regex = r"Hello (?P<name>.*)|Hi (?P<name>.*)"
+
+            mock_skill_named_groups = await self.getMockSkill()
+            opsdroid.skills.append(match_regex(regex)(mock_skill_named_groups))
+
+            mock_connector = amock.CoroutineMock()
+            message = Message("Hello opsdroid", "user", "default", mock_connector)
+
+            [skill] = await opsdroid.get_ranked_skills(opsdroid.skills, message)
+            parsed_message = skill["message"]
+            self.assertEqual(len(parsed_message.entities.keys()), 1)
+            self.assertTrue("name" in parsed_message.entities.keys())
+            self.assertEqual(parsed_message.entities["name"]["value"], "opsdroid")