diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py index 1ab395b75..079044720 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py @@ -6,7 +6,7 @@ # -------------------------------------------------------------------------- from .about import __version__ -from .slack_options import SlackAdapterOptions +from .slack_client_options import SlackClientOptions from .slack_client import SlackClient from .slack_adapter import SlackAdapter from .slack_payload import SlackPayload @@ -15,10 +15,11 @@ from .activity_resourceresponse import ActivityResourceResponse from .slack_request_body import SlackRequestBody from .slack_helper import SlackHelper +from .slack_adatper_options import SlackAdapterOptions __all__ = [ "__version__", - "SlackAdapterOptions", + "SlackClientOptions", "SlackClient", "SlackAdapter", "SlackPayload", @@ -27,4 +28,5 @@ "ActivityResourceResponse", "SlackRequestBody", "SlackHelper", + "SlackAdapterOptions", ] diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py index 3a03d7553..ad32e6e6c 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py @@ -20,6 +20,7 @@ from .activity_resourceresponse import ActivityResourceResponse from .slack_client import SlackClient from .slack_helper import SlackHelper +from .slack_adatper_options import SlackAdapterOptions class SlackAdapter(BotAdapter, ABC): @@ -32,10 +33,12 @@ def __init__( self, client: SlackClient, on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None, + options: SlackAdapterOptions = None, ): super().__init__(on_turn_error) self.slack_client = client self.slack_logged_in = False + self.options = options if options else SlackAdapterOptions() async def send_activities( self, context: TurnContext, activities: List[Activity] @@ -62,7 +65,7 @@ async def send_activities( if activity.type == ActivityTypes.message: message = SlackHelper.activity_to_slack(activity) - slack_response = await self.slack_client.post_message_to_slack(message) + slack_response = await self.slack_client.post_message(message) if slack_response and slack_response.status_code / 100 == 2: resource_response = ActivityResourceResponse( @@ -99,8 +102,8 @@ async def update_activity(self, context: TurnContext, activity: Activity): raise Exception("Activity.conversation is required") message = SlackHelper.activity_to_slack(activity) - results = await self.slack_client.update( - timestamp=message.ts, channel_id=message.channel, text=message.text, + results = await self.slack_client.chat_update( + ts=message.ts, channel=message.channel, text=message.text, ) if results.status_code / 100 != 2: @@ -130,17 +133,17 @@ async def delete_activity( if not context.activity.timestamp: raise Exception("Activity.timestamp is required") - await self.slack_client.delete_message( - channel_id=reference.channel_id, timestamp=context.activity.timestamp + await self.slack_client.chat_delete( + channel=reference.conversation.id, ts=reference.activity_id ) async def continue_conversation( self, reference: ConversationReference, callback: Callable, - bot_id: str = None, + bot_id: str = None, # pylint: disable=unused-argument claims_identity: ClaimsIdentity = None, - audience: str = None, + audience: str = None, # pylint: disable=unused-argument ): """ Send a proactive message to a conversation. @@ -203,15 +206,20 @@ async def process(self, req: Request, logic: Callable) -> Response: self.slack_logged_in = True body = await req.text() + + if ( + self.options.verify_incoming_requests + and not self.slack_client.verify_signature(req, body) + ): + return SlackHelper.response( + req, 401, "Rejected due to mismatched header signature" + ) + slack_body = SlackHelper.deserialize_body(req.content_type, body) if slack_body.type == "url_verification": return SlackHelper.response(req, 200, slack_body.challenge) - if not self.slack_client.verify_signature(req, body): - text = "Rejected due to mismatched header signature" - return SlackHelper.response(req, 401, text) - if ( not self.slack_client.options.slack_verification_token and slack_body.token != self.slack_client.options.slack_verification_token @@ -231,7 +239,9 @@ async def process(self, req: Request, logic: Callable) -> Response: slack_body, self.slack_client ) else: - raise Exception(f"Unknown Slack event type {slack_body.type}") + return SlackHelper.response( + req, 200, f"Unknown Slack event type {slack_body.type}" + ) context = TurnContext(self, activity) await self.run_pipeline(context, logic) diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adatper_options.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adatper_options.py new file mode 100644 index 000000000..6532f8e5f --- /dev/null +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adatper_options.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class SlackAdapterOptions: + """ + Class for defining implementation of the SlackAdapter Options. + """ + + def __init__(self): + self.verify_incoming_requests = True diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py index 297fb6b8e..70dbc0eaa 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py @@ -14,7 +14,7 @@ from slack.web.slack_response import SlackResponse from botbuilder.schema import Activity -from botbuilder.adapters.slack import SlackAdapterOptions +from botbuilder.adapters.slack.slack_client_options import SlackClientOptions from botbuilder.adapters.slack.slack_message import SlackMessage POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage" @@ -26,7 +26,7 @@ class SlackClient(WebClient): Slack client that extends https://github.com/slackapi/python-slackclient. """ - def __init__(self, options: SlackAdapterOptions): + def __init__(self, options: SlackClientOptions): if not options or not options.slack_bot_token: raise Exception("SlackAdapterOptions and bot_token are required") @@ -374,19 +374,10 @@ async def files_upload_ex( return await self.files_upload(file=file, content=content, **args) - async def get_bot_user_by_team(self, activity: Activity) -> str: - if self.identity: - return self.identity - - if not activity.conversation.properties["team"]: - return None - - user = await self.options.get_bot_user_by_team( - activity.conversation.properties["team"] - ) - if user: - return user - raise Exception("Missing credentials for team.") + async def get_bot_user_identity( + self, activity: Activity # pylint: disable=unused-argument + ) -> str: + return self.identity def verify_signature(self, req: Request, body: str) -> bool: timestamp = req.headers["X-Slack-Request-Timestamp"] @@ -402,7 +393,7 @@ def verify_signature(self, req: Request, body: str) -> bool: return computed_signature == received_signature - async def post_message_to_slack(self, message: SlackMessage) -> SlackResponse: + async def post_message(self, message: SlackMessage) -> SlackResponse: if not message: return None diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_options.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client_options.py similarity index 53% rename from libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_options.py rename to libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client_options.py index 11cc9b62b..244aeb3f6 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_options.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client_options.py @@ -1,53 +1,32 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class SlackAdapterOptions: - """ - Defines the implementation of the SlackAdapter options. - """ - - def __init__( - self, - slack_verification_token: str, - slack_bot_token: str, - slack_client_signing_secret: str, - ): - """ - Initializes a new instance of SlackAdapterOptions. - - :param slack_verification_token: A token for validating the origin of incoming webhooks. - :type slack_verification_token: str - :param slack_bot_token: A token for a bot to work on a single workspace. - :type slack_bot_token: str - :param slack_client_signing_secret: The token used to validate that incoming webhooks originated from Slack. - :type slack_client_signing_secret: str - """ - self.slack_verification_token = slack_verification_token - self.slack_bot_token = slack_bot_token - self.slack_client_signing_secret = slack_client_signing_secret - self.slack_client_id = None - self.slack_client_secret = None - self.slack_redirect_uri = None - self.slack_scopes = [str] - - async def get_token_for_team(self, team_id: str) -> str: - """ - Receives a Slack team ID and returns the bot token associated with that team. Required for multi-team apps. - - :param team_id: The team ID. - :type team_id: str - :raises: :func:`NotImplementedError` - """ - raise NotImplementedError() - - async def get_bot_user_by_team(self, team_id: str) -> str: - """ - A method that receives a Slack team ID and returns the bot user ID associated with that team. Required for - multi-team apps. - - :param team_id: The team ID. - :type team_id: str - :raises: :func:`NotImplementedError` - """ - raise NotImplementedError() +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class SlackClientOptions: + """ + Defines the implementation of the SlackClient options. + """ + + def __init__( + self, + slack_verification_token: str, + slack_bot_token: str, + slack_client_signing_secret: str, + ): + """ + Initializes a new instance of SlackClientOptions. + + :param slack_verification_token: A token for validating the origin of incoming webhooks. + :type slack_verification_token: str + :param slack_bot_token: A token for a bot to work on a single workspace. + :type slack_bot_token: str + :param slack_client_signing_secret: The token used to validate that incoming webhooks originated from Slack. + :type slack_client_signing_secret: str + """ + self.slack_verification_token = slack_verification_token + self.slack_bot_token = slack_bot_token + self.slack_client_signing_secret = slack_client_signing_secret + self.slack_client_id = None + self.slack_client_secret = None + self.slack_redirect_uri = None + self.slack_scopes = [str] diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py index d71fd7852..273771f1c 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py @@ -124,24 +124,35 @@ def payload_to_activity(payload: SlackPayload) -> Activity: activity = Activity( channel_id="slack", - conversation=ConversationAccount(id=payload.channel.id, properties={}), + conversation=ConversationAccount(id=payload.channel["id"], properties={}), from_property=ChannelAccount( - id=payload.message.bot_id if payload.message.bot_id else payload.user.id + id=payload.message.bot_id + if payload.message.bot_id + else payload.user["id"] ), recipient=ChannelAccount(), channel_data=payload, text=None, type=ActivityTypes.event, + value=payload, ) if payload.thread_ts: activity.conversation.properties["thread_ts"] = payload.thread_ts - if payload.actions and ( - payload.type == "block_actions" or payload.type == "interactive_message" - ): - activity.type = ActivityTypes.message - activity.text = payload.actions.value + if payload.actions: + action = payload.actions[0] + + if action["type"] == "button": + activity.text = action["value"] + elif action["type"] == "select": + selected_option = action["selected_options"] + activity.text = selected_option["value"] if selected_option else None + elif action["type"] == "static_select": + activity.text = action["selected_options"]["value"] + + if activity.text: + activity.type = ActivityTypes.message return activity @@ -176,26 +187,27 @@ async def event_to_activity(event: SlackEvent, client: SlackClient) -> Activity: type=ActivityTypes.event, ) - if event.thread_ts: - activity.conversation.properties["thread_ts"] = event.thread_ts - if not activity.conversation.id: if event.item and event.item_channel: activity.conversation.id = event.item_channel else: activity.conversation.id = event.team - activity.recipient.id = await client.get_bot_user_by_team(activity=activity) + activity.recipient.id = await client.get_bot_user_identity(activity=activity) + + if event.thread_ts: + activity.conversation.properties["thread_ts"] = event.thread_ts - # If this is a message originating from a user, we'll mark it as such - # If this is a message from a bot (bot_id != None), we want to ignore it by - # leaving the activity type as Event. This will stop it from being included in dialogs, - # but still allow the Bot to act on it if it chooses (via ActivityHandler.on_event_activity). - # NOTE: This catches a message from ANY bot, including this bot. - # Note also, bot_id here is not the same as bot_user_id so we can't (yet) identify messages - # originating from this bot without doing an additional API call. if event.type == "message" and not event.subtype and not event.bot_id: - activity.type = ActivityTypes.message + if not event.subtype: + activity.type = ActivityTypes.message + activity.text = event.text + + activity.conversation.properties["channel_type"] = event.channel_type + activity.value = event + else: + activity.name = event.type + activity.value = event return activity @@ -226,9 +238,11 @@ async def command_to_activity( channel_data=body, text=body.text, type=ActivityTypes.event, + name="Command", + value=body.command, ) - activity.recipient.id = await client.get_bot_user_by_team(activity) + activity.recipient.id = await client.get_bot_user_identity(activity) activity.conversation.properties["team"] = body.team_id return activity diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py index 38a7e3297..f0cca5e24 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py @@ -22,11 +22,11 @@ def __init__(self, **kwargs): self.icons = kwargs.get("icons") self.blocks: [Block] = kwargs.get("blocks") - self.attachments = None - if "attachments" in kwargs: - # Create proper Attachment objects - # It would appear that we can get dict fields from the wire that aren't defined - # in the Attachment class. So only pass in known fields. + # Create proper Attachment objects + # It would appear that we can get dict fields from the wire that aren't defined + # in the Attachment class. So only pass in known fields. + attachments = kwargs.get("attachments") + if attachments is not None: self.attachments = [ Attachment(**{x: att[x] for x in att if x in Attachment.attributes}) for att in kwargs.get("attachments") diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py index 9b7438619..3d929d362 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py @@ -1,31 +1,32 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -from typing import Optional, List -from slack.web.classes.actions import Action +import json +from typing import List from botbuilder.adapters.slack.slack_message import SlackMessage class SlackPayload: def __init__(self, **kwargs): - self.type: List[str] = kwargs.get("type") - self.token: str = kwargs.get("token") - self.channel: str = kwargs.get("channel") - self.thread_ts: str = kwargs.get("thread_ts") - self.team: str = kwargs.get("team") - self.user: str = kwargs.get("user") - self.actions: Optional[List[Action]] = None - self.trigger_id: str = kwargs.get("trigger_id") - self.action_ts: str = kwargs.get("action_ts") - self.submission: str = kwargs.get("submission") - self.callback_id: str = kwargs.get("callback_id") - self.state: str = kwargs.get("state") - self.response_url: str = kwargs.get("response_url") + payload = json.loads(kwargs.get("payload")) + + self.type: List[str] = payload.get("type") + self.token: str = payload.get("token") + self.channel: str = payload.get("channel") + self.thread_ts: str = payload.get("thread_ts") + self.team: str = payload.get("team") + self.user: str = payload.get("user") + self.actions = payload.get("actions") + self.trigger_id: str = payload.get("trigger_id") + self.action_ts: str = payload.get("action_ts") + self.submission: str = payload.get("submission") + self.callback_id: str = payload.get("callback_id") + self.state: str = payload.get("state") + self.response_url: str = payload.get("response_url") - if "message" in kwargs: - message = kwargs.get("message") + if "message" in payload: + message = payload.get("message") self.message = ( message - if isinstance(message) is SlackMessage + if isinstance(message, SlackMessage) else SlackMessage(**message) ) diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py index b8ad4bd06..f740c6a07 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py @@ -23,8 +23,8 @@ def __init__(self, **kwargs): self.command = kwargs.get("command") self.payload: SlackPayload = None - if "payload" in kwargs: - payload = kwargs.get("payload") + payload = kwargs.get("payload") + if payload is not None: self.payload = ( payload if isinstance(payload, SlackPayload) @@ -32,6 +32,6 @@ def __init__(self, **kwargs): ) self.event: SlackEvent = None - if "event" in kwargs: - event = kwargs.get("event") + event = kwargs.get("event") + if event is not None: self.event = event if isinstance(event, SlackEvent) else SlackEvent(**event)