1
1
# Copyright (c) Microsoft Corporation. All rights reserved.
2
2
# Licensed under the MIT License.
3
3
4
- from abc import ABC
5
- from typing import List
4
+ import base64
5
+ from copy import deepcopy
6
+ import json
7
+ import requests
8
+ from typing import Dict , List
6
9
7
- from botbuilder .core import Bot , BotAdapter
10
+ from botbuilder .core import Bot , BotAdapter , InvokeResponse
8
11
from botbuilder .schema import (
9
12
Activity ,
10
13
AttachmentData ,
18
21
RoleTypes ,
19
22
Transcript ,
20
23
)
21
- from botframework .connector .auth import ClaimsIdentity
24
+ from botframework .connector .auth import (
25
+ AuthenticationConfiguration ,
26
+ ChannelProvider ,
27
+ ClaimsIdentity ,
28
+ CredentialProvider ,
29
+ GovernmentConstants ,
30
+ MicrosoftAppCredentials ,
31
+ )
22
32
33
+ from .bot_framework_skill import BotFrameworkSkill
23
34
from .channel_api_args import ChannelApiArgs
24
35
from .channel_api_methods import ChannelApiMethods
25
36
from .channel_api_middleware import ChannelApiMiddleware
26
37
from .skill_conversation import SkillConversation
27
38
28
39
29
- class SkillClient ( ABC ) :
40
+ class BotFrameworkSkillClient :
30
41
31
42
"""
32
43
A skill host adapter implements API to forward activity to a skill and
33
44
implements routing ChannelAPI calls from the Skill up through the bot/adapter.
34
45
"""
35
46
36
47
INVOKE_ACTIVITY_NAME = "SkillEvents.ChannelApiInvoke"
48
+ _BOT_IDENTITY_KEY = "BotIdentity"
37
49
38
- def __init__ (self , adapter : BotAdapter , logger : object = None ):
39
-
40
- self ._bot_adapter = adapter
50
+ def __init__ (
51
+ self ,
52
+ credential_provider : CredentialProvider ,
53
+ auth_config : AuthenticationConfiguration ,
54
+ channel_provider : ChannelProvider = None ,
55
+ logger : object = None ,
56
+ ):
57
+ if not credential_provider :
58
+ raise TypeError ("credential_provider can't be None" )
59
+
60
+ if not auth_config :
61
+ raise TypeError ("auth_config can't be None" )
62
+ self ._credential_provider = credential_provider
63
+ self ._auth_config = auth_config
64
<
9E7A
code class="diff-text syntax-highlighted-line addition">+ self ._channel_provider = channel_provider
41
65
self ._logger = logger
42
66
43
- if not any (
44
- isinstance (middleware , ChannelApiMiddleware )
45
- for middleware in adapter .middleware_set
46
- ):
47
- adapter .middleware_set .use (ChannelApiMiddleware (self ))
67
+ self ._app_credentials_cache : Dict [str :MicrosoftAppCredentials ] = {}
68
+
69
+ async def forward_activity (
70
+ self ,
71
+ bot_id : str ,
72
+ skill : BotFrameworkSkill ,
73
+ skill_host_endpoint : str ,
74
+ activity : Activity ,
75
+ ) -> InvokeResponse :
76
+ app_credentials = await self ._get_app_credentials (bot_id , skill .app_id )
77
+
78
+ if not app_credentials :
79
+ raise RuntimeError ("Unable to get appCredentials to connect to the skill" )
80
+
81
+ # Get token for the skill call
82
+ token = app_credentials .get_access_token ()
83
+ activity_clone = deepcopy (activity )
84
+
85
+ # TODO use SkillConversation class here instead of hard coded encoding...
86
+ # Encode original bot service URL and ConversationId in the new conversation ID so we can unpack it later.
87
+ # var skillConversation = new SkillConversation()
88
+ # { ServiceUrl = activity.ServiceUrl, ConversationId = activity.Conversation.Id };
89
+ # activity.Conversation.Id = skillConversation.GetSkillConversationId()
90
+ json_str = json .dumps (
91
+ [activity_clone .conversation .conversation_id , activity_clone .service_url ]
92
+ )
93
+ activity_clone .conversation .id = str (
94
+ base64 .b64encode (json_str .encode ("utf-8" )), "utf-8"
95
+ )
96
+ activity_clone .service_url = skill_host_endpoint
97
+ activity_clone .recipient .properties ["skillId" ] = skill .id
98
+ json_content = json .dumps (activity_clone .as_dict ())
99
+ with requests .Session () as session :
100
+ resp = session .post (
101
+ skill .skill_endpoint ,
102
+ data = json_content .encode ("utf-8" ),
103
+ headers = {
104
+ "Authorization" : f"Bearer:{ token } " ,
105
+ "Content-type" : "application/json; charset=utf-8" ,
106
+ },
107
+ )
108
+ resp .raise_for_status ()
109
+ content = resp .json
110
+
111
+ if content :
112
+ return InvokeResponse (status = resp .status_code , body = content )
113
+
114
+ return None
48
115
49
116
async def get_conversations (
50
117
self ,
@@ -454,20 +521,28 @@ async def upload_attachment(
454
521
455
522
async def _invoke_channel_api (
456
523
self ,
524
+ adapter : BotAdapter ,
457
525
bot : Bot ,
458
526
claims_identity : ClaimsIdentity ,
459
527
method : str ,
460
528
conversation_id : str ,
461
529
* args ,
462
530
) -> object :
531
+
532
+ if not any (
533
+ isinstance (middleware , ChannelApiMiddleware )
534
+ for middleware in adapter .middleware_set
535
+ ):
536
+ adapter .middleware_set .use (ChannelApiMiddleware (self ))
537
+
463
538
if self ._logger :
464
539
self ._logger .log (f'InvokeChannelApiAsync(). Invoking method "{ method } "' )
465
540
466
541
skill_conversation = SkillConversation (conversation_id )
467
542
468
543
# TODO: Extension for create_invoke_activity
469
544
channel_api_invoke_activity : Activity = Activity .create_invoke_activity ()
470
- channel_api_invoke_activity .name = SkillClient .INVOKE_ACTIVITY_NAME
545
+ channel_api_invoke_activity .name = BotFrameworkSkillClient .INVOKE_ACTIVITY_NAME
471
546
channel_api_invoke_activity .channel_id = "unknown"
472
547
channel_api_invoke_activity .service_url = skill_conversation .service_url
473
548
channel_api_invoke_activity .conversation = ConversationAccount (
@@ -507,7 +582,7 @@ async def _invoke_channel_api(
507
582
channel_api_invoke_activity .value = channel_api_args
508
583
509
584
# send up to the bot to process it...
510
- await self . _bot_adapter .process_activity_with_claims (
585
+ await adapter .process_activity_with_claims (
511
586
claims_identity , channel_api_invoke_activity , bot .on_turn
512
587
)
513
588
@@ -516,3 +591,30 @@ async def _invoke_channel_api(
516
591
517
592
# Return the result that was captured in the middleware handler.
518
593
return channel_api_args .result
594
+
595
+ async def _get_app_credentials (
596
+ self , app_id : str , oauth_scope : str
597
+ ) -> MicrosoftAppCredentials :
598
+ if not app_id :
599
+ return MicrosoftAppCredentials (None , None )
600
+
601
+ cache_key = f"{ app_id } { oauth_scope } "
602
+ app_credentials = self ._app_credentials_cache .get (cache_key )
603
+
604
+ if app_credentials :
605
+ return app_credentials
606
+
607
+ app_password = await self ._credential_provider .get_app_password (app_id )
608
+ app_credentials = MicrosoftAppCredentials (
609
+ app_id , app_password , oauth_scope = oauth_scope
610
+ )
611
+ if self ._channel_provider .is_government ():
612
+ app_credentials .oauth_endpoint = (
613
+ GovernmentConstants .TO_CHANNEL_FROM_BOT_LOGIN_URL
614
+ )
615
+ app_credentials .oauth_scope = (
616
+ GovernmentConstants .TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
617
+ )
618
+
619
+ self ._app_credentials_cache [cache_key ] = app_credentials
620
+ return app_credentials
0 commit comments