From cea5ccc784b790fd8b10299570daa02652d73922 Mon Sep 17 00:00:00 2001 From: Linda Date: Tue, 16 Jul 2024 13:23:22 -0700 Subject: [PATCH 01/40] Raise PermissionError if access_token not available on auth_token object (#2138) --- .../connector/auth/microsoft_app_credentials.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py index 532071667..6056fec5e 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -52,7 +52,12 @@ def get_access_token(self, force_refresh: bool = False) -> str: if not auth_token: # No suitable token exists in cache. Let's get a new one from AAD. auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes) - return auth_token["access_token"] + if "access_token" in auth_token: + return auth_token["access_token"] + else: + error = auth_token["error"] if "error" in auth_token else "Unknown error" + error_description = auth_token["error_description"] if "error_description" in auth_token else "Unknown error description" + raise PermissionError(f"Failed to get access token with error: {error}, error_description: {error_description}") def __get_msal_app(self): if not self.app: From b8dd2052fd963f7e1dcca1609298239eca2781ff Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:15:19 +0530 Subject: [PATCH 02/40] Port: [Teams] Add support for meeting participants added/removed events (#2137) * [Teams] Add support for meeting participants added/removed events * test case issue and code format fix * code format fix for microsoft_app_credentials.py * removing unnecessary else --- .../core/teams/teams_activity_handler.py | 41 +++++++++++ .../teams/test_teams_activity_handler.py | 71 +++++++++++++++++++ .../botbuilder/schema/teams/__init__.py | 6 ++ .../botbuilder/schema/teams/_models_py3.py | 62 ++++++++++++++++ .../auth/microsoft_app_credentials.py | 13 ++-- 5 files changed, 189 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 2e5774cc2..33b4e419c 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -27,6 +27,7 @@ TaskModuleResponse, TabRequest, TabSubmit, + MeetingParticipantsEventDetails, ) from botframework.connector import Channels from ..serializer_helper import deserializer_helper @@ -913,6 +914,20 @@ async def on_event_activity(self, turn_context: TurnContext): return await self.on_teams_meeting_end_event( turn_context.activity.value, turn_context ) + if ( + turn_context.activity.name + == "application/vnd.microsoft.meetingParticipantJoin" + ): + return await self.on_teams_meeting_participants_join_event( + turn_context.activity.value, turn_context + ) + if ( + turn_context.activity.name + == "application/vnd.microsoft.meetingParticipantLeave" + ): + return await self.on_teams_meeting_participants_leave_event( + turn_context.activity.value, turn_context + ) return await super().on_event_activity(turn_context) @@ -941,3 +956,29 @@ async def on_teams_meeting_end_event( :returns: A task that represents the work queued to execute. """ return + + async def on_teams_meeting_participants_join_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when meeting participants are added. + + :param meeting: The details of the meeting. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + + async def on_teams_meeting_participants_leave_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when meeting participants are removed. + + :param meeting: The details of the meeting. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 32e9f2edb..390df6191 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -32,6 +32,7 @@ TabRequest, TabSubmit, TabContext, + MeetingParticipantsEventDetails, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -333,6 +334,22 @@ async def on_teams_meeting_end_event( turn_context.activity.value, turn_context ) + async def on_teams_meeting_participants_join_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): + self.record.append("on_teams_meeting_participants_join_event") + return await super().on_teams_meeting_participants_join_event( + turn_context.activity.value, turn_context + ) + + async def on_teams_meeting_participants_leave_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): + self.record.append("on_teams_meeting_participants_leave_event") + return await super().on_teams_meeting_participants_leave_event( + turn_context.activity.value, turn_context + ) + class NotImplementedAdapter(BotAdapter): async def delete_activity( @@ -1157,3 +1174,57 @@ async def test_on_teams_meeting_end_event(self): assert len(bot.record) == 2 assert bot.record[0] == "on_event_activity" assert bot.record[1] == "on_teams_meeting_end_event" + + async def test_on_teams_meeting_participants_join_event(self): + # arrange + activity = Activity( + type=ActivityTypes.event, + channel_id=Channels.ms_teams, + name="application/vnd.microsoft.meetingParticipantJoin", + value={ + "members": [ + { + "user": {"id": "123", "name": "name"}, + "meeting": {"role": "role", "in_meeting": True}, + } + ], + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_event_activity" + assert bot.record[1] == "on_teams_meeting_participants_join_event" + + async def test_on_teams_meeting_participants_leave_event(self): + # arrange + activity = Activity( + type=ActivityTypes.event, + channel_id=Channels.ms_teams, + name="application/vnd.microsoft.meetingParticipantLeave", + value={ + "members": [ + { + "user": {"id": "id", "name": "name"}, + "meeting": {"role": "role", "in_meeting": True}, + } + ], + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_event_activity" + assert bot.record[1] == "on_teams_meeting_participants_leave_event" diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 7824e4571..55901f7a4 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -77,6 +77,9 @@ from ._models_py3 import TabSubmitData from ._models_py3 import TabSuggestedActions from ._models_py3 import TaskModuleCardResponse +from ._models_py3 import UserMeetingDetails +from ._models_py3 import TeamsMeetingMember +from ._models_py3 import MeetingParticipantsEventDetails __all__ = [ "AppBasedLinkQuery", @@ -155,4 +158,7 @@ "TabSubmitData", "TabSuggestedActions", "TaskModuleCardResponse", + "UserMeetingDetails", + "TeamsMeetingMember", + "MeetingParticipantsEventDetails", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index bb860c8da..260442cf8 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2506,3 +2506,65 @@ class MeetingEndEventDetails(MeetingDetailsBase): def __init__(self, *, end_time: str = None, **kwargs): super(MeetingEndEventDetails, self).__init__(**kwargs) self.end_time = end_time + + +class UserMeetingDetails(Model): + """Specific details of a user in a Teams meeting. + + :param role: Role of the participant in the current meeting. + :type role: str + :param in_meeting: True, if the participant is in the meeting. + :type in_meeting: bool + """ + + _attribute_map = { + "role": {"key": "role", "type": "str"}, + "in_meeting": {"key": "inMeeting", "type": "bool"}, + } + + def __init__(self, *, role: str = None, in_meeting: bool = None, **kwargs) -> None: + super(UserMeetingDetails, self).__init__(**kwargs) + self.in_meeting = in_meeting + self.role = role + + +class TeamsMeetingMember(Model): + """Data about the meeting participants. + + :param user: The channel user data. + :type user: TeamsChannelAccount + :param meeting: The user meeting details. + :type meeting: UserMeetingDetails + """ + + _attribute_map = { + "user": {"key": "user", "type": "TeamsChannelAccount"}, + "meeting": {"key": "meeting", "type": "UserMeetingDetails"}, + } + + def __init__( + self, + *, + user: TeamsChannelAccount = None, + meeting: UserMeetingDetails = None, + **kwargs + ) -> None: + super(TeamsMeetingMember, self).__init__(**kwargs) + self.user = user + self.meeting = meeting + + +class MeetingParticipantsEventDetails(Model): + """Data about the meeting participants. + + :param members: The members involved in the meeting event. + :type members: list[~botframework.connector.models.TeamsMeetingMember] + """ + + _attribute_map = { + "conversations": {"key": "members", "type": "[TeamsMeetingMember]"}, + } + + def __init__(self, *, members: List[TeamsMeetingMember] = None, **kwargs) -> None: + super(MeetingParticipantsEventDetails, self).__init__(**kwargs) + self.members = members diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py index 6056fec5e..523977b08 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -54,10 +54,15 @@ def get_access_token(self, force_refresh: bool = False) -> str: auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes) if "access_token" in auth_token: return auth_token["access_token"] - else: - error = auth_token["error"] if "error" in auth_token else "Unknown error" - error_description = auth_token["error_description"] if "error_description" in auth_token else "Unknown error description" - raise PermissionError(f"Failed to get access token with error: {error}, error_description: {error_description}") + error = auth_token["error"] if "error" in auth_token else "Unknown error" + error_description = ( + auth_token["error_description"] + if "error_description" in auth_token + else "Unknown error description" + ) + raise PermissionError( + f"Failed to get access token with error: {error}, error_description: {error_description}" + ) def __get_msal_app(self): if not self.app: From a2cccb32d1d23639f304288b6d7d970bb1bbd3ba Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:50:10 +0530 Subject: [PATCH 03/40] Add conversation id header in skill requests (#2141) --- .../botbuilder/integration/aiohttp/bot_framework_http_client.py | 1 + .../aio/operations_async/_conversations_operations_async.py | 1 + .../connector/operations/_conversations_operations.py | 1 + 3 files changed, 3 insertions(+) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py index b09b3ac93..c57c042c2 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py @@ -116,6 +116,7 @@ async def _post_content( ) -> Tuple[int, object]: headers_dict = { "Content-type": "application/json; charset=utf-8", + "x-ms-conversation-id": activity.conversation.id, } if token: headers_dict.update( diff --git a/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py b/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py index e6416e42d..553248342 100644 --- a/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py +++ b/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py @@ -510,6 +510,7 @@ async def reply_to_activity( header_parameters = {} header_parameters["Accept"] = "application/json" header_parameters["Content-Type"] = "application/json; charset=utf-8" + header_parameters["x-ms-conversation-id"] = conversation_id if custom_headers: header_parameters.update(custom_headers) diff --git a/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py b/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py index 6834f7a28..48d3c23fc 100644 --- a/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py +++ b/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py @@ -495,6 +495,7 @@ def reply_to_activity( header_parameters = {} header_parameters["Accept"] = "application/json" header_parameters["Content-Type"] = "application/json; charset=utf-8" + header_parameters["x-ms-conversation-id"] = conversation_id if custom_headers: header_parameters.update(custom_headers) From 9a70940b58cfe7d2a24cc24e114ee749fca37052 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:57:13 +0530 Subject: [PATCH 04/40] Adding directlinespeech & test channels in channels.py (#2143) Co-authored-by: tracyboehrer --- .../botbuilder/adapters/slack/slack_helper.py | 7 ++++--- .../botbuilder/core/adapters/test_adapter.py | 5 +++-- .../tests/test_bot_framework_adapter.py | 13 +++++++------ .../tests/test_conversation_state.py | 11 ++++++++--- .../tests/test_memory_transcript_store.py | 5 +++-- .../tests/test_private_conversation_state.py | 3 ++- .../tests/test_telemetry_middleware.py | 2 +- .../botbuilder-core/tests/test_test_adapter.py | 9 +++++---- libraries/botbuilder-core/tests/test_user_state.py | 7 +++++-- .../botbuilder/dialogs/choices/channel.py | 2 ++ .../tests/choices/test_channel.py | 2 ++ .../tests/memory/scopes/test_memory_scopes.py | 3 ++- .../botbuilder-dialogs/tests/test_dialog_manager.py | 3 ++- .../botframework/connector/channels.py | 6 ++++++ .../tests/test_attachments.py | 4 ++-- .../tests/test_attachments_async.py | 3 ++- .../tests/test_conversations.py | 4 ++-- .../tests/test_conversations_async.py | 3 ++- 18 files changed, 60 insertions(+), 32 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py index 35f720c8d..c40dd21e9 100644 --- a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py +++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py @@ -15,6 +15,7 @@ ChannelAccount, ActivityTypes, ) +from botframework.connector import Channels from .slack_message import SlackMessage from .slack_client import SlackClient @@ -125,7 +126,7 @@ def payload_to_activity(payload: SlackPayload) -> Activity: raise Exception("payload is required") activity = Activity( - channel_id="slack", + channel_id=Channels.slack, conversation=ConversationAccount(id=payload.channel["id"], properties={}), from_property=ChannelAccount( id=( @@ -178,7 +179,7 @@ async def event_to_activity(event: SlackEvent, client: SlackClient) -> Activity: activity = Activity( id=event.event_ts, - channel_id="slack", + channel_id=Channels.slack, conversation=ConversationAccount( id=event.channel if event.channel else event.channel_id, properties={} ), @@ -235,7 +236,7 @@ async def command_to_activity( activity = Activity( id=body.trigger_id, - channel_id="slack", + channel_id=Channels.slack, conversation=ConversationAccount(id=body.channel_id, properties={}), from_property=ChannelAccount(id=body.user_id), recipient=ChannelAccount(id=None), diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py index 56f122344..79070d732 100644 --- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py @@ -31,6 +31,7 @@ from ..bot_adapter import BotAdapter from ..turn_context import TurnContext from ..oauth.extended_user_token_provider import ExtendedUserTokenProvider +from botframework.connector import Channels class UserToken: @@ -121,7 +122,7 @@ def __init__( template_or_conversation if isinstance(template_or_conversation, Activity) else Activity( - channel_id="test", + channel_id=Channels.test, service_url="https://test.com", from_property=ChannelAccount(id="User1", name="user"), recipient=ChannelAccount(id="bot", name="Bot"), @@ -308,7 +309,7 @@ def create_conversation_reference( name: str, user: str = "User1", bot: str = "Bot" ) -> ConversationReference: return ConversationReference( - channel_id="test", + channel_id=Channels.test, service_url="https://test.com", conversation=ConversationAccount( is_group=False, diff --git a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py index 7e9268ee7..ee8faa773 100644 --- a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py +++ b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py @@ -27,6 +27,7 @@ TokenExchangeInvokeRequest, TokenExchangeInvokeResponse, ) +from botframework.connector import Channels from botframework.connector.token_api.models import ( TokenExchangeRequest, TokenResponse as ConnectorTokenResponse, @@ -44,7 +45,7 @@ REFERENCE = ConversationReference( activity_id="1234", - channel_id="test", + channel_id=Channels.test, locale="en-uS", # Intentionally oddly-cased to check that it isn't defaulted somewhere, but tests stay in English service_url="https://example.org/channel", user=ChannelAccount(id="user", name="User Name"), @@ -305,7 +306,7 @@ async def test_should_migrate_tenant_id_for_msteams(self): is_incoming=True, ) - incoming.channel_id = "msteams" + incoming.channel_id = Channels.ms_teams adapter = AdapterUnderTest() async def aux_func_assert_tenant_id_copied(context): @@ -501,7 +502,7 @@ async def callback(context: TurnContext): sut = BotFrameworkAdapter(settings) await sut.process_activity_with_identity( Activity( - channel_id="emulator", + channel_id=Channels.emulator, service_url=service_url, text="test", ), @@ -549,7 +550,7 @@ async def callback(context: TurnContext): sut = BotFrameworkAdapter(settings) await sut.process_activity_with_identity( Activity( - channel_id="emulator", + channel_id=Channels.emulator, service_url=service_url, text="test", ), @@ -709,7 +710,7 @@ async def callback(context: TurnContext): inbound_activity = Activity( type=ActivityTypes.message, - channel_id="emulator", + channel_id=Channels.emulator, service_url="http://tempuri.org/whatever", delivery_mode=DeliveryModes.expect_replies, text="hello world", @@ -754,7 +755,7 @@ async def callback(context: TurnContext): inbound_activity = Activity( type=ActivityTypes.message, - channel_id="emulator", + channel_id=Channels.emulator, service_url="http://tempuri.org/whatever", delivery_mode=DeliveryModes.normal, text="hello world", diff --git a/libraries/botbuilder-core/tests/test_conversation_state.py b/libraries/botbuilder-core/tests/test_conversation_state.py index 4c4e74c19..79d90ca54 100644 --- a/libraries/botbuilder-core/tests/test_conversation_state.py +++ b/libraries/botbuilder-core/tests/test_conversation_state.py @@ -6,20 +6,25 @@ from botbuilder.core import TurnContext, MemoryStorage, ConversationState from botbuilder.core.adapters import TestAdapter from botbuilder.schema import Activity, ConversationAccount +from botframework.connector import Channels RECEIVED_MESSAGE = Activity( type="message", text="received", - channel_id="test", + channel_id=Channels.test, conversation=ConversationAccount(id="convo"), ) MISSING_CHANNEL_ID = Activity( type="message", text="received", conversation=ConversationAccount(id="convo") ) -MISSING_CONVERSATION = Activity(type="message", text="received", channel_id="test") +MISSING_CONVERSATION = Activity( + type="message", + text="received", + channel_id=Channels.test, +) END_OF_CONVERSATION = Activity( type="endOfConversation", - channel_id="test", + channel_id=Channels.test, conversation=ConversationAccount(id="convo"), ) diff --git a/libraries/botbuilder-core/tests/test_memory_transcript_store.py b/libraries/botbuilder-core/tests/test_memory_transcript_store.py index 14b313c92..f7ace436e 100644 --- a/libraries/botbuilder-core/tests/test_memory_transcript_store.py +++ b/libraries/botbuilder-core/tests/test_memory_transcript_store.py @@ -25,6 +25,7 @@ ConversationAccount, ConversationReference, ) +from botframework.connector import Channels # pylint: disable=line-too-long,missing-docstring @@ -98,7 +99,7 @@ def create_activities(self, conversation_id: str, date: datetime, count: int = 5 timestamp=time_stamp, id=str(uuid.uuid4()), text=str(i), - channel_id="test", + channel_id=Channels.test, from_property=ChannelAccount(id=f"User{i}"), conversation=ConversationAccount(id=conversation_id), recipient=ChannelAccount(id="bot1", name="2"), @@ -112,7 +113,7 @@ def create_activities(self, conversation_id: str, date: datetime, count: int = 5 timestamp=date, id=str(uuid.uuid4()), text=str(i), - channel_id="test", + channel_id=Channels.test, from_property=ChannelAccount(id="Bot1", name="2"), conversation=ConversationAccount(id=conversation_id), recipient=ChannelAccount(id=f"User{i}"), diff --git a/libraries/botbuilder-core/tests/test_private_conversation_state.py b/libraries/botbuilder-core/tests/test_private_conversation_state.py index 802a2678b..5fb4507e3 100644 --- a/libraries/botbuilder-core/tests/test_private_conversation_state.py +++ b/libraries/botbuilder-core/tests/test_private_conversation_state.py @@ -6,11 +6,12 @@ from botbuilder.core import MemoryStorage, TurnContext, PrivateConversationState from botbuilder.core.adapters import TestAdapter from botbuilder.schema import Activity, ChannelAccount, ConversationAccount +from botframework.connector import Channels RECEIVED_MESSAGE = Activity( text="received", type="message", - channel_id="test", + channel_id=Channels.test, conversation=ConversationAccount(id="convo"), from_property=ChannelAccount(id="user"), ) diff --git a/libraries/botbuilder-core/tests/test_telemetry_middleware.py b/libraries/botbuilder-core/tests/test_telemetry_middleware.py index ee3504d1b..7fdd83109 100644 --- a/libraries/botbuilder-core/tests/test_telemetry_middleware.py +++ b/libraries/botbuilder-core/tests/test_telemetry_middleware.py @@ -40,7 +40,7 @@ async def test_do_not_throw_on_null_from(self): adapter = TestAdapter( template_or_conversation=Activity( - channel_id="test", + channel_id=Channels.test, recipient=ChannelAccount(id="bot", name="Bot"), conversation=ConversationAccount(id=str(uuid.uuid4())), ) diff --git a/libraries/botbuilder-core/tests/test_test_adapter.py b/libraries/botbuilder-core/tests/test_test_adapter.py index 447f74ead..269a5197f 100644 --- a/libraries/botbuilder-core/tests/test_test_adapter.py +++ b/libraries/botbuilder-core/tests/test_test_adapter.py @@ -7,6 +7,7 @@ from botbuilder.core import TurnContext from botbuilder.core.adapters import TestAdapter from botbuilder.schema import Activity, ConversationReference, ChannelAccount +from botframework.connector import Channels RECEIVED_MESSAGE = Activity(type="message", text="received") UPDATED_ACTIVITY = Activity(type="message", text="update") @@ -141,7 +142,7 @@ async def logic(context: TurnContext): async def test_get_user_token_returns_null(self): adapter = TestAdapter() activity = Activity( - channel_id="directline", from_property=ChannelAccount(id="testuser") + channel_id=Channels.direct_line, from_property=ChannelAccount(id="testuser") ) turn_context = TurnContext(adapter, activity) @@ -158,7 +159,7 @@ async def test_get_user_token_returns_null(self): async def test_get_user_token_returns_null_with_code(self): adapter = TestAdapter() activity = Activity( - channel_id="directline", from_property=ChannelAccount(id="testuser") + channel_id=Channels.direct_line, from_property=ChannelAccount(id="testuser") ) turn_context = TurnContext(adapter, activity) @@ -180,7 +181,7 @@ async def test_get_user_token_returns_null_with_code(self): async def test_get_user_token_returns_token(self): adapter = TestAdapter() connection_name = "myConnection" - channel_id = "directline" + channel_id = Channels.direct_line user_id = "testUser" token = "abc123" activity = Activity( @@ -207,7 +208,7 @@ async def test_get_user_token_returns_token(self): async def test_get_user_token_returns_token_with_magice_code(self): adapter = TestAdapter() connection_name = "myConnection" - channel_id = "directline" + channel_id = Channels.direct_line user_id = "testUser" token = "abc123" magic_code = "888999" diff --git a/libraries/botbuilder-core/tests/test_user_state.py b/libraries/botbuilder-core/tests/test_user_state.py index a39ee107a..9f7e22679 100644 --- a/libraries/botbuilder-core/tests/test_user_state.py +++ b/libraries/botbuilder-core/tests/test_user_state.py @@ -6,17 +6,20 @@ from botbuilder.core import TurnContext, MemoryStorage, UserState from botbuilder.core.adapters import TestAdapter from botbuilder.schema import Activity, ChannelAccount +from botframework.connector import Channels RECEIVED_MESSAGE = Activity( type="message", text="received", - channel_id="test", + channel_id=Channels.test, from_property=ChannelAccount(id="user"), ) MISSING_CHANNEL_ID = Activity( type="message", text="received", from_property=ChannelAccount(id="user") ) -MISSING_FROM_PROPERTY = Activity(type="message", text="received", channel_id="test") +MISSING_FROM_PROPERTY = Activity( + type="message", text="received", channel_id=Channels.test +) class TestUserState(aiounittest.AsyncTestCase): diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py index cd36ac632..aa19a2740 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py @@ -34,6 +34,7 @@ def supports_suggested_actions(channel_id: str, button_cnt: int = 100) -> bool: Channels.telegram: 100, Channels.emulator: 100, Channels.direct_line: 100, + Channels.direct_line_speech: 100, Channels.webchat: 100, } return ( @@ -64,6 +65,7 @@ def supports_card_actions(channel_id: str, button_cnt: int = 100) -> bool: Channels.telegram: 100, Channels.emulator: 100, Channels.direct_line: 100, + Channels.direct_line_speech: 100, Channels.webchat: 100, } return ( diff --git a/libraries/botbuilder-dialogs/tests/choices/test_channel.py b/libraries/botbuilder-dialogs/tests/choices/test_channel.py index 269aaae1a..23d26ac4a 100644 --- a/libraries/botbuilder-dialogs/tests/choices/test_channel.py +++ b/libraries/botbuilder-dialogs/tests/choices/test_channel.py @@ -25,6 +25,7 @@ def test_supports_suggested_actions_many(self): (Channels.kik, 21, False), (Channels.emulator, 100, True), (Channels.emulator, 101, False), + (Channels.direct_line_speech, 100, True), ] for channel, button_cnt, expected in supports_suggested_actions_data: @@ -41,6 +42,7 @@ def test_supports_card_actions_many(self): (Channels.slack, 100, True), (Channels.skype, 3, True), (Channels.skype, 5, False), + (Channels.direct_line_speech, 99, True), ] for channel, button_cnt, expected in supports_card_action_data: diff --git a/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py b/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py index 5101c7070..d7b305358 100644 --- a/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py +++ b/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py @@ -32,6 +32,7 @@ ChannelAccount, ConversationAccount, ) +from botframework.connector import Channels class TestDialog(Dialog): @@ -92,7 +93,7 @@ class MemoryScopesTests(aiounittest.AsyncTestCase): begin_message = Activity( text="begin", type=ActivityTypes.message, - channel_id="test", + channel_id=Channels.test, from_property=ChannelAccount(id="user"), recipient=ChannelAccount(id="bot"), conversation=ConversationAccount(id="convo1"), diff --git a/libraries/botbuilder-dialogs/tests/test_dialog_manager.py b/libraries/botbuilder-dialogs/tests/test_dialog_manager.py index 57b264e60..3c5a4b34a 100644 --- a/libraries/botbuilder-dialogs/tests/test_dialog_manager.py +++ b/libraries/botbuilder-dialogs/tests/test_dialog_manager.py @@ -42,6 +42,7 @@ InputHints, ) from botframework.connector.auth import AuthenticationConstants, ClaimsIdentity +from botframework.connector import Channels class SkillFlowTestCase(str, Enum): @@ -103,7 +104,7 @@ async def create_test_flow( user_state = UserState(storage) activity = Activity( - channel_id="test", + channel_id=Channels.test, service_url="https://test.com", from_property=ChannelAccount(id="user1", name="User1"), recipient=ChannelAccount(id="bot", name="Bot"), diff --git a/libraries/botframework-connector/botframework/connector/channels.py b/libraries/botframework-connector/botframework/connector/channels.py index be59cc5f5..569596b9c 100644 --- a/libraries/botframework-connector/botframework/connector/channels.py +++ b/libraries/botframework-connector/botframework/connector/channels.py @@ -18,6 +18,9 @@ class Channels(str, Enum): direct_line = "directline" """Direct Line channel.""" + direct_line_speech = "directlinespeech" + """Direct Line Speech channel.""" + email = "email" """Email channel.""" @@ -54,5 +57,8 @@ class Channels(str, Enum): telegram = "telegram" """Telegram channel.""" + test = "test" + """Test channel.""" + webchat = "webchat" """WebChat channel.""" diff --git a/libraries/botframework-connector/tests/test_attachments.py b/libraries/botframework-connector/tests/test_attachments.py index bd280c840..b6d171250 100644 --- a/libraries/botframework-connector/tests/test_attachments.py +++ b/libraries/botframework-connector/tests/test_attachments.py @@ -8,13 +8,13 @@ import msrest from botbuilder.schema import AttachmentData, ErrorResponseException -from botframework.connector import ConnectorClient +from botframework.connector import ConnectorClient, Channels from botframework.connector.auth import MicrosoftAppCredentials from authentication_stub import MicrosoftTokenAuthenticationStub SERVICE_URL = "https://slack.botframework.com" -CHANNEL_ID = "slack" +CHANNEL_ID = Channels.slack BOT_NAME = "botbuilder-pc-bot" BOT_ID = "B21UTEF8S:T03CWQ0QB" RECIPIENT_ID = "U19KH8EHJ:T03CWQ0QB" diff --git a/libraries/botframework-connector/tests/test_attachments_async.py b/libraries/botframework-connector/tests/test_attachments_async.py index c0f7c9458..fe0434184 100644 --- a/libraries/botframework-connector/tests/test_attachments_async.py +++ b/libraries/botframework-connector/tests/test_attachments_async.py @@ -12,9 +12,10 @@ from botframework.connector.auth import MicrosoftAppCredentials from authentication_stub import MicrosoftTokenAuthenticationStub +from botframework.connector import Channels SERVICE_URL = "https://slack.botframework.com" -CHANNEL_ID = "slack" +CHANNEL_ID = Channels.slack BOT_NAME = "botbuilder-pc-bot" BOT_ID = "B21UTEF8S:T03CWQ0QB" RECIPIENT_ID = "U19KH8EHJ:T03CWQ0QB" diff --git a/libraries/botframework-connector/tests/test_conversations.py b/libraries/botframework-connector/tests/test_conversations.py index c64926643..ea94a247b 100644 --- a/libraries/botframework-connector/tests/test_conversations.py +++ b/libraries/botframework-connector/tests/test_conversations.py @@ -15,13 +15,13 @@ ErrorResponseException, HeroCard, ) -from botframework.connector import ConnectorClient +from botframework.connector import ConnectorClient, Channels from botframework.connector.auth import MicrosoftAppCredentials from authentication_stub import MicrosoftTokenAuthenticationStub SERVICE_URL = "https://slack.botframework.com" -CHANNEL_ID = "slack" +CHANNEL_ID = Channels.slack BOT_NAME = "botbuilder-pc-bot" BOT_ID = "B21UTEF8S:T03CWQ0QB" RECIPIENT_ID = "U19KH8EHJ:T03CWQ0QB" diff --git a/libraries/botframework-connector/tests/test_conversations_async.py b/libraries/botframework-connector/tests/test_conversations_async.py index 074247c82..5e0c8fcc5 100644 --- a/libraries/botframework-connector/tests/test_conversations_async.py +++ b/libraries/botframework-connector/tests/test_conversations_async.py @@ -19,9 +19,10 @@ from botframework.connector.auth import MicrosoftAppCredentials from authentication_stub import MicrosoftTokenAuthenticationStub +from botframework.connector import Channels SERVICE_URL = "https://slack.botframework.com" -CHANNEL_ID = "slack" +CHANNEL_ID = Channels.slack BOT_NAME = "botbuilder-pc-bot" BOT_ID = "B21UTEF8S:T03CWQ0QB" RECIPIENT_ID = "U19KH8EHJ:T03CWQ0QB" From 1099ffdfba5daa8fceae14135339bb8f2e87b9fe Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:46:20 +0530 Subject: [PATCH 05/40] port: Fix first activity from bot to user has fake replyToId value (#2145) * Fix first activity from bot to user has fake replyToId value * Removed duplicate activity_id --- .../botbuilder/core/adapters/test_adapter.py | 4 +++- .../botbuilder/schema/_models_py3.py | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py index 79070d732..77f566625 100644 --- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py @@ -142,7 +142,9 @@ async def process_activity( if activity.type is None: activity.type = ActivityTypes.message - activity.channel_id = self.template.channel_id + if activity.channel_id is None: + activity.channel_id = self.template.channel_id + activity.from_property = self.template.from_property activity.recipient = self.template.recipient activity.conversation = self.template.conversation diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index e9eb1529d..b75cc9f82 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -639,7 +639,12 @@ def create_reply(self, text: str = None, locale: str = None): id=self.from_property.id if self.from_property else None, name=self.from_property.name if self.from_property else None, ), - reply_to_id=self.id, + reply_to_id=( + self.id + if not type == ActivityTypes.conversation_update + or self.channel_id not in ["directline", "webchat"] + else None + ), service_url=self.service_url, channel_id=self.channel_id, conversation=ConversationAccount( @@ -681,7 +686,12 @@ def create_trace( id=self.from_property.id if self.from_property else None, name=self.from_property.name if self.from_property else None, ), - reply_to_id=self.id, + reply_to_id=( + self.id + if not type == ActivityTypes.conversation_update + or self.channel_id not in ["directline", "webchat"] + else None + ), service_url=self.service_url, channel_id=self.channel_id, conversation=ConversationAccount( @@ -737,7 +747,12 @@ def get_conversation_reference(self): :returns: A conversation reference for the conversation that contains this activity. """ return ConversationReference( - activity_id=self.id, + activity_id=( + self.id + if not type == ActivityTypes.conversation_update + or self.channel_id not in ["directline", "webchat"] + else None + ), user=self.from_property, bot=self.recipient, conversation=self.conversation, From 51e3cdabbcdceffd40fdfb2512b315d85d810b7c Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 7 Aug 2024 19:27:20 +0530 Subject: [PATCH 06/40] Bump PyYAML 6.0 to 6.0.1 (#2151) --- libraries/botframework-connector/tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt index dfbb418bd..d6c057b7e 100644 --- a/libraries/botframework-connector/tests/requirements.txt +++ b/libraries/botframework-connector/tests/requirements.txt @@ -1,5 +1,5 @@ pytest-cov>=2.6.0 pytest~=7.3.1 -pyyaml==6.0 +pyyaml==6.0.1 pytest-asyncio==0.15.1 ddt==1.2.1 \ No newline at end of file From 734b6d1e98b085e3ccbcecc95efc235df5680bc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:52:55 -0500 Subject: [PATCH 07/40] Bump the pip group across 2 directories with 1 update (#2152) Bumps the pip group with 1 update in the /libraries/botbuilder-adapters-slack directory: [aiohttp](https://github.com/aio-libs/aiohttp). Bumps the pip group with 1 update in the /libraries/botbuilder-integration-aiohttp directory: [aiohttp](https://github.com/aio-libs/aiohttp). Updates `aiohttp` from 3.9.5 to 3.10.2 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2) Updates `aiohttp` from 3.9.5 to 3.10.2 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libraries/botbuilder-adapters-slack/requirements.txt | 2 +- libraries/botbuilder-integration-aiohttp/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 8d2c7b043..88ce85a22 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.5 +aiohttp==3.10.2 pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 9ce978580..6228ea580 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.9.5 +aiohttp==3.10.2 From 86673e2054ac80b7ed1f0939a45739aa3afe17a5 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Fri, 30 Aug 2024 12:34:54 -0500 Subject: [PATCH 08/40] Update botbuilder-python-ci.yml for Azure Pipelines --- pipelines/botbuilder-python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/botbuilder-python-ci.yml b/pipelines/botbuilder-python-ci.yml index 6460bb907..b622bab3f 100644 --- a/pipelines/botbuilder-python-ci.yml +++ b/pipelines/botbuilder-python-ci.yml @@ -59,7 +59,7 @@ jobs: pip install -r ./libraries/botbuilder-core/tests/requirements.txt pip install -r ./libraries/botbuilder-ai/tests/requirements.txt pip install coveralls - pip install pylint==2.17 + pip install pylint==3.2.6 pip install black==24.4.2 displayName: 'Install dependencies' From b80253582cfa57faef55d8579cc953fbb6f4beee Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:39:58 +0530 Subject: [PATCH 09/40] aiohttp 3.10.2 (#2158) * aiohttp 3.10.2 * Fixing pylint errors --------- Co-authored-by: tracyboehrer --- libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py | 11 ++++++++++- libraries/botbuilder-ai/setup.py | 2 +- .../flask/flask_telemetry_middleware.py | 2 ++ .../botbuilder/dialogs/choices/choice_factory.py | 3 +-- .../botbuilder-dialogs/botbuilder/dialogs/dialog.py | 3 +-- .../aiohttp/streaming/aiohttp_web_socket.py | 2 ++ libraries/botbuilder-integration-aiohttp/setup.py | 2 +- .../setup.py | 2 +- .../botbuilder/schema/_models_py3.py | 6 +++--- 9 files changed, 22 insertions(+), 11 deletions(-) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py index 825e08e8e..773c487e6 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import asyncio import json from typing import Dict, List, NamedTuple, Union from aiohttp import ClientSession, ClientTimeout @@ -52,8 +53,16 @@ def __init__( opt = options or QnAMakerOptions() self._validate_options(opt) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + instance_timeout = ClientTimeout(total=opt.timeout / 1000) - self._http_client = http_client or ClientSession(timeout=instance_timeout) + self._http_client = http_client or ClientSession( + timeout=instance_timeout, loop=loop + ) self.telemetry_client: Union[BotTelemetryClient, NullTelemetryClient] = ( telemetry_client or NullTelemetryClient() diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index 105f1a4c9..2242efed9 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.9.5", + "aiohttp==3.10.2", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py index 8003074c9..5cc2676f2 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py @@ -38,6 +38,8 @@ def __call__(self, environ, start_response): def process_request(self, environ) -> bool: """Process the incoming Flask request.""" + body_unicode = None + # Bot Service doesn't handle anything over 256k length = int(environ.get("CONTENT_LENGTH", "0")) if length > 256 * 1024: diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py index 0e5edd8e1..ef1dfc117 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py @@ -46,8 +46,7 @@ def for_channel( else: size = len(choice.value) - if size > max_title_length: - max_title_length = size + max_title_length = max(max_title_length, size) # Determine list style supports_suggested_actions = Channel.supports_suggested_actions( diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py index 22dfe342b..43cfe3052 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py @@ -176,7 +176,6 @@ def _register_source_location( Registers a SourceRange in the provided location. :param path: The path to the source file. :param line_number: The line number where the source will be located on the file. - :return: """ if path: # This will be added when debbuging support is ported. @@ -185,4 +184,4 @@ def _register_source_location( # start_point = SourcePoint(line_index = line_number, char_index = 0 ), # end_point = SourcePoint(line_index = line_number + 1, char_index = 0 ), # ) - return + pass diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py index 2cd4ec13b..aa4a94a8e 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py @@ -40,6 +40,8 @@ async def receive(self) -> WebSocketMessage: try: message = await self._aiohttp_ws.receive() + message_data = None + if message.type == WSMsgType.TEXT: message_data = list(str(message.data).encode("ascii")) elif message.type == WSMsgType.BINARY: diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index 891647bb7..a777d50c9 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.9.5", + "aiohttp==3.10.2", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index d40487403..d8b0f09cf 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.9.5", + "aiohttp==3.10.2", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index b75cc9f82..9976c9809 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -641,7 +641,7 @@ def create_reply(self, text: str = None, locale: str = None): ), reply_to_id=( self.id - if not type == ActivityTypes.conversation_update + if type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"] else None ), @@ -688,7 +688,7 @@ def create_trace( ), reply_to_id=( self.id - if not type == ActivityTypes.conversation_update + if type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"] else None ), @@ -749,7 +749,7 @@ def get_conversation_reference(self): return ConversationReference( activity_id=( self.id - if not type == ActivityTypes.conversation_update + if type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"] else None ), From f3d50f8c6c13b9015fdc126f922cdcdccb9998ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:20:49 -0500 Subject: [PATCH 10/40] Bump aiohttp (#2157) Bumps the pip group with 1 update in the /libraries/botbuilder-integration-aiohttp directory: [aiohttp](https://github.com/aio-libs/aiohttp). Updates `aiohttp` from 3.10.2 to 3.10.5 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.2...v3.10.5) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libraries/botbuilder-integration-aiohttp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 6228ea580..de0f13750 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.10.2 +aiohttp==3.10.5 From bb1561e8a1648e8d1d50c055c6fbaf7181ae7195 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:03:50 +0530 Subject: [PATCH 11/40] Fixing python313 deprecation warnings (#2159) * Fixing DeprecationWarnings in Python 3.13.0rc1 * Fix black issues * Fixing pylint issue --- .../botbuilder-ai/botbuilder/ai/luis/activity_util.py | 4 ++-- libraries/botbuilder-ai/tests/qna/test_qna.py | 4 +--- .../processor/telemetry_processor.py | 2 +- libraries/botbuilder-applicationinsights/setup.py | 2 +- .../botbuilder/core/adapters/test_adapter.py | 4 ++-- .../botbuilder/core/inspection/trace_activity.py | 8 ++++---- .../botbuilder/core/transcript_logger.py | 6 +++--- .../botbuilder-core/botbuilder/core/turn_context.py | 4 ++-- .../botbuilder/dialogs/prompts/oauth_prompt.py | 6 +++++- .../botbuilder-schema/botbuilder/schema/_models_py3.py | 6 +++--- libraries/botframework-connector/azure_bdist_wheel.py | 4 +--- .../botframework-connector/tests/requirements.txt | 5 +++-- .../botframework-connector/tests/test_attachments.py | 10 +++++++++- 13 files changed, 37 insertions(+), 28 deletions(-) diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py b/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py index d2656a3ba..303917fbb 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py +++ b/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from datetime import datetime +from datetime import datetime, timezone from botbuilder.schema import ( Activity, @@ -51,7 +51,7 @@ def create_trace( reply = Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), from_property=from_property, recipient=ChannelAccount( id=turn_activity.from_property.id, name=turn_activity.from_property.name diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index 236594ac0..8a3f595ed 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -347,8 +347,6 @@ async def test_trace_test(self): self._knowledge_base_id, trace_activity.value.knowledge_base_id ) - return result - async def test_returns_answer_with_timeout(self): question: str = "how do I clean the stove?" options = QnAMakerOptions(timeout=999999) @@ -823,7 +821,7 @@ async def test_call_train(self): QnAMaker, "call_train", return_value=None ) as mocked_call_train: qna = QnAMaker(QnaApplicationTest.tests_endpoint) - qna.call_train(feedback_records) + await qna.call_train(feedback_records) mocked_call_train.assert_called_once_with(feedback_records) diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py index dfe451e3f..0802f3cdf 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py @@ -3,7 +3,7 @@ import base64 import json from abc import ABC, abstractmethod -from _sha256 import sha256 +from hashlib import sha256 class TelemetryProcessor(ABC): diff --git a/libraries/botbuilder-applicationinsights/setup.py b/libraries/botbuilder-applicationinsights/setup.py index 0932ff98f..9573e27f2 100644 --- a/libraries/botbuilder-applicationinsights/setup.py +++ b/libraries/botbuilder-applicationinsights/setup.py @@ -12,7 +12,7 @@ ] TESTS_REQUIRES = [ "aiounittest==1.3.0", - "django==3.2.24", # For samples + "django==4.2.15", # For samples "djangorestframework==3.14.0", # For samples "flask==2.2.5", # For samples ] diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py index 77f566625..ebfeb303a 100644 --- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py @@ -8,7 +8,7 @@ import asyncio import inspect import uuid -from datetime import datetime +from datetime import datetime, timezone from uuid import uuid4 from typing import Awaitable, Coroutine, Dict, List, Callable, Union from copy import copy @@ -155,7 +155,7 @@ async def process_activity( finally: self._conversation_lock.release() - activity.timestamp = activity.timestamp or datetime.utcnow() + activity.timestamp = activity.timestamp or datetime.now(timezone.utc) await self.run_pipeline(self.create_turn_context(activity), logic) async def send_activities( diff --git a/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py b/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py index 307ef64cd..37cb33151 100644 --- a/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py +++ b/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, Union from botbuilder.core import BotState @@ -11,7 +11,7 @@ def make_command_activity(command: str) -> Activity: return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name="Command", label="Command", value=command, @@ -22,7 +22,7 @@ def make_command_activity(command: str) -> Activity: def from_activity(activity: Activity, name: str, label: str) -> Activity: return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name=name, label=label, value=activity, @@ -33,7 +33,7 @@ def from_activity(activity: Activity, name: str, label: str) -> Activity: def from_state(bot_state: Union[BotState, Dict]) -> Activity: return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name="Bot State", label="BotState", value=bot_state, diff --git a/libraries/botbuilder-core/botbuilder/core/transcript_logger.py b/libraries/botbuilder-core/botbuilder/core/transcript_logger.py index e9536c1b6..5aa1ea726 100644 --- a/libraries/botbuilder-core/botbuilder/core/transcript_logger.py +++ b/libraries/botbuilder-core/botbuilder/core/transcript_logger.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. """Logs incoming and outgoing activities to a TranscriptStore..""" -import datetime +from datetime import datetime, timezone import copy import random import string @@ -86,11 +86,11 @@ async def send_activities_handler( prefix = "g_" + "".join( random.choice(alphanumeric) for i in range(5) ) - epoch = datetime.datetime.utcfromtimestamp(0) + epoch = datetime.fromtimestamp(0, timezone.utc) if cloned_activity.timestamp: reference = cloned_activity.timestamp else: - reference = datetime.datetime.today() + reference = datetime.now(timezone.utc) delta = (reference - epoch).total_seconds() * 1000 cloned_activity.id = f"{prefix}{delta}" await self.log_activity(transcript, cloned_activity) diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 90ab99bd0..6c4a4eef3 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -3,7 +3,7 @@ import re from copy import copy, deepcopy -from datetime import datetime +from datetime import datetime, timezone from typing import List, Callable, Union, Dict from botframework.connector import Channels from botbuilder.schema import ( @@ -308,7 +308,7 @@ async def send_trace_activity( ) -> ResourceResponse: trace_activity = Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name=name, value=value, value_type=value_type, diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index c5b066913..d31a0b56a 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -420,7 +420,11 @@ async def _recognize_token( ) elif OAuthPrompt._is_teams_verification_invoke(context): - code = context.activity.value["state"] + code = ( + context.activity.value.get("state", None) + if context.activity.value + else None + ) try: token = await _UserTokenAccess.get_user_token( context, self._settings, code diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index 9976c9809..1b6a631c6 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -4,7 +4,7 @@ from typing import List from botbuilder.schema._connector_client_enums import ActivityTypes -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from msrest.serialization import Model from msrest.exceptions import HttpOperationError @@ -630,7 +630,7 @@ def create_reply(self, text: str = None, locale: str = None): """ return Activity( type=ActivityTypes.message, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), from_property=ChannelAccount( id=self.recipient.id if self.recipient else None, name=self.recipient.name if self.recipient else None, @@ -677,7 +677,7 @@ def create_trace( return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), from_property=ChannelAccount( id=self.recipient.id if self.recipient else None, name=self.recipient.name if self.recipient else None, diff --git a/libraries/botframework-connector/azure_bdist_wheel.py b/libraries/botframework-connector/azure_bdist_wheel.py index d33af36bd..56a1b0b20 100644 --- a/libraries/botframework-connector/azure_bdist_wheel.py +++ b/libraries/botframework-connector/azure_bdist_wheel.py @@ -555,9 +555,7 @@ def write_record(self, bdist_dir, distinfo_dir): for azure_sub_package in folder_with_init: init_file = os.path.join(bdist_dir, azure_sub_package, "__init__.py") if os.path.isfile(init_file): - logger.info( - "manually remove {} while building the wheel".format(init_file) - ) + logger.info("manually remove %s while building the wheel", init_file) os.remove(init_file) else: raise ValueError( diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt index d6c057b7e..5f0d9558d 100644 --- a/libraries/botframework-connector/tests/requirements.txt +++ b/libraries/botframework-connector/tests/requirements.txt @@ -1,5 +1,6 @@ pytest-cov>=2.6.0 pytest~=7.3.1 pyyaml==6.0.1 -pytest-asyncio==0.15.1 -ddt==1.2.1 \ No newline at end of file +pytest-asyncio==0.23.8 +ddt==1.2.1 +setuptools==72.1.0 \ No newline at end of file diff --git a/libraries/botframework-connector/tests/test_attachments.py b/libraries/botframework-connector/tests/test_attachments.py index b6d171250..a4b8b36b8 100644 --- a/libraries/botframework-connector/tests/test_attachments.py +++ b/libraries/botframework-connector/tests/test_attachments.py @@ -46,7 +46,15 @@ def read_base64(path_to_file): return encoded_string -LOOP = asyncio.get_event_loop() +# Ensure there's an event loop and get the auth token +# LOOP = asyncio.get_event_loop() +try: + LOOP = asyncio.get_running_loop() +except RuntimeError: + LOOP = asyncio.new_event_loop() + asyncio.set_event_loop(LOOP) + +# Run the async function to get the auth token AUTH_TOKEN = LOOP.run_until_complete(get_auth_token()) From 693acf0f18330a236d9293e63731df1943f249dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:37:51 -0500 Subject: [PATCH 12/40] Bump the pip group across 3 directories with 1 update (#2163) Bumps the pip group with 1 update in the /libraries/botbuilder-core directory: [cryptography](https://github.com/pyca/cryptography). Bumps the pip group with 1 update in the /libraries/botbuilder-dialogs directory: [cryptography](https://github.com/pyca/cryptography). Bumps the pip group with 1 update in the /libraries/botframework-connector directory: [cryptography](https://github.com/pyca/cryptography). Updates `cryptography` from 42.0.4 to 43.0.1 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...43.0.1) Updates `cryptography` from 42.0.4 to 43.0.1 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...43.0.1) Updates `cryptography` from 42.0.4 to 43.0.1 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...43.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production dependency-group: pip - dependency-name: cryptography dependency-type: direct:production dependency-group: pip - dependency-name: cryptography dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libraries/botbuilder-core/requirements.txt | 2 +- libraries/botbuilder-dialogs/requirements.txt | 2 +- libraries/botframework-connector/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/botbuilder-core/requirements.txt b/libraries/botbuilder-core/requirements.txt index 6ce30f68f..4b9aabc5a 100644 --- a/libraries/botbuilder-core/requirements.txt +++ b/libraries/botbuilder-core/requirements.txt @@ -4,5 +4,5 @@ botbuilder-schema==4.17.0 botframework-streaming==4.17.0 requests==2.32.0 PyJWT==2.4.0 -cryptography==42.0.4 +cryptography==43.0.1 aiounittest==1.3.0 \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/requirements.txt b/libraries/botbuilder-dialogs/requirements.txt index 920200124..d8f2cb4f2 100644 --- a/libraries/botbuilder-dialogs/requirements.txt +++ b/libraries/botbuilder-dialogs/requirements.txt @@ -4,5 +4,5 @@ botbuilder-schema==4.17.0 botbuilder-core==4.17.0 requests==2.32.0 PyJWT==2.4.0 -cryptography==42.0.4 +cryptography==43.0.1 aiounittest==1.3.0 diff --git a/libraries/botframework-connector/requirements.txt b/libraries/botframework-connector/requirements.txt index 0632606a7..5a6d8d4e9 100644 --- a/libraries/botframework-connector/requirements.txt +++ b/libraries/botframework-connector/requirements.txt @@ -2,5 +2,5 @@ msrest==0.7.* botbuilder-schema==4.17.0 requests==2.32.0 PyJWT==2.4.0 -cryptography==42.0.4 +cryptography==43.0.1 msal>=1.29.0 From 31dd5693bc10e79800d352c21869ae533ebcb74a Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 9 Sep 2024 21:00:44 +0530 Subject: [PATCH 13/40] Bump aiohttp (#2164) --- libraries/botbuilder-adapters-slack/requirements.txt | 2 +- libraries/botbuilder-ai/setup.py | 2 +- libraries/botbuilder-integration-aiohttp/setup.py | 2 +- .../botbuilder-integration-applicationinsights-aiohttp/setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 88ce85a22..98af56627 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.2 +aiohttp==3.10.5 pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index 2242efed9..e842fdc34 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.10.2", + "aiohttp==3.10.5", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index a777d50c9..1fe5d5ccc 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.10.2", + "aiohttp==3.10.5", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index d8b0f09cf..30c45b2f7 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.10.2", + "aiohttp==3.10.5", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", From 1e5102b2e4b7dd6b8fd75d1553b4f282557e7e6e Mon Sep 17 00:00:00 2001 From: Nikita-Gz <61112041+Nikita-Gz@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:38:03 +0300 Subject: [PATCH 14/40] Fixed type hint in turn_context.py (#2147) Previous type hint implied that send_activity will always return ResourceResponse, meanwhile it can also return None Co-authored-by: tracyboehrer --- libraries/botbuilder-core/botbuilder/core/turn_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 6c4a4eef3..852fd1f31 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -158,7 +158,7 @@ async def send_activity( activity_or_text: Union[Activity, str], speak: str = None, input_hint: str = None, - ) -> ResourceResponse: + ) -> Union[ResourceResponse, None]: """ Sends a single activity or message to the user. :param activity_or_text: From e493609b831f15c6f3dd80d939d9ad88b4ca2922 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 10 Sep 2024 20:38:23 +0100 Subject: [PATCH 15/40] Remove urllib3 version pin (#2144) Co-authored-by: tracyboehrer --- libraries/botbuilder-schema/setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-schema/setup.py b/libraries/botbuilder-schema/setup.py index 2075a5f20..43855c655 100644 --- a/libraries/botbuilder-schema/setup.py +++ b/libraries/botbuilder-schema/setup.py @@ -6,7 +6,10 @@ NAME = "botbuilder-schema" VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.17.0" -REQUIRES = ["msrest== 0.7.*", "urllib3<2.0.0"] +REQUIRES = [ + "msrest== 0.7.*", + "urllib3", +] root = os.path.abspath(os.path.dirname(__file__)) From b3e74361f3802a003e9fb1bf59a5ad8054a7f67f Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 10 Sep 2024 15:11:09 -0500 Subject: [PATCH 16/40] Fixed ExpectReplies response (#2166) * Fixed ExpectReplies response * Fix formatting --------- Co-authored-by: Tracy Boehrer --- .../botbuilder-core/botbuilder/core/cloud_adapter_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py b/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py index 454861257..0f695a2a7 100644 --- a/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py +++ b/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py @@ -436,7 +436,9 @@ def _process_turn_results(self, context: TurnContext) -> InvokeResponse: if context.activity.delivery_mode == DeliveryModes.expect_replies: return InvokeResponse( status=HTTPStatus.OK, - body=ExpectedReplies(activities=context.buffered_reply_activities), + body=ExpectedReplies( + activities=context.buffered_reply_activities + ).serialize(), ) # Handle Invoke scenarios where the bot will return a specific body and return code. From 50e72c0fa8a1991bb474e871987d8fff2b1bfc6e Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 10 Sep 2024 15:18:57 -0500 Subject: [PATCH 17/40] Updated azure-cosmos to 4.7.0, requiring dropped support for obsolete CosmosDBStorage class. (#2165) Co-authored-by: Tracy Boehrer --- .../botbuilder/azure/__init__.py | 4 +- .../azure/cosmosdb_partitioned_storage.py | 179 +++++---- .../botbuilder/azure/cosmosdb_storage.py | 378 ------------------ libraries/botbuilder-azure/setup.py | 2 +- .../tests/test_cosmos_partitioned_storage.py | 13 +- .../tests/test_cosmos_storage.py | 300 -------------- .../botbuilder/testing/storage_base_tests.py | 4 +- 7 files changed, 105 insertions(+), 775 deletions(-) delete mode 100644 libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py delete mode 100644 libraries/botbuilder-azure/tests/test_cosmos_storage.py diff --git a/libraries/botbuilder-azure/botbuilder/azure/__init__.py b/libraries/botbuilder-azure/botbuilder/azure/__init__.py index e625500a3..e6c70e7fc 100644 --- a/libraries/botbuilder-azure/botbuilder/azure/__init__.py +++ b/libraries/botbuilder-azure/botbuilder/azure/__init__.py @@ -7,10 +7,10 @@ from .about import __version__ from .azure_queue_storage import AzureQueueStorage -from .cosmosdb_storage import CosmosDbStorage, CosmosDbConfig, CosmosDbKeyEscape from .cosmosdb_partitioned_storage import ( CosmosDbPartitionedStorage, CosmosDbPartitionedConfig, + CosmosDbKeyEscape, ) from .blob_storage import BlobStorage, BlobStorageSettings @@ -18,8 +18,6 @@ "AzureQueueStorage", "BlobStorage", "BlobStorageSettings", - "CosmosDbStorage", - "CosmosDbConfig", "CosmosDbKeyEscape", "CosmosDbPartitionedStorage", "CosmosDbPartitionedConfig", diff --git a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py index 982ac5974..cfe66f8d8 100644 --- a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py +++ b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py @@ -6,14 +6,14 @@ from typing import Dict, List from threading import Lock import json - +from hashlib import sha256 +from azure.core import MatchConditions from azure.cosmos import documents, http_constants from jsonpickle.pickler import Pickler from jsonpickle.unpickler import Unpickler import azure.cosmos.cosmos_client as cosmos_client # pylint: disable=no-name-in-module,import-error -import azure.cosmos.errors as cosmos_errors # pylint: disable=no-name-in-module,import-error +import azure.cosmos.exceptions as cosmos_exceptions from botbuilder.core.storage import Storage -from botbuilder.azure import CosmosDbKeyEscape class CosmosDbPartitionedConfig: @@ -63,6 +63,49 @@ def __init__( self.compatibility_mode = compatibility_mode or kwargs.get("compatibility_mode") +class CosmosDbKeyEscape: + @staticmethod + def sanitize_key( + key: str, key_suffix: str = "", compatibility_mode: bool = True + ) -> str: + """Return the sanitized key. + + Replace characters that are not allowed in keys in Cosmos. + + :param key: The provided key to be escaped. + :param key_suffix: The string to add a the end of all RowKeys. + :param compatibility_mode: True if keys should be truncated in order to support previous CosmosDb + max key length of 255. This behavior can be overridden by setting + cosmosdb_partitioned_config.compatibility_mode to False. + :return str: + """ + # forbidden characters + bad_chars = ["\\", "?", "/", "#", "\t", "\n", "\r", "*"] + # replace those with with '*' and the + # Unicode code point of the character and return the new string + key = "".join(map(lambda x: "*" + str(ord(x)) if x in bad_chars else x, key)) + + if key_suffix is None: + key_suffix = "" + + return CosmosDbKeyEscape.truncate_key(f"{key}{key_suffix}", compatibility_mode) + + @staticmethod + def truncate_key(key: str, compatibility_mode: bool = True) -> str: + max_key_len = 255 + + if not compatibility_mode: + return key + + if len(key) > max_key_len: + aux_hash = sha256(key.encode("utf-8")) + aux_hex = aux_hash.hexdigest() + + key = key[0 : max_key_len - len(aux_hex)] + aux_hex + + return key + + class CosmosDbPartitionedStorage(Storage): """A CosmosDB based storage provider using partitioning for a bot.""" @@ -99,7 +142,8 @@ async def read(self, keys: List[str]) -> Dict[str, object]: :return dict: """ if not keys: - raise Exception("Keys are required when reading") + # No keys passed in, no result to return. Back-compat with original CosmosDBStorage. + return {} await self.initialize() @@ -111,8 +155,8 @@ async def read(self, keys: List[str]) -> Dict[str, object]: key, self.config.key_suffix, self.config.compatibility_mode ) - read_item_response = self.client.ReadItem( - self.__item_link(escaped_key), self.__get_partition_key(escaped_key) + read_item_response = self.container.read_item( + escaped_key, self.__get_partition_key(escaped_key) ) document_store_item = read_item_response if document_store_item: @@ -122,13 +166,8 @@ async def read(self, keys: List[str]) -> Dict[str, object]: # When an item is not found a CosmosException is thrown, but we want to # return an empty collection so in this instance we catch and do not rethrow. # Throw for any other exception. - except cosmos_errors.HTTPFailure as err: - if ( - err.status_code - == cosmos_errors.http_constants.StatusCodes.NOT_FOUND - ): - continue - raise err + except cosmos_exceptions.CosmosResourceNotFoundError: + continue except Exception as err: raise err return store_items @@ -162,20 +201,16 @@ async def write(self, changes: Dict[str, object]): if e_tag == "": raise Exception("cosmosdb_storage.write(): etag missing") - access_condition = { - "accessCondition": {"type": "IfMatch", "condition": e_tag} - } - options = ( - access_condition if e_tag != "*" and e_tag and e_tag != "" else None - ) + access_condition = e_tag != "*" and e_tag and e_tag != "" + try: - self.client.UpsertItem( - database_or_Container_link=self.__container_link, - document=doc, - options=options, + self.container.upsert_item( + body=doc, + etag=e_tag if access_condition else None, + match_condition=( + MatchConditions.IfNotModified if access_condition else None + ), ) - except cosmos_errors.HTTPFailure as err: - raise err except Exception as err: raise err @@ -192,69 +227,66 @@ async def delete(self, keys: List[str]): key, self.config.key_suffix, self.config.compatibility_mode ) try: - self.client.DeleteItem( - document_link=self.__item_link(escaped_key), - options=self.__get_partition_key(escaped_key), + self.container.delete_item( + escaped_key, + self.__get_partition_key(escaped_key), ) - except cosmos_errors.HTTPFailure as err: - if ( - err.status_code - == cosmos_errors.http_constants.StatusCodes.NOT_FOUND - ): - continue - raise err + except cosmos_exceptions.CosmosResourceNotFoundError: + continue except Exception as err: raise err async def initialize(self): if not self.container: if not self.client: + connection_policy = self.config.cosmos_client_options.get( + "connection_policy", documents.ConnectionPolicy() + ) + + # kwargs 'connection_verify' is to handle CosmosClient overwriting the + # ConnectionPolicy.DisableSSLVerification value. self.client = cosmos_client.CosmosClient( self.config.cosmos_db_endpoint, - {"masterKey": self.config.auth_key}, - self.config.cosmos_client_options.get("connection_policy", None), + self.config.auth_key, self.config.cosmos_client_options.get("consistency_level", None), + **{ + "connection_policy": connection_policy, + "connection_verify": not connection_policy.DisableSSLVerification, + }, ) if not self.database: with self.__lock: - try: - if not self.database: - self.database = self.client.CreateDatabase( - {"id": self.config.database_id} - ) - except cosmos_errors.HTTPFailure: - self.database = self.client.ReadDatabase( - "dbs/" + self.config.database_id + if not self.database: + self.database = self.client.create_database_if_not_exists( + self.config.database_id ) self.__get_or_create_container() def __get_or_create_container(self): with self.__lock: - container_def = { - "id": self.config.container_id, - "partitionKey": { - "paths": ["/id"], - "kind": documents.PartitionKind.Hash, - }, + partition_key = { + "paths": ["/id"], + "kind": documents.PartitionKind.Hash, } try: if not self.container: - self.container = self.client.CreateContainer( - "dbs/" + self.database["id"], - container_def, - {"offerThroughput": self.config.container_throughput}, + self.container = self.database.create_container( + self.config.container_id, + partition_key, + offer_throughput=self.config.container_throughput, ) - except cosmos_errors.HTTPFailure as err: + except cosmos_exceptions.CosmosHttpResponseError as err: if err.status_code == http_constants.StatusCodes.CONFLICT: - self.container = self.client.ReadContainer( - "dbs/" + self.database["id"] + "/colls/" + container_def["id"] + self.container = self.database.get_container_client( + self.config.container_id ) - if "partitionKey" not in self.container: + properties = self.container.read() + if "partitionKey" not in properties: self.compatability_mode_partition_key = True else: - paths = self.container["partitionKey"]["paths"] + paths = properties["partitionKey"]["paths"] if "/partitionKey" in paths: self.compatability_mode_partition_key = True elif "/id" not in paths: @@ -267,7 +299,7 @@ def __get_or_create_container(self): raise err def __get_partition_key(self, key: str) -> str: - return None if self.compatability_mode_partition_key else {"partitionKey": key} + return None if self.compatability_mode_partition_key else key @staticmethod def __create_si(result) -> object: @@ -303,28 +335,3 @@ def __create_dict(store_item: object) -> Dict: # loop through attributes and write and return a dict return json_dict - - def __item_link(self, identifier) -> str: - """Return the item link of a item in the container. - - :param identifier: - :return str: - """ - return self.__container_link + "/docs/" + identifier - - @property - def __container_link(self) -> str: - """Return the container link in the database. - - :param: - :return str: - """ - return self.__database_link + "/colls/" + self.config.container_id - - @property - def __database_link(self) -> str: - """Return the database link. - - :return str: - """ - return "dbs/" + self.config.database_id diff --git a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py deleted file mode 100644 index 2e383666f..000000000 --- a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py +++ /dev/null @@ -1,378 +0,0 @@ -"""Implements a CosmosDB based storage provider. -""" - -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from hashlib import sha256 -from typing import Dict, List -from threading import Semaphore -import json -import warnings -from jsonpickle.pickler import Pickler -from jsonpickle.unpickler import Unpickler -import azure.cosmos.cosmos_client as cosmos_client # pylint: disable=no-name-in-module,import-error -import azure.cosmos.errors as cosmos_errors # pylint: disable=no-name-in-module,import-error -from botbuilder.core.storage import Storage - - -class CosmosDbConfig: - """The class for CosmosDB configuration for the Azure Bot Framework.""" - - def __init__( - self, - endpoint: str = None, - masterkey: str = None, - database: str = None, - container: str = None, - partition_key: str = None, - database_creation_options: dict = None, - container_creation_options: dict = None, - **kwargs, - ): - """Create the Config object. - - :param endpoint: - :param masterkey: - :param database: - :param container: - :param filename: - :return CosmosDbConfig: - """ - self.__config_file = kwargs.get("filename") - if self.__config_file: - kwargs = json.load(open(self.__config_file)) - self.endpoint = endpoint or kwargs.get("endpoint") - self.masterkey = masterkey or kwargs.get("masterkey") - self.database = database or kwargs.get("database", "bot_db") - self.container = container or kwargs.get("container", "bot_container") - self.partition_key = partition_key or kwargs.get("partition_key") - self.database_creation_options = database_creation_options or kwargs.get( - "database_creation_options" - ) - self.container_creation_options = container_creation_options or kwargs.get( - "container_creation_options" - ) - - -class CosmosDbKeyEscape: - @staticmethod - def sanitize_key( - key: str, key_suffix: str = "", compatibility_mode: bool = True - ) -> str: - """Return the sanitized key. - - Replace characters that are not allowed in keys in Cosmos. - - :param key: The provided key to be escaped. - :param key_suffix: The string to add a the end of all RowKeys. - :param compatibility_mode: True if keys should be truncated in order to support previous CosmosDb - max key length of 255. This behavior can be overridden by setting - cosmosdb_partitioned_config.compatibility_mode to False. - :return str: - """ - # forbidden characters - bad_chars = ["\\", "?", "/", "#", "\t", "\n", "\r", "*"] - # replace those with with '*' and the - # Unicode code point of the character and return the new string - key = "".join(map(lambda x: "*" + str(ord(x)) if x in bad_chars else x, key)) - - if key_suffix is None: - key_suffix = "" - - return CosmosDbKeyEscape.truncate_key(f"{key}{key_suffix}", compatibility_mode) - - @staticmethod - def truncate_key(key: str, compatibility_mode: bool = True) -> str: - max_key_len = 255 - - if not compatibility_mode: - return key - - if len(key) > max_key_len: - aux_hash = sha256(key.encode("utf-8")) - aux_hex = aux_hash.hexdigest() - - key = key[0 : max_key_len - len(aux_hex)] + aux_hex - - return key - - -class CosmosDbStorage(Storage): - """A CosmosDB based storage provider for a bot.""" - - def __init__( - self, config: CosmosDbConfig, client: cosmos_client.CosmosClient = None - ): - """Create the storage object. - - :param config: - """ - super(CosmosDbStorage, self).__init__() - warnings.warn( - "CosmosDbStorage is obsolete. Use CosmosDbPartitionedStorage instead." - ) - self.config = config - self.client = client or cosmos_client.CosmosClient( - self.config.endpoint, {"masterKey": self.config.masterkey} - ) - # these are set by the functions that check - # the presence of the database and container or creates them - self.database = None - self.container = None - self._database_creation_options = config.database_creation_options - self._container_creation_options = config.container_creation_options - self.__semaphore = Semaphore() - - async def read(self, keys: List[str]) -> Dict[str, object]: - """Read storeitems from storage. - - :param keys: - :return dict: - """ - try: - # check if the database and container exists and if not create - if not self.__container_exists: - self.__create_db_and_container() - if keys: - # create the parameters object - parameters = [ - { - "name": f"@id{i}", - "value": f"{CosmosDbKeyEscape.sanitize_key(key)}", - } - for i, key in enumerate(keys) - ] - # get the names of the params - parameter_sequence = ",".join(param.get("name") for param in parameters) - # create the query - query = { - "query": f"SELECT c.id, c.realId, c.document, c._etag FROM c WHERE c.id in ({parameter_sequence})", - "parameters": parameters, - } - - if self.config.partition_key: - options = {"partitionKey": self.config.partition_key} - else: - options = {"enableCrossPartitionQuery": True} - - # run the query and store the results as a list - results = list( - self.client.QueryItems(self.__container_link, query, options) - ) - # return a dict with a key and an object - return {r.get("realId"): self.__create_si(r) for r in results} - - # No keys passed in, no result to return. - return {} - except TypeError as error: - raise error - - async def write(self, changes: Dict[str, object]): - """Save storeitems to storage. - - :param changes: - :return: - """ - if changes is None: - raise Exception("Changes are required when writing") - if not changes: - return - try: - # check if the database and container exists and if not create - if not self.__container_exists: - self.__create_db_and_container() - # iterate over the changes - for key, change in changes.items(): - # store the e_tag - e_tag = None - if isinstance(change, dict): - e_tag = change.get("e_tag", None) - elif hasattr(change, "e_tag"): - e_tag = change.e_tag - # create the new document - doc = { - "id": CosmosDbKeyEscape.sanitize_key(key), - "realId": key, - "document": self.__create_dict(change), - } - if e_tag == "": - raise Exception("cosmosdb_storage.write(): etag missing") - # the e_tag will be * for new docs so do an insert - if e_tag == "*" or not e_tag: - self.client.UpsertItem( - database_or_Container_link=self.__container_link, - document=doc, - options={"disableAutomaticIdGeneration": True}, - ) - # if we have an etag, do opt. concurrency replace - elif e_tag: - access_condition = {"type": "IfMatch", "condition": e_tag} - self.client.ReplaceItem( - document_link=self.__item_link( - CosmosDbKeyEscape.sanitize_key(key) - ), - new_document=doc, - options={"accessCondition": access_condition}, - ) - except Exception as error: - raise error - - async def delete(self, keys: List[str]): - """Remove storeitems from storage. - - :param keys: - :return: - """ - try: - # check if the database and container exists and if not create - if not self.__container_exists: - self.__create_db_and_container() - - options = {} - if self.config.partition_key: - options["partitionKey"] = self.config.partition_key - - # call the function for each key - for key in keys: - self.client.DeleteItem( - document_link=self.__item_link(CosmosDbKeyEscape.sanitize_key(key)), - options=options, - ) - # print(res) - except cosmos_errors.HTTPFailure as http_failure: - # print(h.status_code) - if http_failure.status_code != 404: - raise http_failure - except TypeError as error: - raise error - - def __create_si(self, result) -> object: - """Create an object from a result out of CosmosDB. - - :param result: - :return object: - """ - # get the document item from the result and turn into a dict - doc = result.get("document") - # read the e_tag from Cosmos - if result.get("_etag"): - doc["e_tag"] = result["_etag"] - - result_obj = Unpickler().restore(doc) - - # create and return the object - return result_obj - - def __create_dict(self, store_item: object) -> Dict: - """Return the dict of an object. - - This eliminates non_magic attributes and the e_tag. - - :param store_item: - :return dict: - """ - # read the content - json_dict = Pickler().flatten(store_item) - if "e_tag" in json_dict: - del json_dict["e_tag"] - - # loop through attributes and write and return a dict - return json_dict - - def __item_link(self, identifier) -> str: - """Return the item link of a item in the container. - - :param identifier: - :return str: - """ - return self.__container_link + "/docs/" + identifier - - @property - def __container_link(self) -> str: - """Return the container link in the database. - - :param: - :return str: - """ - return self.__database_link + "/colls/" + self.container - - @property - def __database_link(self) -> str: - """Return the database link. - - :return str: - """ - return "dbs/" + self.database - - @property - def __container_exists(self) -> bool: - """Return whether the database and container have been created. - - :return bool: - """ - return self.database and self.container - - def __create_db_and_container(self): - """Call the get or create methods.""" - with self.__semaphore: - db_id = self.config.database - container_name = self.config.container - self.database = self._get_or_create_database(self.client, db_id) - self.container = self._get_or_create_container(self.client, container_name) - - def _get_or_create_database( # pylint: disable=invalid-name - self, doc_client, id - ) -> str: - """Return the database link. - - Check if the database exists or create the database. - - :param doc_client: - :param id: - :return str: - """ - # query CosmosDB for a database with that name/id - dbs = list( - doc_client.QueryDatabases( - { - "query": "SELECT * FROM r WHERE r.id=@id", - "parameters": [{"name": "@id", "value": id}], - } - ) - ) - # if there are results, return the first (database names are unique) - if dbs: - return dbs[0]["id"] - - # create the database if it didn't exist - res = doc_client.CreateDatabase({"id": id}, self._database_creation_options) - return res["id"] - - def _get_or_create_container(self, doc_client, container) -> str: - """Return the container link. - - Check if the container exists or create the container. - - :param doc_client: - :param container: - :return str: - """ - # query CosmosDB for a container in the database with that name - containers = list( - doc_client.QueryContainers( - self.__database_link, - { - "query": "SELECT * FROM r WHERE r.id=@id", - "parameters": [{"name": "@id", "value": container}], - }, - ) - ) - # if there are results, return the first (container names are unique) - if containers: - return containers[0]["id"] - - # Create a container if it didn't exist - res = doc_client.CreateContainer( - self.__database_link, {"id": container}, self._container_creation_options - ) - return res["id"] diff --git a/libraries/botbuilder-azure/setup.py b/libraries/botbuilder-azure/setup.py index 04fd479cb..9c40b3ab5 100644 --- a/libraries/botbuilder-azure/setup.py +++ b/libraries/botbuilder-azure/setup.py @@ -5,7 +5,7 @@ from setuptools import setup REQUIRES = [ - "azure-cosmos==3.2.0", + "azure-cosmos==4.7.0", "azure-storage-blob==12.7.0", "azure-storage-queue==12.4.0", "botbuilder-schema==4.17.0", diff --git a/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py b/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py index cb6dd0822..d52733fd9 100644 --- a/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py +++ b/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import azure.cosmos.errors as cosmos_errors +import azure.cosmos.exceptions as cosmos_exceptions from azure.cosmos import documents import pytest from botbuilder.azure import CosmosDbPartitionedStorage, CosmosDbPartitionedConfig @@ -27,8 +27,8 @@ async def reset(): storage = CosmosDbPartitionedStorage(get_settings()) await storage.initialize() try: - storage.client.DeleteDatabase(database_link="dbs/" + get_settings().database_id) - except cosmos_errors.HTTPFailure: + storage.client.delete_database(get_settings().database_id) + except cosmos_exceptions.HttpResponseError: pass @@ -99,9 +99,12 @@ async def test_passes_cosmos_client_options(self): client = CosmosDbPartitionedStorage(settings_with_options) await client.initialize() - assert client.client.connection_policy.DisableSSLVerification is True assert ( - client.client.default_headers["x-ms-consistency-level"] + client.client.client_connection.connection_policy.DisableSSLVerification + is True + ) + assert ( + client.client.client_connection.default_headers["x-ms-consistency-level"] == documents.ConsistencyLevel.Eventual ) diff --git a/libraries/botbuilder-azure/tests/test_cosmos_storage.py b/libraries/botbuilder-azure/tests/test_cosmos_storage.py deleted file mode 100644 index c66660857..000000000 --- a/libraries/botbuilder-azure/tests/test_cosmos_storage.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from unittest.mock import Mock -import azure.cosmos.errors as cosmos_errors -from azure.cosmos.cosmos_client import CosmosClient -import pytest -from botbuilder.core import StoreItem -from botbuilder.azure import CosmosDbStorage, CosmosDbConfig -from botbuilder.testing import StorageBaseTests - -# local cosmosdb emulator instance cosmos_db_config -COSMOS_DB_CONFIG = CosmosDbConfig( - endpoint="https://localhost:8081", - masterkey="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", - database="test-db", - container="bot-storage", -) -EMULATOR_RUNNING = False - - -def get_storage(): - return CosmosDbStorage(COSMOS_DB_CONFIG) - - -async def reset(): - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - try: - storage.client.DeleteDatabase(database_link="dbs/" + COSMOS_DB_CONFIG.database) - except cosmos_errors.HTTPFailure: - pass - - -def get_mock_client(identifier: str = "1"): - # pylint: disable=attribute-defined-outside-init, invalid-name - mock = MockClient() - - mock.QueryDatabases = Mock(return_value=[]) - mock.QueryContainers = Mock(return_value=[]) - mock.CreateDatabase = Mock(return_value={"id": identifier}) - mock.CreateContainer = Mock(return_value={"id": identifier}) - - return mock - - -class MockClient(CosmosClient): - def __init__(self): # pylint: disable=super-init-not-called - pass - - -class SimpleStoreItem(StoreItem): - def __init__(self, counter=1, e_tag="*"): - super(SimpleStoreItem, self).__init__() - self.counter = counter - self.e_tag = e_tag - - -class TestCosmosDbStorageConstructor: - @pytest.mark.asyncio - async def test_cosmos_storage_init_should_error_without_cosmos_db_config(self): - try: - CosmosDbStorage(CosmosDbConfig()) - except Exception as error: - assert error - - @pytest.mark.asyncio - async def test_creation_request_options_are_being_called(self): - # pylint: disable=protected-access - test_config = CosmosDbConfig( - endpoint="https://localhost:8081", - masterkey="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", - database="test-db", - container="bot-storage", - database_creation_options={"OfferThroughput": 1000}, - container_creation_options={"OfferThroughput": 500}, - ) - - test_id = "1" - client = get_mock_client(identifier=test_id) - storage = CosmosDbStorage(test_config, client) - storage.database = test_id - - assert storage._get_or_create_database(doc_client=client, id=test_id), test_id - client.CreateDatabase.assert_called_with( - {"id": test_id}, test_config.database_creation_options - ) - assert storage._get_or_create_container( - doc_client=client, container=test_id - ), test_id - client.CreateContainer.assert_called_with( - "dbs/" + test_id, {"id": test_id}, test_config.container_creation_options - ) - - -class TestCosmosDbStorageBaseStorageTests: - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_return_empty_object_when_reading_unknown_key(self): - await reset() - - test_ran = await StorageBaseTests.return_empty_object_when_reading_unknown_key( - get_storage() - ) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_handle_null_keys_when_reading(self): - await reset() - - test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_handle_null_keys_when_writing(self): - await reset() - - test_ran = await StorageBaseTests.handle_null_keys_when_writing(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_does_not_raise_when_writing_no_items(self): - await reset() - - test_ran = await StorageBaseTests.does_not_raise_when_writing_no_items( - get_storage() - ) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_create_object(self): - await reset() - - test_ran = await StorageBaseTests.create_object(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_handle_crazy_keys(self): - await reset() - - test_ran = await StorageBaseTests.handle_crazy_keys(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_update_object(self): - await reset() - - test_ran = await StorageBaseTests.update_object(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_delete_object(self): - await reset() - - test_ran = await StorageBaseTests.delete_object(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_perform_batch_operations(self): - await reset() - - test_ran = await StorageBaseTests.perform_batch_operations(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_proceeds_through_waterfall(self): - await reset() - - test_ran = await StorageBaseTests.proceeds_through_waterfall(get_storage()) - - assert test_ran - - -class TestCosmosDbStorage: - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_init_should_work_with_just_endpoint_and_key(self): - storage = CosmosDbStorage( - CosmosDbConfig( - endpoint=COSMOS_DB_CONFIG.endpoint, masterkey=COSMOS_DB_CONFIG.masterkey - ) - ) - await storage.write({"user": SimpleStoreItem()}) - data = await storage.read(["user"]) - assert "user" in data - assert data["user"].counter == 1 - assert len(data.keys()) == 1 - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_read_update_should_return_new_etag(self): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem(counter=1)}) - data_result = await storage.read(["test"]) - data_result["test"].counter = 2 - await storage.write(data_result) - data_updated = await storage.read(["test"]) - assert data_updated["test"].counter == 2 - assert data_updated["test"].e_tag != data_result["test"].e_tag - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_read_with_invalid_key_should_return_empty_dict(self): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - data = await storage.read(["test"]) - - assert isinstance(data, dict) - assert not data.keys() - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"user": SimpleStoreItem()}) - - await storage.write({"user": SimpleStoreItem(counter=10, e_tag="*")}) - data = await storage.read(["user"]) - assert data["user"].counter == 10 - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_should_delete_multiple_values_when_given_multiple_valid_keys( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem(), "test2": SimpleStoreItem(2)}) - - await storage.delete(["test", "test2"]) - data = await storage.read(["test", "test2"]) - assert not data.keys() - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_should_delete_values_when_given_multiple_valid_keys_and_ignore_other_data( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write( - { - "test": SimpleStoreItem(), - "test2": SimpleStoreItem(counter=2), - "test3": SimpleStoreItem(counter=3), - } - ) - - await storage.delete(["test", "test2"]) - data = await storage.read(["test", "test2", "test3"]) - assert len(data.keys()) == 1 - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_invalid_key_should_do_nothing_and_not_affect_cached_data( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem()}) - - await storage.delete(["foo"]) - data = await storage.read(["test"]) - assert len(data.keys()) == 1 - data = await storage.read(["foo"]) - assert not data.keys() - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_invalid_keys_should_do_nothing_and_not_affect_cached_data( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem()}) - - await storage.delete(["foo", "bar"]) - data = await storage.read(["test"]) - assert len(data.keys()) == 1 diff --git a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py index e196099a0..e374a3401 100644 --- a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py +++ b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py @@ -24,7 +24,7 @@ async def test_handle_null_keys_when_reading(self): assert test_ran """ import pytest -from botbuilder.azure import CosmosDbStorage +from botbuilder.azure import CosmosDbPartitionedStorage from botbuilder.core import ( ConversationState, TurnContext, @@ -57,7 +57,7 @@ async def return_empty_object_when_reading_unknown_key(storage) -> bool: @staticmethod async def handle_null_keys_when_reading(storage) -> bool: - if isinstance(storage, (CosmosDbStorage, MemoryStorage)): + if isinstance(storage, (CosmosDbPartitionedStorage, MemoryStorage)): result = await storage.read(None) assert len(result.keys()) == 0 # Catch-all From 0fba27be15cd72bffb7f34bf1ed20c05c3df0957 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:37:15 +0530 Subject: [PATCH 18/40] port: Add Teams read receipt event (#6356) (#2167) * port: Add Teams read receipt event (#6356) * remove unused variables --- .../core/teams/teams_activity_handler.py | 18 +++++++ .../teams/test_teams_activity_handler.py | 27 ++++++++++ .../botbuilder/schema/teams/__init__.py | 2 + .../botbuilder/schema/teams/_models_py3.py | 51 +++++++++++++++++++ .../tests/teams/test_read_receipt_info.py | 28 ++++++++++ 5 files changed, 126 insertions(+) create mode 100644 libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 33b4e419c..4fd6d4ee1 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -28,6 +28,7 @@ TabRequest, TabSubmit, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from ..serializer_helper import deserializer_helper @@ -906,6 +907,10 @@ async def on_event_activity(self, turn_context: TurnContext): the scope of a channel. """ if turn_context.activity.channel_id == Channels.ms_teams: + if turn_context.activity.name == "application/vnd.microsoft.readReceipt": + return await self.on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) if turn_context.activity.name == "application/vnd.microsoft.meetingStart": return await self.on_teams_meeting_start_event( turn_context.activity.value, turn_context @@ -931,6 +936,19 @@ async def on_event_activity(self, turn_context: TurnContext): return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when the bot receives a read receipt event. + + :param read_receipt_info: Information regarding the read receipt. i.e. Id of the message last read by the user. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): # pylint: disable=unused-argument diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 390df6191..af9bcd4ab 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -33,6 +33,7 @@ TabSubmit, TabContext, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -318,6 +319,14 @@ async def on_event_activity(self, turn_context: TurnContext): self.record.append("on_event_activity") return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): + self.record.append("on_teams_read_receipt_event") + return await super().on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): @@ -1141,6 +1150,24 @@ async def test_typing_activity(self): assert len(bot.record) == 1 assert bot.record[0] == "on_typing_activity" + async def test_on_teams_read_receipt_event(self): + activity = Activity( + type=ActivityTypes.event, + name="application/vnd.microsoft.readReceipt", + channel_id=Channels.ms_teams, + value={"lastReadMessageId": "10101010"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 2 + assert bot.record[0] == "on_event_activity" + assert bot.record[1] == "on_teams_read_receipt_event" + async def test_on_teams_meeting_start_event(self): activity = Activity( type=ActivityTypes.event, diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 55901f7a4..0bac60e96 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -80,6 +80,7 @@ from ._models_py3 import UserMeetingDetails from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails +from ._models_py3 import ReadReceiptInfo __all__ = [ "AppBasedLinkQuery", @@ -161,4 +162,5 @@ "UserMeetingDetails", "TeamsMeetingMember", "MeetingParticipantsEventDetails", + "ReadReceiptInfo", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 260442cf8..a507467cc 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2568,3 +2568,54 @@ class MeetingParticipantsEventDetails(Model): def __init__(self, *, members: List[TeamsMeetingMember] = None, **kwargs) -> None: super(MeetingParticipantsEventDetails, self).__init__(**kwargs) self.members = members + + +class ReadReceiptInfo(Model): + """General information about a read receipt. + + :param last_read_message_id: The id of the last read message. + :type last_read_message_id: str + """ + + _attribute_map = { + "last_read_message_id": {"key": "lastReadMessageId", "type": "str"}, + } + + def __init__(self, *, last_read_message_id: str = None, **kwargs) -> None: + super(ReadReceiptInfo, self).__init__(**kwargs) + self.last_read_message_id = last_read_message_id + + @staticmethod + def is_message_read(compare_message_id, last_read_message_id): + """ + Helper method useful for determining if a message has been read. + This method converts the strings to integers. If the compare_message_id is + less than or equal to the last_read_message_id, then the message has been read. + + :param compare_message_id: The id of the message to compare. + :param last_read_message_id: The id of the last message read by the user. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + if not compare_message_id or not last_read_message_id: + return False + + try: + compare_message_id_long = int(compare_message_id) + last_read_message_id_long = int(last_read_message_id) + except ValueError: + return False + + return compare_message_id_long <= last_read_message_id_long + + def is_message_read_instance(self, compare_message_id): + """ + Helper method useful for determining if a message has been read. + If the compare_message_id is less than or equal to the last_read_message_id, + then the message has been read. + + :param compare_message_id: The id of the message to compare. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + return ReadReceiptInfo.is_message_read( + compare_message_id, self.last_read_message_id + ) diff --git a/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py new file mode 100644 index 000000000..e6aad9bf3 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ReadReceiptInfo + + +class TestReadReceiptInfo(aiounittest.AsyncTestCase): + def test_read_receipt_info(self): + # Arrange + test_cases = [ + ("1000", "1000", True), + ("1001", "1000", True), + ("1000", "1001", False), + ("1000", None, False), + (None, "1000", False), + ] + + for last_read, compare, is_read in test_cases: + # Act + info = ReadReceiptInfo(last_read_message_id=last_read) + + # Assert + self.assertEqual(info.last_read_message_id, last_read) + self.assertEqual(info.is_message_read_instance(compare), is_read) + self.assertEqual( + ReadReceiptInfo.is_message_read(compare, last_read), is_read + ) From fddd368dca2c0889c01cf4ac101666236aa9298b Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:23:37 +0530 Subject: [PATCH 19/40] Add new Teams invoke types 'config/fetch' and 'config/submit' (#2170) --- .../core/teams/teams_activity_handler.py | 42 ++++++++++ .../teams/test_teams_activity_handler.py | 52 ++++++++++++ .../botbuilder/schema/teams/__init__.py | 8 ++ .../botbuilder/schema/teams/_models_py3.py | 80 +++++++++++++++++++ .../tests/teams/test_bot_config_auth.py | 14 ++++ .../tests/teams/test_config_auth_response.py | 14 ++++ .../tests/teams/test_config_response.py | 13 +++ .../tests/teams/test_config_task_response.py | 14 ++++ 8 files changed, 237 insertions(+) create mode 100644 libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py create mode 100644 libraries/botbuilder-schema/tests/teams/test_config_auth_response.py create mode 100644 libraries/botbuilder-schema/tests/teams/test_config_response.py create mode 100644 libraries/botbuilder-schema/tests/teams/test_config_task_response.py diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 4fd6d4ee1..7832887dc 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -186,6 +186,22 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ) ) + if turn_context.activity.name == "config/fetch": + return self._create_invoke_response( + await self.on_teams_config_fetch( + turn_context, + turn_context.activity.value, + ) + ) + + if turn_context.activity.name == "config/submit": + return self._create_invoke_response( + await self.on_teams_config_submit( + turn_context, + turn_context.activity.value, + ) + ) + return await super().on_invoke_activity(turn_context) except _InvokeResponseException as invoke_exception: @@ -515,6 +531,32 @@ async def on_teams_tab_submit( # pylint: disable=unused-argument """ raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_config_fetch( # pylint: disable=unused-argument + self, turn_context: TurnContext, config_data: any + ): + """ + Override this in a derived class to provide logic for when a config is fetched. + + :param turn_context: A context object for this turn. + :param config_data: The config fetch invoke request value payload. + + :returns: A Config Response for the request. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_config_submit( # pylint: disable=unused-argument + self, turn_context: TurnContext, config_data: any + ): + """ + Override this in a derived class to provide logic for when a config is submitted. + + :param turn_context: A context object for this turn. + :param config_data: The config fetch invoke request value payload. + + :returns: A Config Response for the request. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_conversation_update_activity(self, turn_context: TurnContext): """ Invoked when a conversation update activity is received from the channel. diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index af9bcd4ab..2ea62717b 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -315,6 +315,14 @@ async def on_teams_tab_submit( self.record.append("on_teams_tab_submit") return await super().on_teams_tab_submit(turn_context, tab_submit) + async def on_teams_config_fetch(self, turn_context: TurnContext, config_data: any): + self.record.append("on_teams_config_fetch") + return await super().on_teams_config_fetch(turn_context, config_data) + + async def on_teams_config_submit(self, turn_context: TurnContext, config_data: any): + self.record.append("on_teams_config_submit") + return await super().on_teams_config_submit(turn_context, config_data) + async def on_event_activity(self, turn_context: TurnContext): self.record.append("on_event_activity") return await super().on_event_activity(turn_context) @@ -1126,6 +1134,50 @@ async def test_on_teams_tab_submit(self): assert bot.record[0] == "on_invoke_activity" assert bot.record[1] == "on_teams_tab_submit" + async def test_on_teams_config_fetch(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="config/fetch", + value={ + "data": {"key": "value", "type": "config/fetch"}, + "context": {"theme": "default"}, + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_config_fetch" + + async def test_on_teams_config_submit(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="config/submit", + value={ + "data": {"key": "value", "type": "config/submit"}, + "context": {"theme": "default"}, + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_config_submit" + async def test_on_end_of_conversation_activity(self): activity = Activity(type=ActivityTypes.end_of_conversation) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 0bac60e96..8fb944b16 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -81,6 +81,10 @@ from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails from ._models_py3 import ReadReceiptInfo +from ._models_py3 import BotConfigAuth +from ._models_py3 import ConfigAuthResponse +from ._models_py3 import ConfigResponse +from ._models_py3 import ConfigTaskResponse __all__ = [ "AppBasedLinkQuery", @@ -163,4 +167,8 @@ "TeamsMeetingMember", "MeetingParticipantsEventDetails", "ReadReceiptInfo", + "BotConfigAuth", + "ConfigAuthResponse", + "ConfigResponse", + "ConfigTaskResponse", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index a507467cc..a1ab30d6c 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2619,3 +2619,83 @@ def is_message_read_instance(self, compare_message_id): return ReadReceiptInfo.is_message_read( compare_message_id, self.last_read_message_id ) + + +class BotConfigAuth(Model): + """Specifies bot config auth, including type and suggestedActions. + + :param type: The type of bot config auth. + :type type: str + :param suggested_actions: The suggested actions of bot config auth. + :type suggested_actions: ~botframework.connector.models.SuggestedActions + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "suggested_actions": {"key": "suggestedActions", "type": "SuggestedActions"}, + } + + def __init__(self, *, type: str = "auth", suggested_actions=None, **kwargs) -> None: + super(BotConfigAuth, self).__init__(**kwargs) + self.type = type + self.suggested_actions = suggested_actions + + +class ConfigResponseBase(Model): + """Specifies Invoke response base, including response type. + + :param response_type: Response type for invoke request + :type response_type: str + """ + + _attribute_map = { + "response_type": {"key": "responseType", "type": "str"}, + } + + def __init__(self, *, response_type: str = None, **kwargs) -> None: + super(ConfigResponseBase, self).__init__(**kwargs) + self.response_type = response_type + + +class ConfigResponse(ConfigResponseBase): + """Envelope for Config Response Payload. + + :param config: The response to the config message. Possible values: 'auth', 'task' + :type config: T + :param cache_info: Response cache info + :type cache_info: ~botframework.connector.teams.models.CacheInfo + """ + + _attribute_map = { + "config": {"key": "config", "type": "object"}, + "cache_info": {"key": "cacheInfo", "type": "CacheInfo"}, + } + + def __init__(self, *, config=None, cache_info=None, **kwargs) -> None: + super(ConfigResponse, self).__init__(response_type="config", **kwargs) + self.config = config + self.cache_info = cache_info + + +class ConfigTaskResponse(ConfigResponse): + """Envelope for Config Task Response. + + This class uses TaskModuleResponseBase as the type for the config parameter. + """ + + def __init__(self, *, config=None, **kwargs) -> None: + super(ConfigTaskResponse, self).__init__( + config=config or TaskModuleResponseBase(), **kwargs + ) + + +class ConfigAuthResponse(ConfigResponse): + """Envelope for Config Auth Response. + + This class uses BotConfigAuth as the type for the config parameter. + """ + + def __init__(self, *, config=None, **kwargs) -> None: + super(ConfigAuthResponse, self).__init__( + config=config or BotConfigAuth(), **kwargs + ) diff --git a/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py b/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py new file mode 100644 index 000000000..f6d771c4e --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import BotConfigAuth + + +class TestBotConfigAuth(aiounittest.AsyncTestCase): + def test_bot_config_auth_inits_with_no_args(self): + bot_config_auth_response = BotConfigAuth() + + self.assertIsNotNone(bot_config_auth_response) + self.assertIsInstance(bot_config_auth_response, BotConfigAuth) + self.assertEqual("auth", bot_config_auth_response.type) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py b/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py new file mode 100644 index 000000000..54221399d --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigAuthResponse + + +class TestConfigAuthResponse(aiounittest.AsyncTestCase): + def test_config_auth_response_init_with_no_args(self): + config_auth_response = ConfigAuthResponse() + + self.assertIsNotNone(config_auth_response) + self.assertIsInstance(config_auth_response, ConfigAuthResponse) + self.assertEqual("config", config_auth_response.response_type) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_response.py b/libraries/botbuilder-schema/tests/teams/test_config_response.py new file mode 100644 index 000000000..39d2ce0d5 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_response.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigResponse + + +class TestConfigResponse(aiounittest.AsyncTestCase): + def test_config_response_inits_with_no_args(self): + config_response = ConfigResponse() + + self.assertIsNotNone(config_response) + self.assertIsInstance(config_response, ConfigResponse) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_task_response.py b/libraries/botbuilder-schema/tests/teams/test_config_task_response.py new file mode 100644 index 000000000..53126388d --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_task_response.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigTaskResponse + + +class TestConfigTaskResponse(aiounittest.AsyncTestCase): + def test_config_task_response_init_with_no_args(self): + config_task_response = ConfigTaskResponse() + + self.assertIsNotNone(config_task_response) + self.assertIsInstance(config_task_response, ConfigTaskResponse) + self.assertEqual("config", config_task_response.response_type) From d4a1867023c851517d6e5bc744bdbe5c2b61fa55 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:51:15 +0530 Subject: [PATCH 20/40] Add settings.selectedChannel to TeamsChannelData and Type to ChannelInfo and TeamDetails (#2173) --- .../botbuilder/core/teams/__init__.py | 2 + .../core/teams/teams_activity_extensions.py | 17 +++++++++ .../tests/teams/test_teams_extension.py | 30 +++++++++++++++ .../botbuilder/schema/teams/__init__.py | 2 + .../botbuilder/schema/teams/_models_py3.py | 38 ++++++++++++++++++- 5 files changed, 88 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/__init__.py b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py index 9d3c4d43d..7e1f1eede 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py @@ -9,6 +9,7 @@ from .teams_info import TeamsInfo from .teams_activity_extensions import ( teams_get_channel_id, + teams_get_selected_channel_id, teams_get_team_info, teams_notify_user, ) @@ -19,6 +20,7 @@ "TeamsInfo", "TeamsSSOTokenExchangeMiddleware", "teams_get_channel_id", + "teams_get_selected_channel_id", "teams_get_team_info", "teams_notify_user", ] diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py index 7b9c2fd0a..04e11583f 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py @@ -31,6 +31,23 @@ def teams_get_channel_id(activity: Activity) -> str: return None +def teams_get_selected_channel_id(activity: Activity) -> str: + if not activity: + return None + + if activity.channel_data: + channel_data = TeamsChannelData().deserialize(activity.channel_data) + return ( + channel_data.settings.selected_channel.id + if channel_data + and channel_data.settings + and channel_data.settings.selected_channel + else None + ) + + return None + + def teams_get_team_info(activity: Activity) -> TeamInfo: if not activity: return None diff --git a/libraries/botbuilder-core/tests/teams/test_teams_extension.py b/libraries/botbuilder-core/tests/teams/test_teams_extension.py index 98c1ee829..ac55cb9a2 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_extension.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_extension.py @@ -7,6 +7,7 @@ from botbuilder.schema.teams import TeamInfo from botbuilder.core.teams import ( teams_get_channel_id, + teams_get_selected_channel_id, teams_get_team_info, teams_notify_user, ) @@ -26,6 +27,35 @@ def test_teams_get_channel_id(self): # Assert assert result == "id123" + def test_teams_get_selected_channel_id(self): + # Arrange + activity = Activity( + channel_data={ + "channel": {"id": "id123", "name": "channel_name"}, + "settings": { + "selectedChannel": {"id": "id12345", "name": "channel_name"} + }, + } + ) + + # Act + result = teams_get_selected_channel_id(activity) + + # Assert + assert result == "id12345" + + def test_teams_get_selected_channel_id_with_no_selected_channel(self): + # Arrange + activity = Activity( + channel_data={"channel": {"id": "id123", "name": "channel_name"}} + ) + + # Act + result = teams_get_selected_channel_id(activity) + + # Assert + assert result is None + def test_teams_get_channel_id_with_no_channel(self): # Arrange activity = Activity( diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 8fb944b16..76e34f5f8 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -59,6 +59,7 @@ from ._models_py3 import TeamDetails from ._models_py3 import TeamInfo from ._models_py3 import TeamsChannelAccount +from ._models_py3 import TeamsChannelDataSettings from ._models_py3 import TeamsChannelData from ._models_py3 import TeamsPagedMembersResult from ._models_py3 import TenantInfo @@ -145,6 +146,7 @@ "TeamDetails", "TeamInfo", "TeamsChannelAccount", + "TeamsChannelDataSettings", "TeamsChannelData", "TeamsPagedMembersResult", "TenantInfo", diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index a1ab30d6c..8b5cebd29 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -87,17 +87,23 @@ class ChannelInfo(Model): :type id: str :param name: Name of the channel :type name: str + :param type: The channel type + :type type: str """ _attribute_map = { "id": {"key": "id", "type": "str"}, "name": {"key": "name", "type": "str"}, + "type": {"key": "type", "type": "str"}, } - def __init__(self, *, id: str = None, name: str = None, **kwargs) -> None: + def __init__( + self, *, id: str = None, name: str = None, type: str = None, **kwargs + ) -> None: super(ChannelInfo, self).__init__(**kwargs) self.id = id self.name = name + self.type = type class CacheInfo(Model): @@ -1820,6 +1826,8 @@ class TeamDetails(Model): :type channel_count: int :param member_count: The count of members in the team. :type member_count: int + :param type: The team type + :type type: str """ _attribute_map = { @@ -1828,6 +1836,7 @@ class TeamDetails(Model): "aad_group_id": {"key": "aadGroupId", "type": "str"}, "channel_count": {"key": "channelCount", "type": "int"}, "member_count": {"key": "memberCount", "type": "int"}, + "type": {"key": "type", "type": "str"}, } def __init__( @@ -1838,6 +1847,7 @@ def __init__( aad_group_id: str = None, member_count: int = None, channel_count: int = None, + type: str = None, **kwargs ) -> None: super(TeamDetails, self).__init__(**kwargs) @@ -1846,6 +1856,7 @@ def __init__( self.aad_group_id = aad_group_id self.channel_count = channel_count self.member_count = member_count + self.type = type class TeamInfo(Model): @@ -1958,6 +1969,26 @@ def __init__( self.members = members +class TeamsChannelDataSettings(Model): + """ + Represents the settings information for a Teams channel data. + + :param selected_channel: Information about the selected Teams channel. + :type selected_channel: ~botframework.connector.teams.models.ChannelInfo + :param additional_properties: Gets or sets properties that are not otherwise defined by the + type but that might appear in the REST JSON object. + :type additional_properties: object + """ + + _attribute_map = { + "selected_channel": {"key": "selectedChannel", "type": "ChannelInfo"}, + } + + def __init__(self, *, selected_channel=None, **kwargs) -> None: + super(TeamsChannelDataSettings, self).__init__(**kwargs) + self.selected_channel = selected_channel + + class TeamsChannelData(Model): """Channel data specific to messages received in Microsoft Teams. @@ -1974,6 +2005,8 @@ class TeamsChannelData(Model): :type tenant: ~botframework.connector.teams.models.TenantInfo :param meeting: Information about the meeting in which the message was sent :type meeting: ~botframework.connector.teams.models.TeamsMeetingInfo + :param meeting: Information about the about the settings in which the message was sent + :type meeting: ~botframework.connector.teams.models.TeamsChannelDataSettings """ _attribute_map = { @@ -1983,6 +2016,7 @@ class TeamsChannelData(Model): "notification": {"key": "notification", "type": "NotificationInfo"}, "tenant": {"key": "tenant", "type": "TenantInfo"}, "meeting": {"key": "meeting", "type": "TeamsMeetingInfo"}, + "settings": {"key": "settings", "type": "TeamsChannelDataSettings"}, } def __init__( @@ -1994,6 +2028,7 @@ def __init__( notification=None, tenant=None, meeting=None, + settings: TeamsChannelDataSettings = None, **kwargs ) -> None: super(TeamsChannelData, self).__init__(**kwargs) @@ -2004,6 +2039,7 @@ def __init__( self.notification = notification self.tenant = tenant self.meeting = meeting + self.settings = settings class TenantInfo(Model): From 0f2cb2adb97c25670e728a714bbf667fe9aee369 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:03:44 +0530 Subject: [PATCH 21/40] Port: Send targeted meeting notification in Teams meeting (#2172) * Send targeted meeting notification in Teams meeting * test cases --------- Co-authored-by: tracyboehrer --- .../botbuilder/core/teams/teams_info.py | 27 ++ .../tests/teams/test_teams_info.py | 129 ++++++++ .../botbuilder/schema/teams/__init__.py | 4 + .../botbuilder/schema/teams/_models_py3.py | 289 ++++++++++++++++++ .../teams/operations/teams_operations.py | 73 +++++ 5 files changed, 522 insertions(+) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index f70f6cccc..4afa50c05 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -25,6 +25,8 @@ TeamsChannelAccount, TeamsPagedMembersResult, TeamsMeetingParticipant, + MeetingNotificationBase, + MeetingNotificationResponse, ) @@ -100,6 +102,31 @@ async def _legacy_send_message_to_teams_channel( ) return (result[0], result[1]) + @staticmethod + async def send_meeting_notification( + turn_context: TurnContext, + notification: MeetingNotificationBase, + meeting_id: str = None, + ) -> MeetingNotificationResponse: + meeting_id = ( + meeting_id + if meeting_id + else teams_get_meeting_info(turn_context.activity).id + ) + if meeting_id is None: + raise TypeError( + "TeamsInfo._send_meeting_notification: method requires a meeting_id or " + "TurnContext that contains a meeting id" + ) + + if notification is None: + raise TypeError("notification is required.") + + connector_client = await TeamsInfo.get_teams_connector_client(turn_context) + return await connector_client.teams.send_meeting_notification( + meeting_id, notification + ) + @staticmethod async def _create_conversation_callback( new_turn_context, diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index dea57030c..00f4ad8a4 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -1,7 +1,19 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import aiounittest +from botbuilder.schema.teams._models_py3 import ( + ContentType, + MeetingNotificationChannelData, + MeetingStageSurface, + MeetingTabIconSurface, + OnBehalfOf, + TargetedMeetingNotification, + TargetedMeetingNotificationValue, + TaskModuleContinueResponse, + TaskModuleTaskInfo, +) from botframework.connector import Channels from botbuilder.core import TurnContext, MessageFactory @@ -234,6 +246,53 @@ async def test_get_meeting_info(self): handler = TeamsActivityHandler() await handler.on_turn(turn_context) + async def test_send_meeting_notificationt(self): + test_cases = [ + ("202", "accepted"), + ( + "207", + "if the notifications are sent only to parital number of recipients\ + because the validation on some recipients' ids failed or some\ + recipients were not found in the roster. In this case, \ + SMBA will return the user MRIs of those failed recipients\ + in a format that was given to a bot (ex: if a bot sent \ + encrypted user MRIs, return encrypted one).", + ), + ( + "400", + "when Meeting Notification request payload validation fails. For instance,\ + Recipients: # of recipients is greater than what the API allows ||\ + all of recipients' user ids were invalid, Surface: Surface list\ + is empty or null, Surface type is invalid, Duplicative \ + surface type exists in one payload", + ), + ( + "403", + "if the bot is not allowed to send the notification. In this case,\ + the payload should contain more detail error message. \ + There can be many reasons: bot disabled by tenant admin,\ + blocked during live site mitigation, the bot does not\ + have a correct RSC permission for a specific surface type, etc", + ), + ] + for status_code, expected_message in test_cases: + adapter = SimpleAdapterWithCreateConversation() + + activity = Activity( + type="targetedMeetingNotification", + text="Test-send_meeting_notificationt", + channel_id=Channels.ms_teams, + from_property=ChannelAccount( + aad_object_id="participantId-1", name=status_code + ), + service_url="https://test.coffee", + conversation=ConversationAccount(id="conversation-id"), + ) + + turn_context = TurnContext(adapter, activity) + handler = TeamsActivityHandler() + await handler.on_turn(turn_context) + class TestTeamsActivityHandler(TeamsActivityHandler): async def on_turn(self, turn_context: TurnContext): @@ -241,6 +300,8 @@ async def on_turn(self, turn_context: TurnContext): if turn_context.activity.text == "test_send_message_to_teams_channel": await self.call_send_message_to_teams(turn_context) + elif turn_context.activity.text == "test_send_meeting_notification": + await self.call_send_meeting_notification(turn_context) async def call_send_message_to_teams(self, turn_context: TurnContext): msg = MessageFactory.text("call_send_message_to_teams") @@ -251,3 +312,71 @@ async def call_send_message_to_teams(self, turn_context: TurnContext): assert reference[0].activity_id == "new_conversation_id" assert reference[1] == "reference123" + + async def call_send_meeting_notification(self, turn_context: TurnContext): + from_property = turn_context.activity.from_property + try: + # Send the meeting notification asynchronously + failed_participants = await TeamsInfo.send_meeting_notification( + turn_context, + self.get_targeted_meeting_notification(from_property), + "meeting-id", + ) + + # Handle based on the 'from_property.name' + if from_property.name == "207": + self.assertEqual( + "failingid", + failed_participants.recipients_failure_info[0].recipient_mri, + ) + elif from_property.name == "202": + assert failed_participants is None + else: + raise TypeError( + f"Expected HttpOperationException with response status code {from_property.name}." + ) + + except ValueError as ex: + # Assert that the response status code matches the from_property.name + assert from_property.name == str(int(ex.response.status_code)) + + # Deserialize the error response content to an ErrorResponse object + error_response = json.loads(ex.response.content) + + # Handle based on error codes + if from_property.name == "400": + assert error_response["error"]["code"] == "BadSyntax" + elif from_property.name == "403": + assert error_response["error"]["code"] == "BotNotInConversationRoster" + else: + raise TypeError( + f"Expected HttpOperationException with response status code {from_property.name}." + ) + + def get_targeted_meeting_notification(self, from_account: ChannelAccount): + recipients = [from_account.id] + + if from_account.name == "207": + recipients.append("failingid") + + meeting_stage_surface = MeetingStageSurface( + content=TaskModuleContinueResponse( + value=TaskModuleTaskInfo(title="title here", height=3, width=2) + ), + content_type=ContentType.Task, + ) + + meeting_tab_icon_surface = MeetingTabIconSurface( + tab_entity_id="test tab entity id" + ) + + value = TargetedMeetingNotificationValue( + recipients=recipients, + surfaces=[meeting_stage_surface, meeting_tab_icon_surface], + ) + + obo = OnBehalfOf(display_name=from_account.name, mri=from_account.id) + + channel_data = MeetingNotificationChannelData(on_behalf_of_list=[obo]) + + return TargetedMeetingNotification(value=value, channel_data=channel_data) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 76e34f5f8..7c837b243 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -86,6 +86,8 @@ from ._models_py3 import ConfigAuthResponse from ._models_py3 import ConfigResponse from ._models_py3 import ConfigTaskResponse +from ._models_py3 import MeetingNotificationBase +from ._models_py3 import MeetingNotificationResponse __all__ = [ "AppBasedLinkQuery", @@ -173,4 +175,6 @@ "ConfigAuthResponse", "ConfigResponse", "ConfigTaskResponse", + "MeetingNotificationBase", + "MeetingNotificationResponse", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 8b5cebd29..72f48d5b6 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from enum import Enum from typing import List from msrest.serialization import Model from botbuilder.schema import ( @@ -2735,3 +2736,291 @@ def __init__(self, *, config=None, **kwargs) -> None: super(ConfigAuthResponse, self).__init__( config=config or BotConfigAuth(), **kwargs ) + + +class OnBehalfOf(Model): + """Specifies attribution for notifications. + + :param item_id: The identification of the item. Default is 0. + :type item_id: int + :param mention_type: The mention type. Default is "person". + :type mention_type: str + :param mri: Message resource identifier (MRI) of the person on whose behalf the message is sent. + :type mri: str + :param display_name: Name of the person. Used as fallback in case name resolution is unavailable. + :type display_name: str + """ + + _attribute_map = { + "item_id": {"key": "itemid", "type": "int"}, + "mention_type": {"key": "mentionType", "type": "str"}, + "mri": {"key": "mri", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__( + self, + *, + item_id: int = 0, + mention_type: str = "person", + mri: str = None, + display_name: str = None, + **kwargs + ) -> None: + super(OnBehalfOf, self).__init__(**kwargs) + self.item_id = item_id + self.mention_type = mention_type + self.mri = mri + self.display_name = display_name + + +class SurfaceType(Enum): + """ + Defines Teams Surface type for use with a Surface object. + + :var Unknown: TeamsSurfaceType is Unknown. + :vartype Unknown: int + :var MeetingStage: TeamsSurfaceType is MeetingStage.. + :vartype MeetingStage: int + :var MeetingTabIcon: TeamsSurfaceType is MeetingTabIcon. + :vartype MeetingTabIcon: int + """ + + Unknown = 0 + + MeetingStage = 1 + + MeetingTabIcon = 2 + + +class ContentType(Enum): + """ + Defines content type. Depending on contentType, content field will have a different structure. + + :var Unknown: Content type is Unknown. + :vartype Unknown: int + :var Task: TContent type is Task. + :vartype Task: int + """ + + Unknown = 0 + + Task = 1 + + +class MeetingNotificationBase(Model): + """Specifies Bot meeting notification base including channel data and type. + + :param type: Type of Bot meeting notification. + :type type: str + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + } + + def __init__(self, *, type: str = None, **kwargs) -> None: + super(MeetingNotificationBase, self).__init__(**kwargs) + self.type = type + + +class MeetingNotification(MeetingNotificationBase): + """Specifies Bot meeting notification including meeting notification value. + + :param value: Teams Bot meeting notification value. + :type value: TargetedMeetingNotificationValue + """ + + _attribute_map = { + "value": {"key": "value", "type": "TargetedMeetingNotificationValue"}, + } + + def __init__( + self, *, value: "TargetedMeetingNotificationValue" = None, **kwargs + ) -> None: + super(MeetingNotification, self).__init__(**kwargs) + self.value = value + + +class MeetingNotificationChannelData(Model): + """Specify Teams Bot meeting notification channel data. + + :param on_behalf_of_list: The Teams Bot meeting notification's OnBehalfOf list. + :type on_behalf_of_list: list[~botframework.connector.teams.models.OnBehalfOf] + """ + + _attribute_map = { + "on_behalf_of_list": {"key": "OnBehalfOf", "type": "[OnBehalfOf]"} + } + + def __init__(self, *, on_behalf_of_list: List["OnBehalfOf"] = None, **kwargs): + super(MeetingNotificationChannelData, self).__init__(**kwargs) + self.on_behalf_of_list = on_behalf_of_list + + +class MeetingNotificationRecipientFailureInfo(Model): + """Information regarding failure to notify a recipient of a meeting notification. + + :param recipient_mri: The MRI for a recipient meeting notification failure. + :type recipient_mri: str + :param error_code: The error code for a meeting notification. + :type error_code: str + :param failure_reason: The reason why a participant meeting notification failed. + :type failure_reason: str + """ + + _attribute_map = { + "recipient_mri": {"key": "recipientMri", "type": "str"}, + "error_code": {"key": "errorcode", "type": "str"}, + "failure_reason": {"key": "failureReason", "type": "str"}, + } + + def __init__( + self, + *, + recipient_mri: str = None, + error_code: str = None, + failure_reason: str = None, + **kwargs + ): + super(MeetingNotificationRecipientFailureInfo, self).__init__(**kwargs) + self.recipient_mri = recipient_mri + self.error_code = error_code + self.failure_reason = failure_reason + + +class MeetingNotificationResponse(Model): + """Specifies Bot meeting notification response. + + Contains list of MeetingNotificationRecipientFailureInfo. + + :param recipients_failure_info: The list of MeetingNotificationRecipientFailureInfo. + :type recipients_failure_info: list[~botframework.connector.teams.models.MeetingNotificationRecipientFailureInfo] + """ + + _attribute_map = { + "recipients_failure_info": { + "key": "recipientsFailureInfo", + "type": "[MeetingNotificationRecipientFailureInfo]", + } + } + + def __init__( + self, + *, + recipients_failure_info: List["MeetingNotificationRecipientFailureInfo"] = None, + **kwargs + ): + super(MeetingNotificationResponse, self).__init__(**kwargs) + self.recipients_failure_info = recipients_failure_info + + +class Surface(Model): + """Specifies where the notification will be rendered in the meeting UX. + + :param type: The value indicating where the notification will be rendered in the meeting UX. + :type type: ~botframework.connector.teams.models.SurfaceType + """ + + _attribute_map = { + "type": {"key": "surface", "type": "SurfaceType"}, + } + + def __init__(self, *, type: SurfaceType = None, **kwargs): + super(Surface, self).__init__(**kwargs) + self.type = type + + +class MeetingStageSurface(Surface): + """Specifies meeting stage surface. + + :param content_type: The content type of this MeetingStageSurface. + :type content_type: ~botframework.connector.teams.models.ContentType + :param content: The content of this MeetingStageSurface. + :type content: object + """ + + _attribute_map = { + "content_type": {"key": "contentType", "type": "ContentType"}, + "content": {"key": "content", "type": "object"}, + } + + def __init__( + self, + *, + content_type: ContentType = ContentType.Task, + content: object = None, + **kwargs + ): + super(MeetingStageSurface, self).__init__(SurfaceType.MeetingStage, **kwargs) + self.content_type = content_type + self.content = content + + +class MeetingTabIconSurface(Surface): + """ + Specifies meeting tab icon surface. + + :param tab_entity_id: The tab entity Id of this MeetingTabIconSurface. + :type tab_entity_id: str + """ + + _attribute_map = { + "tab_entity_id": {"key": "tabEntityId", "type": "str"}, + } + + def __init__(self, *, tab_entity_id: str = None, **kwargs): + super(MeetingTabIconSurface, self).__init__( + SurfaceType.MeetingTabIcon, **kwargs + ) + self.tab_entity_id = tab_entity_id + + +class TargetedMeetingNotificationValue(Model): + """Specifies the targeted meeting notification value, including recipients and surfaces. + + :param recipients: The collection of recipients of the targeted meeting notification. + :type recipients: list[str] + :param surfaces: The collection of surfaces on which to show the notification. + :type surfaces: list[~botframework.connector.teams.models.Surface] + """ + + _attribute_map = { + "recipients": {"key": "recipients", "type": "[str]"}, + "surfaces": {"key": "surfaces", "type": "[Surface]"}, + } + + def __init__( + self, *, recipients: List[str] = None, surfaces: List[Surface] = None, **kwargs + ): + super(TargetedMeetingNotificationValue, self).__init__(**kwargs) + self.recipients = recipients + self.surfaces = surfaces + + +class TargetedMeetingNotification(MeetingNotification): + """Specifies Teams targeted meeting notification. + + :param value: The value of the TargetedMeetingNotification. + :type value: ~botframework.connector.teams.models.TargetedMeetingNotificationValue + :param channel_data: Teams Bot meeting notification channel data. + :type channel_data: ~botframework.connector.teams.models.MeetingNotificationChannelData + """ + + _attribute_map = { + "value": {"key": "value", "type": "TargetedMeetingNotificationValue"}, + "channel_data": { + "key": "channelData", + "type": "MeetingNotificationChannelData", + }, + } + + def __init__( + self, + *, + value: "TargetedMeetingNotificationValue" = None, + channel_data: "MeetingNotificationChannelData" = None, + **kwargs + ): + super(TargetedMeetingNotification, self).__init__(value=value, **kwargs) + self.channel_data = channel_data diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py index ff1bdb18c..6e453ae23 100644 --- a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -266,3 +266,76 @@ def fetch_meeting( return deserialized fetch_participant.metadata = {"url": "/v1/meetings/{meetingId}"} + + def send_meeting_notification( + self, + meeting_id: str, + notification: models.MeetingNotificationBase, + custom_headers=None, + raw=False, + **operation_config + ): + """Send a teams meeting notification. + + :param meeting_id: Meeting Id, encoded as a BASE64 string. + :type meeting_id: str + :param notification: The notification to send to Teams + :type notification: ~botframework.connector.teams.models.MeetingNotificationBase + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: MeetingNotificationResponse or ClientRawResponse if raw=true + :rtype: ~botframework.connector.teams.models.MeetingNotificationResponse or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + + # Construct URL + url = self.send_meeting_notification.metadata["url"] + path_format_arguments = { + "meetingId": self._serialize.url("meeting_id", meeting_id, "str"), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters["Accept"] = "application/json" + header_parameters["Content-Type"] = "application/json; charset=utf-8" + if custom_headers: + header_parameters.update(custom_headers) + + # Construct body + body_content = self._serialize.body(notification, "notification") + + # Construct and send request + request = self._client.post( + url, query_parameters, header_parameters, body_content + ) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201, 202]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize("MeetingNotificationResponse", response) + if response.status_code == 201: + deserialized = self._deserialize("MeetingNotificationResponse", response) + if response.status_code == 202: + deserialized = self._deserialize("MeetingNotificationResponse", response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + send_meeting_notification.metadata = { + "url": "/v1/meetings/{meetingId}/notification" + } From 1617885d2e710af888cc37b97ae61c53306e4355 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:22:42 +0530 Subject: [PATCH 22/40] Port: TeamsChannelData need OnBehalfOf [#6609] (#2175) * Adds the OnBehalfOf property to send messages via bots on behalf of another user in Teams. * black issue fix * Using List from typing --- .../core/teams/teams_activity_extensions.py | 13 +++++ .../tests/teams/test_teams_channel_data.py | 56 +++++++++++++++++++ .../tests/teams/test_teams_extension.py | 27 ++++++++- .../botbuilder/schema/teams/__init__.py | 2 + .../botbuilder/schema/teams/_models_py3.py | 9 ++- 5 files changed, 104 insertions(+), 3 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py index 04e11583f..e604cb088 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from typing import List from botbuilder.schema import Activity from botbuilder.schema.teams import ( NotificationInfo, TeamsChannelData, TeamInfo, TeamsMeetingInfo, + OnBehalfOf, ) @@ -84,3 +86,14 @@ def teams_get_meeting_info(activity: Activity) -> TeamsMeetingInfo: return channel_data.meeting return None + + +def teams_get_team_on_behalf_of(activity: Activity) -> List[OnBehalfOf]: + if not activity: + return None + + if activity.channel_data: + channel_data = TeamsChannelData().deserialize(activity.channel_data) + return channel_data.on_behalf_of + + return None diff --git a/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py b/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py index e468526bc..324749ce5 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py @@ -1,11 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from uuid import uuid4 import aiounittest from botbuilder.schema import Activity from botbuilder.schema.teams import TeamsChannelData from botbuilder.core.teams import teams_get_team_info +from botbuilder.schema.teams._models_py3 import ( + ChannelInfo, + NotificationInfo, + OnBehalfOf, + TeamInfo, + TeamsChannelDataSettings, + TeamsMeetingInfo, + TenantInfo, +) class TestTeamsChannelData(aiounittest.AsyncTestCase): @@ -28,3 +38,49 @@ def test_teams_get_team_info(self): # Assert assert team_info.aad_group_id == "teamGroup123" + + def test_teams_channel_data_inits(self): + # Arrange + channel = ChannelInfo(id="general", name="General") + event_type = "eventType" + team = TeamInfo(id="supportEngineers", name="Support Engineers") + notification = NotificationInfo(alert=True) + tenant = TenantInfo(id="uniqueTenantId") + meeting = TeamsMeetingInfo(id="BFSE Stand Up") + settings = TeamsChannelDataSettings(selected_channel=channel) + on_behalf_of = [ + OnBehalfOf( + display_name="onBehalfOfTest", + item_id=0, + mention_type="person", + mri=str(uuid4()), + ) + ] + + # Act + channel_data = TeamsChannelData( + channel=channel, + event_type=event_type, + team=team, + notification=notification, + tenant=tenant, + meeting=meeting, + settings=settings, + on_behalf_of=on_behalf_of, + ) + + # Assert + self.assertIsNotNone(channel_data) + self.assertIsInstance(channel_data, TeamsChannelData) + self.assertEqual(channel, channel_data.channel) + self.assertEqual(event_type, channel_data.event_type) + self.assertEqual(team, channel_data.team) + self.assertEqual(notification, channel_data.notification) + self.assertEqual(tenant, channel_data.tenant) + self.assertEqual(meeting, channel_data.meeting) + self.assertEqual(settings, channel_data.settings) + self.assertEqual(on_behalf_of, channel_data.on_behalf_of) + self.assertEqual(on_behalf_of[0].display_name, "onBehalfOfTest") + self.assertEqual(on_behalf_of[0].mention_type, "person") + self.assertIsNotNone(on_behalf_of[0].mri) + self.assertEqual(on_behalf_of[0].item_id, 0) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_extension.py b/libraries/botbuilder-core/tests/teams/test_teams_extension.py index ac55cb9a2..1d2520bc4 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_extension.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_extension.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from uuid import uuid4 import aiounittest from botbuilder.schema import Activity @@ -11,7 +12,11 @@ teams_get_team_info, teams_notify_user, ) -from botbuilder.core.teams.teams_activity_extensions import teams_get_meeting_info +from botbuilder.core.teams.teams_activity_extensions import ( + teams_get_meeting_info, + teams_get_team_on_behalf_of, +) +from botbuilder.schema.teams._models_py3 import OnBehalfOf class TestTeamsActivityHandler(aiounittest.AsyncTestCase): @@ -190,3 +195,23 @@ def test_teams_meeting_info(self): # Assert assert meeting_id == "meeting123" + + def test_teams_channel_data_existing_on_behalf_of(self): + # Arrange + on_behalf_of_list = [ + OnBehalfOf( + display_name="onBehalfOfTest", + item_id=0, + mention_type="person", + mri=str(uuid4()), + ) + ] + + activity = Activity(channel_data={"onBehalfOf": on_behalf_of_list}) + + # Act + on_behalf_of_list = teams_get_team_on_behalf_of(activity) + + # Assert + self.assertEqual(1, len(on_behalf_of_list)) + self.assertEqual("onBehalfOfTest", on_behalf_of_list[0].display_name) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 7c837b243..be9aa11ce 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -88,6 +88,7 @@ from ._models_py3 import ConfigTaskResponse from ._models_py3 import MeetingNotificationBase from ._models_py3 import MeetingNotificationResponse +from ._models_py3 import OnBehalfOf __all__ = [ "AppBasedLinkQuery", @@ -177,4 +178,5 @@ "ConfigTaskResponse", "MeetingNotificationBase", "MeetingNotificationResponse", + "OnBehalfOf", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 72f48d5b6..0b6e0e899 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2006,8 +2006,10 @@ class TeamsChannelData(Model): :type tenant: ~botframework.connector.teams.models.TenantInfo :param meeting: Information about the meeting in which the message was sent :type meeting: ~botframework.connector.teams.models.TeamsMeetingInfo - :param meeting: Information about the about the settings in which the message was sent - :type meeting: ~botframework.connector.teams.models.TeamsChannelDataSettings + :param settings: Information about the about the settings in which the message was sent + :type settings: ~botframework.connector.teams.models.TeamsChannelDataSettings + :param on_behalf_of: The OnBehalfOf list for user attribution + :type on_behalf_of: list[~botframework.connector.teams.models.OnBehalfOf] """ _attribute_map = { @@ -2018,6 +2020,7 @@ class TeamsChannelData(Model): "tenant": {"key": "tenant", "type": "TenantInfo"}, "meeting": {"key": "meeting", "type": "TeamsMeetingInfo"}, "settings": {"key": "settings", "type": "TeamsChannelDataSettings"}, + "on_behalf_of": {"key": "onBehalfOf", "type": "[OnBehalfOf]"}, } def __init__( @@ -2030,6 +2033,7 @@ def __init__( tenant=None, meeting=None, settings: TeamsChannelDataSettings = None, + on_behalf_of: List["OnBehalfOf"] = None, **kwargs ) -> None: super(TeamsChannelData, self).__init__(**kwargs) @@ -2041,6 +2045,7 @@ def __init__( self.tenant = tenant self.meeting = meeting self.settings = settings + self.on_behalf_of = on_behalf_of if on_behalf_of is not None else [] class TenantInfo(Model): From cf1d0453af207a239333c4f0da4ca484e1b78244 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:47:00 +0530 Subject: [PATCH 23/40] port: SkillDialog.InterceptOAuthCardsAsync doesn't support CloudAdapter (#6848) (#2179) * Updated the InterceptOAuthCardsAsync method in SkillDialog to support CloudAdapter in combination with expect replies delivery mode * minor fix --- .../botbuilder/dialogs/skills/skill_dialog.py | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index e5be50c24..d848c13c7 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -26,6 +26,8 @@ from .begin_skill_dialog_options import BeginSkillDialogOptions from .skill_dialog_options import SkillDialogOptions +from botbuilder.dialogs.prompts import OAuthPromptSettings +from .._user_token_access import _UserTokenAccess class SkillDialog(Dialog): @@ -275,50 +277,55 @@ async def _intercept_oauth_cards( """ Tells is if we should intercept the OAuthCard message. """ - if not connection_name or not isinstance( - context.adapter, ExtendedUserTokenProvider - ): + if not connection_name or connection_name.isspace(): # The adapter may choose not to support token exchange, in which case we fallback to # showing an oauth card to the user. return False oauth_card_attachment = next( - attachment - for attachment in activity.attachments - if attachment.content_type == ContentTypes.oauth_card + ( + attachment + for attachment in activity.attachments + if attachment.content_type == ContentTypes.oauth_card + ), + None, ) - if oauth_card_attachment: - oauth_card = oauth_card_attachment.content - if ( - oauth_card - and oauth_card.token_exchange_resource - and oauth_card.token_exchange_resource.uri - ): - try: - result = await context.adapter.exchange_token( - turn_context=context, - connection_name=connection_name, - user_id=context.activity.from_property.id, - exchange_request=TokenExchangeRequest( - uri=oauth_card.token_exchange_resource.uri - ), - ) + if oauth_card_attachment is None: + return False - if result and result.token: - # If token above is null, then SSO has failed and hence we return false. - # If not, send an invoke to the skill with the token. - return await self._send_token_exchange_invoke_to_skill( - activity, - oauth_card.token_exchange_resource.id, - oauth_card.connection_name, - result.token, - ) - except: - # Failures in token exchange are not fatal. They simply mean that the user needs - # to be shown the OAuth card. - return False - - return False + oauth_card = oauth_card_attachment.content + if ( + not oauth_card + or not oauth_card.token_exchange_resource + or not oauth_card.token_exchange_resource.uri + ): + return False + + try: + settings = OAuthPromptSettings( + connection_name=connection_name, title="Sign In" + ) + result = await _UserTokenAccess.exchange_token( + context, + settings, + TokenExchangeRequest(uri=oauth_card.token_exchange_resource.uri), + ) + + if not result or not result.token: + # If token above is null, then SSO has failed and hence we return false. + return False + + # If not, send an invoke to the skill with the token. + return await self._send_token_exchange_invoke_to_skill( + activity, + oauth_card.token_exchange_resource.id, + oauth_card.connection_name, + result.token, + ) + except: + # Failures in token exchange are not fatal. They simply mean that the user needs + # to be shown the OAuth card. + return False async def _send_token_exchange_invoke_to_skill( self, From 3e76e543477500b7e56f53ba20258b730ec4f924 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:26:51 +0530 Subject: [PATCH 24/40] Add missing paran in exchange_token() call (#2182) --- .../botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py index 0aa005789..ba25a0baa 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py @@ -104,6 +104,7 @@ async def exchange_token( channel_id = turn_context.activity.channel_id return await user_token_client.exchange_token( user_id, + settings.connection_name, channel_id, token_exchange_request, ) From a74532b9a92234d9b2a4bcf54e133d0d024331d9 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:20:11 +0530 Subject: [PATCH 25/40] Support Teams message edit, message soft delete, message undelete activities (#2183) Co-authored-by: tracyboehrer --- .../botbuilder/core/activity_handler.py | 32 ++++ .../core/teams/teams_activity_handler.py | 70 +++++++++ .../teams/test_teams_activity_handler.py | 139 ++++++++++++++++++ .../tests/test_activity_handler.py | 34 +++++ 4 files changed, 275 insertions(+) diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py index be847739e..51aefb245 100644 --- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py @@ -68,6 +68,10 @@ async def on_turn( if turn_context.activity.type == ActivityTypes.message: await self.on_message_activity(turn_context) + elif turn_context.activity.type == ActivityTypes.message_update: + await self.on_message_update_activity(turn_context) + elif turn_context.activity.type == ActivityTypes.message_delete: + await self.on_message_delete_activity(turn_context) elif turn_context.activity.type == ActivityTypes.conversation_update: await self.on_conversation_update_activity(turn_context) elif turn_context.activity.type == ActivityTypes.message_reaction: @@ -107,6 +111,34 @@ async def on_message_activity( # pylint: disable=unused-argument """ return + async def on_message_update_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext + ): + """ + Override this method in a derived class to provide logic specific to activities, + such as the conversational logic. + + :param turn_context: The context object for this turn + :type turn_context: :class:`botbuilder.core.TurnContext` + + :returns: A task that represents the work queued to execute + """ + return + + async def on_message_delete_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext + ): + """ + Override this method in a derived class to provide logic specific to activities, + such as the conversational logic. + + :param turn_context: The context object for this turn + :type turn_context: :class:`botbuilder.core.TurnContext` + + :returns: A task that represents the work queued to execute + """ + return + async def on_conversation_update_activity(self, turn_context: TurnContext): """ Invoked when a conversation update activity is received from the channel when the base behavior of diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 7832887dc..4b7558e6c 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -1042,3 +1042,73 @@ async def on_teams_meeting_participants_leave_event( :returns: A task that represents the work queued to execute. """ return + + async def on_message_update_activity(self, turn_context: TurnContext): + """ + Invoked when a message update activity is received, such as a message edit or undelete. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + if turn_context.activity.channel_id == Channels.ms_teams: + channel_data = TeamsChannelData().deserialize( + turn_context.activity.channel_data + ) + + if channel_data: + if channel_data.event_type == "editMessage": + return await self.on_teams_message_edit(turn_context) + if channel_data.event_type == "undeleteMessage": + return await self.on_teams_message_undelete(turn_context) + + return await super().on_message_update_activity(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext): + """ + Invoked when a message delete activity is received, such as a soft delete message. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + if turn_context.activity.channel_id == Channels.ms_teams: + channel_data = TeamsChannelData().deserialize( + turn_context.activity.channel_data + ) + + if channel_data: + if channel_data.event_type == "softDeleteMessage": + return await self.on_teams_message_soft_delete(turn_context) + + return await super().on_message_delete_activity(turn_context) + + async def on_teams_message_edit(self, turn_context: TurnContext): + """ + Invoked when a Teams edit message event activity is received. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + + async def on_teams_message_undelete(self, turn_context: TurnContext): + """ + Invoked when a Teams undo soft delete message event activity is received. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + + async def on_teams_message_soft_delete(self, turn_context: TurnContext): + """ + Invoked when a Teams soft delete message event activity is received. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 2ea62717b..bef959d14 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -34,6 +34,7 @@ TabContext, MeetingParticipantsEventDetails, ReadReceiptInfo, + TeamsChannelData, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -351,6 +352,26 @@ async def on_teams_meeting_end_event( turn_context.activity.value, turn_context ) + async def on_message_update_activity(self, turn_context: TurnContext): + self.record.append("on_message_update_activity") + return await super().on_message_update_activity(turn_context) + + async def on_teams_message_edit(self, turn_context: TurnContext): + self.record.append("on_teams_message_edit") + return await super().on_teams_message_edit(turn_context) + + async def on_teams_message_undelete(self, turn_context: TurnContext): + self.record.append("on_teams_message_undelete") + return await super().on_teams_message_undelete(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext): + self.record.append("on_message_delete_activity") + return await super().on_message_delete_activity(turn_context) + + async def on_teams_message_soft_delete(self, turn_context: TurnContext): + self.record.append("on_teams_message_soft_delete") + return await super().on_teams_message_soft_delete(turn_context) + async def on_teams_meeting_participants_join_event( self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext ): @@ -1254,6 +1275,124 @@ async def test_on_teams_meeting_end_event(self): assert bot.record[0] == "on_event_activity" assert bot.record[1] == "on_teams_meeting_end_event" + async def test_message_update_activity_teams_message_edit(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_data=TeamsChannelData(event_type="editMessage"), + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(2, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + self.assertEqual("on_teams_message_edit", bot.record[1]) + + async def test_message_update_activity_teams_message_undelete(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_data=TeamsChannelData(event_type="undeleteMessage"), + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(2, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + self.assertEqual("on_teams_message_undelete", bot.record[1]) + + async def test_message_update_activity_teams_message_undelete_no_msteams(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_data=TeamsChannelData(event_type="undeleteMessage"), + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + + async def test_message_update_activity_teams_no_channel_data(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + + async def test_message_delete_activity_teams_message_soft_delete(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_delete, + channel_data=TeamsChannelData(event_type="softDeleteMessage"), + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(2, len(bot.record)) + self.assertEqual("on_message_delete_activity", bot.record[0]) + self.assertEqual("on_teams_message_soft_delete", bot.record[1]) + + async def test_message_delete_activity_teams_message_soft_delete_no_msteams(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_delete, + channel_data=TeamsChannelData(event_type="softDeleteMessage"), + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_delete_activity", bot.record[0]) + + async def test_message_delete_activity_teams_no_channel_data(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_delete, + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_delete_activity", bot.record[0]) + async def test_on_teams_meeting_participants_join_event(self): # arrange activity = Activity( diff --git a/libraries/botbuilder-core/tests/test_activity_handler.py b/libraries/botbuilder-core/tests/test_activity_handler.py index fedc03e96..1ee0c5414 100644 --- a/libraries/botbuilder-core/tests/test_activity_handler.py +++ b/libraries/botbuilder-core/tests/test_activity_handler.py @@ -26,6 +26,14 @@ async def on_message_activity(self, turn_context: TurnContext): self.record.append("on_message_activity") return await super().on_message_activity(turn_context) + async def on_message_update_activity(self, turn_context: TurnContext): + self.record.append("on_message_update_activity") + return await super().on_message_update_activity(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext): + self.record.append("on_message_delete_activity") + return await super().on_message_delete_activity(turn_context) + async def on_members_added_activity( self, members_added: ChannelAccount, turn_context: TurnContext ): @@ -208,6 +216,32 @@ async def test_invoke_should_not_match(self): assert bot.record[0] == "on_invoke_activity" assert adapter.activity.value.status == int(HTTPStatus.NOT_IMPLEMENTED) + async def test_on_message_update_activity(self): + activity = Activity(type=ActivityTypes.message_update) + + adapter = TestInvokeAdapter() + turn_context = TurnContext(adapter, activity) + + # Act + bot = TestingActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 1 + assert bot.record[0] == "on_message_update_activity" + + async def test_on_message_delete_activity(self): + activity = Activity(type=ActivityTypes.message_delete) + + adapter = TestInvokeAdapter() + turn_context = TurnContext(adapter, activity) + + # Act + bot = TestingActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 1 + assert bot.record[0] == "on_message_delete_activity" + async def test_on_end_of_conversation_activity(self): activity = Activity(type=ActivityTypes.end_of_conversation) From a9298c877b039c74810368a9a53cc92854d1bde3 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:07:55 +0530 Subject: [PATCH 26/40] Add new handler for anonymous query link invoke (#2184) Co-authored-by: tracyboehrer --- .../core/teams/teams_activity_handler.py | 23 ++++++++++++++++ .../teams/test_teams_activity_handler.py | 27 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 4b7558e6c..9344e343e 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -90,6 +90,16 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ) ) + if turn_context.activity.name == "composeExtension/anonymousQueryLink": + return self._create_invoke_response( + await self.on_teams_anonymous_app_based_link_query( + turn_context, + deserializer_helper( + AppBasedLinkQuery, turn_context.activity.value + ), + ) + ) + if turn_context.activity.name == "composeExtension/query": return self._create_invoke_response( await self.on_teams_messaging_extension_query( @@ -331,6 +341,19 @@ async def on_teams_app_based_link_query( # pylint: disable=unused-argument """ raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_anonymous_app_based_link_query( # pylint: disable=unused-argument + self, turn_context: TurnContext, query: AppBasedLinkQuery + ) -> MessagingExtensionResponse: + """ + Invoked when an anonymous app based link query activity is received from the connector. + + :param turn_context: A context object for this turn. + :param query: The invoke request body type for app-based link query. + + :returns: The Messaging Extension Response for the query. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_messaging_extension_query( # pylint: disable=unused-argument self, turn_context: TurnContext, query: MessagingExtensionQuery ) -> MessagingExtensionResponse: diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index bef959d14..22550764e 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -220,6 +220,14 @@ async def on_teams_messaging_extension_query( self.record.append("on_teams_messaging_extension_query") return await super().on_teams_messaging_extension_query(turn_context, query) + async def on_teams_anonymous_app_based_link_query( + self, turn_context: TurnContext, query: AppBasedLinkQuery + ): + self.record.append("on_teams_anonymous_app_based_link_query") + return await super().on_teams_anonymous_app_based_link_query( + turn_context, query + ) + async def on_teams_messaging_extension_submit_action_dispatch( self, turn_context: TurnContext, action: MessagingExtensionAction ): @@ -837,6 +845,25 @@ async def test_on_app_based_link_query(self): assert bot.record[0] == "on_invoke_activity" assert bot.record[1] == "on_teams_messaging_extension_query" + async def test_compose_extension_anonymous_query_link(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/anonymousQueryLink", + value={"url": "http://www.test.com"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_anonymous_app_based_link_query" + async def test_on_teams_messaging_extension_bot_message_preview_edit_activity(self): # Arrange From 62d736622a2814c1b090d39a73fb738ea59afadd Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:46:38 +0530 Subject: [PATCH 27/40] Set TeamsNotifyUser alert to opposite of alert_in_meeting (#2185) Co-authored-by: tracyboehrer --- .../core/teams/teams_activity_extensions.py | 2 +- .../tests/teams/test_teams_extension.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py index e604cb088..253b31f5c 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py @@ -71,7 +71,7 @@ def teams_notify_user( activity.channel_data = {} channel_data = TeamsChannelData().deserialize(activity.channel_data) - channel_data.notification = NotificationInfo(alert=True) + channel_data.notification = NotificationInfo(alert=not alert_in_meeting) channel_data.notification.alert_in_meeting = alert_in_meeting channel_data.notification.external_resource_url = external_resource_url activity.channel_data = channel_data diff --git a/libraries/botbuilder-core/tests/teams/test_teams_extension.py b/libraries/botbuilder-core/tests/teams/test_teams_extension.py index 1d2520bc4..406d3cb39 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_extension.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_extension.py @@ -155,6 +155,17 @@ def test_teams_notify_user(self): # Assert assert activity.channel_data.notification.alert + def test_teams_notify_user_alert_in_meeting(self): + # Arrange + activity = Activity() + + # Act + teams_notify_user(activity, alert_in_meeting=True) + + # Assert + assert activity.channel_data.notification.alert_in_meeting is True + assert activity.channel_data.notification.alert is False + def test_teams_notify_user_with_no_activity(self): # Arrange activity = None From 3a55f8a8e97b04900af7bd81e28f22c4a5334850 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:29:50 +0530 Subject: [PATCH 28/40] Added missing parameters: appType, UMSIName, UMSIResourceGroupName, and tenantId to the ARM template for Python bot deployment. (#2191) --- ...ameters-for-template-AzureBot-with-rg.json | 12 +++ ...arameters-for-template-BotApp-with-rg.json | 14 ++- .../deployUseExistResourceGroup/readme.md | 58 ++++++++----- .../template-AzureBot-with-rg.json | 64 +++++++++++++- .../template-BotApp-with-rg.json | 85 +++++++++++++++++-- ...rameters-for-template-AzureBot-new-rg.json | 12 +++ ...parameters-for-template-BotApp-new-rg.json | 12 +++ .../deployWithNewResourceGroup/readme.md | 54 +++++++----- .../template-AzureBot-new-rg.json | 40 ++++++++- .../template-BotApp-new-rg.json | 76 ++++++++++++++++- ...ameters-for-template-AzureBot-with-rg.json | 12 +++ ...arameters-for-template-BotApp-with-rg.json | 14 ++- .../deployUseExistResourceGroup/readme.md | 58 ++++++++----- .../template-AzureBot-with-rg.json | 64 +++++++++++++- .../template-BotApp-with-rg.json | 85 +++++++++++++++++-- ...rameters-for-template-AzureBot-new-rg.json | 12 +++ ...parameters-for-template-BotApp-new-rg.json | 12 +++ .../deployWithNewResourceGroup/readme.md | 54 +++++++----- .../template-AzureBot-new-rg.json | 40 ++++++++- .../template-BotApp-new-rg.json | 76 ++++++++++++++++- ...ameters-for-template-AzureBot-with-rg.json | 12 +++ ...arameters-for-template-BotApp-with-rg.json | 14 ++- .../deployUseExistResourceGroup/readme.md | 58 ++++++++----- .../template-AzureBot-with-rg.json | 64 +++++++++++++- .../template-BotApp-with-rg.json | 83 ++++++++++++++++-- ...rameters-for-template-AzureBot-new-rg.json | 12 +++ ...parameters-for-template-BotApp-new-rg.json | 12 +++ .../deployWithNewResourceGroup/readme.md | 54 +++++++----- .../template-AzureBot-new-rg.json | 40 ++++++++- .../template-BotApp-new-rg.json | 76 ++++++++++++++++- 30 files changed, 1115 insertions(+), 164 deletions(-) diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json index cc1800c0d..92b2e84c9 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -14,8 +14,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json index eea65b7c3..eb36c03fb 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -15,7 +15,7 @@ "value": "" }, "newAppServicePlanLocation": { - "value": "" + "value": "West US" }, "newAppServicePlanSku": { "value": { @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md index f8f1d1e56..19d77be80 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -1,28 +1,48 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment group create --resource-group --template-file --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment group create --resource-group --template-file --parameters @` # parameters-for-template-BotApp-with-rg: -**appServiceName**:(required) The Name of the Bot App Service. +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -(choose an existingAppServicePlan or create a new AppServicePlan) -**existingAppServicePlanName**: The name of the App Service Plan. -**existingAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanName**: The name of the App Service Plan. -**newAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-with-rg: -**azureBotId**:(required) The globally unique and immutable bot ID. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json index f7d08b75d..b2b686dcc 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -9,8 +9,8 @@ } }, "azureBotSku": { - "defaultValue": "S1", "type": "string", + "defaultValue": "S1", "metadata": { "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." } @@ -24,15 +24,72 @@ }, "botEndpoint": { "type": "string", + "defaultValue": "", "metadata": { "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" } }, "resources": [ @@ -49,8 +106,11 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 9b1c79ae9..23a23b1cc 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -4,7 +4,6 @@ "parameters": { "appServiceName": { "type": "string", - "defaultValue": "", "metadata": { "description": "The globally unique name of the Web App." } @@ -18,18 +17,21 @@ }, "existingAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } }, "newAppServicePlanName": { "type": "string", + "defaultValue": "", "metadata": { "description": "The name of the new App Service Plan." } }, "newAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -47,6 +49,18 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -59,13 +73,58 @@ "metadata": { "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } } }, "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", - "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "useExistingServicePlan": "[not(empty(parameters('existingAppServicePlanName')))]", "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", - "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]" + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -89,13 +148,15 @@ "comments": "Create a Web App using an App Service Plan", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", - "name": "[parameters('appServiceName')]", "location": "[variables('servicePlanLocation')]", "kind": "app,linux", "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { + "name": "[parameters('appServiceName')]", "enabled": true, "hostNameSslStates": [ { @@ -124,6 +185,10 @@ "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true" }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" @@ -131,6 +196,10 @@ { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -167,7 +236,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -201,7 +270,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json index f18061813..e51036f85 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -20,8 +20,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json index f3f07b497..de2dba051 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md index d88b160f1..4c752364b 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -1,31 +1,45 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment sub create --template-file --location --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment sub create --template-file --location --parameters @` # parameters-for-template-BotApp-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appServiceName**:(required) The location of the App Service Plan. -**appServicePlanName**:(required) The name of the App Service Plan. -**appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. -**appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. -**azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json index f79264452..63fbf970d 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -41,11 +41,47 @@ "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]" } }, "resources": [ @@ -85,8 +121,10 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json index b33b21510..381b57abf 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -28,6 +28,7 @@ }, "appServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -45,6 +46,25 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -53,16 +73,56 @@ }, "appSecret": { "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types." + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." } } }, "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", "appServicePlanName": "[parameters('appServicePlanName')]", "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", "appServiceName": "[parameters('appServiceName')]", - "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -114,6 +174,7 @@ "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" ], "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { "name": "[variables('appServiceName')]", "hostNameSslStates": [ @@ -136,12 +197,19 @@ "value": "true" }, { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" }, { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -179,7 +247,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -213,7 +281,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json index cc1800c0d..92b2e84c9 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -14,8 +14,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json index eea65b7c3..eb36c03fb 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -15,7 +15,7 @@ "value": "" }, "newAppServicePlanLocation": { - "value": "" + "value": "West US" }, "newAppServicePlanSku": { "value": { @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md index f8f1d1e56..19d77be80 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -1,28 +1,48 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment group create --resource-group --template-file --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment group create --resource-group --template-file --parameters @` # parameters-for-template-BotApp-with-rg: -**appServiceName**:(required) The Name of the Bot App Service. +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -(choose an existingAppServicePlan or create a new AppServicePlan) -**existingAppServicePlanName**: The name of the App Service Plan. -**existingAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanName**: The name of the App Service Plan. -**newAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-with-rg: -**azureBotId**:(required) The globally unique and immutable bot ID. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json index f7d08b75d..b2b686dcc 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -9,8 +9,8 @@ } }, "azureBotSku": { - "defaultValue": "S1", "type": "string", + "defaultValue": "S1", "metadata": { "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." } @@ -24,15 +24,72 @@ }, "botEndpoint": { "type": "string", + "defaultValue": "", "metadata": { "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" } }, "resources": [ @@ -49,8 +106,11 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 9b1c79ae9..23a23b1cc 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -4,7 +4,6 @@ "parameters": { "appServiceName": { "type": "string", - "defaultValue": "", "metadata": { "description": "The globally unique name of the Web App." } @@ -18,18 +17,21 @@ }, "existingAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } }, "newAppServicePlanName": { "type": "string", + "defaultValue": "", "metadata": { "description": "The name of the new App Service Plan." } }, "newAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -47,6 +49,18 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -59,13 +73,58 @@ "metadata": { "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } } }, "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", - "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "useExistingServicePlan": "[not(empty(parameters('existingAppServicePlanName')))]", "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", - "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]" + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -89,13 +148,15 @@ "comments": "Create a Web App using an App Service Plan", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", - "name": "[parameters('appServiceName')]", "location": "[variables('servicePlanLocation')]", "kind": "app,linux", "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { + "name": "[parameters('appServiceName')]", "enabled": true, "hostNameSslStates": [ { @@ -124,6 +185,10 @@ "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true" }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" @@ -131,6 +196,10 @@ { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -167,7 +236,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -201,7 +270,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json index f18061813..e51036f85 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -20,8 +20,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json index f3f07b497..de2dba051 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md index d88b160f1..4c752364b 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -1,31 +1,45 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment sub create --template-file --location --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment sub create --template-file --location --parameters @` # parameters-for-template-BotApp-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appServiceName**:(required) The location of the App Service Plan. -**appServicePlanName**:(required) The name of the App Service Plan. -**appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. -**appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. -**azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json index f79264452..63fbf970d 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -41,11 +41,47 @@ "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]" } }, "resources": [ @@ -85,8 +121,10 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json index b33b21510..381b57abf 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -28,6 +28,7 @@ }, "appServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -45,6 +46,25 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -53,16 +73,56 @@ }, "appSecret": { "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types." + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." } } }, "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", "appServicePlanName": "[parameters('appServicePlanName')]", "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", "appServiceName": "[parameters('appServiceName')]", - "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -114,6 +174,7 @@ "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" ], "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { "name": "[variables('appServiceName')]", "hostNameSslStates": [ @@ -136,12 +197,19 @@ "value": "true" }, { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" }, { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -179,7 +247,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -213,7 +281,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json index cc1800c0d..92b2e84c9 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -14,8 +14,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json index eea65b7c3..eb36c03fb 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -15,7 +15,7 @@ "value": "" }, "newAppServicePlanLocation": { - "value": "" + "value": "West US" }, "newAppServicePlanSku": { "value": { @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md index f8f1d1e56..19d77be80 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -1,28 +1,48 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment group create --resource-group --template-file --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment group create --resource-group --template-file --parameters @` # parameters-for-template-BotApp-with-rg: -**appServiceName**:(required) The Name of the Bot App Service. +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -(choose an existingAppServicePlan or create a new AppServicePlan) -**existingAppServicePlanName**: The name of the App Service Plan. -**existingAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanName**: The name of the App Service Plan. -**newAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-with-rg: -**azureBotId**:(required) The globally unique and immutable bot ID. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json index f7d08b75d..b2b686dcc 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -9,8 +9,8 @@ } }, "azureBotSku": { - "defaultValue": "S1", "type": "string", + "defaultValue": "S1", "metadata": { "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." } @@ -24,15 +24,72 @@ }, "botEndpoint": { "type": "string", + "defaultValue": "", "metadata": { "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" } }, "resources": [ @@ -49,8 +106,11 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 9b1c79ae9..979ec221b 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -4,7 +4,6 @@ "parameters": { "appServiceName": { "type": "string", - "defaultValue": "", "metadata": { "description": "The globally unique name of the Web App." } @@ -18,18 +17,21 @@ }, "existingAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } }, "newAppServicePlanName": { "type": "string", + "defaultValue": "", "metadata": { "description": "The name of the new App Service Plan." } }, "newAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -47,6 +49,18 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -59,13 +73,58 @@ "metadata": { "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } } }, "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", - "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "useExistingServicePlan": "[not(empty(parameters('existingAppServicePlanName')))]", "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", - "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]" + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -89,13 +148,15 @@ "comments": "Create a Web App using an App Service Plan", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", - "name": "[parameters('appServiceName')]", "location": "[variables('servicePlanLocation')]", "kind": "app,linux", "dependsOn": [ "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { + "name": "[parameters('appServiceName')]", "enabled": true, "hostNameSslStates": [ { @@ -124,6 +185,10 @@ "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true" }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" @@ -131,6 +196,10 @@ { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -167,7 +236,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -201,7 +270,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json index f18061813..e51036f85 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -20,8 +20,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json index f3f07b497..de2dba051 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md index d88b160f1..4c752364b 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -1,31 +1,45 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment sub create --template-file --location --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment sub create --template-file --location --parameters @` # parameters-for-template-BotApp-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appServiceName**:(required) The location of the App Service Plan. -**appServicePlanName**:(required) The name of the App Service Plan. -**appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. -**appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. -**azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json index f79264452..63fbf970d 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -41,11 +41,47 @@ "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]" } }, "resources": [ @@ -85,8 +121,10 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json index b33b21510..381b57abf 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -28,6 +28,7 @@ }, "appServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -45,6 +46,25 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -53,16 +73,56 @@ }, "appSecret": { "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types." + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." } } }, "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", "appServicePlanName": "[parameters('appServicePlanName')]", "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", "appServiceName": "[parameters('appServiceName')]", - "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -114,6 +174,7 @@ "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" ], "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { "name": "[variables('appServiceName')]", "hostNameSslStates": [ @@ -136,12 +197,19 @@ "value": "true" }, { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" }, { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -179,7 +247,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -213,7 +281,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } From 394d3d6b3a4946fe32a875ef1a0fdc230310edc5 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:41:42 +0530 Subject: [PATCH 29/40] Bumped pytest (#2189) Co-authored-by: tracyboehrer --- .../tests/teams/test_teams_activity_handler.py | 4 ++++ .../tests/choices/test_choice_recognizers.py | 7 +++---- libraries/botbuilder-testing/setup.py | 2 +- libraries/botframework-connector/tests/requirements.txt | 6 +++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 22550764e..257dc75f9 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -5,6 +5,10 @@ from typing import List import aiounittest +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from botbuilder.core import BotAdapter, TurnContext from botbuilder.core.teams import TeamsActivityHandler from botbuilder.schema import ( diff --git a/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py b/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py index c37243fd1..ac202d044 100644 --- a/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py +++ b/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py @@ -57,10 +57,9 @@ def assert_choice(result, value, index, score, synonym=None): resolution.score == score ), f"Invalid resolution.score of '{resolution.score}' for '{value}' choice." if synonym: - assert ( # pylint: disable=assert-on-tuple - resolution.synonym == synonym, - f"Invalid resolution.synonym of '{resolution.synonym}' for '{value}' choice.", - ) + assert ( + resolution.synonym == synonym + ), f"Invalid resolution.synonym of '{resolution.synonym}' for '{value}' choice." _color_choices: List[str] = ["red", "green", "blue"] diff --git a/libraries/botbuilder-testing/setup.py b/libraries/botbuilder-testing/setup.py index 9fed4e3ac..9ee855a41 100644 --- a/libraries/botbuilder-testing/setup.py +++ b/libraries/botbuilder-testing/setup.py @@ -9,7 +9,7 @@ "botbuilder-core==4.17.0", "botbuilder-dialogs==4.17.0", "botbuilder-azure==4.17.0", - "pytest~=7.3.1", + "pytest~=8.3.3", ] TESTS_REQUIRES = ["aiounittest==1.3.0"] diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt index 5f0d9558d..6facda892 100644 --- a/libraries/botframework-connector/tests/requirements.txt +++ b/libraries/botframework-connector/tests/requirements.txt @@ -1,6 +1,6 @@ -pytest-cov>=2.6.0 -pytest~=7.3.1 +pytest-cov>=5.0.0 +pytest~=8.3.3 pyyaml==6.0.1 -pytest-asyncio==0.23.8 +pytest-asyncio==0.24.0 ddt==1.2.1 setuptools==72.1.0 \ No newline at end of file From a7f5d91398bf502128645ec5d698024c5aaeda3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 08:23:34 -0600 Subject: [PATCH 30/40] Bump: aiohttp from 3.10.5 to 3.10.11 (#2190) * Bump aiohttp Bumps the pip group with 1 update in the /libraries/botbuilder-integration-aiohttp directory: [aiohttp](https://github.com/aio-libs/aiohttp). Updates `aiohttp` from 3.10.5 to 3.10.11 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.5...v3.10.11) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] * Bump: aiohttp to 3.10.11 in SDK packages --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ganapathi Diddi Co-authored-by: tracyboehrer --- libraries/botbuilder-adapters-slack/requirements.txt | 2 +- libraries/botbuilder-ai/setup.py | 2 +- libraries/botbuilder-integration-aiohttp/requirements.txt | 2 +- libraries/botbuilder-integration-aiohttp/setup.py | 2 +- .../botbuilder-integration-applicationinsights-aiohttp/setup.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 98af56627..8dd02c6be 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.5 +aiohttp==3.10.11 pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index e842fdc34..707369967 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.10.5", + "aiohttp==3.10.11", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index de0f13750..4344965ca 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.10.5 +aiohttp==3.10.11 diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index 1fe5d5ccc..635e26fc1 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.10.5", + "aiohttp==3.10.11", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index 30c45b2f7..3ef0ca426 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.10.5", + "aiohttp==3.10.11", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", From ef5ecd36f76529f30727929128330fd790dc5625 Mon Sep 17 00:00:00 2001 From: Kostya <53430368+kostyaplis@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:54:12 +0800 Subject: [PATCH 31/40] Port auth error handling from microsoft_app_credentials to certificate_app_credentials (#2196) Co-authored-by: Konstantin Plis --- .../connector/auth/certificate_app_credentials.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py index 89dbe882d..31e845eb6 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py @@ -67,7 +67,17 @@ def get_access_token(self, force_refresh: bool = False) -> str: if not auth_token: # No suitable token exists in cache. Let's get a new one from AAD. auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes) - return auth_token["access_token"] + if "access_token" in auth_token: + return auth_token["access_token"] + error = auth_token["error"] if "error" in auth_token else "Unknown error" + error_description = ( + auth_token["error_description"] + if "error_description" in auth_token + else "Unknown error description" + ) + raise PermissionError( + f"Failed to get access token with error: {error}, error_description: {error_description}" + ) def __get_msal_app(self): if not self.app: From bd5662abb1297ebd457ace2918c26e5d611271e9 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 11 Feb 2025 12:38:07 -0600 Subject: [PATCH 32/40] Updated MSAL package (#2203) Co-authored-by: Tracy Boehrer --- libraries/botframework-connector/requirements.txt | 2 +- libraries/botframework-connector/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botframework-connector/requirements.txt b/libraries/botframework-connector/requirements.txt index 5a6d8d4e9..515030672 100644 --- a/libraries/botframework-connector/requirements.txt +++ b/libraries/botframework-connector/requirements.txt @@ -3,4 +3,4 @@ botbuilder-schema==4.17.0 requests==2.32.0 PyJWT==2.4.0 cryptography==43.0.1 -msal>=1.29.0 +msal>=1.31.1 diff --git a/libraries/botframework-connector/setup.py b/libraries/botframework-connector/setup.py index 8a99b3e19..1bfc05d49 100644 --- a/libraries/botframework-connector/setup.py +++ b/libraries/botframework-connector/setup.py @@ -11,7 +11,7 @@ # "requests>=2.23.0,<2.26", "PyJWT>=2.4.0", "botbuilder-schema==4.17.0", - "msal>=1.29.0", + "msal>=1.31.1", ] root = os.path.abspath(os.path.dirname(__file__)) From e2015ebea8bbe000cb19f9c2c25c20b9575085e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20Su=C3=A1rez?= Date: Fri, 21 Mar 2025 12:24:45 -0600 Subject: [PATCH 33/40] MentionRemoved fix (#2216) --- .../botbuilder/core/turn_context.py | 6 ++- .../tests/test_turn_context.py | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 852fd1f31..72e25726c 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -396,9 +396,13 @@ def remove_mention_text(activity: Activity, identifier: str) -> str: mentions = TurnContext.get_mentions(activity) for mention in mentions: if mention.additional_properties["mentioned"]["id"] == identifier: + replace_text = ( + mention.additional_properties.get("text") + or mention.additional_properties.get("mentioned")["name"] + ) mention_name_match = re.match( r"(.*?)<\/at>", - escape(mention.additional_properties["text"]), + escape(replace_text), re.IGNORECASE, ) if mention_name_match: diff --git a/libraries/botbuilder-core/tests/test_turn_context.py b/libraries/botbuilder-core/tests/test_turn_context.py index 473580ef0..7247caab9 100644 --- a/libraries/botbuilder-core/tests/test_turn_context.py +++ b/libraries/botbuilder-core/tests/test_turn_context.py @@ -350,6 +350,48 @@ def test_should_remove_at_mention_with_regex_characters(self): assert text == " test activity" assert activity.text == " test activity" + def test_should_remove_custom_mention_from_activity(self): + activity = Activity( + text="Hallo", + text_format="plain", + type="message", + timestamp="2025-03-11T14:16:47.0093935Z", + id="1741702606984", + channel_id="msteams", + service_url="https://smba.trafficmanager.net/emea/REDACTED/", + from_property=ChannelAccount( + id="29:1J-K4xVh-sLpdwQ-R5GkOZ_TB0W3ec_37p710aH8qe8bITA0zxdgIGc9l-MdDdkdE_jasSfNOeWXyyL1nsrHtBQ", + name="", + aad_object_id="REDACTED", + ), + conversation=ConversationAccount( + is_group=True, + conversation_type="groupChat", + tenant_id="REDACTED", + id="19:Ql86tXNM2lTBXNKJdqKdwIF9ltGZwpvluLvnJdA0tmg1@thread.v2", + ), + recipient=ChannelAccount( + id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", name="Azure AI Agent" + ), + entities=[ + Entity().deserialize( + Mention( + type="mention", + mentioned=ChannelAccount( + id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", + name="Custom Agent", + ), + ).serialize() + ) + ], + channel_data={"tenant": {"id": "REDACTED"}, "productContext": "COPILOT"}, + ) + + text = TurnContext.remove_mention_text(activity, activity.recipient.id) + + assert text == "Hallo" + assert activity.text == "Hallo" + async def test_should_send_a_trace_activity(self): context = TurnContext(SimpleAdapter(), ACTIVITY) called = False From ec22a895ddea708d4ebcb6497f9521f9de58f4ba Mon Sep 17 00:00:00 2001 From: Alex Recuenco <26118630+alexrecuenco@users.noreply.github.com> Date: Tue, 25 Mar 2025 18:23:32 +0100 Subject: [PATCH 34/40] fix: Method name is emit_event, not emit (#2215) --- libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py index 43cfe3052..f07a8afa5 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py @@ -125,7 +125,7 @@ async def on_dialog_event( # Bubble as needed if (not handled) and dialog_event.bubble and dialog_context.parent: - handled = await dialog_context.parent.emit( + handled = await dialog_context.parent.emit_event( dialog_event.name, dialog_event.value, True, False ) From 13ee2f25ec3b7470fc4f570d7624b59487a98d38 Mon Sep 17 00:00:00 2001 From: Alex Recuenco <26118630+alexrecuenco@users.noreply.github.com> Date: Tue, 25 Mar 2025 18:24:05 +0100 Subject: [PATCH 35/40] Typo on link (#2217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contributing goes to a 404 — file not found, https://github.com/microsoft/botbuilder-python/blob/main/contributing.md The correct link is https://github.com/microsoft/botbuilder-python/blob/main/Contributing.md Co-authored-by: tracyboehrer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92a7c1332..feace0e06 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ We use the [@msbotframework](https://twitter.com/msbotframework) account on twit The [Gitter Channel](https://gitter.im/Microsoft/BotBuilder) provides a place where the Community can get together and collaborate. ## Contributing and our code of conduct -We welcome contributions and suggestions. Please see our [contributing guidelines](./contributing.md) for more information. +We welcome contributions and suggestions. Please see our [contributing guidelines](./Contributing.md) for more information. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). From ac28ca3d54a124cfc2c38765f64b9008e76801ab Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Wed, 28 May 2025 12:17:53 -0500 Subject: [PATCH 36/40] Teams SSO and OAuth fixes (#2226) * Fixed Teams SSO & OAuth * Formatting --------- Co-authored-by: Tracy Boehrer --- .../requirements.txt | 2 +- libraries/botbuilder-ai/setup.py | 2 +- .../botbuilder/core/__init__.py | 2 ++ .../botbuilder/core/activity_handler.py | 11 +++++-- .../core/teams/teams_activity_handler.py | 11 ++----- .../teams_sso_token_exchange_middleware.py | 19 ++++++++++-- .../dialogs/prompts/oauth_prompt.py | 8 +++++ .../integration/aiohttp/cloud_adapter.py | 6 +++- .../requirements.txt | 2 +- .../botbuilder-integration-aiohttp/setup.py | 2 +- .../setup.py | 2 +- .../botbuilder/schema/_models_py3.py | 3 ++ .../connector/token_api/models/__init__.py | 3 ++ .../connector/token_api/models/_models.py | 24 ++++++++++++++ .../connector/token_api/models/_models_py3.py | 31 ++++++++++++++++++- .../skills-buffered/child/requirements.txt | 2 +- .../skills-buffered/parent/requirements.txt | 2 +- 17 files changed, 110 insertions(+), 22 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 8dd02c6be..50f1af767 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.11 +aiohttp pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index 707369967..10bc3ee5c 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.10.11", + "aiohttp>=3.10,<4.0", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py index c5c038353..0769d9100 100644 --- a/libraries/botbuilder-core/botbuilder/core/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/__init__.py @@ -48,6 +48,7 @@ from .user_state import UserState from .register_class_middleware import RegisterClassMiddleware from .adapter_extensions import AdapterExtensions +from .serializer_helper import serializer_helper __all__ = [ "ActivityHandler", @@ -100,5 +101,6 @@ "TurnContext", "UserState", "UserTokenProvider", + "serializer_helper", "__version__", ] diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py index 51aefb245..4dbf04f0b 100644 --- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py @@ -478,12 +478,19 @@ async def on_invoke_activity( # pylint: disable=unused-argument if ( turn_context.activity.name == SignInConstants.verify_state_operation_name - or turn_context.activity.name - == SignInConstants.token_exchange_operation_name ): await self.on_sign_in_invoke(turn_context) return self._create_invoke_response() + # This is for back-compat with previous versions of Python SDK. This method does not + # exist in the C# SDK, and is not used in the Python SDK. + if ( + turn_context.activity.name + == SignInConstants.token_exchange_operation_name + ): + await self.on_teams_signin_token_exchange(turn_context) + return self._create_invoke_response() + if turn_context.activity.name == "adaptiveCard/action": invoke_value = self._get_adaptive_card_invoke_value( turn_context.activity diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 9344e343e..2cc2e6fb6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -56,13 +56,6 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ): return await self.on_teams_card_action_invoke(turn_context) - if ( - turn_context.activity.name - == SignInConstants.token_exchange_operation_name - ): - await self.on_teams_signin_token_exchange(turn_context) - return self._create_invoke_response() - if turn_context.activity.name == "fileConsent/invoke": return await self.on_teams_file_consent( turn_context, @@ -250,7 +243,9 @@ async def on_teams_signin_verify_state(self, turn_context: TurnContext): raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) async def on_teams_signin_token_exchange(self, turn_context: TurnContext): - raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + # This is for back-compat with previous versions of Python SDK. This method does not + # exist in the C# SDK, and is not used in the Python SDK. + return await self.on_teams_signin_verify_state(turn_context) async def on_teams_file_consent( self, diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py index 1dec1210a..5a6fa5de6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py @@ -26,6 +26,7 @@ StoreItem, TurnContext, ) +from botframework.connector.auth.user_token_client import UserTokenClient class _TokenStoreItem(StoreItem): @@ -147,17 +148,29 @@ async def _exchanged_token(self, turn_context: TurnContext) -> bool: token_exchange_response: TokenResponse = None aux_dict = {} if turn_context.activity.value: - for prop in ["id", "connection_name", "token", "properties"]: + for prop in ["id", "connectionName", "token", "properties"]: aux_dict[prop] = turn_context.activity.value.get(prop) token_exchange_request = TokenExchangeInvokeRequest( id=aux_dict["id"], - connection_name=aux_dict["connection_name"], + connection_name=aux_dict["connectionName"], token=aux_dict["token"], properties=aux_dict["properties"], ) try: adapter = turn_context.adapter - if isinstance(turn_context.adapter, ExtendedUserTokenProvider): + + user_token_client: UserTokenClient = turn_context.turn_state.get( + UserTokenClient.__name__, None + ) + if user_token_client: + # If the adapter has UserTokenClient, use it to exchange the token. + token_exchange_response = await user_token_client.exchange_token( + turn_context.activity.from_property.id, + token_exchange_request.connection_name, + turn_context.activity.channel_id, + TokenExchangeRequest(token=token_exchange_request.token), + ) + elif isinstance(turn_context.adapter, ExtendedUserTokenProvider): token_exchange_response = await adapter.exchange_token( turn_context, self._oauth_connection_name, diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index d31a0b56a..270d4f324 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -341,6 +341,13 @@ async def _send_oauth_card( if sign_in_resource.token_exchange_resource else None ) + + json_token_ex_post = ( + sign_in_resource.token_post_resource.as_dict() + if sign_in_resource.token_post_resource + else None + ) + prompt.attachments.append( CardFactory.oauth_card( OAuthCard( @@ -355,6 +362,7 @@ async def _send_oauth_card( ) ], token_exchange_resource=json_token_ex_resource, + token_post_resource=json_token_ex_post, ) ) ) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py index 0f9131871..576c5125c 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json + from typing import Awaitable, Callable, Optional from aiohttp.web import ( @@ -17,6 +19,7 @@ Bot, CloudAdapterBase, InvokeResponse, + serializer_helper, TurnContext, ) from botbuilder.core.streaming import ( @@ -102,7 +105,8 @@ async def process( # Write the response, serializing the InvokeResponse if invoke_response: return json_response( - data=invoke_response.body, status=invoke_response.status + data=serializer_helper(invoke_response.body), + status=invoke_response.status, ) return Response(status=201) else: diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 4344965ca..d66ba0327 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.10.11 +aiohttp==3.*.* diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index 635e26fc1..2624c9dc8 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.10.11", + "aiohttp>=3.10,<4.0", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index 3ef0ca426..78c32e5eb 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.10.11", + "aiohttp>=3.10,<4.0", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index 1b6a631c6..e7dd1f789 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -1909,6 +1909,7 @@ class OAuthCard(Model): "connection_name": {"key": "connectionName", "type": "str"}, "buttons": {"key": "buttons", "type": "[CardAction]"}, "token_exchange_resource": {"key": "tokenExchangeResource", "type": "object"}, + "token_post_resource": {"key": "tokenPostResource", "type": "object"}, } def __init__( @@ -1918,6 +1919,7 @@ def __init__( connection_name: str = None, buttons=None, token_exchange_resource=None, + token_post_resource=None, **kwargs ) -> None: super(OAuthCard, self).__init__(**kwargs) @@ -1925,6 +1927,7 @@ def __init__( self.connection_name = connection_name self.buttons = buttons self.token_exchange_resource = token_exchange_resource + self.token_post_resource = token_post_resource class PagedMembersResult(Model): diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py index f4593e21a..0f1f158da 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py @@ -13,6 +13,7 @@ from ._models_py3 import SignInUrlResponse from ._models_py3 import TokenExchangeRequest from ._models_py3 import TokenExchangeResource + from ._models_py3 import TokenPostResource from ._models_py3 import TokenResponse from ._models_py3 import TokenStatus except (SyntaxError, ImportError): @@ -23,6 +24,7 @@ from ._models import SignInUrlResponse from ._models import TokenExchangeRequest from ._models import TokenExchangeResource + from ._models import TokenPostResource from ._models import TokenResponse from ._models import TokenStatus @@ -35,6 +37,7 @@ "SignInUrlResponse", "TokenExchangeRequest", "TokenExchangeResource", + "TokenPostResource", "TokenResponse", "TokenStatus", ] diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py index 5f69104cd..8b526324a 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py @@ -104,6 +104,9 @@ class SignInUrlResponse(Model): :param token_exchange_resource: :type token_exchange_resource: ~botframework.tokenapi.models.TokenExchangeResource + :param token_post_resource: + :type token_post_resource: + ~botframework.tokenapi.models.TokenPostResource """ _attribute_map = { @@ -112,12 +115,17 @@ class SignInUrlResponse(Model): "key": "tokenExchangeResource", "type": "TokenExchangeResource", }, + "token_post_resource": { + "key": "tokenPostResource", + "type": "TokenPostResource", + }, } def __init__(self, **kwargs): super(SignInUrlResponse, self).__init__(**kwargs) self.sign_in_link = kwargs.get("sign_in_link", None) self.token_exchange_resource = kwargs.get("token_exchange_resource", None) + self.token_exchange_resource = kwargs.get("token_post_resource", None) class TokenExchangeRequest(Model): @@ -164,6 +172,22 @@ def __init__(self, **kwargs): self.provider_id = kwargs.get("provider_id", None) +class TokenPostResource(Model): + """TokenPostResource. + + :param sas_url: + :type id: str + """ + + _attribute_map = { + "sas_url": {"key": "sasUrl", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TokenPostResource, self).__init__(**kwargs) + self.sas_url = kwargs.get("sas_url", None) + + class TokenResponse(Model): """TokenResponse. diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py index 60ab62c92..512e85356 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py @@ -106,6 +106,9 @@ class SignInUrlResponse(Model): :param token_exchange_resource: :type token_exchange_resource: ~botframework.tokenapi.models.TokenExchangeResource + :param token_post_resource: + :type token_post_resource: + ~botframework.tokenapi.models.TokenPostResource """ _attribute_map = { @@ -114,14 +117,24 @@ class SignInUrlResponse(Model): "key": "tokenExchangeResource", "type": "TokenExchangeResource", }, + "token_post_resource": { + "key": "tokenPostResource", + "type": "TokenPostResource", + }, } def __init__( - self, *, sign_in_link: str = None, token_exchange_resource=None, **kwargs + self, + *, + sign_in_link: str = None, + token_exchange_resource=None, + token_post_resource=None, + **kwargs ) -> None: super(SignInUrlResponse, self).__init__(**kwargs) self.sign_in_link = sign_in_link self.token_exchange_resource = token_exchange_resource + self.token_post_resource = token_post_resource class TokenExchangeRequest(Model): @@ -170,6 +183,22 @@ def __init__( self.provider_id = provider_id +class TokenPostResource(Model): + """TokenPostResource. + + :param sas_url: + :type id: str + """ + + _attribute_map = { + "sas_url": {"key": "sasUrl", "type": "str"}, + } + + def __init__(self, *, sas_url: str = None, **kwargs) -> None: + super(TokenPostResource, self).__init__(**kwargs) + self.sas_url = sas_url + + class TokenResponse(Model): """TokenResponse. diff --git a/tests/skills/skills-buffered/child/requirements.txt b/tests/skills/skills-buffered/child/requirements.txt index 20f8f8fe5..9e79c8115 100644 --- a/tests/skills/skills-buffered/child/requirements.txt +++ b/tests/skills/skills-buffered/child/requirements.txt @@ -1,2 +1,2 @@ botbuilder-core>=4.7.1 -aiohttp +aiohttp==3.*.* diff --git a/tests/skills/skills-buffered/parent/requirements.txt b/tests/skills/skills-buffered/parent/requirements.txt index 20f8f8fe5..9e79c8115 100644 --- a/tests/skills/skills-buffered/parent/requirements.txt +++ b/tests/skills/skills-buffered/parent/requirements.txt @@ -1,2 +1,2 @@ botbuilder-core>=4.7.1 -aiohttp +aiohttp==3.*.* From e535fac0c3cd1d0a33395c678d3a38a29cd6a7a4 Mon Sep 17 00:00:00 2001 From: Mark Jan van Kampen Date: Tue, 23 Dec 2025 20:08:24 +0100 Subject: [PATCH 37/40] chore: relaxes json pickle version constraint (#2240) --- libraries/botbuilder-azure/setup.py | 2 +- libraries/botbuilder-core/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-azure/setup.py b/libraries/botbuilder-azure/setup.py index 9c40b3ab5..7ff214d2e 100644 --- a/libraries/botbuilder-azure/setup.py +++ b/libraries/botbuilder-azure/setup.py @@ -10,7 +10,7 @@ "azure-storage-queue==12.4.0", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", - "jsonpickle>=1.2,<1.5", + "jsonpickle>=1.2,<4", ] TEST_REQUIRES = ["aiounittest==1.3.0"] diff --git a/libraries/botbuilder-core/setup.py b/libraries/botbuilder-core/setup.py index a4a0ed1af..24267bfb6 100644 --- a/libraries/botbuilder-core/setup.py +++ b/libraries/botbuilder-core/setup.py @@ -9,7 +9,7 @@ "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botframework-streaming==4.17.0", - "jsonpickle>=1.2,<1.5", + "jsonpickle>=1.2,<4", ] root = os.path.abspath(os.path.dirname(__file__)) From df350e43f411e8b471f4bffc0df246d35c9440a6 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 23 Dec 2025 13:53:28 -0600 Subject: [PATCH 38/40] README update (#2245) Co-authored-by: Tracy Boehrer --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index feace0e06..4e90050c0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # ![Bot Framework SDK v4 Python](./doc/media/FrameWorkPython.png) +# ARCHIVE NOTICE: + +> We are in the process of archiving the Bot Framework Python SDK repository on GitHub. This means that this project will no longer be updated or maintained. Customers using this tool will not be disrupted. However, the tool will no longer be supported through +> service tickets in the Azure portal and will not receive product updates. + +> To build agents with your choice of AI services, orchestration, and knowledge, consider using the [Microsoft 365 Agents SDK](https://github.com/microsoft/agents). The Agents SDK is GA and has support for C#, JavaScript or Python. You can learn more about the Agents SDK at aka.ms/agents. If you're looking for a SaaS-based agent platform, consider Microsoft Copilot Studio. If you have an existing bot built with the Bot Framework SDK, you can update your bot to the Agents SDK. You can review the core changes and updates at Bot Framework SDK to Agents SDK migration guidance [here](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/bf-migration-guidance). Support tickets for the Bot Framework SDK will no longer be serviced as of December 31, 2025. + +> We plan to archive this project no later than end of December of 2025. + This repository contains code for the Python version of the [Microsoft Bot Framework SDK](https://github.com/Microsoft/botframework-sdk), which is part of the Microsoft Bot Framework - a comprehensive framework for building enterprise-grade conversational AI experiences. This SDK enables developers to model conversation and build sophisticated bot applications using Python. SDKs for [JavaScript](https://github.com/Microsoft/botbuilder-js) and [.NET](https://github.com/Microsoft/botbuilder-dotnet) are also available. From 72fa3be2be2c5fdbb88a074ceb2c0f7e1fe21558 Mon Sep 17 00:00:00 2001 From: Hiroshi Yoshioka <40815708+hyoshioka0128@users.noreply.github.com> Date: Tue, 30 Dec 2025 01:53:36 +0900 Subject: [PATCH 39/40] Fix typo in docstring for O365 connector card action (#2244) https://github.com/microsoft/botbuilder-python/blob/main/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py #PingMSFTDocs --- .../botbuilder/core/teams/teams_activity_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 2cc2e6fb6..af45ba5b6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -314,7 +314,7 @@ async def on_teams_o365_connector_card_action( # pylint: disable=unused-argumen self, turn_context: TurnContext, query: O365ConnectorCardActionQuery ): """ - Invoked when a O365 Connector Card Action activity is received from the connector. + Invoked when an O365 Connector Card Action activity is received from the connector. :param turn_context: A context object for this turn. :param query: The O365 connector card HttpPOST invoke query. From e07ec54ed9a863b69a7cfae5162629383924709d Mon Sep 17 00:00:00 2001 From: warnov Date: Mon, 29 Dec 2025 11:54:12 -0500 Subject: [PATCH 40/40] Update template-BotApp-with-rg.json (#2209) update remoteDebuggingVersion from VS2017 to VS2022 Co-authored-by: tracyboehrer --- .../deployUseExistResourceGroup/template-BotApp-with-rg.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 23a23b1cc..71425ee9a 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -239,7 +239,7 @@ "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", + "remoteDebuggingVersion": "VS2022", "httpLoggingEnabled": true, "logsDirectorySizeLimit": 35, "detailedErrorLoggingEnabled": false, @@ -275,4 +275,4 @@ } } ] -} \ No newline at end of file +}