diff --git a/libraries/botbuilder-core/botbuilder/core/skills/__init__.py b/libraries/botbuilder-core/botbuilder/core/skills/__init__.py index 6bd5a66b8..116f9aeef 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/__init__.py @@ -7,12 +7,14 @@ from .bot_framework_skill import BotFrameworkSkill from .conversation_id_factory import ConversationIdFactoryBase -from .skill_conversation_id_factory import SkillConversationIdFactory from .skill_handler import SkillHandler +from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions +from .skill_conversation_reference import SkillConversationReference __all__ = [ "BotFrameworkSkill", "ConversationIdFactoryBase", - "SkillConversationIdFactory", + "SkillConversationIdFactoryOptions", + "SkillConversationReference", "SkillHandler", ] diff --git a/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py b/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py index 7c015de08..35b1d8b6a 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py @@ -1,22 +1,66 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + from abc import ABC, abstractmethod +from typing import Union from botbuilder.schema import ConversationReference +from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions +from .skill_conversation_reference import SkillConversationReference class ConversationIdFactoryBase(ABC): + """ + Handles creating conversation ids for skill and should be subclassed. + + .. remarks:: + Derive from this class to handle creation of conversation ids, retrieval of + SkillConversationReferences and deletion. + """ + @abstractmethod async def create_skill_conversation_id( - self, conversation_reference: ConversationReference + self, + options_or_conversation_reference: Union[ + SkillConversationIdFactoryOptions, ConversationReference + ], ) -> str: + """ + Using the options passed in, creates a conversation id and + SkillConversationReference, storing them for future use. + + :param options_or_conversation_reference: The options contain properties useful + for generating a SkillConversationReference and conversation id. + :type options_or_conversation_reference: :class: + `Union[SkillConversationIdFactoryOptions, ConversationReference]` + + :returns: A skill conversation id. + + .. note:: + SkillConversationIdFactoryOptions is the preferred parameter type, while ConversationReference + type is provided for backwards compatability. + """ raise NotImplementedError() @abstractmethod async def get_conversation_reference( self, skill_conversation_id: str - ) -> ConversationReference: + ) -> Union[SkillConversationReference, ConversationReference]: + """ + Retrieves a SkillConversationReference using a conversation id passed in. + + :param skill_conversation_id: The conversation id for which to retrieve + the SkillConversationReference. + :type skill_conversation_id: str + + .. note:: + SkillConversationReference is the preferred return type, while ConversationReference + type is provided for backwards compatability. + """ raise NotImplementedError() @abstractmethod async def delete_conversation_reference(self, skill_conversation_id: str): + """ + Removes any reference to objects keyed on the conversation id passed in. + """ raise NotImplementedError() diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory.py deleted file mode 100644 index 6b01865fc..000000000 --- a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import hashlib -from typing import Dict, Tuple - -from botbuilder.core import Storage -from botbuilder.schema import ConversationReference - -from .conversation_id_factory import ConversationIdFactoryBase - - -class SkillConversationIdFactory(ConversationIdFactoryBase): - def __init__(self, storage: Storage): - if not storage: - raise TypeError("storage can't be None") - - self._storage = storage - self._forward_x_ref: Dict[str, str] = {} - self._backward_x_ref: Dict[str, Tuple[str, str]] = {} - - async def create_skill_conversation_id( - self, conversation_reference: ConversationReference - ) -> str: - if not conversation_reference: - raise TypeError("conversation_reference can't be None") - - if not conversation_reference.conversation.id: - raise TypeError("conversation id in conversation reference can't be None") - - if not conversation_reference.channel_id: - raise TypeError("channel id in conversation reference can't be None") - - storage_key = hashlib.md5( - f"{conversation_reference.conversation.id}{conversation_reference.channel_id}".encode() - ).hexdigest() - - skill_conversation_info = {storage_key: conversation_reference} - - await self._storage.write(skill_conversation_info) - - return storage_key - - async def get_conversation_reference( - self, skill_conversation_id: str - ) -> ConversationReference: - if not skill_conversation_id: - raise TypeError("skill_conversation_id can't be None") - - skill_conversation_info = await self._storage.read([skill_conversation_id]) - - return skill_conversation_info.get(skill_conversation_id) - - async def delete_conversation_reference(self, skill_conversation_id: str): - await self._storage.delete([skill_conversation_id]) diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py new file mode 100644 index 000000000..9eae6ec75 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.schema import Activity +from .bot_framework_skill import BotFrameworkSkill + + +class SkillConversationIdFactoryOptions: + def __init__( + self, + from_bot_oauth_scope: str, + from_bot_id: str, + activity: Activity, + bot_framework_skill: BotFrameworkSkill, + ): + if from_bot_oauth_scope is None: + raise TypeError( + "SkillConversationIdFactoryOptions(): from_bot_oauth_scope cannot be None." + ) + + if from_bot_id is None: + raise TypeError( + "SkillConversationIdFactoryOptions(): from_bot_id cannot be None." + ) + + if activity is None: + raise TypeError( + "SkillConversationIdFactoryOptions(): activity cannot be None." + ) + + if bot_framework_skill is None: + raise TypeError( + "SkillConversationIdFactoryOptions(): bot_framework_skill cannot be None." + ) + + self.from_bot_oauth_scope = from_bot_oauth_scope + self.from_bot_id = from_bot_id + self.activity = activity + self.bot_framework_skill = bot_framework_skill diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py new file mode 100644 index 000000000..877f83141 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from botbuilder.schema import ConversationReference + + +class SkillConversationReference: + """ + ConversationReference implementation for Skills ConversationIdFactory. + """ + + def __init__(self, conversation_reference: ConversationReference, oauth_scope: str): + if conversation_reference is None: + raise TypeError( + "SkillConversationReference(): conversation_reference cannot be None." + ) + + if oauth_scope is None: + raise TypeError("SkillConversationReference(): oauth_scope cannot be None.") + + self.conversation_reference = conversation_reference + self.oauth_scope = oauth_scope diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py index 3158d35e6..0aabdc9f3 100644 --- a/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py @@ -12,12 +12,14 @@ ) from botframework.connector.auth import ( AuthenticationConfiguration, + AuthenticationConstants, ChannelProvider, ClaimsIdentity, CredentialProvider, + GovernmentConstants, ) - -from .skill_conversation_id_factory import SkillConversationIdFactory +from .skill_conversation_reference import SkillConversationReference +from .conversation_id_factory import ConversationIdFactoryBase class SkillHandler(ChannelServiceHandler): @@ -30,7 +32,7 @@ def __init__( self, adapter: BotAdapter, bot: Bot, - conversation_id_factory: SkillConversationIdFactory, + conversation_id_factory: ConversationIdFactoryBase, credential_provider: CredentialProvider, auth_configuration: AuthenticationConfiguration, channel_provider: ChannelProvider = None, @@ -118,14 +120,29 @@ async def _process_activity( reply_to_activity_id: str, activity: Activity, ) -> ResourceResponse: - conversation_reference = await self._conversation_id_factory.get_conversation_reference( + conversation_reference_result = await self._conversation_id_factory.get_conversation_reference( conversation_id ) + oauth_scope = None + conversation_reference = None + if isinstance(conversation_reference_result, SkillConversationReference): + oauth_scope = conversation_reference_result.oauth_scope + conversation_reference = ( + conversation_reference_result.conversation_reference + ) + else: + conversation_reference = conversation_reference_result + oauth_scope = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + if self._channel_provider and self._channel_provider.is_government() + else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + if not conversation_reference: raise KeyError("ConversationReference not found") - skill_conversation_reference = ConversationReference( + activity_conversation_reference = ConversationReference( activity_id=activity.id, user=activity.from_property, bot=activity.recipient, @@ -137,7 +154,7 @@ async def _process_activity( async def callback(context: TurnContext): context.turn_state[ SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY - ] = skill_conversation_reference + ] = activity_conversation_reference TurnContext.apply_conversation_reference(activity, conversation_reference) context.activity.id = reply_to_activity_id @@ -154,7 +171,10 @@ async def callback(context: TurnContext): await context.send_activity(activity) await self._adapter.continue_conversation( - conversation_reference, callback, claims_identity=claims_identity + conversation_reference, + callback, + claims_identity=claims_identity, + audience=oauth_scope, ) return ResourceResponse(id=str(uuid4())) diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py new file mode 100644 index 000000000..8699c0ad8 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_http_client.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import ( + BotFrameworkHttpClient, + InvokeResponse, +) +from botbuilder.core.skills import ( + ConversationIdFactoryBase, + SkillConversationIdFactoryOptions, + BotFrameworkSkill, +) +from botbuilder.schema import Activity +from botframework.connector.auth import ( + AuthenticationConstants, + ChannelProvider, + GovernmentConstants, + SimpleCredentialProvider, +) + + +class SkillHttpClient(BotFrameworkHttpClient): + def __init__( + self, + credential_provider: SimpleCredentialProvider, + skill_conversation_id_factory: ConversationIdFactoryBase, + channel_provider: ChannelProvider = None, + ): + if not skill_conversation_id_factory: + raise TypeError( + "SkillHttpClient(): skill_conversation_id_factory can't be None" + ) + + super().__init__(credential_provider) + + self._skill_conversation_id_factory = skill_conversation_id_factory + self._channel_provider = channel_provider + + async def post_activity_to_skill( + self, + from_bot_id: str, + to_skill: BotFrameworkSkill, + service_url: str, + activity: Activity, + originating_audience: str = None, + ) -> InvokeResponse: + + if originating_audience is None: + originating_audience = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + if self._channel_provider is not None + and self._channel_provider.IsGovernment() + else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + + options = SkillConversationIdFactoryOptions( + from_bot_oauth_scope=originating_audience, + from_bot_id=from_bot_id, + activity=activity, + bot_framework_skill=to_skill, + ) + + skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id( + options + ) + + return await super().post_activity( + from_bot_id, + to_skill.app_id, + to_skill.skill_endpoint, + service_url, + skill_conversation_id, + activity, + ) diff --git a/libraries/botbuilder-core/tests/skills/test_skill_handler.py b/libraries/botbuilder-core/tests/skills/test_skill_handler.py index cf0de8570..cbe61d0d0 100644 --- a/libraries/botbuilder-core/tests/skills/test_skill_handler.py +++ b/libraries/botbuilder-core/tests/skills/test_skill_handler.py @@ -34,7 +34,7 @@ class ConversationIdFactoryForTest(ConversationIdFactoryBase): def __init__(self): self._conversation_refs: Dict[str, str] = {} - async def create_skill_conversation_id( + async def create_skill_conversation_id( # pylint: disable=W0221 self, conversation_reference: ConversationReference ) -> str: cr_json = json.dumps(conversation_reference.serialize())