Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slack Button Fix and Threaded Messaging #6020

Merged
merged 48 commits into from
Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c40ef48
button block debug
rgstephens May 19, 2020
f1a7689
threads in progress
rgstephens May 19, 2020
78a8f38
Updated threading for non-text responses and added fixes for button p…
b-quachtran Jun 2, 2020
d5ea128
Removed comments
b-quachtran Jun 3, 2020
284225b
Fixed bug when text value is empty
b-quachtran Jun 4, 2020
33c1ae8
Removed comment
b-quachtran Jun 4, 2020
0d29b3c
Reverted button text condition
b-quachtran Jun 4, 2020
be9f38f
Removed testing debug statements
b-quachtran Jun 4, 2020
f1023e1
Code cleanup
b-quachtran Jun 9, 2020
aa13d65
Initial doc update
b-quachtran Jun 9, 2020
5db35fe
Code linting
b-quachtran Jun 9, 2020
25ba94c
Updated changelog
b-quachtran Jun 10, 2020
3db2435
Black formatting
b-quachtran Jun 10, 2020
30f4f85
Type check update
b-quachtran Jun 10, 2020
597dff4
Fix type test errors
b-quachtran Jun 10, 2020
535565d
Update slack.py
b-quachtran Jun 10, 2020
05dd45a
Update slack.py
b-quachtran Jun 10, 2020
3bb7c08
Update slack.py
b-quachtran Jun 10, 2020
233098c
Merge branch 'master' into slack-enhancements
b-quachtran Jun 10, 2020
90d84fd
Update slack.py
b-quachtran Jun 10, 2020
344ce53
Update slack.py
b-quachtran Jun 10, 2020
554ef75
Initial test thread functions
b-quachtran Jun 11, 2020
a8450ce
Merge branch 'slack-enhancements' of https://github.com/RasaHQ/rasa i…
b-quachtran Jun 11, 2020
40436ba
Added header check for request
b-quachtran Jun 12, 2020
3d78a8a
Created test cases for threaded Slack messages
b-quachtran Jun 12, 2020
dacc08f
Removed old changelog
b-quachtran Jun 12, 2020
d1f8b35
Code linting
b-quachtran Jun 12, 2020
289aa6a
Merge branch 'master' into slack-enhancements
b-quachtran Jun 12, 2020
b73d955
Update slack.py
b-quachtran Jun 12, 2020
47c20a3
Added additional Slack connector test cases
b-quachtran Jun 12, 2020
7dcaa94
Merge branch 'slack-enhancements' of https://github.com/RasaHQ/rasa i…
b-quachtran Jun 12, 2020
7d75b61
Created changelog
b-quachtran Jun 12, 2020
d319832
Merge branch 'master' into slack-enhancements
b-quachtran Jun 25, 2020
dca2f45
Update rasa/core/channels/slack.py
b-quachtran Jul 15, 2020
b778ba1
Update rasa/core/channels/slack.py
b-quachtran Jul 15, 2020
ade06e7
Update rasa/core/channels/slack.py
b-quachtran Jul 15, 2020
1c79ba1
Review changes
b-quachtran Jul 15, 2020
ae00cd5
Merge branch 'master' into slack-enhancements
b-quachtran Jul 15, 2020
c9814f7
Updated custom_json function call
b-quachtran Jul 16, 2020
ae7aadb
Merge branch 'slack-enhancements' of https://github.com/RasaHQ/rasa i…
b-quachtran Jul 16, 2020
7b42a2e
Update rasa/core/channels/slack.py
b-quachtran Jul 27, 2020
5c6b800
Renamed ts to thread_id, added comments, moved constant into channels…
b-quachtran Jul 27, 2020
88f0efa
Update test_channels.py
b-quachtran Jul 27, 2020
01c0d88
Fixed ts key
b-quachtran Jul 27, 2020
e0a1692
Merge branch 'slack-enhancements' of https://github.com/RasaHQ/rasa i…
b-quachtran Jul 27, 2020
9921160
Updated slack tests
b-quachtran Jul 27, 2020
b4abe9a
Test bug fix
b-quachtran Jul 27, 2020
eeaf1ff
Merge branch 'master' into slack-enhancements
b-quachtran Jul 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions changelog/6020.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 22 additions & 1 deletion docs/core/domains.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/connectors/slack.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://api.slack.com/events-api#errors>`_.

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
Expand Down
122 changes: 87 additions & 35 deletions rasa/core/channels/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,34 @@ 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__()

@staticmethod
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",
)

Expand All @@ -44,15 +56,16 @@ 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],
)

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,
)

Expand Down Expand Up @@ -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,
Expand All @@ -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):
Expand All @@ -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

Expand All @@ -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.

Expand All @@ -149,13 +165,16 @@ def __init__(
included in this list will be ignored.
Error codes are listed
`here <https://api.slack.com/events-api#errors>`_.
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
self.slack_channel = slack_channel
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:
Expand Down Expand Up @@ -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")
Copy link
Contributor

@chkoss chkoss Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@b-quachtran This line confuses me - shouldn't it be metadata.get("thread_id") now? If I'm not mistaken, can you create a PR with a quick fix?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then we should probably also extend the tests to include a case that would have caught this (e.g. a test where we get a message event, post a reply to the same thread, and assert that everything went as expected).
If you have time, this could go into the PR with the fix, otherwise let's just create an issue to add such a test.

else:
thread_id = None
else:
output_channel = None
b-quachtran marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -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"),
}
b-quachtran marked this conversation as resolved.
Show resolved Hide resolved

return {}

def blueprint(
self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
Expand All @@ -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", "")
Expand All @@ -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
)
b-quachtran marked this conversation as resolved.
Show resolved Hide resolved

return response.text("Bot message delivered.")

return slack_webhook
Expand All @@ -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
Loading