|
| 1 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +# Licensed under the MIT License. |
| 3 | + |
| 4 | +from abc import ABC |
| 5 | +from typing import List, Callable, Awaitable |
| 6 | + |
| 7 | +from aiohttp.web_request import Request |
| 8 | +from aiohttp.web_response import Response |
| 9 | +from botframework.connector.auth import ClaimsIdentity |
| 10 | +from botbuilder.core import conversation_reference_extension |
| 11 | +from botbuilder.core import BotAdapter, TurnContext |
| 12 | +from botbuilder.schema import ( |
| 13 | + Activity, |
| 14 | + ResourceResponse, |
| 15 | + ActivityTypes, |
| 16 | + ConversationAccount, |
| 17 | + ConversationReference, |
| 18 | +) |
| 19 | + |
|
F987
20 | +from .activity_resourceresponse import ActivityResourceResponse |
| 21 | +from .slack_client import SlackClient |
| 22 | +from .slack_helper import SlackHelper |
| 23 | + |
| 24 | + |
| 25 | +class SlackAdapter(BotAdapter, ABC): |
| 26 | + """ |
| 27 | + BotAdapter that can handle incoming slack events. Incoming slack events are deserialized to an Activity |
| 28 | + that is dispatch through the middleware and bot pipeline. |
| 29 | + """ |
| 30 | + |
| 31 | + def __init__( |
| 32 | + self, |
| 33 | + client: SlackClient, |
| 34 | + on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None, |
| 35 | + ): |
| 36 | + super().__init__(on_turn_error) |
| 37 | + self.slack_client = client |
| 38 | + self.slack_logged_in = False |
| 39 | + |
| 40 | + async def send_activities( |
| 41 | + self, context: TurnContext, activities: List[Activity] |
| 42 | + ) -> List[ResourceResponse]: |
| 43 | + """ |
| 44 | + Standard BotBuilder adapter method to send a message from the bot to the messaging API. |
| 45 | +
|
| 46 | + :param context: A TurnContext representing the current incoming message and environment. |
| 47 | + :param activities: An array of outgoing activities to be sent back to the messaging API. |
| 48 | + :return: An array of ResourceResponse objects containing the IDs that Slack assigned to the sent messages. |
| 49 | + """ |
| 50 | + |
| 51 | + if not context: |
| 52 | + raise Exception("TurnContext is required") |
| 53 | + if not activities: |
| 54 | + raise Exception("List[Activity] is required") |
| 55 | + |
| 56 | + responses = [] |
| 57 | + |
| 58 | + for activity in activities: |
| 59 | + if activity.type == ActivityTypes.message: |
| 60 | + message = SlackHelper.activity_to_slack(activity) |
| 61 | + |
| 62 | + slack_response = await self.slack_client.post_message_to_slack(message) |
| 63 | + |
| 64 | + if slack_response and slack_response.status_code / 100 == 2: |
| 65 | + resource_response = ActivityResourceResponse( |
| 66 | + id=slack_response.data["ts"], |
| 67 | + activity_id=slack_response.data["ts"], |
| 68 | + conversation=ConversationAccount( |
| 69 | + id=slack_response.data["channel"] |
| 70 | + ), |
| 71 | + ) |
| 72 | + |
| 73 | + responses.append(resource_response) |
| 74 | + |
| 75 | + return responses |
| 76 | + |
| 77 | + async def update_activity(self, context: TurnContext, activity: Activity): |
| 78 | + """ |
| 79 | + Standard BotBuilder adapter method to update a previous message with new content. |
| 80 | +
|
| 81 | + :param context: A TurnContext representing the current incoming message and environment. |
| 82 | + :param activity: The updated activity in the form '{id: `id of activity to update`, ...}'. |
| 83 | + :return: A resource response with the Id of the updated activity. |
| 84 | + """ |
| 85 | + |
| 86 | + if not context: |
| 87 | + raise Exception("TurnContext is required") |
| 88 | + if not activity: |
| 89 | + raise Exception("Activity is required") |
| 90 | + if not activity.id: |
| 91 | + raise Exception("Activity.id is required") |
| 92 | + if not activity.conversation: |
| 93 | + raise Exception("Activity.conversation is required") |
| 94 | + |
| 95 | + message = SlackHelper.activity_to_slack(activity) |
| 96 | + results = await self.slack_client.update( |
| 97 | + timestamp=message.ts, channel_id=message.channel, text=message.text, |
| 98 | + ) |
| 99 | + |
| 100 | + if results.status_code / 100 != 2: |
| 101 | + raise Exception(f"Error updating activity on slack: {results}") |
| 102 | + |
| 103 | + return ResourceResponse(id=activity.id) |
| 104 | + |
| 105 | + async def delete_activity( |
| 106 | + self, context: TurnContext, reference: ConversationReference |
| 107 | + ): |
| 108 | + """ |
| 109 | + Standard BotBuilder adapter method to delete a previous message. |
| 110 | +
|
| 111 | + :param context: A TurnContext representing the current incoming message and environment. |
| 112 | + :param reference: An object in the form "{activityId: `id of message to delete`, |
| 113 | + conversation: { id: `id of slack channel`}}". |
| 114 | + """ |
| 115 | + |
| 116 | + if not context: |
| 117 | + raise Exception("TurnContext is required") |
| 118 | + if not reference: |
| 119 | + raise Exception("ConversationReference is required") |
| 120 | + if not reference.channel_id: |
| 121 | + raise Exception("ConversationReference.channel_id is required") |
| 122 | + if not context.activity.timestamp: |
| 123 | + raise Exception("Activity.timestamp is required") |
| 124 | + |
| 125 | + await self.slack_client.delete_message( |
| 126 | + channel_id=reference.channel_id, timestamp=context.activity.timestamp |
| 127 | + ) |
| 128 | + |
| 129 | + async def continue_conversation( |
| 130 | + self, |
| 131 | + reference: ConversationReference, |
| 132 | + callback: Callable, |
| 133 | + bot_id: str = None, # pylint: disable=unused-argument |
| 134 | + claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument |
| 135 | + ): |
| 136 | + """ |
| 137 | + Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation. |
| 138 | + Most _channels require a user to initiate a conversation with a bot before the bot can send activities |
| 139 | + to the user. |
| 140 | + :param bot_id: The application ID of the bot. This parameter is ignored in |
| 141 | + single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter |
| 142 | + which is multi-tenant aware. </param> |
| 143 | + :param reference: A reference to the conversation to continue.</param> |
| 144 | + :param callback: The method to call for the resulting bot turn.</param> |
| 145 | + :param claims_identity: |
| 146 | + """ |
| 147 | + |
| 148 | + if not reference: |
| 149 | + raise Exception("ConversationReference is required") |
| 150 | + if not callback: |
| 151 | + raise Exception("callback is required") |
| 152 | + |
| 153 | + request = TurnContext.apply_conversation_reference( |
| 154 | + conversation_reference_extension.get_continuation_activity(reference), |
| 155 | + reference, |
| 156 | + ) |
| 157 | + context = TurnContext(self, request) |
| 158 | + |
| 159 | + return await self.run_pipeline(context, callback) |
| 160 | + |
| 161 | + async def process(self, req: Request, logic: Callable) -> Response: |
| 162 | + """ |
| 163 | + Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic. |
| 164 | +
|
| 165 | + :param req: The aoihttp Request object |
| 166 | + :param logic: The method to call for the resulting bot turn.</param> |
| 167 | + :return: The aoihttp Response |
| 168 | + """ |
| 169 | + if not req: |
| 170 | + raise Exception("Request is required") |
| 171 | + |
| 172 | + if not self.slack_logged_in: |
| 173 | + await self.slack_client.login_with_slack() |
| 174 | + self.slack_logged_in = True |
| 175 | + |
| 176 | + body = await req.text() |
| 177 | + slack_body = SlackHelper.deserialize_body(req.content_type, body) |
| 178 | + |
| 179 | + if slack_body.type == "url_verification": |
| 180 | + return SlackHelper.response(req, 200, slack_body.challenge) |
| 181 | + |
| 182 | + if not self.slack_client.verify_signature(req, body): |
| 183 | + text = "Rejected due to mismatched header signature" |
| 184 | + return SlackHelper.response(req, 401, text) |
| 185 | + |
| 186 | + if ( |
| 187 | + not self.slack_client.options.slack_verification_token |
| 188 | + and slack_body.token != self.slack_client.options.slack_verification_token |
| 189 | + ): |
| 190 | + text = f"Rejected due to mismatched verificationToken:{body}" |
| 191 | + return SlackHelper.response(req, 403, text) |
| 192 | + |
| 193 | + if slack_body.payload: |
| 194 | + # handle interactive_message callbacks and block_actions |
| 195 | + activity = SlackHelper.payload_to_activity(slack_body.payload) |
| 196 | + elif slack_body.type == "event_callback": |
| 197 | + activity = await SlackHelper.event_to_activity( |
| 198 | + slack_body.event, self.slack_client |
| 199 | + ) |
| 200 | + elif slack_body.command: |
| 201 | + activity = await SlackHelper.command_to_activity( |
| 202 | + slack_body, self.slack_client |
| 203 | + ) |
| 204 | + else: |
| 205 | + raise Exception(f"Unknown Slack event type {slack_body.type}") |
| 206 | + |
| 207 | + context = TurnContext(self, activity) |
| 208 | + await self.run_pipeline(context, logic) |
| 209 | + |
| 210 | + return SlackHelper.response(req, 200) |
0 commit comments