Repository: opsdroid/opsdroid

Issue:
Feature Request: Suggest implementing 'modal' support for the Slack connector
<!-- Before you post an issue or if you are unsure about something join our matrix channel https://app.element.io/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->
# Description
I'm developing the skill with slack connector, and need to using 'modal' type in the skill to temporarily displaying dynamic and interactive information. So suggest opsdroid slack connector to have the capability to send a message as 'modal' type and handle the respond of the modal. 


## Expected Functionality
The slack connector is able to send a message as 'modal' type and handle the respond of the modal. 


## Experienced Functionality
when I attempted to send message using the type `Modal` which is provided by Slack, I got the error as below.  It seems the modal type is not supported yet. 
```
The server responded with: {'ok': False, 'error': 'invalid_blocks', 'errors': ['unsupported type: modal [json-pointer:/blocks/0/type]'], 'response_metadata': {'messages': ['[ERROR] unsupported type: modal [json-pointer:/blocks/0/type]']}}
```

## Versions
- **Opsdroid version:** 0.24.1
- **Python version:** 3.8
- **OS/Docker version:** Redhat 8


## Additional Details
Any other details you wish to include such as screenshots, console messages, etc.


<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion:

Resolving patch:
diff --git a/opsdroid/connector/slack/connector.py b/opsdroid/connector/slack/connector.py
--- a/opsdroid/connector/slack/connector.py
+++ b/opsdroid/connector/slack/connector.py
@@ -5,14 +5,13 @@
 import os
 import re
 import ssl
+import urllib.parse
 
 import aiohttp
 import certifi
-import opsdroid.events
 from emoji import demojize
-from opsdroid.connector import Connector, register_event
-from opsdroid.connector.slack.create_events import SlackEventCreator
-from opsdroid.connector.slack.events import Blocks, EditedBlocks
+
+
 from slack_sdk.errors import SlackApiError
 from slack_sdk.socket_mode.aiohttp import SocketModeClient
 from slack_sdk.socket_mode.request import SocketModeRequest
@@ -20,6 +19,19 @@
 from slack_sdk.web.async_client import AsyncWebClient
 from voluptuous import Required
 
+
+import opsdroid.events
+from opsdroid.connector import Connector, register_event
+from opsdroid.connector.slack.create_events import SlackEventCreator
+from opsdroid.connector.slack.events import (
+    Blocks,
+    EditedBlocks,
+    ModalOpen,
+    ModalPush,
+    ModalUpdate,
+)
+
+
 _LOGGER = logging.getLogger(__name__)
 
 _USE_BOT_TOKEN_MSG = (
@@ -212,7 +224,6 @@ async def socket_event_handler(
 
         response = SocketModeResponse(envelope_id=req.envelope_id)
         await client.send_socket_mode_response(response)
-
         payload = req.payload
 
         await self.event_handler(payload)
@@ -236,7 +247,19 @@ async def web_event_handler(self, request):
             if "payload" in req:
                 payload = json.loads(req["payload"])
             else:
-                payload = dict(req)
+                # Some payloads (ie: view_submission) don't come with proper formatting
+                # Convert the request to text, and later attempt to load the json
+
+                if len(req.keys()) == 1:
+                    req = await request.text()
+                    req = urllib.parse.unquote(req)
+
+                    if "payload={" in req:
+                        req = req.replace("payload=", "")
+                        payload = json.loads(req)
+                else:
+                    payload = dict(req)
+
         elif request.content_type == "application/json":
             payload = await request.json()
 
@@ -400,6 +423,46 @@ async def _edit_blocks(self, blocks):
             data=data,
         )
 
+    @register_event(ModalOpen)
+    async def _open_modal(self, modal):
+        """Respond with opening a Modal.
+
+        https://api.slack.com/methods/views.open
+        """
+        _LOGGER.debug(_("Opening modal with trigger id: %s."), modal.trigger_id)
+
+        return await self.slack_web_client.api_call(
+            "views.open",
+            data={"trigger_id": modal.trigger_id, "view": modal.view},
+        )
+
+    @register_event(ModalUpdate)
+    async def _update_modal(self, modal):
+        """Respond an update to a Modal.
+
+        https://api.slack.com/methods/views.update
+        """
+        _LOGGER.debug(_("Opening modal with trigger id: %s."), modal.external_id)
+        data = {"external_id": modal.external_id, "view": modal.view}
+
+        if modal.hash:
+            data["hash"] = modal.hash
+
+        return await self.slack_web_client.api_call("views.update", data=data)
+
+    @register_event(ModalPush)
+    async def _push_modal(self, modal):
+        """Respond by pushing a view onto the stack of a root view.
+
+        https://api.slack.com/methods/views.push
+        """
+        _LOGGER.debug(_("Pushing modal with trigger id: %s."), modal.trigger_id)
+
+        return await self.slack_web_client.api_call(
+            "views.push",
+            data={"trigger_id": modal.trigger_id, "view": modal.view},
+        )
+
     @register_event(opsdroid.events.Reaction)
     async def send_reaction(self, reaction):
         """React to a message."""
diff --git a/opsdroid/connector/slack/create_events.py b/opsdroid/connector/slack/create_events.py
--- a/opsdroid/connector/slack/create_events.py
+++ b/opsdroid/connector/slack/create_events.py
@@ -58,6 +58,7 @@ async def _get_user_name(self, event):
 
     async def handle_bot_message(self, event, channel):
         """Check that a bot message is opsdroid if not create the message"""
+
         if event["bot_id"] != self.connector.bot_id:
             return await self.create_message(event, channel)
 
@@ -248,13 +249,18 @@ async def message_action_triggered(self, event, channel):
     async def view_submission_triggered(self, event, channel):
         """Send a ViewSubmission event."""
 
-        return slack_events.ViewSubmission(
+        view_submission = slack_events.ViewSubmission(
             event,
             user=event["user"]["id"],
             target=event["user"]["id"],
             connector=self.connector,
         )
 
+        if callback_id := event.get("view", {}).get("callback_id"):
+            view_submission.update_entity("callback_id", callback_id)
+
+        return view_submission
+
     async def view_closed_triggered(self, event, channel):
         """Send a ViewClosed event."""
 
diff --git a/opsdroid/connector/slack/events.py b/opsdroid/connector/slack/events.py
--- a/opsdroid/connector/slack/events.py
+++ b/opsdroid/connector/slack/events.py
@@ -117,6 +117,93 @@ def __init__(self, payload, *args, **kwargs):
         super().__init__(payload, *args, **kwargs)
 
 
+class Modal(events.Event):
+    """Base Event class to represent a Modal in Slack.
+
+    Modals are the Slack app equivalent of alert boxes, pop-ups, or dialog boxes.
+    https://api.slack.com/surfaces/modals/using
+
+    args:
+        view: a view payload. this can be a dict or a json encoded string
+    """
+
+    def __init__(self, view, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.view = view
+
+        if isinstance(self.view, dict):
+            self.view = json.dumps(self.view)
+
+
+class ModalOpen(Modal):
+    """Event class to represent a new Modal in Slack.
+
+    args:
+        trigger_id: trigger to post to a user
+
+    **Basic Usage in a Skill:**
+
+    .. code-block:: python
+
+        from opsdroid.skill import Skill
+        from opsdroid.matchers import match_event
+
+        class ModalSkill(Skill):
+            @match_event(SlashCommand, command="/testcommand")
+            async def open_modal(self, event):
+                view = {
+                    "type": "modal",
+                    "title": {"type": "plain_text", "text": "Modal title"},
+                    "blocks": [
+                        {
+                            "type": "input",
+                            "label": {"type": "plain_text", "text": "Input label"},
+                            "element": {
+                                "type": "plain_text_input",
+                                "action_id": "input1",
+                                "placeholder": {
+                                    "type": "plain_text",
+                                    "text": "Type in here",
+                                },
+                                "multiline": False,
+                            },
+                            "optional": False,
+                        },
+                    ],
+                    "close": {"type": "plain_text", "text": "Cancel"},
+                    "submit": {"type": "plain_text", "text": "Save"},
+                    "private_metadata": "Shhhhhhhh",
+                    "callback_id": "view_identifier_12",
+                }
+                await self.opsdroid.send(
+                    ModalOpen(view=view, trigger_id=event.payload["trigger_id"])
+                )
+    """
+
+    def __init__(self, trigger_id, view, *args, **kwargs):
+        super().__init__(view, *args, **kwargs)
+        self.trigger_id = trigger_id
+
+
+class ModalUpdate(Modal):
+    """Event class to represent a Modal Update in Slack
+
+    args:
+        external_id: A unique identifier of the view set by the developer
+        _hash: A string that represents view state to protect against possible race conditions
+    """
+
+    def __init__(self, external_id, view, *args, hash_=None, **kwargs):
+        self.external_id = external_id
+        self.hash = hash_
+        super().__init__(view, *args, **kwargs)
+
+
+class ModalPush(ModalOpen):
+    """Event class to represent a Modal Push in Slack."""
+
+
 class ViewSubmission(InteractiveAction):
     """Event class to represent view_submission in Slack."""
 
@@ -141,7 +228,7 @@ class SlashCommand(InteractiveAction):
     .. code-block:: python
 
         from opsdroid.skill import Skill
-        from opsdroid.matchers import match_regex
+        from opsdroid.matchers import match_event
 
         class CommandsSkill(Skill):
             @match_event(SlashCommand, command="/testcommand")


Test patch:
diff --git a/opsdroid/connector/slack/tests/responses/payload_view_submission.urlencoded b/opsdroid/connector/slack/tests/responses/payload_view_submission.urlencoded
--- a/opsdroid/connector/slack/tests/responses/payload_view_submission.urlencoded
+++ b/opsdroid/connector/slack/tests/responses/payload_view_submission.urlencoded
@@ -1 +1 @@
-payload=%7B%22type%22%3A%22view_submission%22%2C%22team%22%3A%7B%22id%22%3A%22T01ND2P8N91%22%2C%22domain%22%3A%22opsdroidtests%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U01NK1K9L68%22%2C%22username%22%3A%22opsdroid.test%22%2C%22name%22%3A%22opsdroid.test%22%2C%22team_id%22%3A%22T01ND2P8N91%22%7D%2C%22api_app_id%22%3A%22A01NK2M4ABE%22%2C%22token%22%3A%22gAGOclMod3fTpSTaxBjyCjCT%22%2C%22trigger_id%22%3A%221835952449765.1761091294307.f04f465a970c18de583c065d0043875e%22%2C%22view%22%3A%7B%22id%22%3A%22V01QKTZBFMK%22%2C%22team_id%22%3A%22T01ND2P8N91%22%2C%22type%22%3A%22modal%22%2C%22blocks%22%3A%5B%7B%22type%22%3A%22input%22%2C%22block_id%22%3A%221%3DzGH%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Label%22%2C%22emoji%22%3Atrue%7D%2C%22hint%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Hint+text%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22dispatch_action%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22sl_input%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Placeholder+text+for+single-line+input%22%2C%22emoji%22%3Atrue%7D%2C%22dispatch_action_config%22%3A%7B%22trigger_actions_on%22%3A%5B%22on_enter_pressed%22%5D%7D%7D%7D%2C%7B%22type%22%3A%22input%22%2C%22block_id%22%3A%227Zh%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Label%22%2C%22emoji%22%3Atrue%7D%2C%22hint%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Hint+text%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22dispatch_action%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22ml_input%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Placeholder+text+for+multi-line+input%22%2C%22emoji%22%3Atrue%7D%2C%22multiline%22%3Atrue%2C%22dispatch_action_config%22%3A%7B%22trigger_actions_on%22%3A%5B%22on_enter_pressed%22%5D%7D%7D%7D%2C%7B%22type%22%3A%22actions%22%2C%22block_id%22%3A%22cbQh%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22conversations_select%22%2C%22action_id%22%3A%22actionId-0%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+a+conversation%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22channels_select%22%2C%22action_id%22%3A%22actionId-1%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+a+channel%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22users_select%22%2C%22action_id%22%3A%22actionId-2%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+a+user%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22static_select%22%2C%22action_id%22%3A%22actionId-3%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+an+item%22%2C%22emoji%22%3Atrue%7D%2C%22options%22%3A%5B%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%2Athis+is+plain_text+text%2A%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22value-0%22%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%2Athis+is+plain_text+text%2A%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22value-1%22%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%2Athis+is+plain_text+text%2A%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22value-2%22%7D%5D%7D%5D%7D%2C%7B%22type%22%3A%22actions%22%2C%22block_id%22%3A%22an8gR%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22conversations_select%22%2C%22action_id%22%3A%22actionId-0%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+private+conversation%22%2C%22emoji%22%3Atrue%7D%2C%22filter%22%3A%7B%22include%22%3A%5B%22private%22%5D%7D%7D%5D%7D%2C%7B%22type%22%3A%22actions%22%2C%22block_id%22%3A%22YWJ%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22datepicker%22%2C%22action_id%22%3A%22actionId-0%22%2C%22initial_date%22%3A%221990-04-28%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+a+date%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22timepicker%22%2C%22action_id%22%3A%22actionId-1%22%2C%22initial_time%22%3A%2213%3A37%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select+time%22%2C%22emoji%22%3Atrue%7D%7D%5D%7D%5D%2C%22private_metadata%22%3A%22%22%2C%22callback_id%22%3A%22%22%2C%22state%22%3A%7B%22values%22%3A%7B%221%3DzGH%22%3A%7B%22sl_input%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22value%22%3A%22ddd%22%7D%7D%2C%227Zh%22%3A%7B%22ml_input%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22value%22%3A%22dddd%22%7D%7D%2C%22cbQh%22%3A%7B%22actionId-0%22%3A%7B%22type%22%3A%22conversations_select%22%2C%22selected_conversation%22%3Anull%7D%2C%22actionId-1%22%3A%7B%22type%22%3A%22channels_select%22%2C%22selected_channel%22%3Anull%7D%2C%22actionId-2%22%3A%7B%22type%22%3A%22users_select%22%2C%22selected_user%22%3Anull%7D%2C%22actionId-3%22%3A%7B%22type%22%3A%22static_select%22%2C%22selected_option%22%3Anull%7D%7D%2C%22an8gR%22%3A%7B%22actionId-0%22%3A%7B%22type%22%3A%22conversations_select%22%2C%22selected_conversation%22%3Anull%7D%7D%2C%22YWJ%22%3A%7B%22actionId-0%22%3A%7B%22type%22%3A%22datepicker%22%2C%22selected_date%22%3A%221990-04-28%22%7D%2C%22actionId-1%22%3A%7B%22type%22%3A%22timepicker%22%2C%22selected_time%22%3A%2202%3A00%22%7D%7D%7D%7D%2C%22hash%22%3A%221615311191.RKSmr8cX%22%2C%22title%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Modal+Title%22%2C%22emoji%22%3Atrue%7D%2C%22clear_on_close%22%3Afalse%2C%22notify_on_close%22%3Afalse%2C%22close%22%3Anull%2C%22submit%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Submit%22%2C%22emoji%22%3Atrue%7D%2C%22previous_view_id%22%3Anull%2C%22root_view_id%22%3A%22V01QKTZBFMK%22%2C%22app_id%22%3A%22A01NK2M4ABE%22%2C%22external_id%22%3A%22%22%2C%22app_installed_team_id%22%3A%22T01ND2P8N91%22%2C%22bot_id%22%3A%22B01MY4ZRP5M%22%7D%2C%22response_urls%22%3A%5B%5D%2C%22is_enterprise_install%22%3Afalse%2C%22enterprise%22%3Anull%7D
+payload%3D%7B%22type%22%3A%22view_submission%22%2C%22team%22%3A%7B%22id%22%3A%22T01ND2P8N91%22%2C%22domain%22%3A%22opsdroidtests%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U01NK1K9L68%22%2C%22username%22%3A%22opsdroid.test%22%2C%22name%22%3A%22opsdroid.test%22%2C%22team_id%22%3A%22T01ND2P8N91%22%7D%2C%22api_app_id%22%3A%22A01NK2M4ABE%22%2C%22token%22%3A%22here-comes-some-token%22%2C%22trigger_id%22%3A%221835952449765.1761091294307.f04f465a970c18de583c065d0043875e%22%2C%22view%22%3A%7B%22id%22%3A%22V01QKTZBFMK%22%2C%22team_id%22%3A%22T01ND2P8N91%22%2C%22type%22%3A%22modal%22%2C%22blocks%22%3A%5B%7B%22type%22%3A%22input%22%2C%22block_id%22%3A%221%3DzGH%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Label%22%2C%22emoji%22%3Atrue%7D%2C%22hint%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Hint%2Btext%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22dispatch_action%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22sl_input%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Placeholder%2Btext%2Bfor%2Bsingle-line%2Binput%22%2C%22emoji%22%3Atrue%7D%2C%22dispatch_action_config%22%3A%7B%22trigger_actions_on%22%3A%5B%22on_enter_pressed%22%5D%7D%7D%7D%2C%7B%22type%22%3A%22input%22%2C%22block_id%22%3A%227Zh%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Label%22%2C%22emoji%22%3Atrue%7D%2C%22hint%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Hint%2Btext%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22dispatch_action%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22ml_input%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Placeholder%2Btext%2Bfor%2Bmulti-line%2Binput%22%2C%22emoji%22%3Atrue%7D%2C%22multiline%22%3Atrue%2C%22dispatch_action_config%22%3A%7B%22trigger_actions_on%22%3A%5B%22on_enter_pressed%22%5D%7D%7D%7D%2C%7B%22type%22%3A%22actions%22%2C%22block_id%22%3A%22cbQh%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22conversations_select%22%2C%22action_id%22%3A%22actionId-0%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Ba%2Bconversation%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22channels_select%22%2C%22action_id%22%3A%22actionId-1%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Ba%2Bchannel%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22users_select%22%2C%22action_id%22%3A%22actionId-2%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Ba%2Buser%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22static_select%22%2C%22action_id%22%3A%22actionId-3%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Ban%2Bitem%22%2C%22emoji%22%3Atrue%7D%2C%22options%22%3A%5B%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22*this%2Bis%2Bplain_text%2Btext*%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22value-0%22%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22*this%2Bis%2Bplain_text%2Btext*%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22value-1%22%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22*this%2Bis%2Bplain_text%2Btext*%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22value-2%22%7D%5D%7D%5D%7D%2C%7B%22type%22%3A%22actions%22%2C%22block_id%22%3A%22an8gR%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22conversations_select%22%2C%22action_id%22%3A%22actionId-0%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Bprivate%2Bconversation%22%2C%22emoji%22%3Atrue%7D%2C%22filter%22%3A%7B%22include%22%3A%5B%22private%22%5D%7D%7D%5D%7D%2C%7B%22type%22%3A%22actions%22%2C%22block_id%22%3A%22YWJ%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22datepicker%22%2C%22action_id%22%3A%22actionId-0%22%2C%22initial_date%22%3A%221990-04-28%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Ba%2Bdate%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22timepicker%22%2C%22action_id%22%3A%22actionId-1%22%2C%22initial_time%22%3A%2213%3A37%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Select%2Btime%22%2C%22emoji%22%3Atrue%7D%7D%5D%7D%5D%2C%22private_metadata%22%3A%22%22%2C%22callback_id%22%3A%22simple_callback_id%22%2C%22state%22%3A%7B%22values%22%3A%7B%221%3DzGH%22%3A%7B%22sl_input%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22value%22%3A%22ddd%22%7D%7D%2C%227Zh%22%3A%7B%22ml_input%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22value%22%3A%22dddd%22%7D%7D%2C%22cbQh%22%3A%7B%22actionId-0%22%3A%7B%22type%22%3A%22conversations_select%22%2C%22selected_conversation%22%3Anull%7D%2C%22actionId-1%22%3A%7B%22type%22%3A%22channels_select%22%2C%22selected_channel%22%3Anull%7D%2C%22actionId-2%22%3A%7B%22type%22%3A%22users_select%22%2C%22selected_user%22%3Anull%7D%2C%22actionId-3%22%3A%7B%22type%22%3A%22static_select%22%2C%22selected_option%22%3Anull%7D%7D%2C%22an8gR%22%3A%7B%22actionId-0%22%3A%7B%22type%22%3A%22conversations_select%22%2C%22selected_conversation%22%3Anull%7D%7D%2C%22YWJ%22%3A%7B%22actionId-0%22%3A%7B%22type%22%3A%22datepicker%22%2C%22selected_date%22%3A%221990-04-28%22%7D%2C%22actionId-1%22%3A%7B%22type%22%3A%22timepicker%22%2C%22selected_time%22%3A%2202%3A00%22%7D%7D%7D%7D%2C%22hash%22%3A%221615311191.RKSmr8cX%22%2C%22title%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Modal%2BTitle%22%2C%22emoji%22%3Atrue%7D%2C%22clear_on_close%22%3Afalse%2C%22notify_on_close%22%3Afalse%2C%22close%22%3Anull%2C%22submit%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Submit%22%2C%22emoji%22%3Atrue%7D%2C%22previous_view_id%22%3Anull%2C%22root_view_id%22%3A%22V01QKTZBFMK%22%2C%22app_id%22%3A%22A01NK2M4ABE%22%2C%22external_id%22%3A%22%22%2C%22app_installed_team_id%22%3A%22T01ND2P8N91%22%2C%22bot_id%22%3A%22B01MY4ZRP5M%22%7D%2C%22response_urls%22%3A%5B%5D%2C%22is_enterprise_install%22%3Afalse%2C%22enterprise%22%3Anull%7D
diff --git a/opsdroid/connector/slack/tests/test_connector.py b/opsdroid/connector/slack/tests/test_connector.py
--- a/opsdroid/connector/slack/tests/test_connector.py
+++ b/opsdroid/connector/slack/tests/test_connector.py
@@ -3,11 +3,16 @@
 
 import asynctest.mock as amock
 import pytest
-from slack_sdk.socket_mode.request import SocketModeRequest
-
 from opsdroid import events
 from opsdroid.connector.slack.connector import SlackApiError
-from opsdroid.connector.slack.events import Blocks, EditedBlocks
+from opsdroid.connector.slack.events import (
+    Blocks,
+    EditedBlocks,
+    ModalOpen,
+    ModalPush,
+    ModalUpdate,
+)
+from slack_sdk.socket_mode.request import SocketModeRequest
 
 from .conftest import get_path
 
@@ -15,6 +20,9 @@
 AUTH_TEST = ("/auth.test", "POST", get_path("method_auth.test.json"), 200)
 CHAT_POST_MESSAGE = ("/chat.postMessage", "POST", {"ok": True}, 200)
 CHAT_UPDATE_MESSAGE = ("/chat.update", "POST", {"ok": True}, 200)
+VIEWS_OPEN = ("/views.open", "POST", {"ok": True}, 200)
+VIEWS_UPDATE = ("/views.update", "POST", {"ok": True}, 200)
+VIEWS_PUSH = ("/views.push", "POST", {"ok": True}, 200)
 REACTIONS_ADD = ("/reactions.add", "POST", {"ok": True}, 200)
 CONVERSATIONS_HISTORY = (
     "/conversations.history",
@@ -312,6 +320,47 @@ async def test_edit_blocks(send_event, connector):
     assert response["ok"]
 
 
+@pytest.mark.asyncio
+@pytest.mark.add_response(*VIEWS_OPEN)
+async def test_open_modal(send_event, connector):
+    event = ModalOpen(trigger_id="123456", view={"key1": "value1", "key2": "value2"})
+    payload, response = await send_event(VIEWS_OPEN, event)
+    assert payload == {
+        "trigger_id": "123456",
+        "view": '{"key1": "value1", "key2": "value2"}',
+    }
+    assert response["ok"]
+
+
+@pytest.mark.asyncio
+@pytest.mark.add_response(*VIEWS_UPDATE)
+async def test_update_modal(send_event, connector):
+    event = ModalUpdate(
+        external_id="123456",
+        view={"key1": "value1", "key2": "value2"},
+        hash_="12345678",
+    )
+    payload, response = await send_event(VIEWS_UPDATE, event)
+    assert payload == {
+        "external_id": "123456",
+        "view": '{"key1": "value1", "key2": "value2"}',
+        "hash": "12345678",
+    }
+    assert response["ok"]
+
+
+@pytest.mark.asyncio
+@pytest.mark.add_response(*VIEWS_PUSH)
+async def test_push_modal(send_event, connector):
+    event = ModalPush(trigger_id="123456", view={"key1": "value1", "key2": "value2"})
+    payload, response = await send_event(VIEWS_PUSH, event)
+    assert payload == {
+        "trigger_id": "123456",
+        "view": '{"key1": "value1", "key2": "value2"}',
+    }
+    assert response["ok"]
+
+
 @pytest.mark.asyncio
 @pytest.mark.add_response(*REACTIONS_ADD)
 async def test_send_reaction(send_event, connector):
diff --git a/opsdroid/connector/slack/tests/test_create_events.py b/opsdroid/connector/slack/tests/test_create_events.py
--- a/opsdroid/connector/slack/tests/test_create_events.py
+++ b/opsdroid/connector/slack/tests/test_create_events.py
@@ -188,7 +188,7 @@ async def receive_slash_command():
 async def test_receive_view_submission(opsdroid, connector, mock_api):
     await opsdroid.load(config=MINIMAL_CONFIG)
 
-    async def receive_message_action():
+    async def receive_view_submission():
         headers, data = get_webhook_payload(
             "payload_view_submission.urlencoded", "urlencoded"
         )
@@ -203,7 +203,7 @@ async def receive_message_action():
 
         return True
 
-    assert await run_unit_test(opsdroid, receive_message_action)
+    assert await run_unit_test(opsdroid, receive_view_submission)
 
 
 @pytest.mark.asyncio