async def test_issue_611(self):
        channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        text = "This message was sent by <https://slack.dev/python-slackclient/|python-slackclient>! (test_issue_611)"

        self.message_count, self.reaction_count = 0, 0

        async def process_messages(**payload):
            self.logger.info(payload)
            if ("subtype" in payload["data"]
                    and payload["data"]["subtype"] == "message_replied"):
                return  # skip

            self.message_count += 1
            raise Exception("something is wrong!"
                            )  # This causes the termination of the process

        async def process_reactions(**payload):
            self.logger.info(payload)
            self.reaction_count += 1

        rtm = RTMClient(token=self.bot_token, run_async=True)
        RTMClient.on(event="message", callback=process_messages)
        RTMClient.on(event="reaction_added", callback=process_reactions)

        web_client = WebClient(token=self.bot_token, run_async=True)
        message = await web_client.chat_postMessage(channel=channel_id,
                                                    text=text)
        ts = message["ts"]

        await asyncio.sleep(3)

        # intentionally not waiting here
        rtm.start()

        try:
            await asyncio.sleep(3)

            first_reaction = await web_client.reactions_add(channel=channel_id,
                                                            timestamp=ts,
                                                            name="eyes")
            self.assertFalse("error" in first_reaction)
            await asyncio.sleep(2)

            should_be_ignored = await web_client.chat_postMessage(
                channel=channel_id, text="Hello?", thread_ts=ts)
            self.assertFalse("error" in should_be_ignored)
            await asyncio.sleep(2)

            second_reaction = await web_client.reactions_add(
                channel=channel_id, timestamp=ts, name="tada")
            self.assertFalse("error" in second_reaction)
            await asyncio.sleep(2)

            self.assertEqual(self.message_count, 1)
            self.assertEqual(self.reaction_count, 2)
        finally:
            if not rtm._stopped:
                rtm.stop()
Exemple #2
0
    async def test_issue_558(self):
        channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        text = "This message was sent by <https://slack.dev/python-slackclient/|python-slackclient>! (test_issue_558)"

        self.message_count, self.reaction_count = 0, 0

        async def process_messages(**payload):
            self.logger.debug(payload)
            self.message_count += 1
            await asyncio.sleep(10)  # this used to block all other handlers

        async def process_reactions(**payload):
            self.logger.debug(payload)
            self.reaction_count += 1

        rtm = RTMClient(token=self.bot_token, run_async=True)
        RTMClient.on(event="message", callback=process_messages)
        RTMClient.on(event="reaction_added", callback=process_reactions)

        web_client = WebClient(token=self.bot_token, run_async=True)
        message = await web_client.chat_postMessage(channel=channel_id,
                                                    text=text)
        self.assertFalse("error" in message)
        ts = message["ts"]
        await asyncio.sleep(3)

        # intentionally not waiting here
        rtm.start()
        await asyncio.sleep(3)

        try:
            first_reaction = await web_client.reactions_add(channel=channel_id,
                                                            timestamp=ts,
                                                            name="eyes")
            self.assertFalse("error" in first_reaction)
            await asyncio.sleep(2)

            message = await web_client.chat_postMessage(channel=channel_id,
                                                        text=text)
            self.assertFalse("error" in message)
            # used to start blocking here

            # This reaction_add event won't be handled due to a bug
            second_reaction = await web_client.reactions_add(
                channel=channel_id, timestamp=ts, name="tada")
            self.assertFalse("error" in second_reaction)
            await asyncio.sleep(2)

            self.assertEqual(self.message_count, 1)
            self.assertEqual(self.reaction_count, 2)  # used to fail
        finally:
            if not rtm._stopped:
                rtm.stop()
 async def test_issue_530_async(self):
     try:
         rtm_client = RTMClient(token="I am not a token", run_async=True)
         await rtm_client.start()
         self.fail("Raising an error here was expected")
     except Exception as e:
         self.assertEqual(
             "The request to the Slack API failed.\n"
             "The server responded with: {'ok': False, 'error': 'invalid_auth'}",
             str(e),
         )
     finally:
         if not rtm_client._stopped:
             rtm_client.stop()
Exemple #4
0
class ConnectorSlack(Connector):
    """A connector for Slack."""
    def __init__(self, config, opsdroid=None):
        """Create the connector."""
        super().__init__(config, opsdroid=opsdroid)
        _LOGGER.debug(_("Starting Slack connector."))
        self.name = "slack"
        self.default_target = config.get("default-room", "#general")
        self.icon_emoji = config.get("icon-emoji", ":robot_face:")
        self.token = config["token"]
        self.user_token = config.get("user-token")
        self.timeout = config.get("connect-timeout", 10)
        self.chat_as_user = config.get("chat-as-user", False)
        self.start_thread = config.get("start_thread", False)
        self.ssl_context = ssl.create_default_context(cafile=certifi.where())
        self.slack = AsyncWebClient(
            token=self.token,
            ssl=self.ssl_context,
            proxy=os.environ.get("HTTPS_PROXY"),
        )
        self.slack_user = AsyncWebClient(
            token=self.user_token,
            ssl=self.ssl_context,
            proxy=os.environ.get("HTTPS_PROXY"),
        )
        self.slack_rtm = RTMClient(
            token=self.token,
            run_async=True,
            ssl=self.ssl_context,
            proxy=os.environ.get("HTTPS_PROXY"),
        )
        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

        self._event_creator = SlackEventCreator(self)

    async def connect(self):
        """Connect to the chat service."""
        _LOGGER.info(_("Connecting to Slack."))

        try:
            # 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"]

            self.opsdroid.web_server.web_app.router.add_post(
                "/connector/{}/interactions".format(self.name),
                self.slack_interactions_handler,
            )

            _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."))
        except SlackApiError as error:
            _LOGGER.error(
                _("Unable to connect to Slack due to %s."
                  "The Slack Connector will not be available."),
                error,
            )
        except Exception:
            await self.disconnect()
            raise

    async def disconnect(self):
        """Disconnect from Slack."""
        self.slack_rtm.stop()
        self.listening = False

    async def listen(self):
        """Listen for and parse new messages."""

    @register_event(opsdroid.events.Message)
    async def _send_message(self, message):
        """Respond with a message."""
        _LOGGER.debug(_("Responding with: '%s' in room  %s."), message.text,
                      message.target)
        data = {
            "channel": message.target,
            "text": message.text,
            "as_user": self.chat_as_user,
            "username": self.bot_name,
            "icon_emoji": self.icon_emoji,
        }

        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"]):
                    # Linked Event is inside a thread
                    data["thread_ts"] = message.linked_event.raw_event[
                        "thread_ts"]
            elif self.start_thread:
                data["thread_ts"] = message.linked_event.event_id

        return await self.slack.api_call(
            "chat.postMessage",
            data=data,
        )

    @register_event(opsdroid.events.EditedMessage)
    async def _edit_message(self, message):
        """Edit a message."""
        _LOGGER.debug(
            _("Editing message with timestamp: '%s' to %s in room  %s."),
            message.linked_event,
            message.text,
            message.target,
        )
        data = {
            "channel": message.target,
            "ts": message.linked_event,
            "text": message.text,
            "as_user": self.chat_as_user,
        }

        return await self.slack.api_call(
            "chat.update",
            data=data,
        )

    @register_event(Blocks)
    async def _send_blocks(self, blocks):
        """Respond with structured blocks."""
        _LOGGER.debug(_("Responding with interactive blocks in room %s."),
                      blocks.target)
        return await self.slack.api_call(
            "chat.postMessage",
            data={
                "channel": blocks.target,
                "as_user": self.chat_as_user,
                "username": self.bot_name,
                "blocks": blocks.blocks,
                "icon_emoji": self.icon_emoji,
            },
        )

    @register_event(EditedBlocks)
    async def _edit_blocks(self, blocks):
        """Edit a particular block."""
        _LOGGER.debug(
            _("Editing interactive blocks with timestamp: '%s' in room  %s."),
            blocks.linked_event,
            blocks.target,
        )
        data = {
            "channel": blocks.target,
            "ts": blocks.linked_event,
            "blocks": blocks.blocks,
            "as_user": self.chat_as_user,
        }

        return await self.slack.api_call(
            "chat.update",
            data=data,
        )

    @register_event(opsdroid.events.Reaction)
    async def send_reaction(self, reaction):
        """React to a message."""
        emoji = demojize(reaction.emoji).replace(":", "")
        _LOGGER.debug(_("Reacting with: %s."), emoji)
        try:
            await self.slack.api_call(
                "reactions.add",
                data={
                    "name": emoji,
                    "channel": reaction.target,
                    "timestamp": reaction.linked_event.event_id,
                },
            )
        except SlackApiError as error:
            if "invalid_name" in str(error):
                _LOGGER.warning(_("Slack does not support the emoji %s."),
                                emoji)
            else:
                raise

    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.slack.users_info(user=userid)
            user_info = response.data["user"]
            if isinstance(user_info, dict):
                self.known_users[userid] = user_info
            else:
                raise ValueError("Returned user is not a dict.")
        return user_info

    async def replace_usernames(self, message):
        """Replace User ID with username in message text."""
        userids = re.findall(r"\<\@([A-Z0-9]+)(?:\|.+)?\>", message)
        for userid in userids:
            user_info = await self.lookup_username(userid)
            message = message.replace("<@{userid}>".format(userid=userid),
                                      user_info["name"])
        return message

    async def slack_interactions_handler(self, request):
        """Handle interactive events in Slack.

        For each entry in request, it will check if the entry is one of the four main
        interaction types in slack: block_actions, message_actions, view_submissions
        and view_closed. Then it will process all the incoming messages.

        Return:
            A 200 OK response. The Messenger Platform will resend the webhook
            event every 20 seconds, until a 200 OK response is received.
            Failing to return a 200 OK may cause your webhook to be
            unsubscribed by the Messenger Platform.

        """

        req_data = await request.post()
        payload = json.loads(req_data["payload"])

        if "type" in payload:
            if payload["type"] == "block_actions":
                for action in payload["actions"]:
                    block_action = BlockActions(
                        payload,
                        user=payload["user"]["id"],
                        target=payload["channel"]["id"],
                        connector=self,
                    )

                    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:
                        block_action.update_entity("value", action_value)
                    await self.opsdroid.parse(block_action)
            elif payload["type"] == "message_action":
                await self.opsdroid.parse(
                    MessageAction(
                        payload,
                        user=payload["user"]["id"],
                        target=payload["channel"]["id"],
                        connector=self,
                    ))
            elif payload["type"] == "view_submission":
                await self.opsdroid.parse(
                    ViewSubmission(
                        payload,
                        user=payload["user"]["id"],
                        target=payload["user"]["id"],
                        connector=self,
                    ))
            elif payload["type"] == "view_closed":
                await self.opsdroid.parse(
                    ViewClosed(
                        payload,
                        user=payload["user"]["id"],
                        target=payload["user"]["id"],
                        connector=self,
                    ))

        return aiohttp.web.Response(text=json.dumps("Received"), status=200)

    @register_event(opsdroid.events.NewRoom)
    async def _send_room_creation(self, creation_event):
        _LOGGER.debug(_("Creating room %s."), creation_event.name)
        return await self.slack_user.api_call(
            "conversations.create", data={"name": creation_event.name})

    @register_event(opsdroid.events.RoomName)
    async def _send_room_name_set(self, name_event):
        _LOGGER.debug(_("Renaming room %s to '%s'."), name_event.target,
                      name_event.name)
        return await self.slack_user.api_call(
            "conversations.rename",
            data={
                "channel": name_event.target,
                "name": name_event.name
            },
        )

    @register_event(opsdroid.events.JoinRoom)
    async def _send_join_room(self, join_event):
        return await self.slack_user.api_call(
            "conversations.join", data={"channel": join_event.target})

    @register_event(opsdroid.events.UserInvite)
    async def _send_user_invitation(self, invite_event):
        _LOGGER.debug(_("Inviting user %s to room '%s'."), invite_event.user,
                      invite_event.target)
        return await self.slack_user.api_call(
            "conversations.invite",
            data={
                "channel": invite_event.target,
                "users": invite_event.user_id
            },
        )

    @register_event(opsdroid.events.RoomDescription)
    async def _send_room_desciption(self, desc_event):
        return await self.slack_user.api_call(
            "conversations.setTopic",
            data={
                "channel": desc_event.target,
                "topic": desc_event.description
            },
        )

    @register_event(opsdroid.events.PinMessage)
    async def _send_pin_message(self, pin_event):
        return await self.slack.api_call(
            "pins.add",
            data={
                "channel": pin_event.target,
                "timestamp": pin_event.linked_event.event_id,
            },
        )

    @register_event(opsdroid.events.UnpinMessage)
    async def _send_unpin_message(self, unpin_event):
        return await self.slack.api_call(
            "pins.remove",
            data={
                "channel": unpin_event.target,
                "timestamp": unpin_event.linked_event.event_id,
            },
        )