44from copy import deepcopy
55from typing import List
66
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 ,
1114)
15+ from botbuilder .core import BotAdapter , TurnContext , ExtendedUserTokenProvider
16+ from botbuilder .core .card_factory import ContentTypes
1217from botbuilder .core .skills import SkillConversationIdFactoryOptions
13-
1418from botbuilder .dialogs import (
1519 Dialog ,
1620 DialogContext ,
1721 DialogEvents ,
1822 DialogReason ,
1923 DialogInstance ,
2024)
25+ from botframework .connector .token_api .models import TokenExchangeRequest
2126
2227from .begin_skill_dialog_options import BeginSkillDialogOptions
2328from .skill_dialog_options import SkillDialogOptions
@@ -31,6 +36,7 @@ def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str):
3136
3237 self .dialog_options = dialog_options
3338 self ._deliver_mode_state_key = "deliverymode"
39+ self ._sso_connection_name_key = "SkillDialog.SSOConnectionName"
3440
3541 async def begin_dialog (self , dialog_context : DialogContext , options : object = None ):
3642 """
@@ -59,8 +65,14 @@ async def begin_dialog(self, dialog_context: DialogContext, options: object = No
5965 self ._deliver_mode_state_key
6066 ] = dialog_args .activity .delivery_mode
6167
68+ dialog_context .active_dialog .state [
69+ self ._sso_connection_name_key
70+ ] = dialog_args .connection_name
71+
6272 # 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+ )
6476 if eoc_activity :
6577 return await dialog_context .end_dialog (eoc_activity .value )
6678
@@ -84,23 +96,21 @@ async def continue_dialog(self, dialog_context: DialogContext):
8496 dialog_context .context .activity .value
8597 )
8698
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 )
104114
105115 return self .end_of_turn
106116
@@ -119,6 +129,7 @@ async def reprompt_dialog( # pylint: disable=unused-argument
119129 is_incoming = True ,
120130 )
121131
132+ # connection Name is not applicable for a RePrompt, as we don't expect as OAuthCard in response.
122133 await self ._send_to_skill (context , reprompt_event )
123134
124135 async def resume_dialog ( # pylint: disable=unused-argument
@@ -147,6 +158,7 @@ async def end_dialog(
147158 activity .channel_data = context .activity .channel_data
148159 activity .additional_properties = context .activity .additional_properties
149160
161+ # connection Name is not applicable for an EndDialog, as we don't expect as OAuthCard in response.
150162 await self ._send_to_skill (context , activity )
151163
152164 await super ().end_dialog (context , instance , reason )
@@ -168,20 +180,10 @@ def _validate_begin_dialog_args(options: object) -> BeginSkillDialogOptions:
168180 "SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None."
169181 )
170182
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-
181183 return dialog_args
182184
183185 async def _send_to_skill (
184- self , context : TurnContext , activity : Activity ,
186+ self , context : TurnContext , activity : Activity , connection_name : str = None
185187 ) -> Activity :
186188 # Create a conversationId to interact with the skill and send the activity
187189 conversation_id_factory_options = SkillConversationIdFactoryOptions (
@@ -226,8 +228,86 @@ async def _send_to_skill(
226228 if from_skill_activity .type == ActivityTypes .end_of_conversation :
227229 # Capture the EndOfConversation activity if it was sent from skill
228230 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
229236 else :
230237 # Send the response back to the channel.
231238 await context .send_activity (from_skill_activity )
232239
233240 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