Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -27,4 +28,5 @@
"ActivityResourceResponse",
"SlackRequestBody",
"SlackHelper",
"SlackAdapterOptions",
]
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Copy link
Member

@axelsrz axelsrz Apr 12, 2021

Choose a reason for hiding this comment

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

This is nitpicking but this could be changed to:
self.options = options or SlackAdapterOptions


async def send_activities(
self, context: TurnContext, activities: List[Activity]
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")

Expand Down Expand Up @@ -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"]
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading