File size: 9,920 Bytes
0827183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# 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
from .slack_adatper_options import SlackAdapterOptions


class SlackAdapter(BotAdapter, ABC):
    """

    BotAdapter that can handle incoming Slack events. Incoming Slack events are deserialized to an Activity that is

     dispatched through the middleware and bot pipeline.

    """

    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]

    ) -> List[ResourceResponse]:
        """

        Send a message from the bot to the messaging API.



        :param context: A TurnContext representing the current incoming message and environment.

        :type context: :class:`botbuilder.core.TurnContext`

        :param activities: An array of outgoing activities to be sent back to the messaging API.

        :type activities: :class:`typing.List[Activity]`

        :return: An array of ResourceResponse objects containing the IDs that Slack assigned to the sent messages.

        :rtype: :class:`typing.List[ResourceResponse]`

        """

        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(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):
        """

        Update a previous message with new content.



        :param context: A TurnContext representing the current incoming message and environment.

        :type context: :class:`botbuilder.core.TurnContext`

        :param activity: The updated activity in the form '{id: `id of activity to update`, ...}'.

        :type activity: :class:`botbuilder.schema.Activity`

        :return: A resource response with the ID of the updated activity.

        :rtype: :class:`botbuilder.schema.ResourceResponse`

        """

        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.chat_update(
            ts=message.ts,
            channel=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

    ):
        """

        Delete a previous message.



        :param context: A TurnContext representing the current incoming message and environment.

        :type context: :class:`botbuilder.core.TurnContext`

        :param reference: An object in the form "{activityId: `id of message to delete`,conversation: { id: `id of Slack

         channel`}}".

        :type reference: :class:`botbuilder.schema.ConversationReference`

        """

        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.chat_delete(
            channel=reference.conversation.id, ts=reference.activity_id
        )

    async def continue_conversation(

        self,

        reference: ConversationReference,

        callback: Callable,

        bot_id: str = None,  # pylint: disable=unused-argument

        claims_identity: ClaimsIdentity = None,

        audience: str = None,  # pylint: disable=unused-argument

    ):
        """

        Send a proactive message to a conversation.



        .. remarks::



            Most channels require a user to initiate a conversation with a bot before the bot can send activities to the

             user.



        :param reference: A reference to the conversation to continue.

        :type reference: :class:`botbuilder.schema.ConversationReference`

        :param callback: The method to call for the resulting bot turn.

        :type callback: :class:`typing.Callable`

        :param bot_id: Unused for this override.

        :type bot_id: str

        :param claims_identity: A ClaimsIdentity for the conversation.

        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`

        :param audience: Unused for this override.

        :type audience: str

        """

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

        if claims_identity:
            request = conversation_reference_extension.get_continuation_activity(
                reference
            )
            context = TurnContext(self, request)
            context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
            context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback
        else:
            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 aiohttp Request object.

        :type req: :class:`aiohttp.web_request.Request`

        :param logic: The method to call for the resulting bot turn.

        :type logic: :class:`tying.Callable`

        :return: The aiohttp Response.

        :rtype: :class:`aiohttp.web_response.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()

        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.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:
            return SlackHelper.response(
                req, 200, f"Unknown Slack event type {slack_body.type}"
            )

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

        return SlackHelper.response(req, 200)