Repository: opsdroid/opsdroid

Issue:
Slash commands do not work
<!-- 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

Using slash commands do not work. There is no event type for them, no docs on using them and opsdroid seems to error if you try.


## Steps to Reproduce

- Create a slash command for your bot
- Use the slash command in Slack
- See opsdroid output


## Expected Functionality

An event should be triggered which can be matched with `match_event`.


## Experienced Functionality

```
EBUG slack_sdk.socket_mode.aiohttp: Received message (type: TEXT, data: {"envelope_id":"c98bb0d2-2522-49e4-ae77-f453bc25055a","payload":{"token":"uLr3No9RxHh6kKW2DbbPMrtK","team_id":"T033XLNMP","team_domain":"jacobtomlinson","channel_id":"D01RFGW6QN9","channel_name":"directmessage","user_id":"U033XLNMR","user_name":"jacobtomlinson","command":"\/opsdroid","text":"","api_app_id":"A01R3RSRBMM","is_enterprise_install":"false","response_url":"https:\/\/hooks.slack.com\/commands\/T033XLNMP\/2212151098501\/CWmAvXOf6OphspoPEPaLyjVz","trigger_id":"2227832050993.3133702737.e40bf7f6909be8a7b97c5cda5b226627"},"type":"slash_commands","accepts_response_payload":true}, extra: )
DEBUG slack_sdk.socket_mode.aiohttp: A new message enqueued (current queue size: 1)
DEBUG slack_sdk.socket_mode.aiohttp: Message processing started (type: slash_commands, envelope_id: c98bb0d2-2522-49e4-ae77-f453bc25055a)
DEBUG slack_sdk.socket_mode.aiohttp: Sending a message: {"envelope_id": "c98bb0d2-2522-49e4-ae77-f453bc25055a"}
ERROR slack_sdk.socket_mode.aiohttp: Failed to run a request listener: local variable 'event' referenced before assignment
Traceback (most recent call last):
  File "/Users/jacob/mambaforge/envs/opsdroid/lib/python3.8/site-packages/slack_sdk/socket_mode/async_client.py", line 133, in run_message_listeners
    await listener(self, request)
  File "/Users/jacob/Projects/opsdroid/opsdroid/opsdroid/connector/slack/connector.py", line 166, in socket_event_handler
    await self.event_handler(payload)
  File "/Users/jacob/Projects/opsdroid/opsdroid/opsdroid/connector/slack/connector.py", line 147, in event_handler
    if isinstance(event, list):
UnboundLocalError: local variable 'event' referenced before assignment
DEBUG slack_sdk.socket_mode.aiohttp: Message processing completed (type: slash_commands, envelope_id: c98bb0d2-2522-49e4-ae77-f453bc25055a)
```

## Versions
- **Opsdroid version:** 0.22.0
- **Python version:** 0.3.8
- **OS/Docker version:** macos 11.2.3

## Configuration File
Please include your version of the configuration file below.

```yaml
# Your code goes here.

```

## 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
@@ -130,6 +130,10 @@ async def listen(self):
         """Listen for and parse new messages."""
 
     async def event_handler(self, payload):
+        """Handle different payload types and parse the resulting events"""
+
+        if "command" in payload:
+            payload["type"] = "command"
 
         if "type" in payload:
             if payload["type"] == "event_callback":
@@ -137,12 +141,12 @@ async def event_handler(self, payload):
             else:
                 event = await self._event_creator.create_event(payload, None)
 
-                if not event:
-                    _LOGGER.info(
-                        f"Payload: {payload['type']} is not implemented. Event wont be parsed"
-                    )
+        if not event:
+            _LOGGER.error(
+                "Payload: %s is not implemented. Event wont be parsed", payload
+            )
 
-                    return
+            return
 
         if isinstance(event, list):
             for e in event:
@@ -180,15 +184,18 @@ async def web_event_handler(self, request):
 
         if request.content_type == "application/x-www-form-urlencoded":
             req = await request.post()
-            payload = json.loads(req["payload"])
+
+            if "payload" in req:
+                payload = json.loads(req["payload"])
+            else:
+                payload = dict(req)
         elif request.content_type == "application/json":
             payload = await request.json()
 
-        if "type" in payload:
-            if payload["type"] == "url_verification":
-                return aiohttp.web.json_response({"challenge": payload["challenge"]})
-            else:
-                await self.event_handler(payload)
+        if payload.get("type") == "url_verification":
+            return aiohttp.web.json_response({"challenge": payload["challenge"]})
+
+        await self.event_handler(payload)
 
         return aiohttp.web.Response(text=json.dumps("Received"), status=200)
 
@@ -288,13 +295,12 @@ async def _send_message(self, message):
         }
 
         if message.linked_event:
-            if "thread_ts" in message.linked_event.raw_event:
-                if (
-                    message.linked_event.event_id
-                    != message.linked_event.raw_event["thread_ts"]
-                ):
+            raw_event = message.linked_event.raw_event
+
+            if isinstance(raw_event, dict) and "thread_ts" in raw_event:
+                if message.linked_event.event_id != raw_event["thread_ts"]:
                     # Linked Event is inside a thread
-                    data["thread_ts"] = message.linked_event.raw_event["thread_ts"]
+                    data["thread_ts"] = raw_event["thread_ts"]
             elif self.start_thread:
                 data["thread_ts"] = message.linked_event.event_id
 
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
@@ -27,6 +27,7 @@ def __init__(self, connector, *args, **kwargs):
         self.event_types["message_action"] = self.message_action_triggered
         self.event_types["view_submission"] = self.view_submission_triggered
         self.event_types["view_closed"] = self.view_closed_triggered
+        self.event_types["command"] = self.slash_command
 
         self.message_subtypes = defaultdict(lambda: self.create_message)
         self.message_subtypes.update(
@@ -257,3 +258,15 @@ async def view_closed_triggered(self, event, channel):
             target=event["user"]["id"],
             connector=self.connector,
         )
+
+    async def slash_command(self, event, channel):
+        """Send a Slash command event"""
+
+        command = slack_events.SlashCommand(
+            payload=event,
+            user=event["user_id"],
+            target=event["channel_id"],
+            connector=self.connector,
+        )
+        command.update_entity("command", event["command"])
+        return command
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
@@ -6,7 +6,6 @@
 
 import aiohttp
 import certifi
-
 from opsdroid import events
 
 _LOGGER = logging.getLogger(__name__)
@@ -89,7 +88,11 @@ async def respond(self, response_event):
                         headers=headers,
                         ssl=self.ssl_context,
                     )
-                    response_txt = await response.json()
+
+                    if response.content_type == "application/json":
+                        response_txt = await response.json()
+                    elif response.content_type == "text/html":
+                        response_txt = await response.text()
             else:
                 response_txt = {"error": "Response URL not available in payload."}
         else:
@@ -125,9 +128,32 @@ def __init__(self, payload, *args, **kwargs):
 class ViewClosed(InteractiveAction):
     """Event class to represent view_closed in Slack."""
 
-    def __init__(self, payload, *args, **kwargs):
-        """Create object with minimum properties."""
-        super().__init__(payload, *args, **kwargs)
+
+class SlashCommand(InteractiveAction):
+    """
+    Event class to represent a slash command.
+
+    args:
+        payload: Incomming payload from the Slack API
+
+    **Basic Usage in a Skill:**
+
+    .. code-block:: python
+
+        from opsdroid.skill import Skill
+        from opsdroid.matchers import match_regex
+
+        class CommandsSkill(Skill):
+            @match_event(SlashCommand, command="/testcommand")
+            async def slash_command(self, event):
+
+                cmd = event.payload["command"]
+
+                # event.payload["text"] holds the arguments from the command
+                arguments = event.payload["text"]
+
+                await message.respond(f"{cmd} {arguments}")
+    """
 
 
 class ChannelArchived(events.Event):


Test patch:
diff --git a/opsdroid/connector/slack/tests/responses/payload_slash_command.urlencoded b/opsdroid/connector/slack/tests/responses/payload_slash_command.urlencoded
new file mode 100644
--- /dev/null
+++ b/opsdroid/connector/slack/tests/responses/payload_slash_command.urlencoded
@@ -0,0 +1 @@
+token=here-comes-some-token&team_id=T01ND2P8N91&team_domain=opsdroidtests&channel_id=C01N639ECTY&channel_name=random&user_id=U01NK1K9L68&user_name=opsdroid.test&command=%2Ftestcommand&text=&api_app_id=A01NK2M4ABE&is_enterprise_install=false&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT01ND2P8N91%2F2086715431956%2FxyP8iEhnQ0BjWLowcMLSPaSX&trigger_id=2093168434705.1761091294307.7f8d01a322363a013721e32e83af484b
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
@@ -4,6 +4,7 @@
 import json
 
 import pytest
+
 from opsdroid.connector.slack.create_events import SlackEventCreator
 from opsdroid.testing import MINIMAL_CONFIG, call_endpoint, run_unit_test
 
@@ -157,6 +158,30 @@ async def receive_message_action():
     assert await run_unit_test(opsdroid, receive_message_action)
 
 
+@pytest.mark.asyncio
+@pytest.mark.add_response(*USERS_INFO)
+@pytest.mark.add_response(*AUTH_TEST)
+async def test_receive_slash_command(opsdroid, connector, mock_api):
+    await opsdroid.load(config=MINIMAL_CONFIG)
+
+    async def receive_slash_command():
+        headers, data = get_webhook_payload(
+            "payload_slash_command.urlencoded", "urlencoded"
+        )
+        resp = await call_endpoint(
+            opsdroid,
+            CONNECTOR_ENDPOINT,
+            "POST",
+            data=data,
+            headers=headers,
+        )
+        assert resp.status == 200
+
+        return True
+
+    assert await run_unit_test(opsdroid, receive_slash_command)
+
+
 @pytest.mark.asyncio
 @pytest.mark.add_response(*USERS_INFO)
 @pytest.mark.add_response(*AUTH_TEST)
diff --git a/opsdroid/connector/slack/tests/test_events.py b/opsdroid/connector/slack/tests/test_events.py
--- a/opsdroid/connector/slack/tests/test_events.py
+++ b/opsdroid/connector/slack/tests/test_events.py
@@ -14,6 +14,14 @@ async def test_respond_response_url_exists(mock_api_obj, mock_api):
     assert response["ok"]
 
 
+@pytest.mark.asyncio
+@pytest.mark.add_response("/", "POST", "ok", 200)
+async def test_respond_response_text_content_type(mock_api_obj, mock_api):
+    action = InteractiveAction({"response_url": mock_api_obj.base_url})
+    response = await action.respond("this is a test response")
+    assert response == "ok"
+
+
 @pytest.mark.asyncio
 async def test_respond_event_response_url_does_not_exist(mock_api_obj, mock_api):
     action = InteractiveAction({})
diff --git a/opsdroid/testing/external_api.py b/opsdroid/testing/external_api.py
--- a/opsdroid/testing/external_api.py
+++ b/opsdroid/testing/external_api.py
@@ -105,6 +105,8 @@ async def _handler(self, request: web.Request) -> web.Response:
         self._payloads[route].append(await request.post())
 
         status, response = self.responses[(route, method)].pop(0)
+        if isinstance(response, str):
+            return web.Response(text=response, status=status, content_type="text/html")
         return web.json_response(response, status=status)
 
     @property