Repository: opsdroid/opsdroid

Issue:
Get multiple responses for one slack input command
<!-- 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 opsdroid with slack connector, there will be multiple responses for one slack input command.  From the debugging log, the same command is been detected and parsed for several times by opsdroid. 

After investigation, it seems the slack will retry if the server took longer than 3 seconds to respond to the previous event delivery attempt (https://api.slack.com/apis/connections/events-api#graceful_retries). Similar with the issue mentioned here https://github.com/slackapi/python-slack-sdk/issues/335

## Steps to Reproduce
Please also include relevant information and steps to reproduce the bug/issue.


## Expected Functionality
Expect opsdroid could provide a config setting for slack to acknowledge the slack API once the command is detected. 


## Experienced Functionality
Get multiple responses for one slack input command.

## Versions
- **Opsdroid version:** 0.24.1
- **Python version:**  3.8
- **OS/Docker version:** Redhat 8

## Configuration File
Please include your version of the configuration file below.

```yaml
# Your code goes here.

```

## Additional Details


<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion:
We also ran into this issue. The slack docs note that one should ack the event before processing it. Right now, it's the other way around in opsdroid:

> Respond to events with a HTTP 200 OK as soon as you can. Avoid actually processing and reacting to events within the same process. Implement a queue to handle inbound events after they are received.

https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events

This is especially problematic if a skill errors after > 3sec in which case slack retries the event multiple times leading quite a bit of error spam in the slack channel.

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
@@ -1,4 +1,5 @@
 """A connector for Slack."""
+import asyncio
 import json
 import logging
 import os
@@ -71,6 +72,16 @@ def __init__(self, config, opsdroid=None):
         self.known_users = {}
 
         self._event_creator = SlackEventCreator(self)
+        self._event_queue = asyncio.Queue()
+        self._event_queue_task = None
+
+    async def _queue_worker(self):
+        while True:
+            payload = await self._event_queue.get()
+            try:
+                await self.event_handler(payload)
+            finally:
+                self._event_queue.task_done()
 
     async def connect(self):
         """Connect to the chat service."""
@@ -108,6 +119,10 @@ async def connect(self):
                 await self.socket_mode_client.connect()
                 _LOGGER.info(_("Connected successfully with socket mode"))
             else:
+                # Create a task for background processing events received by
+                # the web event handler.
+                self._event_queue_task = asyncio.create_task(self._queue_worker())
+
                 self.opsdroid.web_server.web_app.router.add_post(
                     f"/connector/{self.name}",
                     self.web_event_handler,
@@ -119,8 +134,13 @@ async def connect(self):
             _LOGGER.debug(_("Default room is %s."), self.default_target)
 
     async def disconnect(self):
-        """Disconnect from Slack. Only needed when socket_mode_client is used
-        as the Events API uses the aiohttp server"""
+        """Disconnect from Slack.
+
+        Cancels the event queue worker task and disconnects the
+        socket_mode_client if socket mode was enabled."""
+        if self._event_queue_task:
+            self._event_queue_task.cancel()
+            await asyncio.gather(self._event_queue_task, return_exceptions=True)
 
         if self.socket_mode_client:
             await self.socket_mode_client.disconnect()
@@ -195,7 +215,13 @@ async def web_event_handler(self, request):
         if payload.get("type") == "url_verification":
             return aiohttp.web.json_response({"challenge": payload["challenge"]})
 
-        await self.event_handler(payload)
+        # Put the event in the queue to process it in the background and
+        # immediately acknowledge the reception by returning status code 200.
+        # Slack will resend events that have not been acknowledged within 3
+        # seconds and we want to avoid that.
+        #
+        # https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+        self._event_queue.put_nowait(payload)
 
         return aiohttp.web.Response(text=json.dumps("Received"), status=200)
 


Test patch: