# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import json import urllib.parse from aiohttp.web_request import Request from aiohttp.web_response import Response from slack.web.classes.attachments import Attachment from botbuilder.schema import ( Activity, ConversationAccount, ChannelAccount, ActivityTypes, ) from .slack_message import SlackMessage from .slack_client import SlackClient from .slack_event import SlackEvent from .slack_payload import SlackPayload from .slack_request_body import SlackRequestBody class SlackHelper: @staticmethod def activity_to_slack(activity: Activity) -> SlackMessage: """ Formats a BotBuilder Activity into an outgoing Slack message. :param activity: A BotBuilder Activity object. :type activity: :class:`botbuilder.schema.Activity` :return: A Slack message object with {text, attachments, channel, thread ts} and any fields found in activity.channelData. :rtype: :class:`SlackMessage` """ if not activity: raise Exception("Activity required") # use ChannelData if available if activity.channel_data: message = activity.channel_data else: message = SlackMessage( ts=activity.timestamp, text=activity.text, channel=activity.conversation.id, ) if activity.attachments: attachments = [] for att in activity.attachments: if att.name == "blocks": message.blocks = att.content else: new_attachment = Attachment( author_name=att.name, thumb_url=att.thumbnail_url, text="", ) attachments.append(new_attachment) if attachments: message.attachments = attachments if ( activity.conversation.properties and "thread_ts" in activity.conversation.properties ): message.thread_ts = activity.conversation.properties["thread_ts"] if message.ephemeral: message.user = activity.recipient.id if ( message.icon_url or not (message.icons and message.icons.status_emoji) or not message.username ): message.as_user = False return message @staticmethod def response( # pylint: disable=unused-argument req: Request, code: int, text: str = None, encoding: str = None ) -> Response: """ Formats an aiohttp Response. :param req: The original aiohttp Request. :type req: :class:`aiohttp.web_request.Request` :param code: The HTTP result code to return. :type code: int :param text: The text to return. :type text: str :param encoding: The text encoding. Defaults to UTF-8. :type encoding: str :return: The aoihttp Response :rtype: :class:`aiohttp.web_response.Response` """ response = Response(status=code) if text: response.content_type = "text/plain" response.body = text.encode(encoding=encoding if encoding else "utf-8") return response @staticmethod def payload_to_activity(payload: SlackPayload) -> Activity: """ Creates an activity based on the Slack event payload. :param payload: The payload of the Slack event. :type payload: :class:`SlackPayload` :return: An activity containing the event data. :rtype: :class:`botbuilder.schema.Activity` """ if not payload: raise Exception("payload is required") activity = Activity( channel_id="slack", conversation=ConversationAccount(id=payload.channel["id"], properties={}), from_property=ChannelAccount( 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: 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 @staticmethod async def event_to_activity(event: SlackEvent, client: SlackClient) -> Activity: """ Creates an activity based on the Slack event data. :param event: The data of the Slack event. :type event: :class:`SlackEvent` :param client: The Slack client. :type client: :class:`SlackClient` :return: An activity containing the event data. :rtype: :class:`botbuilder.schema.Activity` """ if not event: raise Exception("slack event is required") activity = Activity( id=event.event_ts, channel_id="slack", conversation=ConversationAccount( id=event.channel if event.channel else event.channel_id, properties={} ), from_property=ChannelAccount( id=event.bot_id if event.bot_id else event.user_id ), recipient=ChannelAccount(id=None), channel_data=event, text=event.text, type=ActivityTypes.event, ) 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_identity(activity=activity) if event.thread_ts: activity.conversation.properties["thread_ts"] = event.thread_ts if event.type == "message" and not event.subtype and not event.bot_id: 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 @staticmethod async def command_to_activity( body: SlackRequestBody, client: SlackClient ) -> Activity: """ Creates an activity based on a Slack event related to a slash command. :param body: The data of the Slack event. :type body: :class:`SlackRequestBody` :param client: The Slack client. :type client: :class:`SlackClient` :return: An activity containing the event data. :rtype: :class:`botbuilder.schema.Activity` """ if not body: raise Exception("body is required") activity = Activity( id=body.trigger_id, channel_id="slack", conversation=ConversationAccount(id=body.channel_id, properties={}), from_property=ChannelAccount(id=body.user_id), recipient=ChannelAccount(id=None), channel_data=body, text=body.text, type=ActivityTypes.event, name="Command", value=body.command, ) activity.recipient.id = await client.get_bot_user_identity(activity) activity.conversation.properties["team"] = body.team_id return activity @staticmethod def query_string_to_dictionary(query: str) -> {}: """ Converts a query string to a dictionary with key-value pairs. :param query: The query string to convert. :type query: str :return: A dictionary with the query values. :rtype: :class:`typing.Dict` """ values = {} if not query: return values pairs = query.replace("+", "%20").split("&") for pair in pairs: key_value = pair.split("=") key = key_value[0] value = urllib.parse.unquote(key_value[1]) values[key] = value return values @staticmethod def deserialize_body(content_type: str, request_body: str) -> SlackRequestBody: """ Deserializes the request's body as a SlackRequestBody object. :param content_type: The content type of the body. :type content_type: str :param request_body: The body of the request. :type request_body: str :return: A SlackRequestBody object. :rtype: :class:`SlackRequestBody` """ if not request_body: return None if content_type == "application/x-www-form-urlencoded": request_dict = SlackHelper.query_string_to_dictionary(request_body) elif content_type == "application/json": request_dict = json.loads(request_body) else: raise Exception("Unknown request content type") if "command=%2F" in request_body: return SlackRequestBody(**request_dict) if "payload=" in request_body: payload = SlackPayload(**request_dict) return SlackRequestBody(payload=payload, token=payload.token) return SlackRequestBody(**request_dict)