4
4
from copy import deepcopy
5
5
from typing import List
6
6
7
- from botbuilder .schema import Activity , ActivityTypes , ExpectedReplies , DeliveryModes
8
- from botbuilder .core import (
9
- BotAdapter ,
10
- TurnContext ,
7
+ from botbuilder .schema import (
8
+ Activity ,
9
+ ActivityTypes ,
10
+ ExpectedReplies ,
11
+ DeliveryModes ,
12
+ SignInConstants ,
13
+ TokenExchangeInvokeRequest ,
11
14
)
15
+ from botbuilder .core import BotAdapter , TurnContext , ExtendedUserTokenProvider
16
+ from botbuilder .core .card_factory import ContentTypes
12
17
from botbuilder .core .skills import SkillConversationIdFactoryOptions
13
-
14
18
from botbuilder .dialogs import (
15
19
Dialog ,
16
20
DialogContext ,
17
21
DialogEvents ,
18
22
DialogReason ,
19
23
DialogInstance ,
20
24
)
25
+ from botframework .connector .token_api .models import TokenExchangeRequest
21
26
22
27
from .begin_skill_dialog_options import BeginSkillDialogOptions
23
28
from .skill_dialog_options import SkillDialogOptions
@@ -31,6 +36,7 @@ def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str):
31
36
32
37
self .dialog_options = dialog_options
33
38
self ._deliver_mode_state_key = "deliverymode"
39
+ self ._sso_connection_name_key = "SkillDialog.SSOConnectionName"
34
40
35
41
async def begin_dialog (self , dialog_context : DialogContext , options : object = None ):
36
42
"""
@@ -59,8 +65,14 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No
59
65
self ._deliver_mode_state_key
60
66
] = dialog_args .activity .delivery_mode
61
67
68
+ dialog_context .active_dialog .state [
69
+ self ._sso_connection_name_key
70
+ ] = dialog_args .connection_name
71
+
62
72
# Send the activity to the skill.
63
- eoc_activity = await self ._send_to_skill (dialog_context .context , skill_activity )
73
+ eoc_activity = await self ._send_to_skill (
74
+ dialog_context .context , skill_activity , dialog_args .connection_name
75
+ )
64
76
if eoc_activity :
65
77
return await dialog_context .end_dialog (eoc_activity .value )
66
78
@@ -84,23 +96,21 @@ async def continue_dialog(self, dialog_context: DialogContext):
84
96
dialog_context .context .activity .value
85
97
)
86
98
87
- # Forward only Message and Event activities to the skill
88
- if (
89
- dialog_context .context .activity .type == ActivityTypes .message
90
- or dialog_context .context .activity .type == ActivityTypes .event
91
- ):
92
- # Create deep clone of the original activity to avoid altering it before forwarding it.
93
- skill_activity = deepcopy (dialog_context .context .activity )
94
- skill_activity .delivery_mode = dialog_context .active_dialog .state [
95
- self ._deliver_mode_state_key
96
- ]
97
-
98
- # Just forward to the remote skill
99
- eoc_activity = await self ._send_to_skill (
100
- dialog_context .context , skill_activity
101
- )
102
- if eoc_activity :
103
- return await dialog_context .end_dialog (eoc_activity .value )
99
+ # Create deep clone of the original activity to avoid altering it before forwarding it.
100
+ skill_activity = deepcopy (dialog_context .context .activity )
101
+ skill_activity .delivery_mode = dialog_context .active_dialog .state [
102
+ self ._deliver_mode_state_key
103
+ ]
104
+ connection_name = dialog_context .active_dialog .state [
105
+ self ._sso_connection_name_key
106
+ ]
107
+
108
+ # Just forward to the remote skill
109
+ eoc_activity = await self ._send_to_skill (
110
+ dialog_context .context , skill_activity , connection_name
111
+ )
112
+ if eoc_activity :
113
+ return await dialog_context .end_dialog (eoc_activity .value )
104
114
105
115
return self .end_of_turn
106
116
@@ -119,6 +129,7 @@ async def reprompt_dialog( # pylint: disable=unused-argument
119
129
is_incoming = True ,
120
130
)
121
131
132
+ # connection Name is not applicable for a RePrompt, as we don't expect as OAuthCard in response.
122
133
await self ._send_to_skill (context , reprompt_event )
123
134
124
135
async def resume_dialog ( # pylint: disable=unused-argument
@@ -147,6 +158,7 @@ async def end_dialog(
147
158
activity .channel_data = context .activity .channel_data
148
159
activity .additional_properties = context .activity .additional_properties
149
160
161
+ # connection Name is not applicable for an EndDialog, as we don't expect as OAuthCard in response.
150
162
await self ._send_to_skill (context , activity )
151
163
152
164
await super ().end_dialog (context , instance , reason )
@@ -168,20 +180,10 @@ def _validate_begin_dialog_args(options: object) -> BeginSkillDialogOptions:
168
180
"SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None."
169
181
)
170
182
171
- # Only accept Message or Event activities
172
- if (
173
- dialog_args .activity .type != ActivityTypes .message
174
- and dialog_args .activity .type != ActivityTypes .event
175
- ):
176
- raise TypeError (
177
- f"Only { ActivityTypes .message } and { ActivityTypes .event } activities are supported."
178
- f" Received activity of type { dialog_args .activity .type } ."
179
- )
180
-
181
183
return dialog_args
182
184
183
185
async def _send_to_skill (
184
- self , context : TurnContext , activity : Activity ,
186
+ self , context : TurnContext , activity : Activity , connection_name : str = None
185
187
) -> Activity :
186
188
# Create a conversationId to interact with the skill and send the activity
187
189
conversation_id_factory_options = SkillConversationIdFactoryOptions (
@@ -226,8 +228,86 @@ async def _send_to_skill(
226
228
if from_skill_activity .type == ActivityTypes .end_of_conversation :
227
229
# Capture the EndOfConversation activity if it was sent from skill
228
230
eoc_activity = from_skill_activity
231
+ elif await self ._intercept_oauth_cards (
232
+ context , from_skill_activity , connection_name
233
+ ):
234
+ # do nothing. Token exchange succeeded, so no oauthcard needs to be shown to the user
235
+ pass
229
236
else :
230
237
# Send the response back to the channel.
231
238
await context .send_activity (from_skill_activity )
232
239
233
240
return eoc_activity
241
+
242
+ async def _intercept_oauth_cards (
243
+ self , context : TurnContext , activity : Activity , connection_name : str
244
+ ):
245
+ """
246
+ Tells is if we should intercept the OAuthCard message.
247
+ """
248
+ if not connection_name or not isinstance (
249
+ context .adapter , ExtendedUserTokenProvider
250
+ ):
251
+ return False
252
+
253
+ oauth_card_attachment = next (
254
+ attachment
255
+ for attachment in activity .attachments
256
+ if attachment .content_type == ContentTypes .oauth_card
257
+ )
258
+ if oauth_card_attachment :
259
+ oauth_card = oauth_card_attachment .content
260
+ if (
261
+ oauth_card
262
+ and oauth_card .token_exchange_resource
263
+ and oauth_card .token_exchange_resource .uri
264
+ ):
265
+ try :
266
+ result = await context .adapter .exchange_token (
267
+ turn_context = context ,
268
+ connection_name = connection_name ,
269
+ user_id = context .activity .from_property .id ,
270
+ exchange_request = TokenExchangeRequest (
271
+ uri = oauth_card .token_exchange_resource .uri
272
+ ),
273
+ )
274
+
275
+ if result and result .token :
276
+ return await self ._send_token_exchange_invoke_to_skill (
277
+ activity ,
278
+ oauth_card .token_exchange_resource .id ,
279
+ oauth_card .connection_name ,
280
+ result .token ,
281
+ )
282
+ except :
283
+ return False
284
+
285
+ return False
286
+
287
+ async def _send_token_exchange_invoke_to_skill (
288
+ self ,
289
+ incoming_activity : Activity ,
290
+ request_id : str ,
291
+ connection_name : str ,
292
+ token : str ,
293
+ ):
294
+ activity = incoming_activity .create_reply ()
295
+ activity .type = ActivityTypes .invoke
296
+ activity .name = SignInConstants .token_exchange_operation_name
297
+ activity .value = TokenExchangeInvokeRequest (
298
+ id = request_id , token = token , connection_name = connection_name ,
299
+ )
300
+
301
+ # route the activity to the skill
302
+ skill_info = self .dialog_options .skill
303
+ response = await self .dialog_options .skill_client .post_activity (
304
+ self .dialog_options .bot_id ,
305
+ skill_info .app_id ,
306
+ skill_info .skill_endpoint ,
307
+ self .dialog_options .skill_host_endpoint ,
308
+ incoming_activity .conversation .id ,
309
+ activity ,
310
+ )
311
+
312
+ # Check response status: true if success, false if failure
313
+ return response .status / 100 == 2
0 commit comments