diff --git a/changelog/6020.feature.rst b/changelog/6020.feature.rst new file mode 100644 index 000000000000..1182c3f37a62 --- /dev/null +++ b/changelog/6020.feature.rst @@ -0,0 +1,11 @@ +You can now enable threaded message responses from Rasa through the Slack connector. +This option is enabled using an optional configuration in the credentials.yml file + +.. code-block:: none + + slack: + slack_token: + slack_channel: + use_threads: True + +Button support has also been added in the Slack connector. diff --git a/docs/core/domains.rst b/docs/core/domains.rst index 527ce16e7630..4b01f93272d1 100644 --- a/docs/core/domains.rst +++ b/docs/core/domains.rst @@ -217,7 +217,28 @@ output payloads that will only work in certain channels. - text: "Which game would you like to play?" channel: "slack" custom: - - # payload for Slack dropdown menu to choose a game + blocks: + - type: actions + elements: + - type: button + text: + type: plain_text + emoji: true + text: "Chess :crown:" + value: '/inform{"game": "chess"}' + - type: button + text: + type: plain_text + emoji: true + text: "Checkers :checkered_flag:" + value: '/inform{"game": "checkers"}' + - type: button + text: + type: plain_text + emoji: true + text: "Fortnite :european_castle:" + value: '/inform{"game": "fortnite"}' + style: danger - text: "Which game would you like to play?" buttons: - title: "Chess" diff --git a/docs/user-guide/connectors/slack.rst b/docs/user-guide/connectors/slack.rst index d88baafc053a..4711b0336d59 100644 --- a/docs/user-guide/connectors/slack.rst +++ b/docs/user-guide/connectors/slack.rst @@ -74,7 +74,7 @@ You need to supply a ``credentials.yml`` with the following content: slack_retry_reason_header: "x-slack-retry-reason" # Slack HTTP header name indicating reason that slack send retry request. This configuration is optional. slack_retry_number_header: "x-slack-retry-num" # Slack HTTP header name indicating the attempt number. This configuration is optional. errors_ignore_retry: None # Any error codes given by Slack included in this list will be ignored. Error codes are listed `here `_. - + use_threads: False # If set to True, bot responses will appear as a threaded message in Slack. This configuration is optional and set to False by default. The endpoint for receiving slack messages is ``http://localhost:5005/webhooks/slack/webhook``, replacing diff --git a/rasa/core/channels/slack.py b/rasa/core/channels/slack.py index 51dfb966286d..93c0ab99bc2d 100644 --- a/rasa/core/channels/slack.py +++ b/rasa/core/channels/slack.py @@ -20,9 +20,15 @@ class SlackBot(OutputChannel): def name(cls) -> Text: return "slack" - def __init__(self, token: Text, slack_channel: Optional[Text] = None) -> None: + def __init__( + self, + token: Text, + slack_channel: Optional[Text] = None, + thread_id: Optional[Text] = None, + ) -> None: self.slack_channel = slack_channel + self.thread_id = thread_id self.client = WebClient(token, run_async=True) super().__init__() @@ -30,12 +36,18 @@ def __init__(self, token: Text, slack_channel: Optional[Text] = None) -> None: def _get_text_from_slack_buttons(buttons: List[Dict]) -> Text: return "".join([b.get("title", "") for b in buttons]) + async def _post_message(self, **kwargs: Any): + if self.thread_id: + await self.client.chat_postMessage(**kwargs, thread_ts=self.thread_id) + else: + await self.client.chat_postMessage(**kwargs) + async def send_text_message( self, recipient_id: Text, text: Text, **kwargs: Any ) -> None: recipient = self.slack_channel or recipient_id for message_part in text.strip().split("\n\n"): - await self.client.chat_postMessage( + await self._post_message( channel=recipient, as_user=True, text=message_part, type="mrkdwn", ) @@ -44,7 +56,8 @@ async def send_image_url( ) -> None: recipient = self.slack_channel or recipient_id image_block = {"type": "image", "image_url": image, "alt_text": image} - await self.client.chat_postMessage( + + await self._post_message( channel=recipient, as_user=True, text=image, blocks=[image_block], ) @@ -52,7 +65,7 @@ async def send_attachment( self, recipient_id: Text, attachment: Dict[Text, Any], **kwargs: Any ) -> None: recipient = self.slack_channel or recipient_id - await self.client.chat_postMessage( + await self._post_message( channel=recipient, as_user=True, attachments=[attachment], **kwargs, ) @@ -83,7 +96,8 @@ async def send_text_with_buttons( "value": button["payload"], } ) - await self.client.chat_postMessage( + + await self._post_message( channel=recipient, as_user=True, text=text, @@ -95,7 +109,7 @@ async def send_custom_json( ) -> None: json_message.setdefault("channel", self.slack_channel or recipient_id) json_message.setdefault("as_user", True) - await self.client.chat_postMessage(**json_message) + await self._post_message(**json_message) class SlackInput(InputChannel): @@ -117,6 +131,7 @@ def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChanne credentials.get("slack_retry_reason_header", "x-slack-retry-reason"), credentials.get("slack_retry_number_header", "x-slack-retry-num"), credentials.get("errors_ignore_retry", None), + credentials.get("use_threads", False), ) # pytype: enable=attribute-error @@ -127,6 +142,7 @@ def __init__( slack_retry_reason_header: Optional[Text] = None, slack_retry_number_header: Optional[Text] = None, errors_ignore_retry: Optional[List[Text]] = None, + use_threads: Optional[bool] = False, ) -> None: """Create a Slack input channel. @@ -149,6 +165,8 @@ def __init__( included in this list will be ignored. Error codes are listed `here `_. + use_threads: If set to True, your bot will send responses in Slack as a threaded message. + Responses will appear as a normal Slack message if set to False. """ self.slack_token = slack_token @@ -156,6 +174,7 @@ def __init__( self.errors_ignore_retry = errors_ignore_retry or ("http_timeout",) self.retry_reason_header = slack_retry_reason_header self.retry_num_header = slack_retry_number_header + self.use_threads = use_threads @staticmethod def _is_app_mention(slack_event: Dict) -> bool: @@ -297,13 +316,18 @@ async def process_message( if metadata is not None: output_channel = metadata.get("out_channel") + if self.use_threads: + thread_id = metadata.get("ts") + else: + thread_id = None else: output_channel = None + thread_id = None try: user_msg = UserMessage( text, - self.get_output_channel(output_channel), + self.get_output_channel(output_channel, thread_id), sender_id, input_channel=self.name(), metadata=metadata, @@ -326,13 +350,34 @@ def get_metadata(self, request: Request) -> Dict[Text, Any]: Metadata extracted from the sent event payload. This includes the output channel for the response, and users that have installed the bot. """ - slack_event = request.json - event = slack_event.get("event", {}) - - return { - "out_channel": event.get("channel"), - "users": slack_event.get("authed_users"), - } + content_type = request.headers.get("content-type") + + # Slack API sends either a JSON-encoded or a URL-encoded body depending on the content + if content_type == "application/json": + # if JSON-encoded message is received + slack_event = request.json + event = slack_event.get("event", {}) + thread_id = event.get("thread_ts", event.get("ts")) + + return { + "out_channel": event.get("channel"), + "thread_id": thread_id, + "users": slack_event.get("authed_users"), + } + + if content_type == "application/x-www-form-urlencoded": + # if URL-encoded message is received + output = request.form + payload = json.loads(output["payload"][0]) + message = payload.get("message", {}) + thread_id = message.get("thread_ts", message.get("ts")) + return { + "out_channel": payload.get("channel", {}).get("id"), + "thread_id": thread_id, + "users": payload.get("user", {}).get("id"), + } + + return {} def blueprint( self, on_new_message: Callable[[UserMessage], Awaitable[Any]] @@ -345,26 +390,11 @@ async def health(_: Request) -> HTTPResponse: @slack_webhook.route("/webhook", methods=["GET", "POST"]) async def webhook(request: Request) -> HTTPResponse: - if request.form: - output = request.form - payload = json.loads(output["payload"][0]) - - if self._is_interactive_message(payload): - sender_id = payload["user"]["id"] - text = self._get_interactive_response(payload["actions"][0]) - if text is not None: - metadata = self.get_metadata(request) - return await self.process_message( - request, on_new_message, text, sender_id, metadata - ) - elif payload["actions"][0]["type"] == "button": - # link buttons don't have "value", don't send their clicks to bot - return response.text("User clicked link button") - return response.text( - "The input message could not be processed.", status=500 - ) + content_type = request.headers.get("content-type") + # Slack API sends either a JSON-encoded or a URL-encoded body depending on the content - elif request.json: + if content_type == "application/json": + # if JSON-encoded message is received output = request.json event = output.get("event", {}) user_message = event.get("text", "") @@ -391,6 +421,26 @@ async def webhook(request: Request) -> HTTPResponse: f"Received message on unsupported channel: {metadata['out_channel']}" ) + elif content_type == "application/x-www-form-urlencoded": + # if URL-encoded message is received + output = request.form + payload = json.loads(output["payload"][0]) + + if self._is_interactive_message(payload): + sender_id = payload["user"]["id"] + text = self._get_interactive_response(payload["actions"][0]) + if text is not None: + metadata = self.get_metadata(request) + return await self.process_message( + request, on_new_message, text, sender_id, metadata + ) + if payload["actions"][0]["type"] == "button": + # link buttons don't have "value", don't send their clicks to bot + return response.text("User clicked link button") + return response.text( + "The input message could not be processed.", status=500 + ) + return response.text("Bot message delivered.") return slack_webhook @@ -402,9 +452,11 @@ def _is_supported_channel(self, slack_event: Dict, metadata: Dict) -> bool: or metadata["out_channel"] == self.slack_channel ) - def get_output_channel(self, channel: Optional[Text] = None) -> OutputChannel: + def get_output_channel( + self, channel: Optional[Text] = None, thread_id: Optional[Text] = None + ) -> OutputChannel: channel = channel or self.slack_channel - return SlackBot(self.slack_token, channel) + return SlackBot(self.slack_token, channel, thread_id) def set_output_channel(self, channel: Text) -> None: self.slack_channel = channel diff --git a/tests/core/test_channels.py b/tests/core/test_channels.py index 6f5b99f399f4..ead0e2ff2d33 100644 --- a/tests/core/test_channels.py +++ b/tests/core/test_channels.py @@ -28,6 +28,38 @@ logger = logging.getLogger(__name__) +SLACK_TEST_ATTACHMENT = { + "fallback": "Financial Advisor Summary", + "color": "#36a64f", + "author_name": "ABE", + "title": "Financial Advisor Summary", + "title_link": "http://tenfactorialrocks.com", + "image_url": "https://r.com/cancel/r12", + "thumb_url": "https://r.com/cancel/r12", + "actions": [ + { + "type": "button", + "text": "\ud83d\udcc8 Dashboard", + "url": "https://r.com/cancel/r12", + "style": "primary", + }, + { + "type": "button", + "text": "\ud83d\udccb Download XL", + "url": "https://r.com/cancel/r12", + "style": "danger", + }, + { + "type": "button", + "text": "\ud83d\udce7 E-Mail", + "url": "https://r.com/cancel/r12", + "style": "danger", + }, + ], + "footer": "Powered by 1010rocks", + "ts": 1531889719, +} + def fake_sanic_run(*args, **kwargs): """Used to replace `run` method of a Sanic server to avoid hanging.""" @@ -477,11 +509,12 @@ def test_botframework_attachments(): def test_slack_metadata(): from rasa.core.channels.slack import SlackInput - from sanic.request import Request user = "user1" channel = "channel1" authed_users = ["XXXXXXX", "YYYYYYY", "ZZZZZZZ"] + ts = "1579802617.000800" + header = {"content-type": "application/json"} direct_message_event = { "authed_users": authed_users, "event": { @@ -489,7 +522,7 @@ def test_slack_metadata(): "type": "message", "text": "hello world", "user": user, - "ts": "1579802617.000800", + "ts": ts, "team": "XXXXXXXXX", "blocks": [ { @@ -515,9 +548,59 @@ def test_slack_metadata(): r = Mock() r.json = direct_message_event + r.headers = header metadata = input_channel.get_metadata(request=r) assert metadata["out_channel"] == channel assert metadata["users"] == authed_users + assert metadata["thread_id"] == ts + + +def test_slack_form_metadata(): + from rasa.core.channels.slack import SlackInput + + user = "user1" + channel = "channel1" + authed_user = "XXXXXXX" + ts = "1579802617.000800" + header = {"content-type": "application/x-www-form-urlencoded"} + payload = { + "type": "block_actions", + "user": {"id": authed_user, "username": user, "name": "name",}, + "channel": {"id": channel}, + "message": { + "type": "message", + "text": "text", + "user": authed_user, + "ts": ts, + "blocks": [ + { + "type": "actions", + "block_id": "XXXXX", + "elements": [ + { + "type": "button", + "action_id": "XXXXX", + "text": {"type": "plain_text", "text": "text",}, + "value": "value", + } + ], + } + ], + }, + } + form_event = {"payload": [json.dumps(payload)]} + + input_channel = SlackInput( + slack_token="YOUR_SLACK_TOKEN", slack_channel="YOUR_SLACK_CHANNEL" + ) + + r = Mock() + r.form = form_event + r.headers = header + metadata = input_channel.get_metadata(request=r) + assert metadata["out_channel"] == channel + assert metadata["users"] == authed_user + assert metadata["thread_id"] == ts def test_slack_metadata_missing_keys(): @@ -525,12 +608,14 @@ def test_slack_metadata_missing_keys(): from sanic.request import Request channel = "channel1" + ts = "1579802617.000800" + header = {"content-type": "application/json"} direct_message_event = { "event": { "client_msg_id": "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "type": "message", "text": "hello world", - "ts": "1579802617.000800", + "ts": ts, "team": "XXXXXXXXX", "blocks": [ { @@ -556,9 +641,67 @@ def test_slack_metadata_missing_keys(): r = Mock() r.json = direct_message_event + r.headers = header + metadata = input_channel.get_metadata(request=r) + assert metadata["users"] is None + assert metadata["out_channel"] == channel + assert metadata["thread_id"] == ts + + +def test_slack_form_metadata_missing_keys(): + from rasa.core.channels.slack import SlackInput + + channel = "channel1" + ts = "1579802617.000800" + header = {"content-type": "application/x-www-form-urlencoded"} + payload = { + "type": "block_actions", + "channel": {"id": channel}, + "message": { + "type": "message", + "text": "text", + "ts": ts, + "blocks": [ + { + "type": "actions", + "block_id": "XXXXX", + "elements": [ + { + "type": "button", + "action_id": "XXXXX", + "text": {"type": "plain_text", "text": "text",}, + "value": "value", + } + ], + } + ], + }, + } + form_event = {"payload": [json.dumps(payload)]} + + input_channel = SlackInput( + slack_token="YOUR_SLACK_TOKEN", slack_channel="YOUR_SLACK_CHANNEL" + ) + + r = Mock() + r.form = form_event + r.headers = header metadata = input_channel.get_metadata(request=r) assert metadata["users"] is None assert metadata["out_channel"] == channel + assert metadata["thread_id"] == ts + + +def test_slack_no_metadata(): + from rasa.core.channels.slack import SlackInput + + input_channel = SlackInput( + slack_token="YOUR_SLACK_TOKEN", slack_channel="YOUR_SLACK_CHANNEL" + ) + + r = Mock() + metadata = input_channel.get_metadata(request=r) + assert metadata == {} def test_slack_message_sanitization(): @@ -635,6 +778,15 @@ def test_slack_init_two_parameters(): assert ch.slack_channel == "test" +def test_slack_init_three_parameters(): + from rasa.core.channels.slack import SlackInput + + ch = SlackInput("xoxb-test", "test", use_threads=True) + assert ch.slack_token == "xoxb-test" + assert ch.slack_channel == "test" + assert ch.use_threads is True + + def test_is_slack_message_none(): from rasa.core.channels.slack import SlackInput @@ -690,6 +842,15 @@ def test_slackbot_init_two_parameter(): assert bot.slack_channel == "General" +def test_slackbot_init_three_parameter(): + from rasa.core.channels.slack import SlackBot + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + assert bot.client.token == "DummyToken" + assert bot.slack_channel == "General" + assert bot.thread_id == "DummyThread" + + # Use monkeypatch for sending attachments, images and plain text. @pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") @pytest.mark.asyncio @@ -703,38 +864,37 @@ async def test_slackbot_send_attachment_only(): ) bot = SlackBot("DummyToken", "General") - attachment = { - "fallback": "Financial Advisor Summary", - "color": "#36a64f", - "author_name": "ABE", - "title": "Financial Advisor Summary", - "title_link": "http://tenfactorialrocks.com", - "image_url": "https://r.com/cancel/r12", - "thumb_url": "https://r.com/cancel/r12", - "actions": [ - { - "type": "button", - "text": "\ud83d\udcc8 Dashboard", - "url": "https://r.com/cancel/r12", - "style": "primary", - }, - { - "type": "button", - "text": "\ud83d\udccb Download XL", - "url": "https://r.com/cancel/r12", - "style": "danger", - }, - { - "type": "button", - "text": "\ud83d\udce7 E-Mail", - "url": "https://r.com/cancel/r12", - "style": "danger", - }, - ], - "footer": "Powered by 1010rocks", - "ts": 1531889719, + attachment = SLACK_TEST_ATTACHMENT + + await bot.send_attachment("ID", attachment) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + assert request_params == { + "channel": "General", + "as_user": True, + "attachments": [attachment], } + +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_attachment_only_threaded(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + attachment = SLACK_TEST_ATTACHMENT + await bot.send_attachment("ID", attachment) r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") @@ -747,6 +907,7 @@ async def test_slackbot_send_attachment_only(): "channel": "General", "as_user": True, "attachments": [attachment], + "thread_ts": "DummyThread", } @@ -762,39 +923,39 @@ async def test_slackbot_send_attachment_with_text(): ) bot = SlackBot("DummyToken", "General") - attachment = { - "fallback": "Financial Advisor Summary", - "color": "#36a64f", - "author_name": "ABE", - "title": "Financial Advisor Summary", - "title_link": "http://tenfactorialrocks.com", - "text": "Here is the summary:", - "image_url": "https://r.com/cancel/r12", - "thumb_url": "https://r.com/cancel/r12", - "actions": [ - { - "type": "button", - "text": "\ud83d\udcc8 Dashboard", - "url": "https://r.com/cancel/r12", - "style": "primary", - }, - { - "type": "button", - "text": "\ud83d\udccb XL", - "url": "https://r.com/cancel/r12", - "style": "danger", - }, - { - "type": "button", - "text": "\ud83d\udce7 E-Mail", - "url": "https://r.com/cancel/r123", - "style": "danger", - }, - ], - "footer": "Powered by 1010rocks", - "ts": 1531889719, + attachment = SLACK_TEST_ATTACHMENT + attachment["text"] = "Here is the summary:" + + await bot.send_attachment("ID", attachment) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + assert request_params == { + "channel": "General", + "as_user": True, + "attachments": [attachment], } + +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_attachment_with_text_threaded(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + attachment = SLACK_TEST_ATTACHMENT + attachment["text"] = "Here is the summary:" + await bot.send_attachment("ID", attachment) r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") @@ -807,6 +968,7 @@ async def test_slackbot_send_attachment_with_text(): "channel": "General", "as_user": True, "attachments": [attachment], + "thread_ts": "DummyThread", } @@ -839,6 +1001,36 @@ async def test_slackbot_send_image_url(): assert request_params["blocks"][0].get("image_url") == "http://www.rasa.net" +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_image_url_threaded(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + url = "http://www.rasa.net" + await bot.send_image_url("ID", url) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + assert request_params["as_user"] is True + assert request_params["channel"] == "General" + assert request_params["thread_ts"] == "DummyThread" + assert len(request_params["blocks"]) == 1 + assert request_params["blocks"][0].get("type") == "image" + assert request_params["blocks"][0].get("alt_text") == "http://www.rasa.net" + assert request_params["blocks"][0].get("image_url") == "http://www.rasa.net" + + @pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") @pytest.mark.asyncio async def test_slackbot_send_text(): @@ -867,6 +1059,179 @@ async def test_slackbot_send_text(): } +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_text_threaded(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + await bot.send_text_message("ID", "my message") + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + assert request_params == { + "as_user": True, + "channel": "General", + "text": "my message", + "type": "mrkdwn", + "thread_ts": "DummyThread", + } + + +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_text_with_buttons(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General") + buttons = [{"title": "title", "payload": "payload"}] + + await bot.send_text_with_buttons("ID", "my message", buttons) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + text_block = { + "type": "section", + "text": {"type": "plain_text", "text": "my message"}, + } + button_block = { + "type": "actions", + "elements": [ + { + "type": "button", + "text": {"type": "plain_text", "text": "title"}, + "value": "payload", + } + ], + } + assert request_params == { + "as_user": True, + "channel": "General", + "text": "my message", + "blocks": [text_block, button_block], + } + + +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_text_with_buttons_threaded(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + buttons = [{"title": "title", "payload": "payload"}] + + await bot.send_text_with_buttons("ID", "my message", buttons) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + text_block = { + "type": "section", + "text": {"type": "plain_text", "text": "my message"}, + } + button_block = { + "type": "actions", + "elements": [ + { + "type": "button", + "text": {"type": "plain_text", "text": "title"}, + "value": "payload", + } + ], + } + assert request_params == { + "as_user": True, + "channel": "General", + "text": "my message", + "blocks": [text_block, button_block], + "thread_ts": "DummyThread", + } + + +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_custom_json(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General") + await bot.send_custom_json("ID", {"test_key": "test_value"}) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + assert request_params == { + "as_user": True, + "channel": "General", + "test_key": "test_value", + } + + +@pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") +@pytest.mark.asyncio +async def test_slackbot_send_custom_json_threaded(): + from rasa.core.channels.slack import SlackBot + + with aioresponses() as mocked: + mocked.post( + "https://www.slack.com/api/chat.postMessage", + payload={"ok": True, "purpose": "Testing bots"}, + ) + + bot = SlackBot("DummyToken", "General", thread_id="DummyThread") + await bot.send_custom_json("ID", {"test_key": "test_value"}) + + r = latest_request(mocked, "POST", "https://www.slack.com/api/chat.postMessage") + + assert r + + request_params = json_of_latest_request(r) + + assert request_params == { + "as_user": True, + "channel": "General", + "thread_ts": "DummyThread", + "test_key": "test_value", + } + + @pytest.mark.filterwarnings("ignore:unclosed.*:ResourceWarning") def test_channel_inheritance(): from rasa.core.channels.channel import RestInput