Repository: opsdroid/opsdroid

Issue:
Slack integration uses old API/Token method
# Description
Opsdroid's Slack integration wants to operate off of a Webhook or API Token, which seems to be a deprecating (not yet deprecated) method of integrating with Slack.  See attached screenshot of the warning message they display when trying to do a 'Webhook' or generic 'Bot' integration in an Enterprise Slack instance.  


## Steps to Reproduce
Try to set up a 'Webhook' or 'API/Bot' integration in the usual manner: 
Menu -> Customize Slack -> Configure Apps -> Custom Integrations

Observe Slack's notice that "Custom integrations will be deprecated and possibly removed in the future.  Learn more about replacing them with apps that have more features and use the latest APIs" 


## Expected Functionality
Ideally, Opsdroid would be able to integrate with Slack using their shiny Apps/not-Webhook API, and I don't have to go through a Security Team review because of the deprecation warning :-) 


## Experienced Functionality
Explain what happened instead(Please include the debug log).

## Versions
- **Opsdroid version:** latest
- **Python version:** 3.2
- **OS/Docker version:** latest

## Configuration File
n/a

## Additional Details

[Custom Integration warning](https://i.imgur.com/fWWFXHw.png)


Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This may be resolved with #1036 

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
@@ -1,13 +1,8 @@
 """A connector for Slack."""
 import logging
-import asyncio
-import json
 import re
 
-import aiohttp
-import websockets
-import slacker
-from aioslacker import Slacker
+import slack
 from emoji import demojize
 
 from opsdroid.connector import Connector, register_event
@@ -30,37 +25,48 @@ def __init__(self, config, opsdroid=None):
         self.icon_emoji = config.get("icon-emoji", ":robot_face:")
         self.token = config["api-token"]
         self.timeout = config.get("connect-timeout", 10)
-        self.slacker = Slacker(token=self.token, timeout=self.timeout)
+        self.slack = slack.WebClient(token=self.token, run_async=True)
+        self.slack_rtm = slack.RTMClient(token=self.token, run_async=True)
         self.websocket = None
         self.bot_name = config.get("bot-name", "opsdroid")
+        self.auth_info = None
+        self.user_info = None
+        self.bot_id = None
         self.known_users = {}
         self.keepalive = None
         self.reconnecting = False
         self.listening = True
         self._message_id = 0
 
+        # Register callbacks
+        slack.RTMClient.on(event="message", callback=self.process_message)
+
     async def connect(self):
         """Connect to the chat service."""
         _LOGGER.info("Connecting to Slack")
 
         try:
-            connection = await self.slacker.rtm.start()
-            self.websocket = await websockets.connect(connection.body["url"])
+            # The slack library recommends you call `self.slack_rtm.start()`` here but it
+            # seems to mess with the event loop's signal handlers which breaks opsdroid.
+            # Therefore we need to directly call the private `_connect_and_read` method
+            # instead. This method also blocks so we need to dispatch it to the loop as a task.
+            self.opsdroid.eventloop.create_task(self.slack_rtm._connect_and_read())
+
+            self.auth_info = (await self.slack.api_call("auth.test")).data
+            self.user_info = (
+                await self.slack.api_call(
+                    "users.info",
+                    http_verb="GET",
+                    params={"user": self.auth_info["user_id"]},
+                )
+            ).data
+            self.bot_id = self.user_info["user"]["profile"]["bot_id"]
 
             _LOGGER.debug("Connected as %s", self.bot_name)
             _LOGGER.debug("Using icon %s", self.icon_emoji)
             _LOGGER.debug("Default room is %s", self.default_target)
             _LOGGER.info("Connected successfully")
-
-            if self.keepalive is None or self.keepalive.done():
-                self.keepalive = self.opsdroid.eventloop.create_task(
-                    self.keepalive_websocket()
-                )
-        except aiohttp.ClientOSError as error:
-            _LOGGER.error(error)
-            _LOGGER.error("Failed to connect to Slack, retrying in 10")
-            await self.reconnect(10)
-        except slacker.Error as error:
+        except slack.errors.SlackApiError as error:
             _LOGGER.error(
                 "Unable to connect to Slack due to %s - "
                 "The Slack Connector will not be available.",
@@ -70,83 +76,67 @@ async def connect(self):
             await self.disconnect()
             raise
 
-    async def reconnect(self, delay=None):
-        """Reconnect to the websocket."""
-        try:
-            self.reconnecting = True
-            if delay is not None:
-                await asyncio.sleep(delay)
-            await self.connect()
-        finally:
-            self.reconnecting = False
-
     async def disconnect(self):
         """Disconnect from Slack."""
-        await self.slacker.close()
+        await self.slack_rtm.stop()
+        self.listening = False
 
     async def listen(self):
         """Listen for and parse new messages."""
-        while self.listening:
-            try:
-                await self.receive_from_websocket()
-            except AttributeError:
-                break
-
-    async def receive_from_websocket(self):
-        """Get the next message from the websocket."""
-        try:
-            content = await self.websocket.recv()
-            await self.process_message(json.loads(content))
-        except websockets.exceptions.ConnectionClosed:
-            _LOGGER.info("Slack websocket closed, reconnecting...")
-            await self.reconnect(5)
 
-    async def process_message(self, message):
+    async def process_message(self, **payload):
         """Process a raw message and pass it to the parser."""
-        if "type" in message and message["type"] == "message" and "user" in message:
-
-            # Ignore bot messages
-            if "subtype" in message and message["subtype"] == "bot_message":
-                return
-
-            # Lookup username
-            _LOGGER.debug("Looking up sender username")
-            try:
-                user_info = await self.lookup_username(message["user"])
-            except ValueError:
-                return
-
-            # Replace usernames in the message
-            _LOGGER.debug("Replacing userids in message with usernames")
-            message["text"] = await self.replace_usernames(message["text"])
-
-            await self.opsdroid.parse(
-                Message(
-                    message["text"],
-                    user_info["name"],
-                    message["channel"],
-                    self,
-                    raw_event=message,
-                )
+        message = payload["data"]
+
+        # Ignore own messages
+        if (
+            "subtype" in message
+            and message["subtype"] == "bot_message"
+            and message["bot_id"] == self.bot_id
+        ):
+            return
+
+        # Lookup username
+        _LOGGER.debug("Looking up sender username")
+        try:
+            user_info = await self.lookup_username(message["user"])
+        except ValueError:
+            return
+
+        # Replace usernames in the message
+        _LOGGER.debug("Replacing userids in message with usernames")
+        message["text"] = await self.replace_usernames(message["text"])
+
+        await self.opsdroid.parse(
+            Message(
+                message["text"],
+                user_info["name"],
+                message["channel"],
+                self,
+                raw_event=message,
             )
+        )
 
     @register_event(Message)
     async def send_message(self, message):
         """Respond with a message."""
         _LOGGER.debug("Responding with: '%s' in room  %s", message.text, message.target)
-        await self.slacker.chat.post_message(
-            message.target,
-            message.text,
-            as_user=False,
-            username=self.bot_name,
-            icon_emoji=self.icon_emoji,
+        await self.slack.api_call(
+            "chat.postMessage",
+            data={
+                "channel": message.target,
+                "text": message.text,
+                "as_user": False,
+                "username": self.bot_name,
+                "icon_emoji": self.icon_emoji,
+            },
         )
 
     @register_event(Blocks)
     async def send_blocks(self, blocks):
         """Respond with structured blocks."""
         _LOGGER.debug("Responding with interactive blocks in room  %s", blocks.target)
-        await self.slacker.chat.post(
+        await self.slack.api_call(
             "chat.postMessage",
             data={
                 "channel": blocks.target,
@@ -162,7 +152,7 @@ async def send_reaction(self, reaction):
         emoji = demojize(reaction.emoji).replace(":", "")
         _LOGGER.debug("Reacting with: %s", emoji)
         try:
-            await self.slacker.reactions.post(
+            await self.slack.api_call(
                 "reactions.add",
                 data={
                     "name": emoji,
@@ -170,42 +160,19 @@ async def send_reaction(self, reaction):
                     "timestamp": reaction.linked_event.raw_event["ts"],
                 },
             )
-        except slacker.Error as error:
-            if str(error) == "invalid_name":
+        except slack.errors.SlackApiError as error:
+            if "invalid_name" in str(error):
                 _LOGGER.warning("Slack does not support the emoji %s", emoji)
             else:
                 raise
 
-    async def keepalive_websocket(self):
-        """Keep pinging the websocket to keep it alive."""
-        while self.listening:
-            await self.ping_websocket()
-
-    async def ping_websocket(self):
-        """Ping the websocket."""
-        await asyncio.sleep(60)
-        self._message_id += 1
-        try:
-            await self.websocket.send(
-                json.dumps({"id": self._message_id, "type": "ping"})
-            )
-        except (
-            websockets.exceptions.InvalidState,
-            websockets.exceptions.ConnectionClosed,
-            aiohttp.ClientOSError,
-            TimeoutError,
-        ):
-            _LOGGER.info("Slack websocket closed, reconnecting...")
-            if not self.reconnecting:
-                await self.reconnect()
-
     async def lookup_username(self, userid):
         """Lookup a username and cache it."""
         if userid in self.known_users:
             user_info = self.known_users[userid]
         else:
-            response = await self.slacker.users.info(userid)
-            user_info = response.body["user"]
+            response = await self.slack.users_info(user=userid)
+            user_info = response.data["user"]
             if isinstance(user_info, dict):
                 self.known_users[userid] = user_info
             else:


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
@@ -5,7 +5,7 @@
 import unittest.mock as mock
 import asynctest
 import asynctest.mock as amock
-import slacker
+import slack
 
 from opsdroid.core import OpsDroid
 from opsdroid.connector.slack import ConnectorSlack
@@ -66,84 +66,39 @@ async def test_connect(self):
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
         opsdroid = amock.CoroutineMock()
         opsdroid.eventloop = self.loop
-        connector.slacker.rtm.start = amock.CoroutineMock()
-        connector.keepalive_websocket = amock.CoroutineMock()
-        with amock.patch(
-            "websockets.connect", new=amock.CoroutineMock()
-        ) as mocked_websocket_connect:
-            await connector.connect()
-        self.assertTrue(connector.slacker.rtm.start.called)
-        self.assertTrue(mocked_websocket_connect.called)
-        self.assertTrue(connector.keepalive_websocket.called)
+        connector.slack_rtm._connect_and_read = amock.CoroutineMock()
+        connector.slack.api_call = amock.CoroutineMock()
+        await connector.connect()
+        self.assertTrue(connector.slack_rtm._connect_and_read.called)
+        self.assertTrue(connector.slack.api_call.called)
 
     async def test_connect_auth_fail(self):
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
         opsdroid = amock.CoroutineMock()
         opsdroid.eventloop = self.loop
-        connector.slacker.rtm.start = amock.CoroutineMock()
-        connector.slacker.rtm.start.side_effect = slacker.Error()
+        connector.slack_rtm._connect_and_read = amock.Mock()
+        connector.slack_rtm._connect_and_read.side_effect = slack.errors.SlackApiError(
+            message="", response=""
+        )
 
         await connector.connect()
         self.assertLogs("_LOGGER", "error")
 
-    async def test_reconnect_on_error(self):
-        import aiohttp
-
-        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.slacker.rtm.start = amock.CoroutineMock()
-        connector.slacker.rtm.start.side_effect = aiohttp.ClientOSError()
-        connector.reconnect = amock.CoroutineMock()
-
-        await connector.connect()
-        self.assertTrue(connector.reconnect.called)
-
     async def test_abort_on_connection_error(self):
         connector = ConnectorSlack({"api-token": "abc123"})
-        connector.slacker.rtm.start = amock.CoroutineMock()
-        connector.slacker.rtm.start.side_effect = Exception()
-        connector.slacker.close = amock.CoroutineMock()
+        connector.slack_rtm._connect_and_read = amock.CoroutineMock()
+        connector.slack_rtm._connect_and_read.side_effect = Exception()
+        connector.slack_rtm.stop = amock.CoroutineMock()
 
         with self.assertRaises(Exception):
             await connector.connect()
-        self.assertTrue(connector.slacker.close.called)
+        self.assertTrue(connector.slack_rtm.stop.called)
 
     async def test_listen_loop(self):
         """Test that listening consumes from the socket."""
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.receive_from_websocket = amock.CoroutineMock()
-        connector.receive_from_websocket.side_effect = Exception()
-        with self.assertRaises(Exception):
-            await connector.listen()
-        self.assertTrue(connector.receive_from_websocket.called)
-
-    async def test_listen_break_loop(self):
-        """Test that listening consumes from the socket."""
-        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.receive_from_websocket = amock.CoroutineMock()
-        connector.receive_from_websocket.side_effect = AttributeError
+        connector.listening = False
         await connector.listen()
-        self.assertTrue(connector.receive_from_websocket.called)
-
-    async def test_receive_from_websocket(self):
-        """Test receive_from_websocket receives and reconnects."""
-        import websockets
-
-        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-
-        connector.websocket = amock.CoroutineMock()
-        connector.websocket.recv = amock.CoroutineMock()
-        connector.websocket.recv.return_value = "[]"
-        connector.process_message = amock.CoroutineMock()
-        await connector.receive_from_websocket()
-        self.assertTrue(connector.websocket.recv.called)
-        self.assertTrue(connector.process_message.called)
-
-        connector.websocket.recv.side_effect = websockets.exceptions.ConnectionClosed(
-            500, "Mock Error"
-        )
-        connector.reconnect = amock.CoroutineMock()
-        await connector.receive_from_websocket()
-        self.assertTrue(connector.reconnect.called)
 
     async def test_process_message(self):
         """Test processing a slack message."""
@@ -161,81 +116,57 @@ async def test_process_message(self):
             "ts": "1355517523.000005",
             "edited": {"user": "U2147483697", "ts": "1355517536.000001"},
         }
-        await connector.process_message(message)
+        await connector.process_message(data=message)
         self.assertTrue(connector.opsdroid.parse.called)
 
         connector.opsdroid.parse.reset_mock()
+        message["bot_id"] = "abc"
         message["subtype"] = "bot_message"
-        await connector.process_message(message)
+        connector.bot_id = message["bot_id"]
+        await connector.process_message(data=message)
         self.assertFalse(connector.opsdroid.parse.called)
+        del message["bot_id"]
         del message["subtype"]
+        connector.bot_id = None
 
         connector.opsdroid.parse.reset_mock()
         connector.lookup_username.side_effect = ValueError
-        await connector.process_message(message)
+        await connector.process_message(data=message)
         self.assertFalse(connector.opsdroid.parse.called)
 
-    async def test_keepalive_websocket_loop(self):
-        """Test that listening consumes from the socket."""
-        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.ping_websocket = amock.CoroutineMock()
-        connector.ping_websocket.side_effect = Exception()
-        with self.assertRaises(Exception):
-            await connector.keepalive_websocket()
-        self.assertTrue(connector.ping_websocket.called)
-
-    async def test_ping_websocket(self):
-        """Test pinging the websocket."""
-        import websockets
-
-        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        with amock.patch("asyncio.sleep", new=amock.CoroutineMock()) as mocked_sleep:
-            connector.websocket = amock.CoroutineMock()
-            connector.websocket.send = amock.CoroutineMock()
-            await connector.ping_websocket()
-            self.assertTrue(mocked_sleep.called)
-            self.assertTrue(connector.websocket.send.called)
-
-            connector.reconnect = amock.CoroutineMock()
-            connector.websocket.send.side_effect = websockets.exceptions.ConnectionClosed(
-                500, "Mock Error"
-            )
-            await connector.ping_websocket()
-            self.assertTrue(connector.reconnect.called)
-
     async def test_lookup_username(self):
         """Test that looking up a username works and that it caches."""
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.slacker.users.info = amock.CoroutineMock()
+        connector.slack.users_info = amock.CoroutineMock()
         mock_user = mock.Mock()
-        mock_user.body = {"user": {"name": "testuser"}}
-        connector.slacker.users.info.return_value = mock_user
+        mock_user.data = {"user": {"name": "testuser"}}
+        connector.slack.users_info.return_value = mock_user
 
         self.assertEqual(len(connector.known_users), 0)
 
         await connector.lookup_username("testuser")
         self.assertTrue(len(connector.known_users), 1)
-        self.assertTrue(connector.slacker.users.info.called)
+        self.assertTrue(connector.slack.users_info.called)
 
-        connector.slacker.users.info.reset_mock()
+        connector.slack.users_info.reset_mock()
         await connector.lookup_username("testuser")
         self.assertEqual(len(connector.known_users), 1)
-        self.assertFalse(connector.slacker.users.info.called)
+        self.assertFalse(connector.slack.users_info.called)
 
         with self.assertRaises(ValueError):
-            mock_user.body = {"user": None}
-            connector.slacker.users.info.return_value = mock_user
+            mock_user.data = {"user": None}
+            connector.slack.users_info.return_value = mock_user
             await connector.lookup_username("invaliduser")
 
     async def test_respond(self):
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.slacker.chat.post_message = amock.CoroutineMock()
+        connector.slack.api_call = amock.CoroutineMock()
         await connector.send(Message("test", "user", "room", connector))
-        self.assertTrue(connector.slacker.chat.post_message.called)
+        self.assertTrue(connector.slack.api_call.called)
 
     async def test_send_blocks(self):
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.slacker.chat.post = amock.CoroutineMock()
+        connector.slack.api_call = amock.CoroutineMock()
         await connector.send(
             Blocks(
                 [{"type": "section", "text": {"type": "mrkdwn", "text": "*Test*"}}],
@@ -244,26 +175,25 @@ async def test_send_blocks(self):
                 connector,
             )
         )
-        self.assertTrue(connector.slacker.chat.post.called)
+        self.assertTrue(connector.slack.api_call.called)
 
     async def test_react(self):
         connector = ConnectorSlack({"api-token": "abc123"})
-        connector.slacker.reactions.post = amock.CoroutineMock()
+        connector.slack.api_call = amock.CoroutineMock()
         prev_message = Message("test", "user", "room", connector, raw_event={"ts": 0})
         with OpsDroid() as opsdroid:
             await prev_message.respond(Reaction("😀"))
-        self.assertTrue(connector.slacker.reactions.post)
+        self.assertTrue(connector.slack.api_call)
         self.assertEqual(
-            connector.slacker.reactions.post.call_args[1]["data"]["name"],
-            "grinning_face",
+            connector.slack.api_call.call_args[1]["data"]["name"], "grinning_face"
         )
 
     async def test_react_invalid_name(self):
-        import slacker
+        import slack
 
         connector = ConnectorSlack({"api-token": "abc123"})
-        connector.slacker.reactions.post = amock.CoroutineMock(
-            side_effect=slacker.Error("invalid_name")
+        connector.slack.api_call = amock.CoroutineMock(
+            side_effect=slack.errors.SlackApiError("invalid_name", "invalid_name")
         )
         prev_message = Message("test", "user", "room", connector, raw_event={"ts": 0})
         with OpsDroid() as opsdroid:
@@ -271,26 +201,18 @@ async def test_react_invalid_name(self):
         self.assertLogs("_LOGGER", "warning")
 
     async def test_react_unknown_error(self):
-        import slacker
+        import slack
 
         connector = ConnectorSlack({"api-token": "abc123"})
-        connector.slacker.reactions.post = amock.CoroutineMock(
-            side_effect=slacker.Error("unknown")
+        connector.slack.api_call = amock.CoroutineMock(
+            side_effect=slack.errors.SlackApiError("unknown", "unknown")
         )
-        with self.assertRaises(slacker.Error), OpsDroid() as opsdroid:
+        with self.assertRaises(slack.errors.SlackApiError), OpsDroid() as opsdroid:
             prev_message = Message(
                 "test", "user", "room", connector, raw_event={"ts": 0}
             )
             await prev_message.respond(Reaction("😀"))
 
-    async def test_reconnect(self):
-        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
-        connector.connect = amock.CoroutineMock()
-        with amock.patch("asyncio.sleep") as mocked_sleep:
-            await connector.reconnect(10)
-            self.assertTrue(connector.connect.called)
-            self.assertTrue(mocked_sleep.called)
-
     async def test_replace_usernames(self):
         connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
         connector.lookup_username = amock.CoroutineMock()