Skip to content
83 changes: 83 additions & 0 deletions libraries/botbuilder-adapters-slack/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

=================================
BotBuilder-Adapters SDK for Python
=================================

.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
:target: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
:align: right
:alt: Azure DevOps status for master branch
.. image:: https://badge.fury.io/py/botbuilder-dialogs.svg
:target: https://badge.fury.io/py/botbuilder-dialogs
:alt: Latest PyPI package version

A dialog stack based conversation manager for Microsoft BotBuilder.

How to Install
==============

.. code-block:: python

pip install botbuilder-dialogs


Documentation/Wiki
==================

You can find more information on the botbuilder-python project by visiting our `Wiki`_.

Requirements
============

* `Python >= 3.7.0`_


Source Code
===========
The latest developer version is available in a github repository:
https://github.com/Microsoft/botbuilder-python/


Contributing
============

This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.

When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the `Microsoft Open Source Code of Conduct`_.
For more information see the `Code of Conduct FAQ`_ or
contact `opencode@microsoft.com`_ with any additional questions or comments.

Reporting Security Issues
=========================

Security issues and bugs should be reported privately, via email, to the Microsoft Security
Response Center (MSRC) at `secure@microsoft.com`_. You should
receive a response within 24 hours. If for some reason you do not, please follow up via
email to ensure we received your original message. Further information, including the
`MSRC PGP`_ key, can be found in
the `Security TechCenter`_.

License
=======

Copyright (c) Microsoft Corporation. All rights reserved.

Licensed under the MIT_ License.

.. _Wiki: https://github.com/Microsoft/botbuilder-python/wiki
.. _Python >= 3.7.0: https://www.python.org/downloads/
.. _MIT: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
.. _Microsoft Open Source Code of Conduct: https://opensource.microsoft.com/codeofconduct/
.. _Code of Conduct FAQ: https://opensource.microsoft.com/codeofconduct/faq/
.. _opencode@microsoft.com: mailto:opencode@microsoft.com
.. _secure@microsoft.com: mailto:secure@microsoft.com
.. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155
.. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt

.. <https://technet.microsoft.com/en-us/security/default>`_
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# coding=utf-8
# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------

from .about import __version__
from .slack_options import SlackAdapterOptions
from .slack_client import SlackClient
from .slack_adapter import SlackAdapter
from .slack_payload import SlackPayload
from .slack_message import SlackMessage
from .slack_event import SlackEvent
from .activity_resourceresponse import ActivityResourceResponse
from .slack_request_body import SlackRequestBody
from .slack_helper import SlackHelper

__all__ = [
"__version__",
"SlackAdapterOptions",
"SlackClient",
"SlackAdapter",
"SlackPayload",
"SlackMessage",
"SlackEvent",
"ActivityResourceResponse",
"SlackRequestBody",
"SlackHelper",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os

__title__ = "botbuilder-adapters-slack"
__version__ = (
os.environ["packageVersion"] if "packageVersion" in os.environ else "4.7.1"
)
__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
__author__ = "Microsoft"
__description__ = "Microsoft Bot Framework Bot Builder"
__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
__license__ = "MIT"
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.

from botbuilder.schema import ResourceResponse, ConversationAccount


class ActivityResourceResponse(ResourceResponse):
def __init__(self, activity_id: str, conversation: ConversationAccount, **kwargs):
super().__init__(**kwargs)
self.activity_id = activity_id
self.conversation = conversation
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC
from typing import List, Callable, Awaitable

from aiohttp.web_request import Request
from aiohttp.web_response import Response
from botframework.connector.auth import ClaimsIdentity
from botbuilder.core import conversation_reference_extension
from botbuilder.core import BotAdapter, TurnContext
from botbuilder.schema import (
Activity,
ResourceResponse,
ActivityTypes,
ConversationAccount,
ConversationReference,
)

from .activity_resourceresponse import ActivityResourceResponse
from .slack_client import SlackClient
from .slack_helper import SlackHelper


class SlackAdapter(BotAdapter, ABC):
"""
BotAdapter that can handle incoming slack events. Incoming slack events are deserialized to an Activity
that is dispatch through the middleware and bot pipeline.
"""

def __init__(
self,
client: SlackClient,
on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None,
):
super().__init__(on_turn_error)
self.slack_client = client
self.slack_logged_in = False

async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
"""
Standard BotBuilder adapter method to send a message from the bot to the messaging API.

:param context: A TurnContext representing the current incoming message and environment.
:param activities: An array of outgoing activities to be sent back to the messaging API.
:return: An array of ResourceResponse objects containing the IDs that Slack assigned to the sent messages.
"""

if not context:
raise Exception("TurnContext is required")
if not activities:
raise Exception("List[Activity] is required")

responses = []

for activity in activities:
if activity.type == ActivityTypes.message:
message = SlackHelper.activity_to_slack(activity)

slack_response = await self.slack_client.post_message_to_slack(message)

if slack_response and slack_response.status_code / 100 == 2:
resource_response = ActivityResourceResponse(
id=slack_response.data["ts"],
activity_id=slack_response.data["ts"],
conversation=ConversationAccount(
id=slack_response.data["channel"]
),
)

responses.append(resource_response)

return responses

async def update_activity(self, context: TurnContext, activity: Activity):
"""
Standard BotBuilder adapter method to update a previous message with new content.

:param context: A TurnContext representing the current incoming message and environment.
:param activity: The updated activity in the form '{id: `id of activity to update`, ...}'.
:return: A resource response with the Id of the updated activity.
"""

if not context:
raise Exception("TurnContext is required")
if not activity:
raise Exception("Activity is required")
if not activity.id:
raise Exception("Activity.id is required")
if not activity.conversation:
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,
)

if results.status_code / 100 != 2:
raise Exception(f"Error updating activity on slack: {results}")

return ResourceResponse(id=activity.id)

async def delete_activity(
self, context: TurnContext, reference: ConversationReference
):
"""
Standard BotBuilder adapter method to delete a previous message.

:param context: A TurnContext representing the current incoming message and environment.
:param reference: An object in the form "{activityId: `id of message to delete`,
conversation: { id: `id of slack channel`}}".
"""

if not context:
raise Exception("TurnContext is required")
if not reference:
raise Exception("ConversationReference is required")
if not reference.channel_id:
raise Exception("ConversationReference.channel_id is required")
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
)

async def continue_conversation(
self,
reference: ConversationReference,
callback: Callable,
bot_id: str = None, # pylint: disable=unused-argument
claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument
):
"""
Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation.
Most _channels require a user to initiate a conversation with a bot before the bot can send activities
to the user.
:param bot_id: The application ID of the bot. This parameter is ignored in
single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter
which is multi-tenant aware. </param>
:param reference: A reference to the conversation to continue.</param>
:param callback: The method to call for the resulting bot turn.</param>
:param claims_identity:
"""

if not reference:
raise Exception("ConversationReference is required")
if not callback:
raise Exception("callback is required")

request = TurnContext.apply_conversation_reference(
conversation_reference_extension.get_continuation_activity(reference),
reference,
)
context = TurnContext(self, request)

return await self.run_pipeline(context, callback)

async def process(self, req: Request, logic: Callable) -> Response:
"""
Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic.

:param req: The aoihttp Request object
:param logic: The method to call for the resulting bot turn.</param>
:return: The aoihttp Response
"""
if not req:
raise Exception("Request is required")

if not self.slack_logged_in:
await self.slack_client.login_with_slack()
self.slack_logged_in = True

body = await req.text()
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
):
text = f"Rejected due to mismatched verificationToken:{body}"
return SlackHelper.response(req, 403, text)

if slack_body.payload:
# handle interactive_message callbacks and block_actions
activity = SlackHelper.payload_to_activity(slack_body.payload)
elif slack_body.type == "event_callback":
activity = await SlackHelper.event_to_activity(
slack_body.event, self.slack_client
)
elif slack_body.command:
activity = await SlackHelper.command_to_activity(
slack_body, self.slack_client
)
else:
raise Exception(f"Unknown Slack event type {slack_body.type}")

context = TurnContext(self, activity)
await self.run_pipeline(context, logic)

return SlackHelper.response(req, 200)
Loading