Repository: opsdroid/opsdroid

Issue:
The Slack Connector interaction handler fails on Overflow block elements
<!-- Before you post an issue or if you are unsure about something join our matrix channel https://riot.im/app/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->
# Description
When an [Overflow block element](https://api.slack.com/reference/block-kit/block-elements#overflow) is clicked in Slack, a `KeyError` is raised from the Slack Connector.


## Steps to Reproduce

1. Setup the Slack Connector.
2. Create a Skill that responds to a message with Blocks that contains an Overflow element:
```python
@match_regex(r'overflow block')
async def overflow_block(self, message):
    sample_response = [{
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": "This block has an overflow menu."
        },
        "accessory": {
            "type": "overflow",
            "options": [
                {
                    "text": {
                        "type": "plain_text",
                        "text": "Option 1",
                        "emoji": True
                    },
                    "value": "value-0"
                },
                {
                    "text": {
                        "type": "plain_text",
                        "text": "Option 2",
                        "emoji": True
                    },
                    "value": "value-1"
                }
            ]
        }
    }]

    await message.respond(Blocks(sample_response))
```
3. And a Slack Interactions handler:
```python
@match_event(BlockActions)
async def slack_interactions(self, event: BlockActions):
    await event.respond(f'Received payload: {event.payload}')
```
4. In Slack, send "overflow block" to the bot and interact with the Overflow menu button it responds with.

## Expected Functionality
The Slack Interactions handler from step 3 above should be called and a response with the message payload should be sent back to the chat.

## Experienced Functionality
In Slack, a small exclamation mark appears near the Overflow action button, indicating something went wrong with the interaction.

In Opsdroid, the following message appears in the log:
```
ERROR aiohttp.server: Error handling request
Traceback (most recent call last):
  File "/path/to/venv/lib/python3.7/site-packages/aiohttp/web_protocol.py", line 418, in start
    resp = await task
  File "/path/to/venv/lib/python3.7/site-packages/aiohttp/web_app.py", line 458, in _handle
    resp = await handler(request)
  File "/path/to/venv/lib/python3.7/site-packages/opsdroid/connector/slack/__init__.py", line 268, in slack_interactions_handler
    await block_action.update_entity("value", action["value"])
KeyError: 'value'
INFO aiohttp.access: 127.0.0.1 [14/Dec/2019:15:01:36 +0000] "POST /connector/slack/interactions HTTP/1.1" 500 244 "-" "Slackbot 1.0 (+https://api.slack.com/robots)"
```

## Versions
- **Opsdroid version:** 0.17.0
- **Python version:** 3.7
- **OS/Docker version:** macOS 10.14.6

## Configuration File
Please include your version of the configuration file below.

```yaml
logging:
  level: debug
  console: true

web:
  host: '127.0.0.1'
  port: 8090

parsers:

connectors:
  - name: slack
    api-token: "*******************"
    default: true

skills:
  - name: skill
    path: /path/to/skill
    no-cache: true
```

## Additional Details
This is happening because the Overflow element payload is a bit different from the payload sent by the Button element which is handled correctly.
This is a Button element action fragment:
```json
{
    "type": "button",
    "block_id": "I1V6",
    "action_id": "nmMCa",
    "text": {
        "type": "plain_text",
        "text": "Button",
        "emoji": true
    },
    "value": "click_me_123",
    "action_ts": "1576336692.912336"
}
```
You can see there is a `value` key there, but in an Overflow action fragment the `value` key is nested under the `selected_option` object.:
```json
{
    "type": "overflow",
    "block_id": "B5XNP",
    "action_id": "BnhtF",
    "selected_option": {
        "text": {
            "type": "plain_text",
            "text": "Option 1",
            "emoji": true
        },
        "value": "value-0"
    },
    "action_ts": "1576336883.317406"
}
```

There are other block elements that will result in the same behaviour:
- Datepicker - here the `value` is `selected_date`:
```json
{
    "type": "datepicker",
    "block_id": "CAwR",
    "action_id": "VS+",
    "selected_date": "2019-12-31",
    "initial_date": "1990-04-28",
    "action_ts": "1576337318.133466"
}
```
- Multi-Select - here there can be multiple selected options:
```json
{
    "type": "multi_static_select",
    "block_id": "rOL",
    "action_id": "Cd9",
    "selected_options": [
        {
            "text": {
                "type": "plain_text",
                "text": "Choice 2",
                "emoji": true
            },
            "value": "value-1"
        },
        {
            "text": {
                "type": "plain_text",
                "text": "Choice 1",
                "emoji": true
            },
            "value": "value-0"
        }
    ],
    "placeholder": {
        "type": "plain_text",
        "text": "Select items",
        "emoji": true
    },
    "action_ts": "1576337351.609054"
}
```
- Select - this one is similar to the Overflow with `value` in the `selected_option`:
```json
{
    "type": "static_select",
    "block_id": "OGN1",
    "action_id": "4jd",
    "selected_option": {
        "text": {
            "type": "plain_text",
            "text": "Choice 2",
            "emoji": true
        },
        "value": "value-1"
    },
    "placeholder": {
        "type": "plain_text",
        "text": "Select an item",
        "emoji": true
    },
    "action_ts": "1576337378.859991"
}
```

<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion:
I would actually like to pick this up and fix it but i'm not sure how to handle the Multi-Select element - is saving all the selected item's values as a list under the "value" entity acceptable?
Thanks so much for taking the time to raise such a thorough issue!

If you're happy to work on it that would be appreciated too. I think a list of items sounds great!

Resolving patch:
diff --git a/opsdroid/connector/slack/__init__.py b/opsdroid/connector/slack/__init__.py
--- a/opsdroid/connector/slack/__init__.py
+++ b/opsdroid/connector/slack/__init__.py
@@ -265,7 +265,19 @@ async def slack_interactions_handler(self, request):
                         target=payload["channel"]["id"],
                         connector=self,
                     )
-                    await block_action.update_entity("value", action["value"])
+
+                    action_value = None
+                    if action["type"] == "button":
+                        action_value = action["value"]
+                    elif action["type"] in ["overflow", "static_select"]:
+                        action_value = action["selected_option"]["value"]
+                    elif action["type"] == "datepicker":
+                        action_value = action["selected_date"]
+                    elif action["type"] == "multi_static_select":
+                        action_value = [v["value"] for v in action["selected_options"]]
+
+                    if action_value:
+                        await block_action.update_entity("value", action_value)
                     await self.opsdroid.parse(block_action)
             elif payload["type"] == "message_action":
                 await self.opsdroid.parse(
diff --git a/opsdroid/parsers/event_type.py b/opsdroid/parsers/event_type.py
--- a/opsdroid/parsers/event_type.py
+++ b/opsdroid/parsers/event_type.py
@@ -33,6 +33,13 @@ async def match_event(event, event_opts):
                     event_value = event_opts.get(key, None)
                     entity_value = event.entities.get(key, {}).get("value", None)
 
+                    if (
+                        isinstance(event_value, list)
+                        and isinstance(entity_value, list)
+                        and sorted(event_value) != sorted(entity_value)
+                    ):
+                        return False
+
                     if event_value != entity_value:
                         return False
 


Test patch:
diff --git a/tests/test_connector_slack.py b/tests/test_connector_slack.py
--- a/tests/test_connector_slack.py
+++ b/tests/test_connector_slack.py
@@ -276,7 +276,77 @@ async def test_block_actions_interactivity(self):
                     "value": "click_me_123",
                     "type": "button",
                     "action_ts": "1548426417.840180",
-                }
+                },
+                {
+                    "type": "overflow",
+                    "block_id": "B5XNP",
+                    "action_id": "BnhtF",
+                    "selected_option": {
+                        "text": {
+                            "type": "plain_text",
+                            "text": "Option 1",
+                            "emoji": True,
+                        },
+                        "value": "value-0",
+                    },
+                    "action_ts": "1576336883.317406",
+                },
+                {
+                    "type": "datepicker",
+                    "block_id": "CAwR",
+                    "action_id": "VS+",
+                    "selected_date": "2019-12-31",
+                    "initial_date": "1990-04-28",
+                    "action_ts": "1576337318.133466",
+                },
+                {
+                    "type": "multi_static_select",
+                    "block_id": "rOL",
+                    "action_id": "Cd9",
+                    "selected_options": [
+                        {
+                            "text": {
+                                "type": "plain_text",
+                                "text": "Choice 1",
+                                "emoji": True,
+                            },
+                            "value": "value-0",
+                        },
+                        {
+                            "text": {
+                                "type": "plain_text",
+                                "text": "Choice 2",
+                                "emoji": True,
+                            },
+                            "value": "value-1",
+                        },
+                    ],
+                    "placeholder": {
+                        "type": "plain_text",
+                        "text": "Select items",
+                        "emoji": True,
+                    },
+                    "action_ts": "1576337351.609054",
+                },
+                {
+                    "type": "static_select",
+                    "block_id": "OGN1",
+                    "action_id": "4jd",
+                    "selected_option": {
+                        "text": {
+                            "type": "plain_text",
+                            "text": "Choice 2",
+                            "emoji": True,
+                        },
+                        "value": "value-1",
+                    },
+                    "placeholder": {
+                        "type": "plain_text",
+                        "text": "Select an item",
+                        "emoji": True,
+                    },
+                    "action_ts": "1576337378.859991",
+                },
             ],
         }
 
@@ -286,6 +356,7 @@ async def test_block_actions_interactivity(self):
 
         response = await connector.slack_interactions_handler(mock_request)
         self.assertTrue(connector.opsdroid.parse.called)
+        self.assertEqual(connector.opsdroid.parse.call_count, len(req_ob["actions"]))
         self.assertEqual(type(response), aiohttp.web.Response)
         self.assertEqual(response.status, 200)