From d056157ba998e92c9bc25cbc99795ec86b81803f Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 26 Nov 2019 09:13:01 -0800 Subject: [PATCH 1/8] Skills Lower layers --- .../botbuilder/core/integration/__init__.py | 16 + .../integration/aiohttp_channel_service.py | 92 ++++ .../integration/bot_framework_http_client.py | 115 +++++ .../integration/channel_service_handler.py | 459 ++++++++++++++++++ 4 files changed, 682 insertions(+) create mode 100644 libraries/botbuilder-core/botbuilder/core/integration/__init__.py create mode 100644 libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py create mode 100644 libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py create mode 100644 libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py diff --git a/libraries/botbuilder-core/botbuilder/core/integration/__init__.py b/libraries/botbuilder-core/botbuilder/core/integration/__init__.py new file mode 100644 index 000000000..8cebe1ad8 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/integration/__init__.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from .bot_framework_http_client import BotFrameworkHttpClient +from .channel_service_handler import ChannelServiceHandler +from .skill_conversation_id_factory import SkillConversationIdFactory + +__all__ = [ + "BotFrameworkHttpClient", + "ChannelServiceHandler", + "SkillConversationIdFactory", +] diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py new file mode 100644 index 000000000..9fa2aab1e --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp.web import RouteTableDef, Request, Response + +from botbuilder.schema import Activity + +from .channel_service_handler import ChannelServiceHandler + + +async def deserialize_activity(request: Request) -> Activity: + if "application/json" in request.headers["Content-Type"]: + body = await request.json() + else: + return Response(status=415) + + return Activity().deserialize(body) + + +def channel_service_routes(handler: ChannelServiceHandler) -> RouteTableDef: + routes = RouteTableDef() + + @routes.post("/{conversation_id}/activities") + async def send_to_conversation(request: Request): + activity = await deserialize_activity(request) + return await handler.handle_send_to_conversation( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + activity, + ) + + @routes.post("/{conversation_id}/activities/{activity_id}") + async def reply_to_activity(request: Request): + activity = await deserialize_activity(request) + return await handler.handle_reply_to_activity( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + activity, + ) + + @routes.put("/{conversation_id}/activities/{activity_id}") + async def update_activity(request: Request): + activity = await deserialize_activity(request) + return await handler.handle_update_activity( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + activity, + ) + + @routes.delete("/{conversation_id}/activities/{activity_id}") + async def delete_activity(request: Request): + return await handler.handle_delete_activity( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + ) + + @routes.get("/{conversation_id}/activities/{activity_id}/members") + async def get_activity_members(request: Request): + raise NotImplementedError("get_activity_members is not supported") + + @routes.post("/") + async def create_conversation(request: Request): + raise NotImplementedError("create_conversation is not supported") + + @routes.get("/") + async def get_conversation(request: Request): + raise NotImplementedError("get_conversation is not supported") + + @routes.get("/{conversation_id}/members") + async def get_conversation_members(request: Request): + raise NotImplementedError("get_activity_members is not supported") + + @routes.get("/{conversation_id}/pagedmembers") + async def get_conversation_paged_members(request: Request): + raise NotImplementedError("get_conversation_paged_members is not supported") + + @routes.delete("/{conversation_id}/members/{member_id}") + async def delete_conversation_members(request: Request): + raise NotImplementedError("delete_conversation_members is not supported") + + @routes.post("/{conversation_id}/activities/history") + async def get_conversation_history(request: Request): + raise NotImplementedError("get_conversation_history is not supported") + + @routes.post("/{conversation_id}/attachments") + async def upload_attachment(request: Request): + raise NotImplementedError("upload_attachment is not supported") + + return routes diff --git a/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py b/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py new file mode 100644 index 000000000..5d7f2f9a2 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py @@ -0,0 +1,115 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +from typing import Dict +from logging import Logger +import aiohttp + +from botbuilder.core import InvokeResponse +from botbuilder.schema import Activity +from botframework.connector.auth import ( + ChannelProvider, + CredentialProvider, + GovernmentConstants, + MicrosoftAppCredentials, +) + + +class BotFrameworkHttpClient: + + """ + A skill host adapter implements API to forward activity to a skill and + implements routing ChannelAPI calls from the Skill up through the bot/adapter. + """ + + INVOKE_ACTIVITY_NAME = "SkillEvents.ChannelApiInvoke" + _BOT_IDENTITY_KEY = "BotIdentity" + _APP_CREDENTIALS_CACHE: Dict[str:MicrosoftAppCredentials] = {} + + def __init__( + self, + credential_provider: CredentialProvider, + channel_provider: ChannelProvider = None, + logger: Logger = None, + ): + if not credential_provider: + raise TypeError("credential_provider can't be None") + + self._credential_provider = credential_provider + self._channel_provider = channel_provider + self._logger = logger + self._session = aiohttp.ClientSession() + + async def post_activity( + self, + from_bot_id: str, + to_bot_id: str, + to_url: str, + service_url: str, + conversation_id: str, + activity: Activity, + ) -> InvokeResponse: + app_credentials = await self._get_app_credentials(from_bot_id, to_bot_id) + + if not app_credentials: + raise RuntimeError("Unable to get appCredentials to connect to the skill") + + # Get token for the skill call + token = app_credentials.get_access_token() + + # Capture current activity settings before changing them. + # TODO: DO we need to set the activity ID? (events that are created manually don't have it). + original_conversation_id = activity.conversation.id + original_service_url = activity.service_url + + try: + activity.conversation.id = conversation_id + activity.service_url = service_url + + json_content = json.dumps(activity.serialize()) + resp = await self._session.post( + to_url, + data=json_content.encode("utf-8"), + headers={ + "Authorization": f"Bearer:{token}", + "Content-type": "application/json; charset=utf-8", + }, + ) + resp.raise_for_status() + content = await resp.json() + + if content: + return InvokeResponse(status=resp.status_code, body=content) + + finally: + # Restore activity properties. + activity.conversation.id = original_conversation_id + activity.service_url = original_service_url + + async def _get_app_credentials( + self, app_id: str, oauth_scope: str + ) -> MicrosoftAppCredentials: + if not app_id: + return MicrosoftAppCredentials(None, None) + + cache_key = f"{app_id}{oauth_scope}" + app_credentials = BotFrameworkHttpClient._APP_CREDENTIALS_CACHE.get(cache_key) + + if app_credentials: + return app_credentials + + app_password = await self._credential_provider.get_app_password(app_id) + app_credentials = MicrosoftAppCredentials( + app_id, app_password, oauth_scope=oauth_scope + ) + if self._channel_provider.is_government(): + app_credentials.oauth_endpoint = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL + ) + app_credentials.oauth_scope = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + + BotFrameworkHttpClient._APP_CREDENTIALS_CACHE[cache_key] = app_credentials + return app_credentials diff --git a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py new file mode 100644 index 000000000..317259c19 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py @@ -0,0 +1,459 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + +from botbuilder.schema import ( + Activity, + AttachmentData, + ChannelAccount, + ConversationParameters, + ConversationsResult, + ConversationResourceResponse, + PagedMembersResult, + ResourceResponse, + Transcript, +) + +from botframework.connector.auth import ( + AuthenticationConfiguration, + ChannelProvider, + ClaimsIdentity, + CredentialProvider, + JwtTokenValidation, +) + + +class ChannelServiceHandler: + """ + Initializes a new instance of the class, + using a credential provider. + """ + + def __init__( + self, + credential_provider: CredentialProvider, + auth_config: AuthenticationConfiguration, + channel_provider: ChannelProvider = None, + ): + if not credential_provider: + raise TypeError("credential_provider can't be None") + + if not auth_config: + raise TypeError("auth_config can't be None") + + self._credential_provider = credential_provider + self._auth_config = auth_config + self._channel_provider = channel_provider + + async def handle_send_to_conversation( + self, auth_header, conversation_id, activity + ) -> ResourceResponse: + claims_identity = await self._authenticate(auth_header) + return await self.on_send_to_conversation( + claims_identity, conversation_id, activity + ) + + async def handle_reply_to_activity( + self, auth_header, conversation_id, activity_id, activity + ) -> ResourceResponse: + claims_identity = await self._authenticate(auth_header) + return await self.on_reply_to_activity( + claims_identity, conversation_id, activity_id, activity + ) + + async def handle_update_activity( + self, auth_header, conversation_id, activity_id, activity + ) -> ResourceResponse: + claims_identity = await self._authenticate(auth_header) + return await self.on_update_activity( + claims_identity, conversation_id, activity_id, activity + ) + + async def handle_delete_activity(self, auth_header, conversation_id, activity_id): + claims_identity = await self._authenticate(auth_header) + await self.on_delete_activity(claims_identity, conversation_id, activity_id) + + async def handle_get_activity_members( + self, auth_header, conversation_id, activity_id + ) -> List[ChannelAccount]: + claims_identity = await self._authenticate(auth_header) + return await self.on_get_activity_members( + claims_identity, conversation_id, activity_id + ) + + async def handle_create_conversation( + self, auth_header, conversation_id, parameters: ConversationParameters + ) -> ConversationResourceResponse: + claims_identity = await self._authenticate(auth_header) + return await self.on_create_conversation( + claims_identity, conversation_id, parameters + ) + + async def handle_get_conversations( + self, auth_header, conversation_id, continuation_token: str = "" + ) -> ConversationsResult: + claims_identity = await self._authenticate(auth_header) + return await self.on_get_conversations( + claims_identity, conversation_id, continuation_token + ) + + async def handle_get_conversation_members( + self, auth_header, conversation_id + ) -> List[ChannelAccount]: + claims_identity = await self._authenticate(auth_header) + return await self.on_get_conversation_members(claims_identity, conversation_id) + + async def handle_get_conversation_paged_members( + self, + auth_header, + conversation_id, + page_size: int = 0, + continuation_token: str = "", + ) -> PagedMembersResult: + claims_identity = await self._authenticate(auth_header) + return await self.on_get_conversation_paged_members( + claims_identity, conversation_id, page_size, continuation_token + ) + + async def handle_delete_conversation_member( + self, auth_header, conversation_id, member_id + ): + claims_identity = await self._authenticate(auth_header) + await self.on_delete_conversation_member( + claims_identity, conversation_id, member_id + ) + + async def handle_send_conversation_history( + self, auth_header, conversation_id, transcript: Transcript + ) -> ResourceResponse: + claims_identity = await self._authenticate(auth_header) + return await self.on_send_conversation_history( + claims_identity, conversation_id, transcript + ) + + async def handle_upload_attachment( + self, auth_header, conversation_id, attachment_upload: AttachmentData + ) -> ResourceResponse: + claims_identity = await self._authenticate(auth_header) + return await self.on_upload_attachment( + claims_identity, conversation_id, attachment_upload + ) + + async def on_get_conversations( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + continuation_token: str = "", + ) -> ConversationsResult: + """ + get_conversations() API for Skill + + List the Conversations in which this bot has participated. + + GET from this method with a skip token + + The return value is a ConversationsResult, which contains an array of + ConversationMembers and a skip token. If the skip token is not empty, then + there are further values to be returned. Call this method again with the + returned token to get more values. + + Each ConversationMembers object contains the ID of the conversation and an + array of ChannelAccounts that describe the members of the conversation. + + :param claims_identity: + :param conversation_id: + :param continuation_token: + :return: + """ + raise NotImplementedError() + + async def on_create_conversation( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + parameters: ConversationParameters, + ) -> ConversationResourceResponse: + """ + create_conversation() API for Skill + + Create a new Conversation. + + POST to this method with a + * Bot being the bot creating the conversation + * IsGroup set to true if this is not a direct message (default is false) + * Array containing the members to include in the conversation + + The return value is a ResourceResponse which contains a conversation id + which is suitable for use + in the message payload and REST API uris. + + Most channels only support the semantics of bots initiating a direct + message conversation. An example of how to do that would be: + + var resource = await connector.conversations.CreateConversation(new + ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new + ChannelAccount("user1") } ); + await connect.Conversations.SendToConversationAsync(resource.Id, new + Activity() ... ) ; + + end. + + :param claims_identity: + :param conversation_id: + :param parameters: + :return: + """ + raise NotImplementedError() + + async def on_send_to_conversation( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity, + ) -> ResourceResponse: + """ + send_to_conversation() API for Skill + + This method allows you to send an activity to the end of a conversation. + + This is slightly different from ReplyToActivity(). + * SendToConversation(conversationId) - will append the activity to the end + of the conversation according to the timestamp or semantics of the channel. + * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply + to another activity, if the channel supports it. If the channel does not + support nested replies, ReplyToActivity falls back to SendToConversation. + + Use ReplyToActivity when replying to a specific activity in the + conversation. + + Use SendToConversation in all other cases. + + :param claims_identity: + :param conversation_id: + :param activity: + :return: + """ + raise NotImplementedError() + + async def on_send_conversation_history( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + transcript: Transcript, + ) -> ResourceResponse: + """ + send_conversation_history() API for Skill. + + This method allows you to upload the historic activities to the + conversation. + + Sender must ensure that the historic activities have unique ids and + appropriate timestamps. The ids are used by the client to deal with + duplicate activities and the timestamps are used by the client to render + the activities in the right order. + + :param claims_identity: + :param conversation_id: + :param transcript: + :return: + """ + raise NotImplementedError() + + async def on_update_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + """ + update_activity() API for Skill. + + Edit an existing activity. + + Some channels allow you to edit an existing activity to reflect the new + state of a bot conversation. + + For example, you can remove buttons after someone has clicked "Approve" + button. + + :param claims_identity: + :param conversation_id: + :param activity_id: + :param activity: + :return: + """ + raise NotImplementedError() + + async def on_reply_to_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + """ + reply_to_activity() API for Skill. + + This method allows you to reply to an activity. + + This is slightly different from SendToConversation(). + * SendToConversation(conversationId) - will append the activity to the end + of the conversation according to the timestamp or semantics of the channel. + * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply + to another activity, if the channel supports it. If the channel does not + support nested replies, ReplyToActivity falls back to SendToConversation. + + Use ReplyToActivity when replying to a specific activity in the + conversation. + + Use SendToConversation in all other cases. + + :param claims_identity: + :param conversation_id: + :param activity_id: + :param activity: + :return: + """ + raise NotImplementedError() + + async def on_delete_activity( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str, + ): + """ + delete_activity() API for Skill. + + Delete an existing activity. + + Some channels allow you to delete an existing activity, and if successful + this method will remove the specified activity. + + :param claims_identity: + :param conversation_id: + :param activity_id: + :return: + """ + raise NotImplementedError() + + async def on_get_conversation_members( + self, claims_identity: ClaimsIdentity, conversation_id: str, + ) -> List[ChannelAccount]: + """ + get_conversation_members() API for Skill. + + Enumerate the members of a conversation. + + This REST API takes a ConversationId and returns a list of ChannelAccount + objects representing the members of the conversation. + + :param claims_identity: + :param conversation_id: + :return: + """ + raise NotImplementedError() + + async def on_get_conversation_paged_members( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + page_size: int = None, + continuation_token: str = "", + ) -> PagedMembersResult: + """ + get_conversation_paged_members() API for Skill. + + Enumerate the members of a conversation one page at a time. + + This REST API takes a ConversationId. Optionally a page_size and/or + continuation_token can be provided. It returns a PagedMembersResult, which + contains an array + of ChannelAccounts representing the members of the conversation and a + continuation token that can be used to get more values. + + One page of ChannelAccounts records are returned with each call. The number + of records in a page may vary between channels and calls. The page_size + parameter can be used as + a suggestion. If there are no additional results the response will not + contain a continuation token. If there are no members in the conversation + the Members will be empty or not present in the response. + + A response to a request that has a continuation token from a prior request + may rarely return members from a previous request. + + :param claims_identity: + :param conversation_id: + :param page_size: + :param continuation_token: + :return: + """ + raise NotImplementedError() + + async def on_delete_conversation_member( + self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str, + ): + """ + delete_conversation_member() API for Skill. + + Deletes a member from a conversation. + + This REST API takes a ConversationId and a memberId (of type string) and + removes that member from the conversation. If that member was the last + member + of the conversation, the conversation will also be deleted. + + :param claims_identity: + :param conversation_id: + :param member_id: + :return: + """ + raise NotImplementedError() + + async def on_get_activity_members( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str, + ) -> List[ChannelAccount]: + """ + get_activity_members() API for Skill. + + Enumerate the members of an activity. + + This REST API takes a ConversationId and a ActivityId, returning an array + of ChannelAccount objects representing the members of the particular + activity in the conversation. + + :param claims_identity: + :param conversation_id: + :param activity_id: + :return: + """ + raise NotImplementedError() + + async def on_upload_attachment( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + attachment_upload: AttachmentData, + ) -> ResourceResponse: + """ + upload_attachment() API for Skill. + + Upload an attachment directly into a channel's blob storage. + + This is useful because it allows you to store data in a compliant store + when dealing with enterprises. + + The response is a ResourceResponse which contains an AttachmentId which is + suitable for using with the attachments API. + + :param claims_identity: + :param conversation_id: + :param attachment_upload: + :return: + """ + raise NotImplementedError() + + async def _authenticate(self, auth_header: str) -> ClaimsIdentity: + return await JwtTokenValidation.validate_auth_header( + auth_header, + self._credential_provider, + self._channel_provider.channel_service, + "unknown", + self._auth_config, + ) From d36aac6a1b8358a674cc124ee77bedd331055687 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 26 Nov 2019 09:35:15 -0800 Subject: [PATCH 2/8] return serialized response in controller --- .../core/integration/aiohttp_channel_service.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py index 9fa2aab1e..42676131a 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py @@ -18,45 +18,54 @@ async def deserialize_activity(request: Request) -> Activity: def channel_service_routes(handler: ChannelServiceHandler) -> RouteTableDef: + # pylint: disable=unused-variable routes = RouteTableDef() @routes.post("/{conversation_id}/activities") async def send_to_conversation(request: Request): activity = await deserialize_activity(request) - return await handler.handle_send_to_conversation( + result = await handler.handle_send_to_conversation( request.headers.get("Authorization"), request.match_info["conversation_id"], activity, ) + return Response(body=result.serialize()) + @routes.post("/{conversation_id}/activities/{activity_id}") async def reply_to_activity(request: Request): activity = await deserialize_activity(request) - return await handler.handle_reply_to_activity( + result = await handler.handle_reply_to_activity( request.headers.get("Authorization"), request.match_info["conversation_id"], request.match_info["activity_id"], activity, ) + return Response(body=result.serialize()) + @routes.put("/{conversation_id}/activities/{activity_id}") async def update_activity(request: Request): activity = await deserialize_activity(request) - return await handler.handle_update_activity( + result = await handler.handle_update_activity( request.headers.get("Authorization"), request.match_info["conversation_id"], request.match_info["activity_id"], activity, ) + return Response(body=result.serialize()) + @routes.delete("/{conversation_id}/activities/{activity_id}") async def delete_activity(request: Request): - return await handler.handle_delete_activity( + await handler.handle_delete_activity( request.headers.get("Authorization"), request.match_info["conversation_id"], request.match_info["activity_id"], ) + return Response() + @routes.get("/{conversation_id}/activities/{activity_id}/members") async def get_activity_members(request: Request): raise NotImplementedError("get_activity_members is not supported") From 0517074671f72f592c768c9b7a7da4dacd4c3d56 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 26 Nov 2019 13:55:32 -0800 Subject: [PATCH 3/8] Updates to controller --- .../integration/aiohttp_channel_service.py | 103 ++++++++++++++---- .../integration/channel_service_handler.py | 23 +--- 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py index 42676131a..71aa4f269 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py @@ -1,20 +1,38 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json +from typing import List, Union, Type from aiohttp.web import RouteTableDef, Request, Response - -from botbuilder.schema import Activity +from botbuilder.schema import ( + Activity, + AttachmentData, + ConversationParameters, + Transcript, +) +from msrest.serialization import Model from .channel_service_handler import ChannelServiceHandler -async def deserialize_activity(request: Request) -> Activity: +async def deserialize_from_body( + request: Request, target_model: Type[Model] +) -> Activity: if "application/json" in request.headers["Content-Type"]: body = await request.json() else: return Response(status=415) - return Activity().deserialize(body) + return target_model().deserialize(body) + + +def get_serialized_response(model_or_list: Union[Model, List[Model]]) -> Response: + if isinstance(model_or_list, Model): + json_obj = model_or_list.serialize() + else: + json_obj = [model.serialize() for model in model_or_list] + + return Response(body=json.dumps(json_obj), content_type="application/json") def channel_service_routes(handler: ChannelServiceHandler) -> RouteTableDef: @@ -23,18 +41,18 @@ def channel_service_routes(handler: ChannelServiceHandler) -> RouteTableDef: @routes.post("/{conversation_id}/activities") async def send_to_conversation(request: Request): - activity = await deserialize_activity(request) + activity = await deserialize_from_body(request, Activity) result = await handler.handle_send_to_conversation( request.headers.get("Authorization"), request.match_info["conversation_id"], activity, ) - return Response(body=result.serialize()) + return get_serialized_response(result) @routes.post("/{conversation_id}/activities/{activity_id}") async def reply_to_activity(request: Request): - activity = await deserialize_activity(request) + activity = await deserialize_from_body(request, Activity) result = await handler.handle_reply_to_activity( request.headers.get("Authorization"), request.match_info["conversation_id"], @@ -42,11 +60,11 @@ async def reply_to_activity(request: Request): activity, ) - return Response(body=result.serialize()) + return get_serialized_response(result) @routes.put("/{conversation_id}/activities/{activity_id}") async def update_activity(request: Request): - activity = await deserialize_activity(request) + activity = await deserialize_from_body(request, Activity) result = await handler.handle_update_activity( request.headers.get("Authorization"), request.match_info["conversation_id"], @@ -54,7 +72,7 @@ async def update_activity(request: Request): activity, ) - return Response(body=result.serialize()) + return get_serialized_response(result) @routes.delete("/{conversation_id}/activities/{activity_id}") async def delete_activity(request: Request): @@ -68,34 +86,79 @@ async def delete_activity(request: Request): @routes.get("/{conversation_id}/activities/{activity_id}/members") async def get_activity_members(request: Request): - raise NotImplementedError("get_activity_members is not supported") + result = await handler.handle_get_activity_members( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + ) + + return get_serialized_response(result) @routes.post("/") async def create_conversation(request: Request): - raise NotImplementedError("create_conversation is not supported") + conversation_parameters = deserialize_from_body(request, ConversationParameters) + result = await handler.handle_create_conversation( + request.headers.get("Authorization"), conversation_parameters + ) + + return get_serialized_response(result) @routes.get("/") async def get_conversation(request: Request): - raise NotImplementedError("get_conversation is not supported") + # TODO: continuation token? + result = await handler.handle_get_conversations( + request.headers.get("Authorization") + ) + + return get_serialized_response(result) @routes.get("/{conversation_id}/members") async def get_conversation_members(request: Request): - raise NotImplementedError("get_activity_members is not supported") + result = await handler.handle_get_conversation_members( + request.headers.get("Authorization"), request.match_info["conversation_id"], + ) + + return get_serialized_response(result) @routes.get("/{conversation_id}/pagedmembers") async def get_conversation_paged_members(request: Request): - raise NotImplementedError("get_conversation_paged_members is not supported") + # TODO: continuation token? page size? + result = await handler.handle_get_conversation_paged_members( + request.headers.get("Authorization"), request.match_info["conversation_id"], + ) + + return get_serialized_response(result) @routes.delete("/{conversation_id}/members/{member_id}") - async def delete_conversation_members(request: Request): - raise NotImplementedError("delete_conversation_members is not supported") + async def delete_conversation_member(request: Request): + result = await handler.handle_delete_conversation_member( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + request.match_info["member_id"], + ) + + return get_serialized_response(result) @routes.post("/{conversation_id}/activities/history") - async def get_conversation_history(request: Request): - raise NotImplementedError("get_conversation_history is not supported") + async def send_conversation_history(request: Request): + transcript = deserialize_from_body(request, Transcript) + result = await handler.handle_send_conversation_history( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + transcript, + ) + + return get_serialized_response(result) @routes.post("/{conversation_id}/attachments") async def upload_attachment(request: Request): - raise NotImplementedError("upload_attachment is not supported") + attachment_data = deserialize_from_body(request, AttachmentData) + result = await handler.handle_upload_attachment( + request.headers.get("Authorization"), + request.match_info["conversation_id"], + attachment_data, + ) + + return get_serialized_response(result) return routes diff --git a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py index 317259c19..a51398c8d 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py @@ -83,20 +83,16 @@ async def handle_get_activity_members( ) async def handle_create_conversation( - self, auth_header, conversation_id, parameters: ConversationParameters + self, auth_header, parameters: ConversationParameters ) -> ConversationResourceResponse: claims_identity = await self._authenticate(auth_header) - return await self.on_create_conversation( - claims_identity, conversation_id, parameters - ) + return await self.on_create_conversation(claims_identity, parameters) async def handle_get_conversations( - self, auth_header, conversation_id, continuation_token: str = "" + self, auth_header, continuation_token: str = "" ) -> ConversationsResult: claims_identity = await self._authenticate(auth_header) - return await self.on_get_conversations( - claims_identity, conversation_id, continuation_token - ) + return await self.on_get_conversations(claims_identity, continuation_token) async def handle_get_conversation_members( self, auth_header, conversation_id @@ -141,10 +137,7 @@ async def handle_upload_attachment( ) async def on_get_conversations( - self, - claims_identity: ClaimsIdentity, - conversation_id: str, - continuation_token: str = "", + self, claims_identity: ClaimsIdentity, continuation_token: str = "", ) -> ConversationsResult: """ get_conversations() API for Skill @@ -169,10 +162,7 @@ async def on_get_conversations( raise NotImplementedError() async def on_create_conversation( - self, - claims_identity: ClaimsIdentity, - conversation_id: str, - parameters: ConversationParameters, + self, claims_identity: ClaimsIdentity, parameters: ConversationParameters, ) -> ConversationResourceResponse: """ create_conversation() API for Skill @@ -200,7 +190,6 @@ async def on_create_conversation( end. :param claims_identity: - :param conversation_id: :param parameters: :return: """ From 0b4cf475f43f521de75c83b83f97467546aa7c84 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 26 Nov 2019 14:04:02 -0800 Subject: [PATCH 4/8] exporting integration module on core --- libraries/botbuilder-core/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botbuilder-core/setup.py b/libraries/botbuilder-core/setup.py index f7ab3ae09..5b667ab06 100644 --- a/libraries/botbuilder-core/setup.py +++ b/libraries/botbuilder-core/setup.py @@ -35,6 +35,7 @@ "botbuilder.core", "botbuilder.core.adapters", "botbuilder.core.inspection", + "botbuilder.core.integration", ], install_requires=REQUIRES, classifiers=[ From 76b7fbaf23f4b883343bd2556e4cae1a58a24b2b Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 26 Nov 2019 15:00:20 -0800 Subject: [PATCH 5/8] pylint: exporting integration module on core --- .../botbuilder/core/integration/aiohttp_channel_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py index 71aa4f269..349eaf25e 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py @@ -4,13 +4,13 @@ from typing import List, Union, Type from aiohttp.web import RouteTableDef, Request, Response +from msrest.serialization import Model from botbuilder.schema import ( Activity, AttachmentData, ConversationParameters, Transcript, ) -from msrest.serialization import Model from .channel_service_handler import ChannelServiceHandler From 7de8ef1faac76ffecab4070592d9958765bb66a7 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 2 Dec 2019 12:15:30 -0800 Subject: [PATCH 6/8] Protocol test working Python to Python --- .../botbuilder/core/integration/__init__.py | 4 +- .../integration/aiohttp_channel_service.py | 26 ++-- .../integration/bot_framework_http_client.py | 26 ++-- .../integration/channel_service_handler.py | 10 ++ .../auth/microsoft_app_credentials.py | 2 +- samples/experimental/test-protocol/app.py | 55 +++++++ samples/experimental/test-protocol/config.py | 17 +++ .../test-protocol/routing_handler.py | 134 ++++++++++++++++++ .../test-protocol/routing_id_factory.py | 22 +++ 9 files changed, 271 insertions(+), 25 deletions(-) create mode 100644 samples/experimental/test-protocol/app.py create mode 100644 samples/experimental/test-protocol/config.py create mode 100644 samples/experimental/test-protocol/routing_handler.py create mode 100644 samples/experimental/test-protocol/routing_id_factory.py diff --git a/libraries/botbuilder-core/botbuilder/core/integration/__init__.py b/libraries/botbuilder-core/botbuilder/core/integration/__init__.py index 8cebe1ad8..3a579402b 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/__init__.py @@ -5,12 +5,12 @@ # license information. # -------------------------------------------------------------------------- +from .aiohttp_channel_service import aiohttp_channel_service_routes from .bot_framework_http_client import BotFrameworkHttpClient from .channel_service_handler import ChannelServiceHandler -from .skill_conversation_id_factory import SkillConversationIdFactory __all__ = [ + "aiohttp_channel_service_routes", "BotFrameworkHttpClient", "ChannelServiceHandler", - "SkillConversationIdFactory", ] diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py index 349eaf25e..d5158251c 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py @@ -35,11 +35,11 @@ def get_serialized_response(model_or_list: Union[Model, List[Model]]) -> Respons return Response(body=json.dumps(json_obj), content_type="application/json") -def channel_service_routes(handler: ChannelServiceHandler) -> RouteTableDef: +def aiohttp_channel_service_routes(handler: ChannelServiceHandler, base_url: str = "") -> RouteTableDef: # pylint: disable=unused-variable routes = RouteTableDef() - @routes.post("/{conversation_id}/activities") + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities") async def send_to_conversation(request: Request): activity = await deserialize_from_body(request, Activity) result = await handler.handle_send_to_conversation( @@ -50,7 +50,7 @@ async def send_to_conversation(request: Request): return get_serialized_response(result) - @routes.post("/{conversation_id}/activities/{activity_id}") + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}") async def reply_to_activity(request: Request): activity = await deserialize_from_body(request, Activity) result = await handler.handle_reply_to_activity( @@ -62,7 +62,7 @@ async def reply_to_activity(request: Request): return get_serialized_response(result) - @routes.put("/{conversation_id}/activities/{activity_id}") + @routes.put(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}") async def update_activity(request: Request): activity = await deserialize_from_body(request, Activity) result = await handler.handle_update_activity( @@ -74,7 +74,7 @@ async def update_activity(request: Request): return get_serialized_response(result) - @routes.delete("/{conversation_id}/activities/{activity_id}") + @routes.delete(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}") async def delete_activity(request: Request): await handler.handle_delete_activity( request.headers.get("Authorization"), @@ -84,7 +84,7 @@ async def delete_activity(request: Request): return Response() - @routes.get("/{conversation_id}/activities/{activity_id}/members") + @routes.get(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}/members") async def get_activity_members(request: Request): result = await handler.handle_get_activity_members( request.headers.get("Authorization"), @@ -94,7 +94,7 @@ async def get_activity_members(request: Request): return get_serialized_response(result) - @routes.post("/") + @routes.post(base_url + "/") async def create_conversation(request: Request): conversation_parameters = deserialize_from_body(request, ConversationParameters) result = await handler.handle_create_conversation( @@ -103,7 +103,7 @@ async def create_conversation(request: Request): return get_serialized_response(result) - @routes.get("/") + @routes.get(base_url + "/") async def get_conversation(request: Request): # TODO: continuation token? result = await handler.handle_get_conversations( @@ -112,7 +112,7 @@ async def get_conversation(request: Request): return get_serialized_response(result) - @routes.get("/{conversation_id}/members") + @routes.get(base_url + "/v3/conversations/{conversation_id}/members") async def get_conversation_members(request: Request): result = await handler.handle_get_conversation_members( request.headers.get("Authorization"), request.match_info["conversation_id"], @@ -120,7 +120,7 @@ async def get_conversation_members(request: Request): return get_serialized_response(result) - @routes.get("/{conversation_id}/pagedmembers") + @routes.get(base_url + "/v3/conversations/{conversation_id}/pagedmembers") async def get_conversation_paged_members(request: Request): # TODO: continuation token? page size? result = await handler.handle_get_conversation_paged_members( @@ -129,7 +129,7 @@ async def get_conversation_paged_members(request: Request): return get_serialized_response(result) - @routes.delete("/{conversation_id}/members/{member_id}") + @routes.delete(base_url + "/v3/conversations/{conversation_id}/members/{member_id}") async def delete_conversation_member(request: Request): result = await handler.handle_delete_conversation_member( request.headers.get("Authorization"), @@ -139,7 +139,7 @@ async def delete_conversation_member(request: Request): return get_serialized_response(result) - @routes.post("/{conversation_id}/activities/history") + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/history") async def send_conversation_history(request: Request): transcript = deserialize_from_body(request, Transcript) result = await handler.handle_send_conversation_history( @@ -150,7 +150,7 @@ async def send_conversation_history(request: Request): return get_serialized_response(result) - @routes.post("/{conversation_id}/attachments") + @routes.post(base_url + "/v3/conversations/{conversation_id}/attachments") async def upload_attachment(request: Request): attachment_data = deserialize_from_body(request, AttachmentData) result = await handler.handle_upload_attachment( diff --git a/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py b/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py index 5d7f2f9a2..b6369a357 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py @@ -25,7 +25,7 @@ class BotFrameworkHttpClient: INVOKE_ACTIVITY_NAME = "SkillEvents.ChannelApiInvoke" _BOT_IDENTITY_KEY = "BotIdentity" - _APP_CREDENTIALS_CACHE: Dict[str:MicrosoftAppCredentials] = {} + _APP_CREDENTIALS_CACHE: Dict[str, MicrosoftAppCredentials] = {} def __init__( self, @@ -56,7 +56,11 @@ async def post_activity( raise RuntimeError("Unable to get appCredentials to connect to the skill") # Get token for the skill call - token = app_credentials.get_access_token() + token = ( + app_credentials.get_access_token() + if app_credentials.microsoft_app_id + else None + ) # Capture current activity settings before changing them. # TODO: DO we need to set the activity ID? (events that are created manually don't have it). @@ -67,17 +71,21 @@ async def post_activity( activity.conversation.id = conversation_id activity.service_url = service_url + headers_dict = { + "Content-type": "application/json; charset=utf-8", + } + if token: + headers_dict.update( + {"Authorization": f"Bearer:{token}",} + ) + json_content = json.dumps(activity.serialize()) resp = await self._session.post( - to_url, - data=json_content.encode("utf-8"), - headers={ - "Authorization": f"Bearer:{token}", - "Content-type": "application/json; charset=utf-8", - }, + to_url, data=json_content.encode("utf-8"), headers=headers_dict, ) resp.raise_for_status() - content = await resp.json() + data = (await resp.read()).decode() + content = json.loads(data) if data else None if content: return InvokeResponse(status=resp.status_code, body=content) diff --git a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py index a51398c8d..89350e0bd 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py @@ -439,6 +439,16 @@ async def on_upload_attachment( raise NotImplementedError() async def _authenticate(self, auth_header: str) -> ClaimsIdentity: + if not auth_header: + is_auth_disabled = await self._credential_provider.is_authentication_disabled() + if is_auth_disabled: + # In the scenario where Auth is disabled, we still want to have the + # IsAuthenticated flag set in the ClaimsIdentity. To do this requires + # adding in an empty claim. + return ClaimsIdentity({}, True) + + raise PermissionError() + return await JwtTokenValidation.validate_auth_header( auth_header, self._credential_provider, 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 317293ede..4191a2a65 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -87,7 +87,7 @@ def __init__( + Constants.TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH ) self.oauth_scope = oauth_scope or AUTH_SETTINGS["refreshScope"] - self.token_cache_key = app_id + "-cache" + self.token_cache_key = app_id + "-cache" if app_id else None # pylint: disable=arguments-differ def signed_session(self, session: requests.Session = None) -> requests.Session: diff --git a/samples/experimental/test-protocol/app.py b/samples/experimental/test-protocol/app.py new file mode 100644 index 000000000..ea5669940 --- /dev/null +++ b/samples/experimental/test-protocol/app.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp import web +from aiohttp.web import Request, Response + +from botframework.connector.auth import AuthenticationConfiguration, SimpleCredentialProvider +from botbuilder.core.integration import BotFrameworkHttpClient, aiohttp_channel_service_routes +from botbuilder.schema import Activity + +from config import DefaultConfig +from routing_id_factory import RoutingIdFactory +from routing_handler import RoutingHandler + + +CONFIG = DefaultConfig() +CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD) +CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER) +AUTH_CONFIG = AuthenticationConfiguration() + +TO_URI = CONFIG.NEXT +SERVICE_URL = CONFIG.SERVICE_URL + +FACTORY = RoutingIdFactory() + +ROUTING_HANDLER = RoutingHandler(FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG) + + +async def messages(req: Request) -> Response: + # Main bot message handler. + if "application/json" in req.headers["Content-Type"]: + body = await req.json() + else: + return Response(status=415) + + inbound_activity: Activity = Activity().deserialize(body) + + current_conversation_id = inbound_activity.conversation.id + current_service_url = inbound_activity.service_url + + next_conversation_id = FACTORY.create_skill_conversation_id(current_conversation_id, current_service_url) + + await CLIENT.post_activity(None, None, TO_URI, SERVICE_URL, next_conversation_id, inbound_activity) + return Response(status=201) + +APP = web.Application() + +APP.router.add_post("/api/messages", messages) +APP.router.add_routes(aiohttp_channel_service_routes(ROUTING_HANDLER, "/api/connector")) + +if __name__ == "__main__": + try: + web.run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/samples/experimental/test-protocol/config.py b/samples/experimental/test-protocol/config.py new file mode 100644 index 000000000..3821e1e50 --- /dev/null +++ b/samples/experimental/test-protocol/config.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +""" Bot Configuration """ + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3428 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + NEXT = "http://localhost:3978/api/messages" + SERVICE_URL = "http://localhost:3428/api/connector" diff --git a/samples/experimental/test-protocol/routing_handler.py b/samples/experimental/test-protocol/routing_handler.py new file mode 100644 index 000000000..0de21123b --- /dev/null +++ b/samples/experimental/test-protocol/routing_handler.py @@ -0,0 +1,134 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + +from botbuilder.core.integration import ChannelServiceHandler +from botbuilder.schema import ( + Activity, + ChannelAccount, + ConversationParameters, + ConversationResourceResponse, + ConversationsResult, + PagedMembersResult, + ResourceResponse +) +from botframework.connector.aio import ConnectorClient +from botframework.connector.auth import ( + AuthenticationConfiguration, + ChannelProvider, + ClaimsIdentity, + CredentialProvider, + MicrosoftAppCredentials +) + +from routing_id_factory import RoutingIdFactory + + +class RoutingHandler(ChannelServiceHandler): + def __init__( + self, + conversation_id_factory: RoutingIdFactory, + credential_provider: CredentialProvider, + auth_configuration: AuthenticationConfiguration, + channel_provider: ChannelProvider = None + ): + super().__init__(credential_provider, auth_configuration, channel_provider) + self._factory = conversation_id_factory + self._credentials = MicrosoftAppCredentials(None, None) + + async def on_reply_to_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id) + connector_client = self._get_connector_client(back_service_url) + activity.conversation.id = back_conversation_id + activity.service_url = back_service_url + + return await connector_client.conversations.send_to_conversation(back_conversation_id, activity) + + async def on_send_to_conversation( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity, + ) -> ResourceResponse: + back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id) + connector_client = self._get_connector_client(back_service_url) + activity.conversation.id = back_conversation_id + activity.service_url = back_service_url + + return await connector_client.conversations.send_to_conversation(back_conversation_id, activity) + + async def on_update_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id) + connector_client = self._get_connector_client(back_service_url) + activity.conversation.id = back_conversation_id + activity.service_url = back_service_url + + return await connector_client.conversations.update_activity(back_conversation_id, activity.id, activity) + + async def on_delete_activity( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str, + ): + back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id) + connector_client = self._get_connector_client(back_service_url) + + return await connector_client.conversations.delete_activity(back_conversation_id, activity_id) + + async def on_create_conversation( + self, claims_identity: ClaimsIdentity, parameters: ConversationParameters, + ) -> ConversationResourceResponse: + # This call will be used in Teams scenarios. + + # Scenario #1 - creating a thread with an activity in a Channel in a Team + # In order to know the serviceUrl in the case of Teams we would need to look it up based upon the + # TeamsChannelData. + # The inbound activity will contain the TeamsChannelData and so will the ConversationParameters. + + # Scenario #2 - starting a one on one conversation with a particular user + # - needs further analysis - + + back_service_url = "http://tempuri" + connector_client = self._get_connector_client(back_service_url) + + return await connector_client.conversations.create_conversation(parameters) + + async def on_delete_conversation_member( + self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str, + ): + return await super().on_delete_conversation_member(claims_identity, conversation_id, member_id) + + async def on_get_activity_members( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str, + ) -> List[ChannelAccount]: + return await super().on_get_activity_members(claims_identity, conversation_id, activity_id) + + async def on_get_conversation_members( + self, claims_identity: ClaimsIdentity, conversation_id: str, + ) -> List[ChannelAccount]: + return await super().on_get_conversation_members(claims_identity, conversation_id) + + async def on_get_conversations( + self, claims_identity: ClaimsIdentity, continuation_token: str = "", + ) -> ConversationsResult: + return await super().on_get_conversations(claims_identity, continuation_token) + + async def on_get_conversation_paged_members( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + page_size: int = None, + continuation_token: str = "", + ) -> PagedMembersResult: + return await super().on_get_conversation_paged_members(claims_identity, conversation_id, continuation_token) + + def _get_connector_client(self, service_url: str): + return ConnectorClient(self._credentials, service_url) diff --git a/samples/experimental/test-protocol/routing_id_factory.py b/samples/experimental/test-protocol/routing_id_factory.py new file mode 100644 index 000000000..c5ddb7524 --- /dev/null +++ b/samples/experimental/test-protocol/routing_id_factory.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from uuid import uuid4 +from typing import Dict, Tuple + + +class RoutingIdFactory: + def __init__(self): + self._forward_x_ref: Dict[str, str] = {} + self._backward_x_ref: Dict[str, Tuple[str, str]] = {} + + def create_skill_conversation_id(self, conversation_id: str, service_url: str) -> str: + result = self._forward_x_ref.get(conversation_id, str(uuid4())) + + self._forward_x_ref[conversation_id] = result + self._backward_x_ref[result] = (conversation_id, service_url) + + return result + + def get_conversation_info(self, encoded_conversation_id) -> Tuple[str, str]: + return self._backward_x_ref[encoded_conversation_id] From 32b633c52ea562b50cc550d238099184954223e7 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 2 Dec 2019 12:25:42 -0800 Subject: [PATCH 7/8] black: Protocol test working Python to Python --- .../integration/aiohttp_channel_service.py | 21 ++++++++++++++----- .../integration/channel_service_handler.py | 4 +++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py index d5158251c..d61c0f0eb 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py @@ -35,7 +35,9 @@ def get_serialized_response(model_or_list: Union[Model, List[Model]]) -> Respons return Response(body=json.dumps(json_obj), content_type="application/json") -def aiohttp_channel_service_routes(handler: ChannelServiceHandler, base_url: str = "") -> RouteTableDef: +def aiohttp_channel_service_routes( + handler: ChannelServiceHandler, base_url: str = "" +) -> RouteTableDef: # pylint: disable=unused-variable routes = RouteTableDef() @@ -50,7 +52,9 @@ async def send_to_conversation(request: Request): return get_serialized_response(result) - @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}") + @routes.post( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) async def reply_to_activity(request: Request): activity = await deserialize_from_body(request, Activity) result = await handler.handle_reply_to_activity( @@ -62,7 +66,9 @@ async def reply_to_activity(request: Request): return get_serialized_response(result) - @routes.put(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}") + @routes.put( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) async def update_activity(request: Request): activity = await deserialize_from_body(request, Activity) result = await handler.handle_update_activity( @@ -74,7 +80,9 @@ async def update_activity(request: Request): return get_serialized_response(result) - @routes.delete(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}") + @routes.delete( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) async def delete_activity(request: Request): await handler.handle_delete_activity( request.headers.get("Authorization"), @@ -84,7 +92,10 @@ async def delete_activity(request: Request): return Response() - @routes.get(base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}/members") + @routes.get( + base_url + + "/v3/conversations/{conversation_id}/activities/{activity_id}/members" + ) async def get_activity_members(request: Request): result = await handler.handle_get_activity_members( request.headers.get("Authorization"), diff --git a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py index 89350e0bd..9f6804973 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py @@ -440,7 +440,9 @@ async def on_upload_attachment( async def _authenticate(self, auth_header: str) -> ClaimsIdentity: if not auth_header: - is_auth_disabled = await self._credential_provider.is_authentication_disabled() + is_auth_disabled = ( + await self._credential_provider.is_authentication_disabled() + ) if is_auth_disabled: # In the scenario where Auth is disabled, we still want to have the # IsAuthenticated flag set in the ClaimsIdentity. To do this requires From 07e027325d17568d3a32497b337955125f88498e Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Thu, 5 Dec 2019 11:30:32 -0800 Subject: [PATCH 8/8] Squash commit: Protocol layer with auth, unit testing pending. --- .../botbuilder/core/bot_framework_adapter.py | 81 +- .../botbuilder/core/bot_state.py | 4 +- .../integration/bot_framework_http_client.py | 4 +- .../integration/channel_service_handler.py | 4 +- .../botbuilder/core/teams/__init__.py | 14 + .../core/teams/teams_activity_handler.py | 429 ++++ .../botbuilder/core/teams/teams_info.py | 118 ++ .../botbuilder/core/turn_context.py | 1 + .../tests/teams/simple_adapter.py | 60 + .../teams/test_teams_activity_handler.py | 764 +++++++ .../tests/test_bot_framework_adapter.py | 14 +- .../schema/_connector_client_enums.py | 1 + .../botbuilder/schema/_models.py | 2 +- .../botbuilder/schema/teams/__init__.py | 184 ++ .../botbuilder/schema/teams/_models.py | 1597 ++++++++++++++ .../botbuilder/schema/teams/_models_py3.py | 1878 +++++++++++++++++ .../botframework/connector/auth/__init__.py | 2 +- .../connector/auth/channel_validation.py | 26 +- .../botframework/connector/auth/constants.py | 31 - .../connector/auth/emulator_validation.py | 10 +- .../connector/auth/jwt_token_validation.py | 5 +- .../auth/microsoft_app_credentials.py | 37 +- .../botframework/connector/models/__init__.py | 1 + .../botframework/connector/teams/__init__.py | 17 + .../connector/teams/operations/__init__.py | 16 + .../teams/operations/teams_operations.py | 147 ++ .../connector/teams/teams_connector_client.py | 83 + .../botframework/connector/teams/version.py | 12 + .../tests/test_microsoft_app_credentials.py | 4 +- samples/experimental/test-protocol/app.py | 2 +- samples/experimental/test-protocol/config.py | 1 + .../activity-update-and-delete/README.md | 30 + scenarios/activity-update-and-delete/app.py | 92 + .../bots/__init__.py | 6 + .../bots/activity_update_and_delete_bot.py | 33 + .../activity-update-and-delete/config.py | 13 + .../requirements.txt | 2 + .../teams_app_manifest/color.png | Bin 0 -> 1229 bytes .../teams_app_manifest/manifest.json | 43 + .../teams_app_manifest/outline.png | Bin 0 -> 383 bytes scenarios/conversation-update/README.md | 30 + scenarios/conversation-update/app.py | 92 + .../conversation-update/bots/__init__.py | 6 + .../bots/conversation_update_bot.py | 56 + scenarios/conversation-update/config.py | 13 + .../conversation-update/requirements.txt | 2 + .../teams_app_manifest/color.png | Bin 0 -> 1229 bytes .../teams_app_manifest/manifest.json | 43 + .../teams_app_manifest/outline.png | Bin 0 -> 383 bytes scenarios/mentions/README.md | 30 + scenarios/mentions/app.py | 92 + scenarios/mentions/bots/__init__.py | 6 + scenarios/mentions/bots/mention_bot.py | 21 + scenarios/mentions/config.py | 13 + scenarios/mentions/requirements.txt | 2 + .../mentions/teams_app_manifest/color.png | Bin 0 -> 1229 bytes .../mentions/teams_app_manifest/manifest.json | 43 + .../mentions/teams_app_manifest/outline.png | Bin 0 -> 383 bytes scenarios/message-reactions/README.md | 30 + scenarios/message-reactions/activity_log.py | 27 + scenarios/message-reactions/app.py | 94 + scenarios/message-reactions/bots/__init__.py | 6 + .../bots/message_reaction_bot.py | 60 + scenarios/message-reactions/config.py | 13 + scenarios/message-reactions/requirements.txt | 2 + .../teams_app_manifest/color.png | Bin 0 -> 1229 bytes .../teams_app_manifest/manifest.json | 43 + .../teams_app_manifest/outline.png | Bin 0 -> 383 bytes .../message-reactions/threading_helper.py | 169 ++ scenarios/roster/README.md | 30 + scenarios/roster/app.py | 92 + scenarios/roster/bots/__init__.py | 6 + scenarios/roster/bots/roster_bot.py | 66 + scenarios/roster/config.py | 13 + scenarios/roster/requirements.txt | 2 + scenarios/roster/teams_app_manifest/color.png | Bin 0 -> 1229 bytes .../roster/teams_app_manifest/manifest.json | 42 + .../roster/teams_app_manifest/outline.png | Bin 0 -> 383 bytes 78 files changed, 6812 insertions(+), 100 deletions(-) create mode 100644 libraries/botbuilder-core/botbuilder/core/teams/__init__.py create mode 100644 libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py create mode 100644 libraries/botbuilder-core/botbuilder/core/teams/teams_info.py create mode 100644 libraries/botbuilder-core/tests/teams/simple_adapter.py create mode 100644 libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/_models.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py delete mode 100644 libraries/botframework-connector/botframework/connector/auth/constants.py create mode 100644 libraries/botframework-connector/botframework/connector/teams/__init__.py create mode 100644 libraries/botframework-connector/botframework/connector/teams/operations/__init__.py create mode 100644 libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py create mode 100644 libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py create mode 100644 libraries/botframework-connector/botframework/connector/teams/version.py create mode 100644 scenarios/activity-update-and-delete/README.md create mode 100644 scenarios/activity-update-and-delete/app.py create mode 100644 scenarios/activity-update-and-delete/bots/__init__.py create mode 100644 scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py create mode 100644 scenarios/activity-update-and-delete/config.py create mode 100644 scenarios/activity-update-and-delete/requirements.txt create mode 100644 scenarios/activity-update-and-delete/teams_app_manifest/color.png create mode 100644 scenarios/activity-update-and-delete/teams_app_manifest/manifest.json create mode 100644 scenarios/activity-update-and-delete/teams_app_manifest/outline.png create mode 100644 scenarios/conversation-update/README.md create mode 100644 scenarios/conversation-update/app.py create mode 100644 scenarios/conversation-update/bots/__init__.py create mode 100644 scenarios/conversation-update/bots/conversation_update_bot.py create mode 100644 scenarios/conversation-update/config.py create mode 100644 scenarios/conversation-update/requirements.txt create mode 100644 scenarios/conversation-update/teams_app_manifest/color.png create mode 100644 scenarios/conversation-update/teams_app_manifest/manifest.json create mode 100644 scenarios/conversation-update/teams_app_manifest/outline.png create mode 100644 scenarios/mentions/README.md create mode 100644 scenarios/mentions/app.py create mode 100644 scenarios/mentions/bots/__init__.py create mode 100644 scenarios/mentions/bots/mention_bot.py create mode 100644 scenarios/mentions/config.py create mode 100644 scenarios/mentions/requirements.txt create mode 100644 scenarios/mentions/teams_app_manifest/color.png create mode 100644 scenarios/mentions/teams_app_manifest/manifest.json create mode 100644 scenarios/mentions/teams_app_manifest/outline.png create mode 100644 scenarios/message-reactions/README.md create mode 100644 scenarios/message-reactions/activity_log.py create mode 100644 scenarios/message-reactions/app.py create mode 100644 scenarios/message-reactions/bots/__init__.py create mode 100644 scenarios/message-reactions/bots/message_reaction_bot.py create mode 100644 scenarios/message-reactions/config.py create mode 100644 scenarios/message-reactions/requirements.txt create mode 100644 scenarios/message-reactions/teams_app_manifest/color.png create mode 100644 scenarios/message-reactions/teams_app_manifest/manifest.json create mode 100644 scenarios/message-reactions/teams_app_manifest/outline.png create mode 100644 scenarios/message-reactions/threading_helper.py create mode 100644 scenarios/roster/README.md create mode 100644 scenarios/roster/app.py create mode 100644 scenarios/roster/bots/__init__.py create mode 100644 scenarios/roster/bots/roster_bot.py create mode 100644 scenarios/roster/config.py create mode 100644 scenarios/roster/requirements.txt create mode 100644 scenarios/roster/teams_app_manifest/color.png create mode 100644 scenarios/roster/teams_app_manifest/manifest.json create mode 100644 scenarios/roster/teams_app_manifest/outline.png diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 5a38be990..bf3443c6e 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -12,19 +12,23 @@ ConversationAccount, ConversationParameters, ConversationReference, - ResourceResponse, TokenResponse, + ResourceResponse, ) from botframework.connector import Channels, EmulatorApiClient from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ( + AuthenticationConfiguration, AuthenticationConstants, ChannelValidation, + ChannelProvider, + ClaimsIdentity, GovernmentChannelValidation, GovernmentConstants, MicrosoftAppCredentials, JwtTokenValidation, SimpleCredentialProvider, + SkillValidation, ) from botframework.connector.token_api import TokenApiClient from botframework.connector.token_api.models import TokenStatus @@ -37,6 +41,7 @@ USER_AGENT = f"Microsoft-BotFramework/3.1 (BotBuilder Python/{__version__})" OAUTH_ENDPOINT = "https://api.botframework.com" US_GOV_OAUTH_ENDPOINT = "https://api.botframework.azure.us" +BOT_IDENTITY_KEY = "BotIdentity" class TokenExchangeState(Model): @@ -72,6 +77,8 @@ def __init__( oauth_endpoint: str = None, open_id_metadata: str = None, channel_service: str = None, + channel_provider: ChannelProvider = None, + auth_configuration: AuthenticationConfiguration = None, ): self.app_id = app_id self.app_password = app_password @@ -79,6 +86,8 @@ def __init__( self.oauth_endpoint = oauth_endpoint self.open_id_metadata = open_id_metadata self.channel_service = channel_service + self.channel_provider = channel_provider + self.auth_configuration = auth_configuration or AuthenticationConfiguration() class BotFrameworkAdapter(BotAdapter, UserTokenProvider): @@ -90,6 +99,7 @@ def __init__(self, settings: BotFrameworkAdapterSettings): self.settings.channel_service = self.settings.channel_service or os.environ.get( AuthenticationConstants.CHANNEL_SERVICE ) + self.settings.open_id_metadata = ( self.settings.open_id_metadata or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY) @@ -163,7 +173,7 @@ async def create_conversation( # Create conversation parameters = ConversationParameters(bot=reference.bot) - client = self.create_connector_client(reference.service_url) + client = await self.create_connector_client(reference.service_url) # Mix in the tenant ID if specified. This is required for MS Teams. if reference.conversation is not None and reference.conversation.tenant_id: @@ -207,8 +217,9 @@ async def process_activity(self, req, auth_header: str, logic: Callable): activity = await self.parse_request(req) auth_header = auth_header or "" - await self.authenticate_request(activity, auth_header) + identity = await self.authenticate_request(activity, auth_header) context = self.create_context(activity) + context.turn_state[BOT_IDENTITY_KEY] = identity # Fix to assign tenant_id from channelData to Conversation.tenant_id. # MS Teams currently sends the tenant ID in channelData and the correct behavior is to expose @@ -228,7 +239,9 @@ async def process_activity(self, req, auth_header: str, logic: Callable): return await self.run_pipeline(context, logic) - async def authenticate_request(self, request: Activity, auth_header: str): + async def authenticate_request( + self, request: Activity, auth_header: str + ) -> ClaimsIdentity: """ Allows for the overriding of authentication in unit tests. :param request: @@ -240,11 +253,14 @@ async def authenticate_request(self, request: Activity, auth_header: str): auth_header, self._credential_provider, self.settings.channel_service, + self.settings.auth_configuration, ) if not claims.is_authenticated: raise Exception("Unauthorized Access. Request is not authorized") + return claims + def create_context(self, activity): """ Allows for the overriding of the context object in unit tests and derived adapters. @@ -306,7 +322,8 @@ async def update_activity(self, context: TurnContext, activity: Activity): :return: """ try: - client = self.create_connector_client(activity.service_url) + identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) + client = await self.create_connector_client(activity.service_url, identity) return await client.conversations.update_activity( activity.conversation.id, activity.id, activity ) @@ -324,7 +341,8 @@ async def delete_activity( :return: """ try: - client = self.create_connector_client(reference.service_url) + identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) + client = await self.create_connector_client(reference.service_url, identity) await client.conversations.delete_activity( reference.conversation.id, reference.activity_id ) @@ -365,7 +383,10 @@ async def send_activities( "BotFrameworkAdapter.send_activity(): conversation.id can not be None." ) - client = self.create_connector_client(activity.service_url) + identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) + client = await self.create_connector_client( + activity.service_url, identity + ) if activity.type == "trace" and activity.channel_id != "emulator": pass elif activity.reply_to_id: @@ -409,7 +430,8 @@ async def delete_conversation_member( ) service_url = context.activity.service_url conversation_id = context.activity.conversation.id - client = self.create_connector_client(service_url) + identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) + client = await self.create_connector_client(service_url, identity) return await client.conversations.delete_conversation_member( conversation_id, member_id ) @@ -446,7 +468,8 @@ async def get_activity_members(self, context: TurnContext, activity_id: str): ) service_url = context.activity.service_url conversation_id = context.activity.conversation.id - client = self.create_connector_client(service_url) + identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) + client = await self.create_connector_client(service_url, identity) return await client.conversations.get_activity_members( conversation_id, activity_id ) @@ -474,7 +497,8 @@ async def get_conversation_members(self, context: TurnContext): ) service_url = context.activity.service_url conversation_id = context.activity.conversation.id - client = self.create_connector_client(service_url) + identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) + client = await self.create_connector_client(service_url, identity) return await client.conversations.get_conversation_members(conversation_id) except Exception as error: raise error @@ -488,7 +512,7 @@ async def get_conversations(self, service_url: str, continuation_token: str = No :param continuation_token: :return: """ - client = self.create_connector_client(service_url) + client = await self.create_connector_client(service_url) return await client.conversations.get_conversations(continuation_token) async def get_user_token( @@ -595,13 +619,44 @@ async def get_aad_tokens( user_id, connection_name, context.activity.channel_id, resource_urls ) - def create_connector_client(self, service_url: str) -> ConnectorClient: + async def create_connector_client( + self, service_url: str, identity: ClaimsIdentity = None + ) -> ConnectorClient: """ Allows for mocking of the connector client in unit tests. :param service_url: + :param identity: :return: """ - client = ConnectorClient(self._credentials, base_url=service_url) + if identity: + bot_app_id_claim = identity.claims.get( + AuthenticationConstants.AUDIENCE_CLAIM + ) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM) + + credentials = None + if bot_app_id_claim and SkillValidation.is_skill_claim(identity.claims): + scope = JwtTokenValidation.get_app_id_from_claims(identity.claims) + + password = await self._credential_provider.get_app_password( + bot_app_id_claim + ) + credentials = MicrosoftAppCredentials( + bot_app_id_claim, password, oauth_scope=scope + ) + if ( + self.settings.channel_provider + and self.settings.channel_provider.is_government() + ): + credentials.oauth_endpoint = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL + ) + credentials.oauth_scope = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + else: + credentials = self._credentials + + client = ConnectorClient(credentials, base_url=service_url) client.config.add_user_agent(USER_AGENT) return client diff --git a/libraries/botbuilder-core/botbuilder/core/bot_state.py b/libraries/botbuilder-core/botbuilder/core/bot_state.py index dc835a9bd..4e615dda0 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_state.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_state.py @@ -4,6 +4,7 @@ from abc import abstractmethod from copy import deepcopy from typing import Callable, Dict, Union +from jsonpickle.pickler import Pickler from botbuilder.core.state_property_accessor import StatePropertyAccessor from .turn_context import TurnContext from .storage import Storage @@ -24,8 +25,7 @@ def is_changed(self) -> bool: return self.hash != self.compute_hash(self.state) def compute_hash(self, obj: object) -> str: - # TODO: Should this be compatible with C# JsonConvert.SerializeObject ? - return str(obj) + return str(Pickler().flatten(obj)) class BotState(PropertyManager): diff --git a/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py b/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py index b6369a357..52a13230b 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/bot_framework_http_client.py @@ -76,7 +76,7 @@ async def post_activity( } if token: headers_dict.update( - {"Authorization": f"Bearer:{token}",} + {"Authorization": f"Bearer {token}",} ) json_content = json.dumps(activity.serialize()) @@ -111,7 +111,7 @@ async def _get_app_credentials( app_credentials = MicrosoftAppCredentials( app_id, app_password, oauth_scope=oauth_scope ) - if self._channel_provider.is_government(): + if self._channel_provider and self._channel_provider.is_government(): app_credentials.oauth_endpoint = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL ) diff --git a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py index 9f6804973..4b9222de7 100644 --- a/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/integration/channel_service_handler.py @@ -454,7 +454,7 @@ async def _authenticate(self, auth_header: str) -> ClaimsIdentity: return await JwtTokenValidation.validate_auth_header( auth_header, self._credential_provider, - self._channel_provider.channel_service, + self._channel_provider, "unknown", - self._auth_config, + auth_configuration=self._auth_config, ) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/__init__.py b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py new file mode 100644 index 000000000..1b9242875 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py @@ -0,0 +1,14 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from .teams_activity_handler import TeamsActivityHandler +from .teams_info import TeamsInfo + +__all__ = [ + "TeamsActivityHandler", + "TeamsInfo", +] diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py new file mode 100644 index 000000000..139f45859 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -0,0 +1,429 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from http import HTTPStatus +from botbuilder.schema import Activity, ActivityTypes, ChannelAccount +from botbuilder.core.turn_context import TurnContext +from botbuilder.core import ActivityHandler, InvokeResponse, BotFrameworkAdapter +from botbuilder.schema.teams import ( + AppBasedLinkQuery, + TeamInfo, + ChannelInfo, + FileConsentCardResponse, + TeamsChannelData, + TeamsChannelAccount, + MessagingExtensionAction, + MessagingExtensionQuery, + O365ConnectorCardActionQuery, + TaskModuleRequest, +) +from botframework.connector import Channels + + +class TeamsActivityHandler(ActivityHandler): + async def on_turn(self, turn_context: TurnContext): + if turn_context is None: + raise TypeError("ActivityHandler.on_turn(): turn_context cannot be None.") + + if not getattr(turn_context, "activity", None): + raise TypeError( + "ActivityHandler.on_turn(): turn_context must have a non-None activity." + ) + + if not getattr(turn_context.activity, "type", None): + raise TypeError( + "ActivityHandler.on_turn(): turn_context activity must have a non-None type." + ) + + if turn_context.activity.type == ActivityTypes.invoke: + invoke_response = await self.on_invoke_activity(turn_context) + if invoke_response and not turn_context.turn_state.get( + BotFrameworkAdapter._INVOKE_RESPONSE_KEY # pylint: disable=protected-access + ): + await turn_context.send_activity( + Activity(value=invoke_response, type=ActivityTypes.invoke_response) + ) + return + + await super().on_turn(turn_context) + + async def on_invoke_activity(self, turn_context: TurnContext): + try: + if ( + not turn_context.activity.name + and turn_context.activity.channel_id == Channels.ms_teams + ): + return await self.on_teams_card_action_invoke_activity(turn_context) + + if turn_context.activity.name == "signin/verifyState": + await self.on_teams_signin_verify_state(turn_context) + return self._create_invoke_response() + + if turn_context.activity.name == "fileConsent/invoke": + return await self.on_teams_file_consent( + turn_context, FileConsentCardResponse(**turn_context.activity.value) + ) + + if turn_context.activity.name == "actionableMessage/executeAction": + await self.on_teams_o365_connector_card_action( + turn_context, + O365ConnectorCardActionQuery(**turn_context.activity.value), + ) + return self._create_invoke_response() + + if turn_context.activity.name == "composeExtension/queryLink": + return self._create_invoke_response( + await self.on_teams_app_based_link_query( + turn_context, AppBasedLinkQuery(**turn_context.activity.value) + ) + ) + + if turn_context.activity.name == "composeExtension/query": + return self._create_invoke_response( + await self.on_teams_messaging_extension_query( + turn_context, + MessagingExtensionQuery(**turn_context.activity.value), + ) + ) + + if turn_context.activity.name == "composeExtension/selectItem": + return self._create_invoke_response( + await self.on_teams_messaging_extension_select_item( + turn_context, turn_context.activity.value + ) + ) + + if turn_context.activity.name == "composeExtension/submitAction": + return self._create_invoke_response( + await self.on_teams_messaging_extension_submit_action_dispatch( + turn_context, + MessagingExtensionAction(**turn_context.activity.value), + ) + ) + + if turn_context.activity.name == "composeExtension/fetchTask": + return self._create_invoke_response( + await self.on_teams_messaging_extension_fetch_task( + turn_context, + MessagingExtensionAction(**turn_context.activity.value), + ) + ) + + if turn_context.activity.name == "composeExtension/querySettingUrl": + return self._create_invoke_response( + await self.on_teams_messaging_extension_configuration_query_settings_url( + turn_context, + MessagingExtensionQuery(**turn_context.activity.value), + ) + ) + + if turn_context.activity.name == "composeExtension/setting": + await self.on_teams_messaging_extension_configuration_setting( + turn_context, turn_context.activity.value + ) + return self._create_invoke_response() + + if turn_context.activity.name == "composeExtension/onCardButtonClicked": + await self.on_teams_messaging_extension_card_button_clicked( + turn_context, turn_context.activity.value + ) + return self._create_invoke_response() + + if turn_context.activity.name == "task/fetch": + return self._create_invoke_response( + await self.on_teams_task_module_fetch( + turn_context, TaskModuleRequest(**turn_context.activity.value) + ) + ) + + if turn_context.activity.name == "task/submit": + return self._create_invoke_response( + await self.on_teams_task_module_submit( + turn_context, TaskModuleRequest(**turn_context.activity.value) + ) + ) + + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + except _InvokeResponseException as err: + return err.create_invoke_response() + + async def on_teams_card_action_invoke_activity(self, turn_context: TurnContext): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_signin_verify_state(self, turn_context: TurnContext): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_file_consent( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ): + if file_consent_card_response.action == "accept": + await self.on_teams_file_consent_accept_activity( + turn_context, file_consent_card_response + ) + return self._create_invoke_response() + + if file_consent_card_response.action == "decline": + await self.on_teams_file_consent_decline_activity( + turn_context, file_consent_card_response + ) + return self._create_invoke_response() + + raise _InvokeResponseException( + HTTPStatus.BAD_REQUEST, + f"{file_consent_card_response.action} is not a supported Action.", + ) + + async def on_teams_file_consent_accept_activity( # pylint: disable=unused-argument + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_file_consent_decline_activity( # pylint: disable=unused-argument + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_o365_connector_card_action( # pylint: disable=unused-argument + self, turn_context: TurnContext, query: O365ConnectorCardActionQuery + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_app_based_link_query( # pylint: disable=unused-argument + self, turn_context: TurnContext, query: AppBasedLinkQuery + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_query( # pylint: disable=unused-argument + self, turn_context: TurnContext, query: MessagingExtensionQuery + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_select_item( # pylint: disable=unused-argument + self, turn_context: TurnContext, query + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_submit_action_dispatch( + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + if not action.bot_message_preview_action: + return await self.on_teams_messaging_extension_submit_action_activity( + turn_context, action + ) + + if action.bot_message_preview_action == "edit": + return await self.on_teams_messaging_extension_bot_message_preview_edit_activity( + turn_context, action + ) + + if action.bot_message_preview_action == "send": + return await self.on_teams_messaging_extension_bot_message_send_activity( + turn_context, action + ) + + raise _InvokeResponseException( + status_code=HTTPStatus.BAD_REQUEST, + body=f"{action.bot_message_preview_action} is not a supported BotMessagePreviewAction", + ) + + async def on_teams_messaging_extension_bot_message_preview_edit_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext, action + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_bot_message_send_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext, action + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_submit_action_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_fetch_task( # pylint: disable=unused-argument + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_configuration_query_settings_url( # pylint: disable=unused-argument + self, turn_context: TurnContext, query: MessagingExtensionQuery + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_configuration_setting( # pylint: disable=unused-argument + self, turn_context: TurnContext, settings + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_messaging_extension_card_button_clicked( # pylint: disable=unused-argument + self, turn_context: TurnContext, card_data + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_task_module_fetch( # pylint: disable=unused-argument + self, turn_context: TurnContext, task_module_request: TaskModuleRequest + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_task_module_submit( # pylint: disable=unused-argument + self, turn_context: TurnContext, task_module_request: TaskModuleRequest + ): + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_conversation_update_activity(self, turn_context: TurnContext): + + if turn_context.activity.channel_id == Channels.ms_teams: + channel_data = TeamsChannelData(**turn_context.activity.channel_data) + if turn_context.activity.members_added: + return await self.on_teams_members_added_dispatch_activity( + turn_context.activity.members_added, channel_data.team, turn_context + ) + + if turn_context.activity.members_removed: + return await self.on_teams_members_removed_dispatch_activity( + turn_context.activity.members_removed, + channel_data.team, + turn_context, + ) + + if channel_data: + if channel_data.event_type == "channelCreated": + return await self.on_teams_channel_created_activity( + ChannelInfo(**channel_data.channel), + channel_data.team, + turn_context, + ) + if channel_data.event_type == "channelDeleted": + return await self.on_teams_channel_deleted_activity( + channel_data.channel, channel_data.team, turn_context + ) + if channel_data.event_type == "channelRenamed": + return await self.on_teams_channel_renamed_activity( + channel_data.channel, channel_data.team, turn_context + ) + if channel_data.event_type == "teamRenamed": + return await self.on_teams_team_renamed_activity( + channel_data.team, turn_context + ) + return await super().on_conversation_update_activity(turn_context) + + return await super().on_conversation_update_activity(turn_context) + + async def on_teams_channel_created_activity( # pylint: disable=unused-argument + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ): + return + + async def on_teams_team_renamed_activity( # pylint: disable=unused-argument + self, team_info: TeamInfo, turn_context: TurnContext + ): + return + + async def on_teams_members_added_dispatch_activity( # pylint: disable=unused-argument + self, + members_added: [ChannelAccount], + team_info: TeamInfo, + turn_context: TurnContext, + ): + """ + team_members = {} + team_members_added = [] + for member in members_added: + if member.additional_properties != {}: + team_members_added.append(TeamsChannelAccount(member)) + else: + if team_members == {}: + result = await TeamsInfo.get_members_async(turn_context) + team_members = { i.id : i for i in result } + + if member.id in team_members: + team_members_added.append(member) + else: + newTeamsChannelAccount = TeamsChannelAccount( + id=member.id, + name = member.name, + aad_object_id = member.aad_object_id, + role = member.role + ) + team_members_added.append(newTeamsChannelAccount) + + return await self.on_teams_members_added_activity(teams_members_added, team_info, turn_context) + """ + team_accounts_added = [] + for member in members_added: + # TODO: fix this + new_account_json = member.serialize() + if "additional_properties" in new_account_json: + del new_account_json["additional_properties"] + member = TeamsChannelAccount(**new_account_json) + team_accounts_added.append(member) + return await self.on_teams_members_added_activity( + team_accounts_added, turn_context + ) + + async def on_teams_members_added_activity( + self, teams_members_added: [TeamsChannelAccount], turn_context: TurnContext + ): + teams_members_added = [ + ChannelAccount(**member.serialize()) for member in teams_members_added + ] + return await super().on_members_added_activity( + teams_members_added, turn_context + ) + + async def on_teams_members_removed_dispatch_activity( # pylint: disable=unused-argument + self, + members_removed: [ChannelAccount], + team_info: TeamInfo, + turn_context: TurnContext, + ): + teams_members_removed = [] + for member in members_removed: + # TODO: fix this + new_account_json = member.serialize() + if "additional_properties" in new_account_json: + del new_account_json["additional_properties"] + teams_members_removed.append(TeamsChannelAccount(**new_account_json)) + + return await self.on_teams_members_removed_activity( + teams_members_removed, turn_context + ) + + async def on_teams_members_removed_activity( + self, teams_members_removed: [TeamsChannelAccount], turn_context: TurnContext + ): + members_removed = [ + ChannelAccount(**member.serialize()) for member in teams_members_removed + ] + return await super().on_members_removed_activity(members_removed, turn_context) + + async def on_teams_channel_deleted_activity( # pylint: disable=unused-argument + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ): + return # Task.CompleteTask + + async def on_teams_channel_renamed_activity( # pylint: disable=unused-argument + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ): + return # Task.CompleteTask + + @staticmethod + def _create_invoke_response(body: object = None) -> InvokeResponse: + return InvokeResponse(status=int(HTTPStatus.OK), body=body) + + +class _InvokeResponseException(Exception): + def __init__(self, status_code: HTTPStatus, body: object = None): + super(_InvokeResponseException, self).__init__() + self._status_code = status_code + self._body = body + + def create_invoke_response(self) -> InvokeResponse: + return InvokeResponse(status=int(self._status_code), body=self._body) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py new file mode 100644 index 000000000..b547180d0 --- /dev/null +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List +from botbuilder.core.turn_context import TurnContext +from botbuilder.schema.teams import ( + ChannelInfo, + TeamDetails, + TeamsChannelData, + TeamsChannelAccount, +) +from botframework.connector.aio import ConnectorClient +from botframework.connector.teams.teams_connector_client import TeamsConnectorClient + + +class TeamsInfo: + @staticmethod + def get_team_details(turn_context: TurnContext, team_id: str = "") -> TeamDetails: + if not team_id: + team_id = TeamsInfo.get_team_id(turn_context) + + if not team_id: + raise TypeError( + "TeamsInfo.get_team_details: method is only valid within the scope of MS Teams Team." + ) + + return TeamsInfo.get_teams_connector_client( + turn_context + ).teams.get_team_details(team_id) + + @staticmethod + def get_team_channels( + turn_context: TurnContext, team_id: str = "" + ) -> List[ChannelInfo]: + if not team_id: + team_id = TeamsInfo.get_team_id(turn_context) + + if not team_id: + raise TypeError( + "TeamsInfo.get_team_channels: method is only valid within the scope of MS Teams Team." + ) + + return ( + TeamsInfo.get_teams_connector_client(turn_context) + .teams.get_teams_channels(team_id) + .conversations + ) + + @staticmethod + async def get_team_members(turn_context: TurnContext, team_id: str = ""): + if not team_id: + team_id = TeamsInfo.get_team_id(turn_context) + + if not team_id: + raise TypeError( + "TeamsInfo.get_team_members: method is only valid within the scope of MS Teams Team." + ) + + return await TeamsInfo._get_members( + TeamsInfo._get_connector_client(turn_context), + turn_context.activity.conversation.id, + ) + + @staticmethod + async def get_members(turn_context: TurnContext) -> List[TeamsChannelAccount]: + team_id = TeamsInfo.get_team_id(turn_context) + if not team_id: + conversation_id = turn_context.activity.conversation.id + return await TeamsInfo._get_members( + TeamsInfo._get_connector_client(turn_context), conversation_id + ) + + return await TeamsInfo.get_team_members(turn_context, team_id) + + @staticmethod + def get_teams_connector_client(turn_context: TurnContext) -> TeamsConnectorClient: + connector_client = TeamsInfo._get_connector_client(turn_context) + return TeamsConnectorClient( + connector_client.config.credentials, turn_context.activity.service_url + ) + + # TODO: should have access to adapter's credentials + # return TeamsConnectorClient(turn_context.adapter._credentials, turn_context.activity.service_url) + + @staticmethod + def get_team_id(turn_context: TurnContext): + channel_data = TeamsChannelData(**turn_context.activity.channel_data) + if channel_data.team: + # urllib.parse.quote_plus( + return channel_data.team["id"] + return "" + + @staticmethod + def _get_connector_client(turn_context: TurnContext) -> ConnectorClient: + return turn_context.adapter.create_connector_client( + turn_context.activity.service_url + ) + + @staticmethod + async def _get_members( + connector_client: ConnectorClient, conversation_id: str + ) -> List[TeamsChannelAccount]: + if connector_client is None: + raise TypeError("TeamsInfo._get_members.connector_client: cannot be None.") + + if not conversation_id: + raise TypeError("TeamsInfo._get_members.conversation_id: cannot be empty.") + + teams_members = [] + members = await connector_client.conversations.get_conversation_members( + conversation_id + ) + + for member in members: + new_account_json = member.serialize() + teams_members.append(TeamsChannelAccount(**new_account_json)) + + return teams_members diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 8e26aa16c..a16eed975 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -377,4 +377,5 @@ def get_mentions(activity: Activity) -> List[Mention]: for entity in activity.entities: if entity.type.lower() == "mention": result.append(entity) + return result diff --git a/libraries/botbuilder-core/tests/teams/simple_adapter.py b/libraries/botbuilder-core/tests/teams/simple_adapter.py new file mode 100644 index 000000000..a80fa29b3 --- /dev/null +++ b/libraries/botbuilder-core/tests/teams/simple_adapter.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from typing import List +from botbuilder.core import BotAdapter, TurnContext +from botbuilder.schema import Activity, ConversationReference, ResourceResponse + + +class SimpleAdapter(BotAdapter): + # pylint: disable=unused-argument + + def __init__(self, call_on_send=None, call_on_update=None, call_on_delete=None): + super(SimpleAdapter, self).__init__() + self.test_aux = unittest.TestCase("__init__") + self._call_on_send = call_on_send + self._call_on_update = call_on_update + self._call_on_delete = call_on_delete + + async def delete_activity( + self, context: TurnContext, reference: ConversationReference + ): + self.test_aux.assertIsNotNone( + reference, "SimpleAdapter.delete_activity: missing reference" + ) + if self._call_on_delete is not None: + self._call_on_delete(reference) + + async def send_activities( + self, context: TurnContext, activities: List[Activity] + ) -> List[ResourceResponse]: + self.test_aux.assertIsNotNone( + activities, "SimpleAdapter.delete_activity: missing reference" + ) + self.test_aux.assertTrue( + len(activities) > 0, + "SimpleAdapter.send_activities: empty activities array.", + ) + + if self._call_on_send is not None: + self._call_on_send(activities) + responses = [] + + for activity in activities: + responses.append(ResourceResponse(id=activity.id)) + + return responses + + async def update_activity(self, context: TurnContext, activity: Activity): + self.test_aux.assertIsNotNone( + activity, "SimpleAdapter.update_activity: missing activity" + ) + if self._call_on_update is not None: + self._call_on_update(activity) + + return ResourceResponse(activity.id) + + async def process_request(self, activity, handler): + context = TurnContext(self, activity) + return self.run_pipeline(context, handler) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py new file mode 100644 index 000000000..540d5742b --- /dev/null +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -0,0 +1,764 @@ +from typing import List + +import aiounittest +from botbuilder.core import BotAdapter, TurnContext +from botbuilder.core.teams import TeamsActivityHandler +from botbuilder.schema import ( + Activity, + ActivityTypes, + ChannelAccount, + ConversationReference, + ResourceResponse, +) +from botbuilder.schema.teams import ( + AppBasedLinkQuery, + ChannelInfo, + FileConsentCardResponse, + MessageActionsPayload, + MessagingExtensionAction, + MessagingExtensionQuery, + O365ConnectorCardActionQuery, + TaskModuleRequest, + TaskModuleRequestContext, + TeamInfo, + TeamsChannelAccount, +) +from botframework.connector import Channels +from simple_adapter import SimpleAdapter + + +class TestingTeamsActivityHandler(TeamsActivityHandler): + def __init__(self): + self.record: List[str] = [] + + async def on_conversation_update_activity(self, turn_context: TurnContext): + self.record.append("on_conversation_update_activity") + return await super().on_conversation_update_activity(turn_context) + + async def on_teams_members_added_activity( + self, teams_members_added: [TeamsChannelAccount], turn_context: TurnContext + ): + self.record.append("on_teams_members_added_activity") + return await super().on_teams_members_added_activity( + teams_members_added, turn_context + ) + + async def on_teams_members_removed_activity( + self, teams_members_removed: [TeamsChannelAccount], turn_context: TurnContext + ): + self.record.append("on_teams_members_removed_activity") + return await super().on_teams_members_removed_activity( + teams_members_removed, turn_context + ) + + 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_token_response_event(self, turn_context: TurnContext): + self.record.append("on_token_response_event") + return await super().on_token_response_event(turn_context) + + async def on_event(self, turn_context: TurnContext): + self.record.append("on_event") + return await super().on_event(turn_context) + + async def on_unrecognized_activity_type(self, turn_context: TurnContext): + self.record.append("on_unrecognized_activity_type") + return await super().on_unrecognized_activity_type(turn_context) + + async def on_teams_channel_created_activity( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ): + self.record.append("on_teams_channel_created_activity") + return await super().on_teams_channel_created_activity( + channel_info, team_info, turn_context + ) + + async def on_teams_channel_renamed_activity( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ): + self.record.append("on_teams_channel_renamed_activity") + return await super().on_teams_channel_renamed_activity( + channel_info, team_info, turn_context + ) + + async def on_teams_channel_deleted_activity( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ): + self.record.append("on_teams_channel_deleted_activity") + return await super().on_teams_channel_renamed_activity( + channel_info, team_info, turn_context + ) + + async def on_teams_team_renamed_activity( + self, team_info: TeamInfo, turn_context: TurnContext + ): + self.record.append("on_teams_team_renamed_activity") + return await super().on_teams_team_renamed_activity(team_info, turn_context) + + async def on_invoke_activity(self, turn_context: TurnContext): + self.record.append("on_invoke_activity") + return await super().on_invoke_activity(turn_context) + + async def on_teams_signin_verify_state(self, turn_context: TurnContext): + self.record.append("on_teams_signin_verify_state") + return await super().on_teams_signin_verify_state(turn_context) + + async def on_teams_file_consent( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ): + self.record.append("on_teams_file_consent") + return await super().on_teams_file_consent( + turn_context, file_consent_card_response + ) + + async def on_teams_file_consent_accept_activity( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ): + self.record.append("on_teams_file_consent_accept_activity") + return await super().on_teams_file_consent_accept_activity( + turn_context, file_consent_card_response + ) + + async def on_teams_file_consent_decline_activity( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ): + self.record.append("on_teams_file_consent_decline_activity") + return await super().on_teams_file_consent_decline_activity( + turn_context, file_consent_card_response + ) + + async def on_teams_o365_connector_card_action( + self, turn_context: TurnContext, query: O365ConnectorCardActionQuery + ): + self.record.append("on_teams_o365_connector_card_action") + return await super().on_teams_o365_connector_card_action(turn_context, query) + + async def on_teams_app_based_link_query( + self, turn_context: TurnContext, query: AppBasedLinkQuery + ): + self.record.append("on_teams_app_based_link_query") + return await super().on_teams_app_based_link_query(turn_context, query) + + async def on_teams_messaging_extension_query( + self, turn_context: TurnContext, query: MessagingExtensionQuery + ): + self.record.append("on_teams_messaging_extension_query") + return await super().on_teams_messaging_extension_query(turn_context, query) + + async def on_teams_messaging_extension_submit_action_dispatch( + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + self.record.append("on_teams_messaging_extension_submit_action_dispatch") + return await super().on_teams_messaging_extension_submit_action_dispatch( + turn_context, action + ) + + async def on_teams_messaging_extension_submit_action_activity( + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + self.record.append("on_teams_messaging_extension_submit_action_activity") + return await super().on_teams_messaging_extension_submit_action_activity( + turn_context, action + ) + + async def on_teams_messaging_extension_bot_message_preview_edit_activity( + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + self.record.append( + "on_teams_messaging_extension_bot_message_preview_edit_activity" + ) + return await super().on_teams_messaging_extension_bot_message_preview_edit_activity( + turn_context, action + ) + + async def on_teams_messaging_extension_bot_message_send_activity( + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + self.record.append("on_teams_messaging_extension_bot_message_send_activity") + return await super().on_teams_messaging_extension_bot_message_send_activity( + turn_context, action + ) + + async def on_teams_messaging_extension_fetch_task( + self, turn_context: TurnContext, action: MessagingExtensionAction + ): + self.record.append("on_teams_messaging_extension_fetch_task") + return await super().on_teams_messaging_extension_fetch_task( + turn_context, action + ) + + async def on_teams_messaging_extension_configuration_query_settings_url( + self, turn_context: TurnContext, query: MessagingExtensionQuery + ): + self.record.append( + "on_teams_messaging_extension_configuration_query_settings_url" + ) + return await super().on_teams_messaging_extension_configuration_query_settings_url( + turn_context, query + ) + + async def on_teams_messaging_extension_configuration_setting( + self, turn_context: TurnContext, settings + ): + self.record.append("on_teams_messaging_extension_configuration_setting") + return await super().on_teams_messaging_extension_configuration_setting( + turn_context, settings + ) + + async def on_teams_messaging_extension_card_button_clicked( + self, turn_context: TurnContext, card_data + ): + self.record.append("on_teams_messaging_extension_card_button_clicked") + return await super().on_teams_messaging_extension_card_button_clicked( + turn_context, card_data + ) + + async def on_teams_task_module_fetch( + self, turn_context: TurnContext, task_module_request + ): + self.record.append("on_teams_task_module_fetch") + return await super().on_teams_task_module_fetch( + turn_context, task_module_request + ) + + async def on_teams_task_module_submit( # pylint: disable=unused-argument + self, turn_context: TurnContext, task_module_request: TaskModuleRequest + ): + self.record.append("on_teams_task_module_submit") + return await super().on_teams_task_module_submit( + turn_context, task_module_request + ) + + +class NotImplementedAdapter(BotAdapter): + async def delete_activity( + self, context: TurnContext, reference: ConversationReference + ): + raise NotImplementedError() + + async def send_activities( + self, context: TurnContext, activities: List[Activity] + ) -> List[ResourceResponse]: + raise NotImplementedError() + + async def update_activity(self, context: TurnContext, activity: Activity): + raise NotImplementedError() + + +class TestTeamsActivityHandler(aiounittest.AsyncTestCase): + async def test_on_teams_channel_created_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.conversation_update, + channel_data={ + "eventType": "channelCreated", + "channel": {"id": "asdfqwerty", "name": "new_channel"}, + }, + channel_id=Channels.ms_teams, + ) + + turn_context = TurnContext(NotImplementedAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_conversation_update_activity" + assert bot.record[1] == "on_teams_channel_created_activity" + + async def test_on_teams_channel_renamed_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.conversation_update, + channel_data={ + "eventType": "channelRenamed", + "channel": {"id": "asdfqwerty", "name": "new_channel"}, + }, + channel_id=Channels.ms_teams, + ) + + turn_context = TurnContext(NotImplementedAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_conversation_update_activity" + assert bot.record[1] == "on_teams_channel_renamed_activity" + + async def test_on_teams_channel_deleted_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.conversation_update, + channel_data={ + "eventType": "channelDeleted", + "channel": {"id": "asdfqwerty", "name": "new_channel"}, + }, + channel_id=Channels.ms_teams, + ) + + turn_context = TurnContext(NotImplementedAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_conversation_update_activity" + assert bot.record[1] == "on_teams_channel_deleted_activity" + + async def test_on_teams_team_renamed_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.conversation_update, + channel_data={ + "eventType": "teamRenamed", + "team": {"id": "team_id_1", "name": "new_team_name"}, + }, + channel_id=Channels.ms_teams, + ) + + turn_context = TurnContext(NotImplementedAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_conversation_update_activity" + assert bot.record[1] == "on_teams_team_renamed_activity" + + async def test_on_teams_members_added_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.conversation_update, + channel_data={"eventType": "teamMemberAdded"}, + members_added=[ + ChannelAccount( + id="123", + name="test_user", + aad_object_id="asdfqwerty", + role="tester", + ) + ], + channel_id=Channels.ms_teams, + ) + + turn_context = TurnContext(NotImplementedAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_conversation_update_activity" + assert bot.record[1] == "on_teams_members_added_activity" + + async def test_on_teams_members_removed_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.conversation_update, + channel_data={"eventType": "teamMemberRemoved"}, + members_removed=[ + ChannelAccount( + id="123", + name="test_user", + aad_object_id="asdfqwerty", + role="tester", + ) + ], + channel_id=Channels.ms_teams, + ) + + turn_context = TurnContext(NotImplementedAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_conversation_update_activity" + assert bot.record[1] == "on_teams_members_removed_activity" + + async def test_on_signin_verify_state(self): + # arrange + activity = Activity(type=ActivityTypes.invoke, name="signin/verifyState") + + 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_signin_verify_state" + + async def test_on_file_consent_accept_activity(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + name="fileConsent/invoke", + value={"action": "accept"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 3 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_file_consent" + assert bot.record[2] == "on_teams_file_consent_accept_activity" + + async def test_on_file_consent_decline_activity(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="fileConsent/invoke", + value={"action": "decline"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 3 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_file_consent" + assert bot.record[2] == "on_teams_file_consent_decline_activity" + + async def test_on_file_consent_bad_action_activity(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="fileConsent/invoke", + value={"action": "bad_action"}, + ) + + 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_file_consent" + + async def test_on_teams_o365_connector_card_action(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + name="actionableMessage/executeAction", + value={"body": "body_here", "actionId": "action_id_here"}, + ) + + 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_o365_connector_card_action" + + async def test_on_app_based_link_query(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/query", + 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_messaging_extension_query" + + async def test_on_teams_messaging_extension_bot_message_preview_edit_activity(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/submitAction", + value={ + "data": {"key": "value"}, + "context": {"theme": "dark"}, + "comamndId": "test_command", + "commandContext": "command_context_test", + "botMessagePreviewAction": "edit", + "botActivityPreview": [Activity().serialize()], + "messagePayload": MessageActionsPayload().serialize(), + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 3 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch" + assert ( + bot.record[2] + == "on_teams_messaging_extension_bot_message_preview_edit_activity" + ) + + async def test_on_teams_messaging_extension_bot_message_send_activity(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/submitAction", + value={ + "data": {"key": "value"}, + "context": {"theme": "dark"}, + "comamndId": "test_command", + "commandContext": "command_context_test", + "botMessagePreviewAction": "send", + "botActivityPreview": [Activity().serialize()], + "messagePayload": MessageActionsPayload().serialize(), + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 3 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch" + assert bot.record[2] == "on_teams_messaging_extension_bot_message_send_activity" + + async def test_on_teams_messaging_extension_bot_message_send_activity_with_none( + self, + ): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/submitAction", + value={ + "data": {"key": "value"}, + "context": {"theme": "dark"}, + "comamndId": "test_command", + "commandContext": "command_context_test", + "botMessagePreviewAction": None, + "botActivityPreview": [Activity().serialize()], + "messagePayload": MessageActionsPayload().serialize(), + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 3 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch" + assert bot.record[2] == "on_teams_messaging_extension_submit_action_activity" + + async def test_on_teams_messaging_extension_bot_message_send_activity_with_empty_string( + self, + ): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/submitAction", + value={ + "data": {"key": "value"}, + "context": {"theme": "dark"}, + "comamndId": "test_command", + "commandContext": "command_context_test", + "botMessagePreviewAction": "", + "botActivityPreview": [Activity().serialize()], + "messagePayload": MessageActionsPayload().serialize(), + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 3 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch" + assert bot.record[2] == "on_teams_messaging_extension_submit_action_activity" + + async def test_on_teams_messaging_extension_fetch_task(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/fetchTask", + value={ + "data": {"key": "value"}, + "context": {"theme": "dark"}, + "comamndId": "test_command", + "commandContext": "command_context_test", + "botMessagePreviewAction": "message_action", + "botActivityPreview": [Activity().serialize()], + "messagePayload": MessageActionsPayload().serialize(), + }, + ) + + 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_messaging_extension_fetch_task" + + async def test_on_teams_messaging_extension_configuration_query_settings_url(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/querySettingUrl", + value={ + "comamndId": "test_command", + "parameters": [], + "messagingExtensionQueryOptions": {"skip": 1, "count": 1}, + "state": "state_string", + }, + ) + + 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_messaging_extension_configuration_query_settings_url" + ) + + async def test_on_teams_messaging_extension_configuration_setting(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/setting", + value={"key": "value"}, + ) + + 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_messaging_extension_configuration_setting" + + async def test_on_teams_messaging_extension_card_button_clicked(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/onCardButtonClicked", + value={"key": "value"}, + ) + + 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_messaging_extension_card_button_clicked" + + async def test_on_teams_task_module_fetch(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="task/fetch", + value={ + "data": {"key": "value"}, + "context": TaskModuleRequestContext().serialize(), + }, + ) + + 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_task_module_fetch" + + async def test_on_teams_task_module_submit(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="task/submit", + value={ + "data": {"key": "value"}, + "context": TaskModuleRequestContext().serialize(), + }, + ) + + 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_task_module_submit" diff --git a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py index 6532b1e52..528bbf719 100644 --- a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py +++ b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py @@ -57,8 +57,8 @@ def __init__(self, settings=None): def aux_test_authenticate_request(self, request: Activity, auth_header: str): return super().authenticate_request(request, auth_header) - def aux_test_create_connector_client(self, service_url: str): - return super().create_connector_client(service_url) + async def aux_test_create_connector_client(self, service_url: str): + return await super().create_connector_client(service_url) async def authenticate_request(self, request: Activity, auth_header: str): self.tester.assertIsNotNone( @@ -71,7 +71,11 @@ async def authenticate_request(self, request: Activity, auth_header: str): ) return not self.fail_auth - def create_connector_client(self, service_url: str) -> ConnectorClient: + async def create_connector_client( + self, + service_url: str, + identity: ClaimsIdentity = None, # pylint: disable=unused-argument + ) -> ConnectorClient: self.tester.assertIsNotNone( service_url, "create_connector_client() not passed service_url." ) @@ -181,9 +185,9 @@ async def aux_func(context): class TestBotFrameworkAdapter(aiounittest.AsyncTestCase): - def test_should_create_connector_client(self): + async def test_should_create_connector_client(self): adapter = AdapterUnderTest() - client = adapter.aux_test_create_connector_client(REFERENCE.service_url) + client = await adapter.aux_test_create_connector_client(REFERENCE.service_url) self.assertIsNotNone(client, "client not returned.") self.assertIsNotNone(client.conversations, "invalid client returned.") diff --git a/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py b/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py index 605600aa9..300ccddd8 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py @@ -27,6 +27,7 @@ class ActivityTypes(str, Enum): end_of_conversation = "endOfConversation" event = "event" invoke = "invoke" + invoke_response = "invokeResponse" delete_user_data = "deleteUserData" message_update = "messageUpdate" message_delete = "messageDelete" diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models.py b/libraries/botbuilder-schema/botbuilder/schema/_models.py index 736ddcf81..9574df14a 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models.py @@ -611,7 +611,7 @@ def __init__(self, **kwargs): super(ChannelAccount, self).__init__(**kwargs) self.id = kwargs.get("id", None) self.name = kwargs.get("name", None) - self.aad_object_id = kwargs.get("aad_object_id", None) + self.aad_object_id = kwargs.get("aadObjectId", None) self.role = kwargs.get("role", None) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py new file mode 100644 index 000000000..bae8bf5cf --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -0,0 +1,184 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +try: + from ._models_py3 import AppBasedLinkQuery + from ._models_py3 import ChannelInfo + from ._models_py3 import ConversationList + from ._models_py3 import FileConsentCard + from ._models_py3 import FileConsentCardResponse + from ._models_py3 import FileDownloadInfo + from ._models_py3 import FileInfoCard + from ._models_py3 import FileUploadInfo + from ._models_py3 import MessageActionsPayload + from ._models_py3 import MessageActionsPayloadApp + from ._models_py3 import MessageActionsPayloadAttachment + from ._models_py3 import MessageActionsPayloadBody + from ._models_py3 import MessageActionsPayloadConversation + from ._models_py3 import MessageActionsPayloadFrom + from ._models_py3 import MessageActionsPayloadMention + from ._models_py3 import MessageActionsPayloadReaction + from ._models_py3 import MessageActionsPayloadUser + from ._models_py3 import MessagingExtensionAction + from ._models_py3 import MessagingExtensionActionResponse + from ._models_py3 import MessagingExtensionAttachment + from ._models_py3 import MessagingExtensionParameter + from ._models_py3 import MessagingExtensionQuery + from ._models_py3 import MessagingExtensionQueryOptions + from ._models_py3 import MessagingExtensionResponse + from ._models_py3 import MessagingExtensionResult + from ._models_py3 import MessagingExtensionSuggestedAction + from ._models_py3 import NotificationInfo + from ._models_py3 import O365ConnectorCard + from ._models_py3 import O365ConnectorCardActionBase + from ._models_py3 import O365ConnectorCardActionCard + from ._models_py3 import O365ConnectorCardActionQuery + from ._models_py3 import O365ConnectorCardDateInput + from ._models_py3 import O365ConnectorCardFact + from ._models_py3 import O365ConnectorCardHttpPOST + from ._models_py3 import O365ConnectorCardImage + from ._models_py3 import O365ConnectorCardInputBase + from ._models_py3 import O365ConnectorCardMultichoiceInput + from ._models_py3 import O365ConnectorCardMultichoiceInputChoice + from ._models_py3 import O365ConnectorCardOpenUri + from ._models_py3 import O365ConnectorCardOpenUriTarget + from ._models_py3 import O365ConnectorCardSection + from ._models_py3 import O365ConnectorCardTextInput + from ._models_py3 import O365ConnectorCardViewAction + from ._models_py3 import SigninStateVerificationQuery + from ._models_py3 import TaskModuleContinueResponse + from ._models_py3 import TaskModuleMessageResponse + from ._models_py3 import TaskModuleRequest + from ._models_py3 import TaskModuleRequestContext + from ._models_py3 import TaskModuleResponse + from ._models_py3 import TaskModuleResponseBase + from ._models_py3 import TaskModuleTaskInfo + from ._models_py3 import TeamDetails + from ._models_py3 import TeamInfo + from ._models_py3 import TeamsChannelAccount + from ._models_py3 import TeamsChannelData + from ._models_py3 import TenantInfo +except (SyntaxError, ImportError): + from ._models import AppBasedLinkQuery + from ._models import ChannelInfo + from ._models import ConversationList + from ._models import FileConsentCard + from ._models import FileConsentCardResponse + from ._models import FileDownloadInfo + from ._models import FileInfoCard + from ._models import FileUploadInfo + from ._models import MessageActionsPayload + from ._models import MessageActionsPayloadApp + from ._models import MessageActionsPayloadAttachment + from ._models import MessageActionsPayloadBody + from ._models import MessageActionsPayloadConversation + from ._models import MessageActionsPayloadFrom + from ._models import MessageActionsPayloadMention + from ._models import MessageActionsPayloadReaction + from ._models import MessageActionsPayloadUser + from ._models import MessagingExtensionAction + from ._models import MessagingExtensionActionResponse + from ._models import MessagingExtensionAttachment + from ._models import MessagingExtensionParameter + from ._models import MessagingExtensionQuery + from ._models import MessagingExtensionQueryOptions + from ._models import MessagingExtensionResponse + from ._models import MessagingExtensionResult + from ._models import MessagingExtensionSuggestedAction + from ._models import NotificationInfo + from ._models import O365ConnectorCard + from ._models import O365ConnectorCardActionBase + from ._models import O365ConnectorCardActionCard + from ._models import O365ConnectorCardActionQuery + from ._models import O365ConnectorCardDateInput + from ._models import O365ConnectorCardFact + from ._models import O365ConnectorCardHttpPOST + from ._models import O365ConnectorCardImage + from ._models import O365ConnectorCardInputBase + from ._models import O365ConnectorCardMultichoiceInput + from ._models import O365ConnectorCardMultichoiceInputChoice + from ._models import O365ConnectorCardOpenUri + from ._models import O365ConnectorCardOpenUriTarget + from ._models import O365ConnectorCardSection + from ._models import O365ConnectorCardTextInput + from ._models import O365ConnectorCardViewAction + from ._models import SigninStateVerificationQuery + from ._models import TaskModuleContinueResponse + from ._models import TaskModuleMessageResponse + from ._models import TaskModuleRequest + from ._models import TaskModuleRequestContext + from ._models import TaskModuleResponse + from ._models import TaskModuleResponseBase + from ._models import TaskModuleTaskInfo + from ._models import TeamDetails + from ._models import TeamInfo + from ._models import TeamsChannelAccount + from ._models import TeamsChannelData + from ._models import TenantInfo + +__all__ = [ + "AppBasedLinkQuery", + "ChannelInfo", + "ConversationList", + "FileConsentCard", + "FileConsentCardResponse", + "FileDownloadInfo", + "FileInfoCard", + "FileUploadInfo", + "MessageActionsPayload", + "MessageActionsPayloadApp", + "MessageActionsPayloadAttachment", + "MessageActionsPayloadBody", + "MessageActionsPayloadConversation", + "MessageActionsPayloadFrom", + "MessageActionsPayloadMention", + "MessageActionsPayloadReaction", + "MessageActionsPayloadUser", + "MessagingExtensionAction", + "MessagingExtensionActionResponse", + "MessagingExtensionAttachment", + "MessagingExtensionParameter", + "MessagingExtensionQuery", + "MessagingExtensionQueryOptions", + "MessagingExtensionResponse", + "MessagingExtensionResult", + "MessagingExtensionSuggestedAction", + "NotificationInfo", + "O365ConnectorCard", + "O365ConnectorCardActionBase", + "O365ConnectorCardActionCard", + "O365ConnectorCardActionQuery", + "O365ConnectorCardDateInput", + "O365ConnectorCardFact", + "O365ConnectorCardHttpPOST", + "O365ConnectorCardImage", + "O365ConnectorCardInputBase", + "O365ConnectorCardMultichoiceInput", + "O365ConnectorCardMultichoiceInputChoice", + "O365ConnectorCardOpenUri", + "O365ConnectorCardOpenUriTarget", + "O365ConnectorCardSection", + "O365ConnectorCardTextInput", + "O365ConnectorCardViewAction", + "SigninStateVerificationQuery", + "TaskModuleContinueResponse", + "TaskModuleMessageResponse", + "TaskModuleRequest", + "TaskModuleRequestContext", + "TaskModuleResponse", + "TaskModuleResponseBase", + "TaskModuleTaskInfo", + "TeamDetails", + "TeamInfo", + "TeamsChannelAccount", + "TeamsChannelData", + "TenantInfo", +] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models.py new file mode 100644 index 000000000..e80cee5f9 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models.py @@ -0,0 +1,1597 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model + + +class AppBasedLinkQuery(Model): + """Invoke request body type for app-based link query. + + :param url: Url queried by user + :type url: str + """ + + _attribute_map = { + "url": {"key": "url", "type": "str"}, + } + + def __init__(self, *, url: str = None, **kwargs) -> None: + super(AppBasedLinkQuery, self).__init__(**kwargs) + self.url = url + + +class ChannelInfo(Model): + """A channel info object which describes the channel. + + :param id: Unique identifier representing a channel + :type id: str + :param name: Name of the channel + :type name: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + } + + def __init__(self, **kwargs): + super(ChannelInfo, self).__init__(**kwargs) + self.id = kwargs.get("id", None) + self.name = kwargs.get("name", None) + + +class ConversationList(Model): + """List of channels under a team. + + :param conversations: + :type conversations: + list[~botframework.connector.teams.models.ChannelInfo] + """ + + _attribute_map = { + "conversations": {"key": "conversations", "type": "[ChannelInfo]"}, + } + + def __init__(self, **kwargs): + super(ConversationList, self).__init__(**kwargs) + self.conversations = kwargs.get("conversations", None) + + +class FileConsentCard(Model): + """File consent card attachment. + + :param description: File description. + :type description: str + :param size_in_bytes: Size of the file to be uploaded in Bytes. + :type size_in_bytes: long + :param accept_context: Context sent back to the Bot if user consented to + upload. This is free flow schema and is sent back in Value field of + Activity. + :type accept_context: object + :param decline_context: Context sent back to the Bot if user declined. + This is free flow schema and is sent back in Value field of Activity. + :type decline_context: object + """ + + _attribute_map = { + "description": {"key": "description", "type": "str"}, + "size_in_bytes": {"key": "sizeInBytes", "type": "long"}, + "accept_context": {"key": "acceptContext", "type": "object"}, + "decline_context": {"key": "declineContext", "type": "object"}, + } + + def __init__(self, **kwargs): + super(FileConsentCard, self).__init__(**kwargs) + self.description = kwargs.get("description", None) + self.size_in_bytes = kwargs.get("size_in_bytes", None) + self.accept_context = kwargs.get("accept_context", None) + self.decline_context = kwargs.get("decline_context", None) + + +class FileConsentCardResponse(Model): + """Represents the value of the invoke activity sent when the user acts on a + file consent card. + + :param action: The action the user took. Possible values include: + 'accept', 'decline' + :type action: str or ~botframework.connector.teams.models.enum + :param context: The context associated with the action. + :type context: object + :param upload_info: If the user accepted the file, contains information + about the file to be uploaded. + :type upload_info: ~botframework.connector.teams.models.FileUploadInfo + """ + + _attribute_map = { + "action": {"key": "action", "type": "str"}, + "context": {"key": "context", "type": "object"}, + "upload_info": {"key": "uploadInfo", "type": "FileUploadInfo"}, + } + + def __init__(self, **kwargs): + super(FileConsentCardResponse, self).__init__(**kwargs) + self.action = kwargs.get("action", None) + self.context = kwargs.get("context", None) + self.upload_info = kwargs.get("upload_info", None) + + +class FileDownloadInfo(Model): + """File download info attachment. + + :param download_url: File download url. + :type download_url: str + :param unique_id: Unique Id for the file. + :type unique_id: str + :param file_type: Type of file. + :type file_type: str + :param etag: ETag for the file. + :type etag: object + """ + + _attribute_map = { + "download_url": {"key": "downloadUrl", "type": "str"}, + "unique_id": {"key": "uniqueId", "type": "str"}, + "file_type": {"key": "fileType", "type": "str"}, + "etag": {"key": "etag", "type": "object"}, + } + + def __init__(self, **kwargs): + super(FileDownloadInfo, self).__init__(**kwargs) + self.download_url = kwargs.get("download_url", None) + self.unique_id = kwargs.get("unique_id", None) + self.file_type = kwargs.get("file_type", None) + self.etag = kwargs.get("etag", None) + + +class FileInfoCard(Model): + """File info card. + + :param unique_id: Unique Id for the file. + :type unique_id: str + :param file_type: Type of file. + :type file_type: str + :param etag: ETag for the file. + :type etag: object + """ + + _attribute_map = { + "unique_id": {"key": "uniqueId", "type": "str"}, + "file_type": {"key": "fileType", "type": "str"}, + "etag": {"key": "etag", "type": "object"}, + } + + def __init__(self, **kwargs): + super(FileInfoCard, self).__init__(**kwargs) + self.unique_id = kwargs.get("unique_id", None) + self.file_type = kwargs.get("file_type", None) + self.etag = kwargs.get("etag", None) + + +class FileUploadInfo(Model): + """Information about the file to be uploaded. + + :param name: Name of the file. + :type name: str + :param upload_url: URL to an upload session that the bot can use to set + the file contents. + :type upload_url: str + :param content_url: URL to file. + :type content_url: str + :param unique_id: ID that uniquely identifies the file. + :type unique_id: str + :param file_type: Type of the file. + :type file_type: str + """ + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "upload_url": {"key": "uploadUrl", "type": "str"}, + "content_url": {"key": "contentUrl", "type": "str"}, + "unique_id": {"key": "uniqueId", "type": "str"}, + "file_type": {"key": "fileType", "type": "str"}, + } + + def __init__(self, **kwargs): + super(FileUploadInfo, self).__init__(**kwargs) + self.name = kwargs.get("name", None) + self.upload_url = kwargs.get("upload_url", None) + self.content_url = kwargs.get("content_url", None) + self.unique_id = kwargs.get("unique_id", None) + self.file_type = kwargs.get("file_type", None) + + +class MessageActionsPayloadApp(Model): + """Represents an application entity. + + :param application_identity_type: The type of application. Possible values + include: 'aadApplication', 'bot', 'tenantBot', 'office365Connector', + 'webhook' + :type application_identity_type: str or + ~botframework.connector.teams.models.enum + :param id: The id of the application. + :type id: str + :param display_name: The plaintext display name of the application. + :type display_name: str + """ + + _attribute_map = { + "application_identity_type": {"key": "applicationIdentityType", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadApp, self).__init__(**kwargs) + self.application_identity_type = kwargs.get("application_identity_type", None) + self.id = kwargs.get("id", None) + self.display_name = kwargs.get("display_name", None) + + +class MessageActionsPayloadAttachment(Model): + """Represents the attachment in a message. + + :param id: The id of the attachment. + :type id: str + :param content_type: The type of the attachment. + :type content_type: str + :param content_url: The url of the attachment, in case of a external link. + :type content_url: str + :param content: The content of the attachment, in case of a code snippet, + email, or file. + :type content: object + :param name: The plaintext display name of the attachment. + :type name: str + :param thumbnail_url: The url of a thumbnail image that might be embedded + in the attachment, in case of a card. + :type thumbnail_url: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "content_type": {"key": "contentType", "type": "str"}, + "content_url": {"key": "contentUrl", "type": "str"}, + "content": {"key": "content", "type": "object"}, + "name": {"key": "name", "type": "str"}, + "thumbnail_url": {"key": "thumbnailUrl", "type": "str"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadAttachment, self).__init__(**kwargs) + self.id = kwargs.get("id", None) + self.content_type = kwargs.get("content_type", None) + self.content_url = kwargs.get("content_url", None) + self.content = kwargs.get("content", None) + self.name = kwargs.get("name", None) + self.thumbnail_url = kwargs.get("thumbnail_url", None) + + +class MessageActionsPayloadBody(Model): + """Plaintext/HTML representation of the content of the message. + + :param content_type: Type of the content. Possible values include: 'html', + 'text' + :type content_type: str or ~botframework.connector.teams.models.enum + :param content: The content of the body. + :type content: str + """ + + _attribute_map = { + "content_type": {"key": "contentType", "type": "str"}, + "content": {"key": "content", "type": "str"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadBody, self).__init__(**kwargs) + self.content_type = kwargs.get("content_type", None) + self.content = kwargs.get("content", None) + + +class MessageActionsPayloadConversation(Model): + """Represents a team or channel entity. + + :param conversation_identity_type: The type of conversation, whether a + team or channel. Possible values include: 'team', 'channel' + :type conversation_identity_type: str or + ~botframework.connector.teams.models.enum + :param id: The id of the team or channel. + :type id: str + :param display_name: The plaintext display name of the team or channel + entity. + :type display_name: str + """ + + _attribute_map = { + "conversation_identity_type": { + "key": "conversationIdentityType", + "type": "str", + }, + "id": {"key": "id", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadConversation, self).__init__(**kwargs) + self.conversation_identity_type = kwargs.get("conversation_identity_type", None) + self.id = kwargs.get("id", None) + self.display_name = kwargs.get("display_name", None) + + +class MessageActionsPayloadFrom(Model): + """Represents a user, application, or conversation type that either sent or + was referenced in a message. + + :param user: Represents details of the user. + :type user: ~botframework.connector.teams.models.MessageActionsPayloadUser + :param application: Represents details of the app. + :type application: + ~botframework.connector.teams.models.MessageActionsPayloadApp + :param conversation: Represents details of the converesation. + :type conversation: + ~botframework.connector.teams.models.MessageActionsPayloadConversation + """ + + _attribute_map = { + "user": {"key": "user", "type": "MessageActionsPayloadUser"}, + "application": {"key": "application", "type": "MessageActionsPayloadApp"}, + "conversation": { + "key": "conversation", + "type": "MessageActionsPayloadConversation", + }, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadFrom, self).__init__(**kwargs) + self.user = kwargs.get("user", None) + self.application = kwargs.get("application", None) + self.conversation = kwargs.get("conversation", None) + + +class MessageActionsPayloadMention(Model): + """Represents the entity that was mentioned in the message. + + :param id: The id of the mentioned entity. + :type id: int + :param mention_text: The plaintext display name of the mentioned entity. + :type mention_text: str + :param mentioned: Provides more details on the mentioned entity. + :type mentioned: + ~botframework.connector.teams.models.MessageActionsPayloadFrom + """ + + _attribute_map = { + "id": {"key": "id", "type": "int"}, + "mention_text": {"key": "mentionText", "type": "str"}, + "mentioned": {"key": "mentioned", "type": "MessageActionsPayloadFrom"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadMention, self).__init__(**kwargs) + self.id = kwargs.get("id", None) + self.mention_text = kwargs.get("mention_text", None) + self.mentioned = kwargs.get("mentioned", None) + + +class MessageActionsPayloadReaction(Model): + """Represents the reaction of a user to a message. + + :param reaction_type: The type of reaction given to the message. Possible + values include: 'like', 'heart', 'laugh', 'surprised', 'sad', 'angry' + :type reaction_type: str or ~botframework.connector.teams.models.enum + :param created_date_time: Timestamp of when the user reacted to the + message. + :type created_date_time: str + :param user: The user with which the reaction is associated. + :type user: ~botframework.connector.teams.models.MessageActionsPayloadFrom + """ + + _attribute_map = { + "reaction_type": {"key": "reactionType", "type": "str"}, + "created_date_time": {"key": "createdDateTime", "type": "str"}, + "user": {"key": "user", "type": "MessageActionsPayloadFrom"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadReaction, self).__init__(**kwargs) + self.reaction_type = kwargs.get("reaction_type", None) + self.created_date_time = kwargs.get("created_date_time", None) + self.user = kwargs.get("user", None) + + +class MessageActionsPayloadUser(Model): + """Represents a user entity. + + :param user_identity_type: The identity type of the user. Possible values + include: 'aadUser', 'onPremiseAadUser', 'anonymousGuest', 'federatedUser' + :type user_identity_type: str or ~botframework.connector.teams.models.enum + :param id: The id of the user. + :type id: str + :param display_name: The plaintext display name of the user. + :type display_name: str + """ + + _attribute_map = { + "user_identity_type": {"key": "userIdentityType", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayloadUser, self).__init__(**kwargs) + self.user_identity_type = kwargs.get("user_identity_type", None) + self.id = kwargs.get("id", None) + self.display_name = kwargs.get("display_name", None) + + +class MessageActionsPayload(Model): + """Represents the individual message within a chat or channel where a message + actions is taken. + + :param id: Unique id of the message. + :type id: str + :param reply_to_id: Id of the parent/root message of the thread. + :type reply_to_id: str + :param message_type: Type of message - automatically set to message. + Possible values include: 'message' + :type message_type: str or ~botframework.connector.teams.models.enum + :param created_date_time: Timestamp of when the message was created. + :type created_date_time: str + :param last_modified_date_time: Timestamp of when the message was edited + or updated. + :type last_modified_date_time: str + :param deleted: Indicates whether a message has been soft deleted. + :type deleted: bool + :param subject: Subject line of the message. + :type subject: str + :param summary: Summary text of the message that could be used for + notifications. + :type summary: str + :param importance: The importance of the message. Possible values include: + 'normal', 'high', 'urgent' + :type importance: str or ~botframework.connector.teams.models.enum + :param locale: Locale of the message set by the client. + :type locale: str + :param from_property: Sender of the message. + :type from_property: + ~botframework.connector.teams.models.MessageActionsPayloadFrom + :param body: Plaintext/HTML representation of the content of the message. + :type body: ~botframework.connector.teams.models.MessageActionsPayloadBody + :param attachment_layout: How the attachment(s) are displayed in the + message. + :type attachment_layout: str + :param attachments: Attachments in the message - card, image, file, etc. + :type attachments: + list[~botframework.connector.teams.models.MessageActionsPayloadAttachment] + :param mentions: List of entities mentioned in the message. + :type mentions: + list[~botframework.connector.teams.models.MessageActionsPayloadMention] + :param reactions: Reactions for the message. + :type reactions: + list[~botframework.connector.teams.models.MessageActionsPayloadReaction] + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "reply_to_id": {"key": "replyToId", "type": "str"}, + "message_type": {"key": "messageType", "type": "str"}, + "created_date_time": {"key": "createdDateTime", "type": "str"}, + "last_modified_date_time": {"key": "lastModifiedDateTime", "type": "str"}, + "deleted": {"key": "deleted", "type": "bool"}, + "subject": {"key": "subject", "type": "str"}, + "summary": {"key": "summary", "type": "str"}, + "importance": {"key": "importance", "type": "str"}, + "locale": {"key": "locale", "type": "str"}, + "from_property": {"key": "from", "type": "MessageActionsPayloadFrom"}, + "body": {"key": "body", "type": "MessageActionsPayloadBody"}, + "attachment_layout": {"key": "attachmentLayout", "type": "str"}, + "attachments": { + "key": "attachments", + "type": "[MessageActionsPayloadAttachment]", + }, + "mentions": {"key": "mentions", "type": "[MessageActionsPayloadMention]"}, + "reactions": {"key": "reactions", "type": "[MessageActionsPayloadReaction]"}, + } + + def __init__(self, **kwargs): + super(MessageActionsPayload, self).__init__(**kwargs) + self.id = kwargs.get("id", None) + self.reply_to_id = kwargs.get("reply_to_id", None) + self.message_type = kwargs.get("message_type", None) + self.created_date_time = kwargs.get("created_date_time", None) + self.last_modified_date_time = kwargs.get("last_modified_date_time", None) + self.deleted = kwargs.get("deleted", None) + self.subject = kwargs.get("subject", None) + self.summary = kwargs.get("summary", None) + self.importance = kwargs.get("importance", None) + self.locale = kwargs.get("locale", None) + self.from_property = kwargs.get("from_property", None) + self.body = kwargs.get("body", None) + self.attachment_layout = kwargs.get("attachment_layout", None) + self.attachments = kwargs.get("attachments", None) + self.mentions = kwargs.get("mentions", None) + self.reactions = kwargs.get("reactions", None) + + +class MessagingExtensionAction(TaskModuleRequest): + """Messaging extension action. + + :param data: User input data. Free payload with key-value pairs. + :type data: object + :param context: Current user context, i.e., the current theme + :type context: + ~botframework.connector.teams.models.TaskModuleRequestContext + :param command_id: Id of the command assigned by Bot + :type command_id: str + :param command_context: The context from which the command originates. + Possible values include: 'message', 'compose', 'commandbox' + :type command_context: str or ~botframework.connector.teams.models.enum + :param bot_message_preview_action: Bot message preview action taken by + user. Possible values include: 'edit', 'send' + :type bot_message_preview_action: str or + ~botframework.connector.teams.models.enum + :param bot_activity_preview: + :type bot_activity_preview: + list[~botframework.connector.teams.models.Activity] + :param message_payload: Message content sent as part of the command + request. + :type message_payload: + ~botframework.connector.teams.models.MessageActionsPayload + """ + + _attribute_map = { + "data": {"key": "data", "type": "object"}, + "context": {"key": "context", "type": "TaskModuleRequestContext"}, + "command_id": {"key": "commandId", "type": "str"}, + "command_context": {"key": "commandContext", "type": "str"}, + "bot_message_preview_action": {"key": "botMessagePreviewAction", "type": "str"}, + "bot_activity_preview": {"key": "botActivityPreview", "type": "[Activity]"}, + "message_payload": {"key": "messagePayload", "type": "MessageActionsPayload"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionAction, self).__init__(**kwargs) + self.command_id = kwargs.get("command_id", None) + self.command_context = kwargs.get("command_context", None) + self.bot_message_preview_action = kwargs.get("botMessagePreviewAction", None) + self.bot_activity_preview = kwargs.get("bot_activity_preview", None) + self.message_payload = kwargs.get("message_payload", None) + + +class MessagingExtensionActionResponse(Model): + """Response of messaging extension action. + + :param task: The JSON for the Adaptive card to appear in the task module. + :type task: ~botframework.connector.teams.models.TaskModuleResponseBase + :param compose_extension: + :type compose_extension: + ~botframework.connector.teams.models.MessagingExtensionResult + """ + + _attribute_map = { + "task": {"key": "task", "type": "TaskModuleResponseBase"}, + "compose_extension": { + "key": "composeExtension", + "type": "MessagingExtensionResult", + }, + } + + def __init__(self, **kwargs): + super(MessagingExtensionActionResponse, self).__init__(**kwargs) + self.task = kwargs.get("task", None) + self.compose_extension = kwargs.get("compose_extension", None) + + +class MessagingExtensionAttachment(Attachment): + """Messaging extension attachment. + + :param content_type: mimetype/Contenttype for the file + :type content_type: str + :param content_url: Content Url + :type content_url: str + :param content: Embedded content + :type content: object + :param name: (OPTIONAL) The name of the attachment + :type name: str + :param thumbnail_url: (OPTIONAL) Thumbnail associated with attachment + :type thumbnail_url: str + :param preview: + :type preview: ~botframework.connector.teams.models.Attachment + """ + + _attribute_map = { + "content_type": {"key": "contentType", "type": "str"}, + "content_url": {"key": "contentUrl", "type": "str"}, + "content": {"key": "content", "type": "object"}, + "name": {"key": "name", "type": "str"}, + "thumbnail_url": {"key": "thumbnailUrl", "type": "str"}, + "preview": {"key": "preview", "type": "Attachment"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionAttachment, self).__init__(**kwargs) + self.preview = kwargs.get("preview", None) + + +class MessagingExtensionParameter(Model): + """Messaging extension query parameters. + + :param name: Name of the parameter + :type name: str + :param value: Value of the parameter + :type value: object + """ + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "value": {"key": "value", "type": "object"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionParameter, self).__init__(**kwargs) + self.name = kwargs.get("name", None) + self.value = kwargs.get("value", None) + + +class MessagingExtensionQuery(Model): + """Messaging extension query. + + :param command_id: Id of the command assigned by Bot + :type command_id: str + :param parameters: Parameters for the query + :type parameters: + list[~botframework.connector.teams.models.MessagingExtensionParameter] + :param query_options: + :type query_options: + ~botframework.connector.teams.models.MessagingExtensionQueryOptions + :param state: State parameter passed back to the bot after + authentication/configuration flow + :type state: str + """ + + _attribute_map = { + "command_id": {"key": "commandId", "type": "str"}, + "parameters": {"key": "parameters", "type": "[MessagingExtensionParameter]"}, + "query_options": { + "key": "queryOptions", + "type": "MessagingExtensionQueryOptions", + }, + "state": {"key": "state", "type": "str"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionQuery, self).__init__(**kwargs) + self.command_id = kwargs.get("command_id", None) + self.parameters = kwargs.get("parameters", None) + self.query_options = kwargs.get("query_options", None) + self.state = kwargs.get("state", None) + + +class MessagingExtensionQueryOptions(Model): + """Messaging extension query options. + + :param skip: Number of entities to skip + :type skip: int + :param count: Number of entities to fetch + :type count: int + """ + + _attribute_map = { + "skip": {"key": "skip", "type": "int"}, + "count": {"key": "count", "type": "int"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionQueryOptions, self).__init__(**kwargs) + self.skip = kwargs.get("skip", None) + self.count = kwargs.get("count", None) + + +class MessagingExtensionResponse(Model): + """Messaging extension response. + + :param compose_extension: + :type compose_extension: + ~botframework.connector.teams.models.MessagingExtensionResult + """ + + _attribute_map = { + "compose_extension": { + "key": "composeExtension", + "type": "MessagingExtensionResult", + }, + } + + def __init__(self, **kwargs): + super(MessagingExtensionResponse, self).__init__(**kwargs) + self.compose_extension = kwargs.get("compose_extension", None) + + +class MessagingExtensionResult(Model): + """Messaging extension result. + + :param attachment_layout: Hint for how to deal with multiple attachments. + Possible values include: 'list', 'grid' + :type attachment_layout: str or ~botframework.connector.teams.models.enum + :param type: The type of the result. Possible values include: 'result', + 'auth', 'config', 'message', 'botMessagePreview' + :type type: str or ~botframework.connector.teams.models.enum + :param attachments: (Only when type is result) Attachments + :type attachments: + list[~botframework.connector.teams.models.MessagingExtensionAttachment] + :param suggested_actions: + :type suggested_actions: + ~botframework.connector.teams.models.MessagingExtensionSuggestedAction + :param text: (Only when type is message) Text + :type text: str + :param activity_preview: (Only when type is botMessagePreview) Message + activity to preview + :type activity_preview: ~botframework.connector.teams.models.Activity + """ + + _attribute_map = { + "attachment_layout": {"key": "attachmentLayout", "type": "str"}, + "type": {"key": "type", "type": "str"}, + "attachments": {"key": "attachments", "type": "[MessagingExtensionAttachment]"}, + "suggested_actions": { + "key": "suggestedActions", + "type": "MessagingExtensionSuggestedAction", + }, + "text": {"key": "text", "type": "str"}, + "activity_preview": {"key": "activityPreview", "type": "Activity"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionResult, self).__init__(**kwargs) + self.attachment_layout = kwargs.get("attachment_layout", None) + self.type = kwargs.get("type", None) + self.attachments = kwargs.get("attachments", None) + self.suggested_actions = kwargs.get("suggested_actions", None) + self.text = kwargs.get("text", None) + self.activity_preview = kwargs.get("activity_preview", None) + + +class MessagingExtensionSuggestedAction(Model): + """Messaging extension Actions (Only when type is auth or config). + + :param actions: Actions + :type actions: list[~botframework.connector.teams.models.CardAction] + """ + + _attribute_map = { + "actions": {"key": "actions", "type": "[CardAction]"}, + } + + def __init__(self, **kwargs): + super(MessagingExtensionSuggestedAction, self).__init__(**kwargs) + self.actions = kwargs.get("actions", None) + + +class NotificationInfo(Model): + """Specifies if a notification is to be sent for the mentions. + + :param alert: true if notification is to be sent to the user, false + otherwise. + :type alert: bool + """ + + _attribute_map = { + "alert": {"key": "alert", "type": "bool"}, + } + + def __init__(self, **kwargs): + super(NotificationInfo, self).__init__(**kwargs) + self.alert = kwargs.get("alert", None) + + +class O365ConnectorCard(Model): + """O365 connector card. + + :param title: Title of the item + :type title: str + :param text: Text for the card + :type text: str + :param summary: Summary for the card + :type summary: str + :param theme_color: Theme color for the card + :type theme_color: str + :param sections: Set of sections for the current card + :type sections: + list[~botframework.connector.teams.models.O365ConnectorCardSection] + :param potential_action: Set of actions for the current card + :type potential_action: + list[~botframework.connector.teams.models.O365ConnectorCardActionBase] + """ + + _attribute_map = { + "title": {"key": "title", "type": "str"}, + "text": {"key": "text", "type": "str"}, + "summary": {"key": "summary", "type": "str"}, + "theme_color": {"key": "themeColor", "type": "str"}, + "sections": {"key": "sections", "type": "[O365ConnectorCardSection]"}, + "potential_action": { + "key": "potentialAction", + "type": "[O365ConnectorCardActionBase]", + }, + } + + def __init__(self, **kwargs): + super(O365ConnectorCard, self).__init__(**kwargs) + self.title = kwargs.get("title", None) + self.text = kwargs.get("text", None) + self.summary = kwargs.get("summary", None) + self.theme_color = kwargs.get("theme_color", None) + self.sections = kwargs.get("sections", None) + self.potential_action = kwargs.get("potential_action", None) + + +class O365ConnectorCardActionBase(Model): + """O365 connector card action base. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardActionBase, self).__init__(**kwargs) + self.type = kwargs.get("type", None) + self.name = kwargs.get("name", None) + self.id = kwargs.get("id", None) + + +class O365ConnectorCardActionCard(O365ConnectorCardActionBase): + """O365 connector card ActionCard action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param inputs: Set of inputs contained in this ActionCard whose each item + can be in any subtype of O365ConnectorCardInputBase + :type inputs: + list[~botframework.connector.teams.models.O365ConnectorCardInputBase] + :param actions: Set of actions contained in this ActionCard whose each + item can be in any subtype of O365ConnectorCardActionBase except + O365ConnectorCardActionCard, as nested ActionCard is forbidden. + :type actions: + list[~botframework.connector.teams.models.O365ConnectorCardActionBase] + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "inputs": {"key": "inputs", "type": "[O365ConnectorCardInputBase]"}, + "actions": {"key": "actions", "type": "[O365ConnectorCardActionBase]"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardActionCard, self).__init__(**kwargs) + self.inputs = kwargs.get("inputs", None) + self.actions = kwargs.get("actions", None) + + +class O365ConnectorCardActionQuery(Model): + """O365 connector card HttpPOST invoke query. + + :param body: The results of body string defined in + IO365ConnectorCardHttpPOST with substituted input values + :type body: str + :param action_id: Action Id associated with the HttpPOST action button + triggered, defined in O365ConnectorCardActionBase. + :type action_id: str + """ + + _attribute_map = { + "body": {"key": "body", "type": "str"}, + "action_id": {"key": "actionId", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardActionQuery, self).__init__(**kwargs) + self.body = kwargs.get("body", None) + # This is how it comes in from Teams + self.action_id = kwargs.get("actionId", None) + + +class O365ConnectorCardDateInput(O365ConnectorCardInputBase): + """O365 connector card date input. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param include_time: Include time input field. Default value is false + (date only). + :type include_time: bool + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + "include_time": {"key": "includeTime", "type": "bool"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardDateInput, self).__init__(**kwargs) + self.include_time = kwargs.get("include_time", None) + + +class O365ConnectorCardFact(Model): + """O365 connector card fact. + + :param name: Display name of the fact + :type name: str + :param value: Display value for the fact + :type value: str + """ + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardFact, self).__init__(**kwargs) + self.name = kwargs.get("name", None) + self.value = kwargs.get("value", None) + + +class O365ConnectorCardHttpPOST(O365ConnectorCardActionBase): + """O365 connector card HttpPOST action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param body: Content to be posted back to bots via invoke + :type body: str + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "body": {"key": "body", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardHttpPOST, self).__init__(**kwargs) + self.body = kwargs.get("body", None) + + +class O365ConnectorCardImage(Model): + """O365 connector card image. + + :param image: URL for the image + :type image: str + :param title: Alternative text for the image + :type title: str + """ + + _attribute_map = { + "image": {"key": "image", "type": "str"}, + "title": {"key": "title", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardImage, self).__init__(**kwargs) + self.image = kwargs.get("image", None) + self.title = kwargs.get("title", None) + + +class O365ConnectorCardInputBase(Model): + """O365 connector card input for ActionCard action. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardInputBase, self).__init__(**kwargs) + self.type = kwargs.get("type", None) + self.id = kwargs.get("id", None) + self.is_required = kwargs.get("is_required", None) + self.title = kwargs.get("title", None) + self.value = kwargs.get("value", None) + + +class O365ConnectorCardMultichoiceInput(O365ConnectorCardInputBase): + """O365 connector card multiple choice input. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param choices: Set of choices whose each item can be in any subtype of + O365ConnectorCardMultichoiceInputChoice. + :type choices: + list[~botframework.connector.teams.models.O365ConnectorCardMultichoiceInputChoice] + :param style: Choice item rendering style. Default value is 'compact'. + Possible values include: 'compact', 'expanded' + :type style: str or ~botframework.connector.teams.models.enum + :param is_multi_select: Define if this input field allows multiple + selections. Default value is false. + :type is_multi_select: bool + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + "choices": { + "key": "choices", + "type": "[O365ConnectorCardMultichoiceInputChoice]", + }, + "style": {"key": "style", "type": "str"}, + "is_multi_select": {"key": "isMultiSelect", "type": "bool"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardMultichoiceInput, self).__init__(**kwargs) + self.choices = kwargs.get("choices", None) + self.style = kwargs.get("style", None) + self.is_multi_select = kwargs.get("is_multi_select", None) + + +class O365ConnectorCardMultichoiceInputChoice(Model): + """O365O365 connector card multiple choice input item. + + :param display: The text rendered on ActionCard. + :type display: str + :param value: The value received as results. + :type value: str + """ + + _attribute_map = { + "display": {"key": "display", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardMultichoiceInputChoice, self).__init__(**kwargs) + self.display = kwargs.get("display", None) + self.value = kwargs.get("value", None) + + +class O365ConnectorCardOpenUriTarget(Model): + """O365 connector card OpenUri target. + + :param os: Target operating system. Possible values include: 'default', + 'iOS', 'android', 'windows' + :type os: str or ~botframework.connector.teams.models.enum + :param uri: Target url + :type uri: str + """ + + _attribute_map = { + "os": {"key": "os", "type": "str"}, + "uri": {"key": "uri", "type": "str"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardOpenUriTarget, self).__init__(**kwargs) + self.os = kwargs.get("os", None) + self.uri = kwargs.get("uri", None) + + +class O365ConnectorCardOpenUri(O365ConnectorCardActionBase): + """O365 connector card OpenUri action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param targets: Target os / urls + :type targets: + list[~botframework.connector.teams.models.O365ConnectorCardOpenUriTarget] + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "targets": {"key": "targets", "type": "[O365ConnectorCardOpenUriTarget]"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardOpenUri, self).__init__(**kwargs) + self.targets = kwargs.get("targets", None) + + +class O365ConnectorCardSection(Model): + """O365 connector card section. + + :param title: Title of the section + :type title: str + :param text: Text for the section + :type text: str + :param activity_title: Activity title + :type activity_title: str + :param activity_subtitle: Activity subtitle + :type activity_subtitle: str + :param activity_text: Activity text + :type activity_text: str + :param activity_image: Activity image + :type activity_image: str + :param activity_image_type: Describes how Activity image is rendered. + Possible values include: 'avatar', 'article' + :type activity_image_type: str or + ~botframework.connector.teams.models.enum + :param markdown: Use markdown for all text contents. Default value is + true. + :type markdown: bool + :param facts: Set of facts for the current section + :type facts: + list[~botframework.connector.teams.models.O365ConnectorCardFact] + :param images: Set of images for the current section + :type images: + list[~botframework.connector.teams.models.O365ConnectorCardImage] + :param potential_action: Set of actions for the current section + :type potential_action: + list[~botframework.connector.teams.models.O365ConnectorCardActionBase] + """ + + _attribute_map = { + "title": {"key": "title", "type": "str"}, + "text": {"key": "text", "type": "str"}, + "activity_title": {"key": "activityTitle", "type": "str"}, + "activity_subtitle": {"key": "activitySubtitle", "type": "str"}, + "activity_text": {"key": "activityText", "type": "str"}, + "activity_image": {"key": "activityImage", "type": "str"}, + "activity_image_type": {"key": "activityImageType", "type": "str"}, + "markdown": {"key": "markdown", "type": "bool"}, + "facts": {"key": "facts", "type": "[O365ConnectorCardFact]"}, + "images": {"key": "images", "type": "[O365ConnectorCardImage]"}, + "potential_action": { + "key": "potentialAction", + "type": "[O365ConnectorCardActionBase]", + }, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardSection, self).__init__(**kwargs) + self.title = kwargs.get("title", None) + self.text = kwargs.get("text", None) + self.activity_title = kwargs.get("activity_title", None) + self.activity_subtitle = kwargs.get("activity_subtitle", None) + self.activity_text = kwargs.get("activity_text", None) + self.activity_image = kwargs.get("activity_image", None) + self.activity_image_type = kwargs.get("activity_image_type", None) + self.markdown = kwargs.get("markdown", None) + self.facts = kwargs.get("facts", None) + self.images = kwargs.get("images", None) + self.potential_action = kwargs.get("potential_action", None) + + +class O365ConnectorCardTextInput(O365ConnectorCardInputBase): + """O365 connector card text input. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param is_multiline: Define if text input is allowed for multiple lines. + Default value is false. + :type is_multiline: bool + :param max_length: Maximum length of text input. Default value is + unlimited. + :type max_length: float + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + "is_multiline": {"key": "isMultiline", "type": "bool"}, + "max_length": {"key": "maxLength", "type": "float"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardTextInput, self).__init__(**kwargs) + self.is_multiline = kwargs.get("is_multiline", None) + self.max_length = kwargs.get("max_length", None) + + +class O365ConnectorCardViewAction(O365ConnectorCardActionBase): + """O365 connector card ViewAction action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param target: Target urls, only the first url effective for card button + :type target: list[str] + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "target": {"key": "target", "type": "[str]"}, + } + + def __init__(self, **kwargs): + super(O365ConnectorCardViewAction, self).__init__(**kwargs) + self.target = kwargs.get("target", None) + + +class SigninStateVerificationQuery(Model): + """Signin state (part of signin action auth flow) verification invoke query. + + :param state: The state string originally received when the signin web + flow is finished with a state posted back to client via tab SDK + microsoftTeams.authentication.notifySuccess(state) + :type state: str + """ + + _attribute_map = { + "state": {"key": "state", "type": "str"}, + } + + def __init__(self, **kwargs): + super(SigninStateVerificationQuery, self).__init__(**kwargs) + self.state = kwargs.get("state", None) + + +class TaskModuleContinueResponse(TaskModuleResponseBase): + """Task Module Response with continue action. + + :param type: Choice of action options when responding to the task/submit + message. Possible values include: 'message', 'continue' + :type type: str or ~botframework.connector.teams.models.enum + :param value: The JSON for the Adaptive card to appear in the task module. + :type value: ~botframework.connector.teams.models.TaskModuleTaskInfo + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "value": {"key": "value", "type": "TaskModuleTaskInfo"}, + } + + def __init__(self, **kwargs): + super(TaskModuleContinueResponse, self).__init__(**kwargs) + self.value = kwargs.get("value", None) + + +class TaskModuleMessageResponse(TaskModuleResponseBase): + """Task Module response with message action. + + :param type: Choice of action options when responding to the task/submit + message. Possible values include: 'message', 'continue' + :type type: str or ~botframework.connector.teams.models.enum + :param value: Teams will display the value of value in a popup message + box. + :type value: str + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TaskModuleMessageResponse, self).__init__(**kwargs) + self.value = kwargs.get("value", None) + + +class TaskModuleRequest(Model): + """Task module invoke request value payload. + + :param data: User input data. Free payload with key-value pairs. + :type data: object + :param context: Current user context, i.e., the current theme + :type context: + ~botframework.connector.teams.models.TaskModuleRequestContext + """ + + _attribute_map = { + "data": {"key": "data", "type": "object"}, + "context": {"key": "context", "type": "TaskModuleRequestContext"}, + } + + def __init__(self, **kwargs): + super(TaskModuleRequest, self).__init__(**kwargs) + self.data = kwargs.get("data", None) + self.context = kwargs.get("context", None) + + +class TaskModuleRequestContext(Model): + """Current user context, i.e., the current theme. + + :param theme: + :type theme: str + """ + + _attribute_map = { + "theme": {"key": "theme", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TaskModuleRequestContext, self).__init__(**kwargs) + self.theme = kwargs.get("theme", None) + + +class TaskModuleResponse(Model): + """Envelope for Task Module Response. + + :param task: The JSON for the Adaptive card to appear in the task module. + :type task: ~botframework.connector.teams.models.TaskModuleResponseBase + """ + + _attribute_map = { + "task": {"key": "task", "type": "TaskModuleResponseBase"}, + } + + def __init__(self, **kwargs): + super(TaskModuleResponse, self).__init__(**kwargs) + self.task = kwargs.get("task", None) + + +class TaskModuleResponseBase(Model): + """Base class for Task Module responses. + + :param type: Choice of action options when responding to the task/submit + message. Possible values include: 'message', 'continue' + :type type: str or ~botframework.connector.teams.models.enum + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TaskModuleResponseBase, self).__init__(**kwargs) + self.type = kwargs.get("type", None) + + +class TaskModuleTaskInfo(Model): + """Metadata for a Task Module. + + :param title: Appears below the app name and to the right of the app icon. + :type title: str + :param height: This can be a number, representing the task module's height + in pixels, or a string, one of: small, medium, large. + :type height: object + :param width: This can be a number, representing the task module's width + in pixels, or a string, one of: small, medium, large. + :type width: object + :param url: The URL of what is loaded as an iframe inside the task module. + One of url or card is required. + :type url: str + :param card: The JSON for the Adaptive card to appear in the task module. + :type card: ~botframework.connector.teams.models.Attachment + :param fallback_url: If a client does not support the task module feature, + this URL is opened in a browser tab. + :type fallback_url: str + :param completion_bot_id: If a client does not support the task module + feature, this URL is opened in a browser tab. + :type completion_bot_id: str + """ + + _attribute_map = { + "title": {"key": "title", "type": "str"}, + "height": {"key": "height", "type": "object"}, + "width": {"key": "width", "type": "object"}, + "url": {"key": "url", "type": "str"}, + "card": {"key": "card", "type": "Attachment"}, + "fallback_url": {"key": "fallbackUrl", "type": "str"}, + "completion_bot_id": {"key": "completionBotId", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TaskModuleTaskInfo, self).__init__(**kwargs) + self.title = kwargs.get("title", None) + self.height = kwargs.get("height", None) + self.width = kwargs.get("width", None) + self.url = kwargs.get("url", None) + self.card = kwargs.get("card", None) + self.fallback_url = kwargs.get("fallback_url", None) + self.completion_bot_id = kwargs.get("completion_bot_id", None) + + +class TeamDetails(Model): + """Details related to a team. + + :param id: Unique identifier representing a team + :type id: str + :param name: Name of team. + :type name: str + :param aad_group_id: Azure Active Directory (AAD) Group Id for the team. + :type aad_group_id: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "aad_group_id": {"key": "aadGroupId", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TeamDetails, self).__init__(**kwargs) + self.id = kwargs.get("id", None) + self.name = kwargs.get("name", None) + self.aad_group_id = kwargs.get("aad_group_id", None) + + +class TeamInfo(Model): + """Describes a team. + + :param id: Unique identifier representing a team + :type id: str + :param name: Name of team. + :type name: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TeamInfo, self).__init__(**kwargs) + self.id = kwargs.get("id", None) + self.name = kwargs.get("name", None) + + +class TeamsChannelAccount(ChannelAccount): + """Teams channel account detailing user Azure Active Directory details. + + :param id: Channel id for the user or bot on this channel (Example: + joe@smith.com, or @joesmith or 123456) + :type id: str + :param name: Display friendly name + :type name: str + :param given_name: Given name part of the user name. + :type given_name: str + :param surname: Surname part of the user name. + :type surname: str + :param email: Email Id of the user. + :type email: str + :param user_principal_name: Unique user principal name + :type user_principal_name: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "given_name": {"key": "givenName", "type": "str"}, + "surname": {"key": "surname", "type": "str"}, + "email": {"key": "email", "type": "str"}, + "userPrincipalName": {"key": "userPrincipalName", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TeamsChannelAccount, self).__init__(**kwargs) + self.given_name = kwargs.get("given_name", None) + self.surname = kwargs.get("surname", None) + self.email = kwargs.get("email", None) + self.user_principal_name = kwargs.get("userPrincipalName", None) + + +class TeamsChannelData(Model): + """Channel data specific to messages received in Microsoft Teams. + + :param channel: Information about the channel in which the message was + sent + :type channel: ~botframework.connector.teams.models.ChannelInfo + :param event_type: Type of event. + :type event_type: str + :param team: Information about the team in which the message was sent + :type team: ~botframework.connector.teams.models.TeamInfo + :param notification: Notification settings for the message + :type notification: ~botframework.connector.teams.models.NotificationInfo + :param tenant: Information about the tenant in which the message was sent + :type tenant: ~botframework.connector.teams.models.TenantInfo + """ + + _attribute_map = { + "channel": {"key": "channel", "type": "ChannelInfo"}, + "event_type": {"key": "eventType", "type": "str"}, + "team": {"key": "team", "type": "TeamInfo"}, + "notification": {"key": "notification", "type": "NotificationInfo"}, + "tenant": {"key": "tenant", "type": "TenantInfo"}, + } + + def __init__(self, **kwargs): + super(TeamsChannelData, self).__init__(**kwargs) + self.channel = kwargs.get("channel", None) + # doing camel case here since that's how the data comes in + self.event_type = kwargs.get("eventType", None) + self.team = kwargs.get("team", None) + self.notification = kwargs.get("notification", None) + self.tenant = kwargs.get("tenant", None) + + +class TenantInfo(Model): + """Describes a tenant. + + :param id: Unique identifier representing a tenant + :type id: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TenantInfo, self).__init__(**kwargs) + self.id = kwargs.get("id", None) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py new file mode 100644 index 000000000..0f3a075a6 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -0,0 +1,1878 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.serialization import Model +from botbuilder.schema import Attachment, ChannelAccount + + +class TaskModuleRequest(Model): + """Task module invoke request value payload. + + :param data: User input data. Free payload with key-value pairs. + :type data: object + :param context: Current user context, i.e., the current theme + :type context: + ~botframework.connector.teams.models.TaskModuleRequestContext + """ + + _attribute_map = { + "data": {"key": "data", "type": "object"}, + "context": {"key": "context", "type": "TaskModuleRequestContext"}, + } + + def __init__(self, *, data=None, context=None, **kwargs) -> None: + super(TaskModuleRequest, self).__init__(**kwargs) + self.data = data + self.context = context + + +class AppBasedLinkQuery(Model): + """Invoke request body type for app-based link query. + + :param url: Url queried by user + :type url: str + """ + + _attribute_map = { + "url": {"key": "url", "type": "str"}, + } + + def __init__(self, *, url: str = None, **kwargs) -> None: + super(AppBasedLinkQuery, self).__init__(**kwargs) + self.url = url + + +class ChannelInfo(Model): + """A channel info object which describes the channel. + + :param id: Unique identifier representing a channel + :type id: str + :param name: Name of the channel + :type name: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + } + + def __init__(self, *, id: str = None, name: str = None, **kwargs) -> None: + super(ChannelInfo, self).__init__(**kwargs) + self.id = id + self.name = name + + +class ConversationList(Model): + """List of channels under a team. + + :param conversations: + :type conversations: + list[~botframework.connector.teams.models.ChannelInfo] + """ + + _attribute_map = { + "conversations": {"key": "conversations", "type": "[ChannelInfo]"}, + } + + def __init__(self, *, conversations=None, **kwargs) -> None: + super(ConversationList, self).__init__(**kwargs) + self.conversations = conversations + + +class FileConsentCard(Model): + """File consent card attachment. + + :param description: File description. + :type description: str + :param size_in_bytes: Size of the file to be uploaded in Bytes. + :type size_in_bytes: long + :param accept_context: Context sent back to the Bot if user consented to + upload. This is free flow schema and is sent back in Value field of + Activity. + :type accept_context: object + :param decline_context: Context sent back to the Bot if user declined. + This is free flow schema and is sent back in Value field of Activity. + :type decline_context: object + """ + + _attribute_map = { + "description": {"key": "description", "type": "str"}, + "size_in_bytes": {"key": "sizeInBytes", "type": "long"}, + "accept_context": {"key": "acceptContext", "type": "object"}, + "decline_context": {"key": "declineContext", "type": "object"}, + } + + def __init__( + self, + *, + description: str = None, + size_in_bytes: int = None, + accept_context=None, + decline_context=None, + **kwargs + ) -> None: + super(FileConsentCard, self).__init__(**kwargs) + self.description = description + self.size_in_bytes = size_in_bytes + self.accept_context = accept_context + self.decline_context = decline_context + + +class FileConsentCardResponse(Model): + """Represents the value of the invoke activity sent when the user acts on a + file consent card. + + :param action: The action the user took. Possible values include: + 'accept', 'decline' + :type action: str or ~botframework.connector.teams.models.enum + :param context: The context associated with the action. + :type context: object + :param upload_info: If the user accepted the file, contains information + about the file to be uploaded. + :type upload_info: ~botframework.connector.teams.models.FileUploadInfo + """ + + _attribute_map = { + "action": {"key": "action", "type": "str"}, + "context": {"key": "context", "type": "object"}, + "upload_info": {"key": "uploadInfo", "type": "FileUploadInfo"}, + } + + def __init__( + self, *, action=None, context=None, upload_info=None, **kwargs + ) -> None: + super(FileConsentCardResponse, self).__init__(**kwargs) + self.action = action + self.context = context + self.upload_info = upload_info + + +class FileDownloadInfo(Model): + """File download info attachment. + + :param download_url: File download url. + :type download_url: str + :param unique_id: Unique Id for the file. + :type unique_id: str + :param file_type: Type of file. + :type file_type: str + :param etag: ETag for the file. + :type etag: object + """ + + _attribute_map = { + "download_url": {"key": "downloadUrl", "type": "str"}, + "unique_id": {"key": "uniqueId", "type": "str"}, + "file_type": {"key": "fileType", "type": "str"}, + "etag": {"key": "etag", "type": "object"}, + } + + def __init__( + self, + *, + download_url: str = None, + unique_id: str = None, + file_type: str = None, + etag=None, + **kwargs + ) -> None: + super(FileDownloadInfo, self).__init__(**kwargs) + self.download_url = download_url + self.unique_id = unique_id + self.file_type = file_type + self.etag = etag + + +class FileInfoCard(Model): + """File info card. + + :param unique_id: Unique Id for the file. + :type unique_id: str + :param file_type: Type of file. + :type file_type: str + :param etag: ETag for the file. + :type etag: object + """ + + _attribute_map = { + "unique_id": {"key": "uniqueId", "type": "str"}, + "file_type": {"key": "fileType", "type": "str"}, + "etag": {"key": "etag", "type": "object"}, + } + + def __init__( + self, *, unique_id: str = None, file_type: str = None, etag=None, **kwargs + ) -> None: + super(FileInfoCard, self).__init__(**kwargs) + self.unique_id = unique_id + self.file_type = file_type + self.etag = etag + + +class FileUploadInfo(Model): + """Information about the file to be uploaded. + + :param name: Name of the file. + :type name: str + :param upload_url: URL to an upload session that the bot can use to set + the file contents. + :type upload_url: str + :param content_url: URL to file. + :type content_url: str + :param unique_id: ID that uniquely identifies the file. + :type unique_id: str + :param file_type: Type of the file. + :type file_type: str + """ + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "upload_url": {"key": "uploadUrl", "type": "str"}, + "content_url": {"key": "contentUrl", "type": "str"}, + "unique_id": {"key": "uniqueId", "type": "str"}, + "file_type": {"key": "fileType", "type": "str"}, + } + + def __init__( + self, + *, + name: str = None, + upload_url: str = None, + content_url: str = None, + unique_id: str = None, + file_type: str = None, + **kwargs + ) -> None: + super(FileUploadInfo, self).__init__(**kwargs) + self.name = name + self.upload_url = upload_url + self.content_url = content_url + self.unique_id = unique_id + self.file_type = file_type + + +class MessageActionsPayloadApp(Model): + """Represents an application entity. + + :param application_identity_type: The type of application. Possible values + include: 'aadApplication', 'bot', 'tenantBot', 'office365Connector', + 'webhook' + :type application_identity_type: str or + ~botframework.connector.teams.models.enum + :param id: The id of the application. + :type id: str + :param display_name: The plaintext display name of the application. + :type display_name: str + """ + + _attribute_map = { + "application_identity_type": {"key": "applicationIdentityType", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__( + self, + *, + application_identity_type=None, + id: str = None, + display_name: str = None, + **kwargs + ) -> None: + super(MessageActionsPayloadApp, self).__init__(**kwargs) + self.application_identity_type = application_identity_type + self.id = id + self.display_name = display_name + + +class MessageActionsPayloadAttachment(Model): + """Represents the attachment in a message. + + :param id: The id of the attachment. + :type id: str + :param content_type: The type of the attachment. + :type content_type: str + :param content_url: The url of the attachment, in case of a external link. + :type content_url: str + :param content: The content of the attachment, in case of a code snippet, + email, or file. + :type content: object + :param name: The plaintext display name of the attachment. + :type name: str + :param thumbnail_url: The url of a thumbnail image that might be embedded + in the attachment, in case of a card. + :type thumbnail_url: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "content_type": {"key": "contentType", "type": "str"}, + "content_url": {"key": "contentUrl", "type": "str"}, + "content": {"key": "content", "type": "object"}, + "name": {"key": "name", "type": "str"}, + "thumbnail_url": {"key": "thumbnailUrl", "type": "str"}, + } + + def __init__( + self, + *, + id: str = None, + content_type: str = None, + content_url: str = None, + content=None, + name: str = None, + thumbnail_url: str = None, + **kwargs + ) -> None: + super(MessageActionsPayloadAttachment, self).__init__(**kwargs) + self.id = id + self.content_type = content_type + self.content_url = content_url + self.content = content + self.name = name + self.thumbnail_url = thumbnail_url + + +class MessageActionsPayloadBody(Model): + """Plaintext/HTML representation of the content of the message. + + :param content_type: Type of the content. Possible values include: 'html', + 'text' + :type content_type: str or ~botframework.connector.teams.models.enum + :param content: The content of the body. + :type content: str + """ + + _attribute_map = { + "content_type": {"key": "contentType", "type": "str"}, + "content": {"key": "content", "type": "str"}, + } + + def __init__(self, *, content_type=None, content: str = None, **kwargs) -> None: + super(MessageActionsPayloadBody, self).__init__(**kwargs) + self.content_type = content_type + self.content = content + + +class MessageActionsPayloadConversation(Model): + """Represents a team or channel entity. + + :param conversation_identity_type: The type of conversation, whether a + team or channel. Possible values include: 'team', 'channel' + :type conversation_identity_type: str or + ~botframework.connector.teams.models.enum + :param id: The id of the team or channel. + :type id: str + :param display_name: The plaintext display name of the team or channel + entity. + :type display_name: str + """ + + _attribute_map = { + "conversation_identity_type": { + "key": "conversationIdentityType", + "type": "str", + }, + "id": {"key": "id", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__( + self, + *, + conversation_identity_type=None, + id: str = None, + display_name: str = None, + **kwargs + ) -> None: + super(MessageActionsPayloadConversation, self).__init__(**kwargs) + self.conversation_identity_type = conversation_identity_type + self.id = id + self.display_name = display_name + + +class MessageActionsPayloadFrom(Model): + """Represents a user, application, or conversation type that either sent or + was referenced in a message. + + :param user: Represents details of the user. + :type user: ~botframework.connector.teams.models.MessageActionsPayloadUser + :param application: Represents details of the app. + :type application: + ~botframework.connector.teams.models.MessageActionsPayloadApp + :param conversation: Represents details of the converesation. + :type conversation: + ~botframework.connector.teams.models.MessageActionsPayloadConversation + """ + + _attribute_map = { + "user": {"key": "user", "type": "MessageActionsPayloadUser"}, + "application": {"key": "application", "type": "MessageActionsPayloadApp"}, + "conversation": { + "key": "conversation", + "type": "MessageActionsPayloadConversation", + }, + } + + def __init__( + self, *, user=None, application=None, conversation=None, **kwargs + ) -> None: + super(MessageActionsPayloadFrom, self).__init__(**kwargs) + self.user = user + self.application = application + self.conversation = conversation + + +class MessageActionsPayloadMention(Model): + """Represents the entity that was mentioned in the message. + + :param id: The id of the mentioned entity. + :type id: int + :param mention_text: The plaintext display name of the mentioned entity. + :type mention_text: str + :param mentioned: Provides more details on the mentioned entity. + :type mentioned: + ~botframework.connector.teams.models.MessageActionsPayloadFrom + """ + + _attribute_map = { + "id": {"key": "id", "type": "int"}, + "mention_text": {"key": "mentionText", "type": "str"}, + "mentioned": {"key": "mentioned", "type": "MessageActionsPayloadFrom"}, + } + + def __init__( + self, *, id: int = None, mention_text: str = None, mentioned=None, **kwargs + ) -> None: + super(MessageActionsPayloadMention, self).__init__(**kwargs) + self.id = id + self.mention_text = mention_text + self.mentioned = mentioned + + +class MessageActionsPayloadReaction(Model): + """Represents the reaction of a user to a message. + + :param reaction_type: The type of reaction given to the message. Possible + values include: 'like', 'heart', 'laugh', 'surprised', 'sad', 'angry' + :type reaction_type: str or ~botframework.connector.teams.models.enum + :param created_date_time: Timestamp of when the user reacted to the + message. + :type created_date_time: str + :param user: The user with which the reaction is associated. + :type user: ~botframework.connector.teams.models.MessageActionsPayloadFrom + """ + + _attribute_map = { + "reaction_type": {"key": "reactionType", "type": "str"}, + "created_date_time": {"key": "createdDateTime", "type": "str"}, + "user": {"key": "user", "type": "MessageActionsPayloadFrom"}, + } + + def __init__( + self, *, reaction_type=None, created_date_time: str = None, user=None, **kwargs + ) -> None: + super(MessageActionsPayloadReaction, self).__init__(**kwargs) + self.reaction_type = reaction_type + self.created_date_time = created_date_time + self.user = user + + +class MessageActionsPayloadUser(Model): + """Represents a user entity. + + :param user_identity_type: The identity type of the user. Possible values + include: 'aadUser', 'onPremiseAadUser', 'anonymousGuest', 'federatedUser' + :type user_identity_type: str or ~botframework.connector.teams.models.enum + :param id: The id of the user. + :type id: str + :param display_name: The plaintext display name of the user. + :type display_name: str + """ + + _attribute_map = { + "user_identity_type": {"key": "userIdentityType", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__( + self, + *, + user_identity_type=None, + id: str = None, + display_name: str = None, + **kwargs + ) -> None: + super(MessageActionsPayloadUser, self).__init__(**kwargs) + self.user_identity_type = user_identity_type + self.id = id + self.display_name = display_name + + +class MessageActionsPayload(Model): + """Represents the individual message within a chat or channel where a message + actions is taken. + + :param id: Unique id of the message. + :type id: str + :param reply_to_id: Id of the parent/root message of the thread. + :type reply_to_id: str + :param message_type: Type of message - automatically set to message. + Possible values include: 'message' + :type message_type: str or ~botframework.connector.teams.models.enum + :param created_date_time: Timestamp of when the message was created. + :type created_date_time: str + :param last_modified_date_time: Timestamp of when the message was edited + or updated. + :type last_modified_date_time: str + :param deleted: Indicates whether a message has been soft deleted. + :type deleted: bool + :param subject: Subject line of the message. + :type subject: str + :param summary: Summary text of the message that could be used for + notifications. + :type summary: str + :param importance: The importance of the message. Possible values include: + 'normal', 'high', 'urgent' + :type importance: str or ~botframework.connector.teams.models.enum + :param locale: Locale of the message set by the client. + :type locale: str + :param from_property: Sender of the message. + :type from_property: + ~botframework.connector.teams.models.MessageActionsPayloadFrom + :param body: Plaintext/HTML representation of the content of the message. + :type body: ~botframework.connector.teams.models.MessageActionsPayloadBody + :param attachment_layout: How the attachment(s) are displayed in the + message. + :type attachment_layout: str + :param attachments: Attachments in the message - card, image, file, etc. + :type attachments: + list[~botframework.connector.teams.models.MessageActionsPayloadAttachment] + :param mentions: List of entities mentioned in the message. + :type mentions: + list[~botframework.connector.teams.models.MessageActionsPayloadMention] + :param reactions: Reactions for the message. + :type reactions: + list[~botframework.connector.teams.models.MessageActionsPayloadReaction] + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "reply_to_id": {"key": "replyToId", "type": "str"}, + "message_type": {"key": "messageType", "type": "str"}, + "created_date_time": {"key": "createdDateTime", "type": "str"}, + "last_modified_date_time": {"key": "lastModifiedDateTime", "type": "str"}, + "deleted": {"key": "deleted", "type": "bool"}, + "subject": {"key": "subject", "type": "str"}, + "summary": {"key": "summary", "type": "str"}, + "importance": {"key": "importance", "type": "str"}, + "locale": {"key": "locale", "type": "str"}, + "from_property": {"key": "from", "type": "MessageActionsPayloadFrom"}, + "body": {"key": "body", "type": "MessageActionsPayloadBody"}, + "attachment_layout": {"key": "attachmentLayout", "type": "str"}, + "attachments": { + "key": "attachments", + "type": "[MessageActionsPayloadAttachment]", + }, + "mentions": {"key": "mentions", "type": "[MessageActionsPayloadMention]"}, + "reactions": {"key": "reactions", "type": "[MessageActionsPayloadReaction]"}, + } + + def __init__( + self, + *, + id: str = None, + reply_to_id: str = None, + message_type=None, + created_date_time: str = None, + last_modified_date_time: str = None, + deleted: bool = None, + subject: str = None, + summary: str = None, + importance=None, + locale: str = None, + from_property=None, + body=None, + attachment_layout: str = None, + attachments=None, + mentions=None, + reactions=None, + **kwargs + ) -> None: + super(MessageActionsPayload, self).__init__(**kwargs) + self.id = id + self.reply_to_id = reply_to_id + self.message_type = message_type + self.created_date_time = created_date_time + self.last_modified_date_time = last_modified_date_time + self.deleted = deleted + self.subject = subject + self.summary = summary + self.importance = importance + self.locale = locale + self.from_property = from_property + self.body = body + self.attachment_layout = attachment_layout + self.attachments = attachments + self.mentions = mentions + self.reactions = reactions + + +class MessagingExtensionAction(TaskModuleRequest): + """Messaging extension action. + + :param data: User input data. Free payload with key-value pairs. + :type data: object + :param context: Current user context, i.e., the current theme + :type context: + ~botframework.connector.teams.models.TaskModuleRequestContext + :param command_id: Id of the command assigned by Bot + :type command_id: str + :param command_context: The context from which the command originates. + Possible values include: 'message', 'compose', 'commandbox' + :type command_context: str or ~botframework.connector.teams.models.enum + :param bot_message_preview_action: Bot message preview action taken by + user. Possible values include: 'edit', 'send' + :type bot_message_preview_action: str or + ~botframework.connector.teams.models.enum + :param bot_activity_preview: + :type bot_activity_preview: + list[~botframework.connector.teams.models.Activity] + :param message_payload: Message content sent as part of the command + request. + :type message_payload: + ~botframework.connector.teams.models.MessageActionsPayload + """ + + _attribute_map = { + "data": {"key": "data", "type": "object"}, + "context": {"key": "context", "type": "TaskModuleRequestContext"}, + "command_id": {"key": "commandId", "type": "str"}, + "command_context": {"key": "commandContext", "type": "str"}, + "bot_message_preview_action": {"key": "botMessagePreviewAction", "type": "str"}, + "bot_activity_preview": {"key": "botActivityPreview", "type": "[Activity]"}, + "message_payload": {"key": "messagePayload", "type": "MessageActionsPayload"}, + } + + def __init__( + self, + *, + data=None, + context=None, + command_id: str = None, + command_context=None, + botMessagePreviewAction=None, + bot_activity_preview=None, + message_payload=None, + **kwargs + ) -> None: + super(MessagingExtensionAction, self).__init__( + data=data, context=context, **kwargs + ) + self.command_id = command_id + self.command_context = command_context + self.bot_message_preview_action = botMessagePreviewAction + self.bot_activity_preview = bot_activity_preview + self.message_payload = message_payload + + +class MessagingExtensionActionResponse(Model): + """Response of messaging extension action. + + :param task: The JSON for the Adaptive card to appear in the task module. + :type task: ~botframework.connector.teams.models.TaskModuleResponseBase + :param compose_extension: + :type compose_extension: + ~botframework.connector.teams.models.MessagingExtensionResult + """ + + _attribute_map = { + "task": {"key": "task", "type": "TaskModuleResponseBase"}, + "compose_extension": { + "key": "composeExtension", + "type": "MessagingExtensionResult", + }, + } + + def __init__(self, *, task=None, compose_extension=None, **kwargs) -> None: + super(MessagingExtensionActionResponse, self).__init__(**kwargs) + self.task = task + self.compose_extension = compose_extension + + +class MessagingExtensionAttachment(Attachment): + """Messaging extension attachment. + + :param content_type: mimetype/Contenttype for the file + :type content_type: str + :param content_url: Content Url + :type content_url: str + :param content: Embedded content + :type content: object + :param name: (OPTIONAL) The name of the attachment + :type name: str + :param thumbnail_url: (OPTIONAL) Thumbnail associated with attachment + :type thumbnail_url: str + :param preview: + :type preview: ~botframework.connector.teams.models.Attachment + """ + + _attribute_map = { + "content_type": {"key": "contentType", "type": "str"}, + "content_url": {"key": "contentUrl", "type": "str"}, + "content": {"key": "content", "type": "object"}, + "name": {"key": "name", "type": "str"}, + "thumbnail_url": {"key": "thumbnailUrl", "type": "str"}, + "preview": {"key": "preview", "type": "Attachment"}, + } + + def __init__( + self, + *, + content_type: str = None, + content_url: str = None, + content=None, + name: str = None, + thumbnail_url: str = None, + preview=None, + **kwargs + ) -> None: + super(MessagingExtensionAttachment, self).__init__( + content_type=content_type, + content_url=content_url, + content=content, + name=name, + thumbnail_url=thumbnail_url, + **kwargs + ) + self.preview = preview + + +class MessagingExtensionParameter(Model): + """Messaging extension query parameters. + + :param name: Name of the parameter + :type name: str + :param value: Value of the parameter + :type value: object + """ + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "value": {"key": "value", "type": "object"}, + } + + def __init__(self, *, name: str = None, value=None, **kwargs) -> None: + super(MessagingExtensionParameter, self).__init__(**kwargs) + self.name = name + self.value = value + + +class MessagingExtensionQuery(Model): + """Messaging extension query. + + :param command_id: Id of the command assigned by Bot + :type command_id: str + :param parameters: Parameters for the query + :type parameters: + list[~botframework.connector.teams.models.MessagingExtensionParameter] + :param query_options: + :type query_options: + ~botframework.connector.teams.models.MessagingExtensionQueryOptions + :param state: State parameter passed back to the bot after + authentication/configuration flow + :type state: str + """ + + _attribute_map = { + "command_id": {"key": "commandId", "type": "str"}, + "parameters": {"key": "parameters", "type": "[MessagingExtensionParameter]"}, + "query_options": { + "key": "queryOptions", + "type": "MessagingExtensionQueryOptions", + }, + "state": {"key": "state", "type": "str"}, + } + + def __init__( + self, + *, + command_id: str = None, + parameters=None, + query_options=None, + state: str = None, + **kwargs + ) -> None: + super(MessagingExtensionQuery, self).__init__(**kwargs) + self.command_id = command_id + self.parameters = parameters + self.query_options = query_options + self.state = state + + +class MessagingExtensionQueryOptions(Model): + """Messaging extension query options. + + :param skip: Number of entities to skip + :type skip: int + :param count: Number of entities to fetch + :type count: int + """ + + _attribute_map = { + "skip": {"key": "skip", "type": "int"}, + "count": {"key": "count", "type": "int"}, + } + + def __init__(self, *, skip: int = None, count: int = None, **kwargs) -> None: + super(MessagingExtensionQueryOptions, self).__init__(**kwargs) + self.skip = skip + self.count = count + + +class MessagingExtensionResponse(Model): + """Messaging extension response. + + :param compose_extension: + :type compose_extension: + ~botframework.connector.teams.models.MessagingExtensionResult + """ + + _attribute_map = { + "compose_extension": { + "key": "composeExtension", + "type": "MessagingExtensionResult", + }, + } + + def __init__(self, *, compose_extension=None, **kwargs) -> None: + super(MessagingExtensionResponse, self).__init__(**kwargs) + self.compose_extension = compose_extension + + +class MessagingExtensionResult(Model): + """Messaging extension result. + + :param attachment_layout: Hint for how to deal with multiple attachments. + Possible values include: 'list', 'grid' + :type attachment_layout: str or ~botframework.connector.teams.models.enum + :param type: The type of the result. Possible values include: 'result', + 'auth', 'config', 'message', 'botMessagePreview' + :type type: str or ~botframework.connector.teams.models.enum + :param attachments: (Only when type is result) Attachments + :type attachments: + list[~botframework.connector.teams.models.MessagingExtensionAttachment] + :param suggested_actions: + :type suggested_actions: + ~botframework.connector.teams.models.MessagingExtensionSuggestedAction + :param text: (Only when type is message) Text + :type text: str + :param activity_preview: (Only when type is botMessagePreview) Message + activity to preview + :type activity_preview: ~botframework.connector.teams.models.Activity + """ + + _attribute_map = { + "attachment_layout": {"key": "attachmentLayout", "type": "str"}, + "type": {"key": "type", "type": "str"}, + "attachments": {"key": "attachments", "type": "[MessagingExtensionAttachment]"}, + "suggested_actions": { + "key": "suggestedActions", + "type": "MessagingExtensionSuggestedAction", + }, + "text": {"key": "text", "type": "str"}, + "activity_preview": {"key": "activityPreview", "type": "Activity"}, + } + + def __init__( + self, + *, + attachment_layout=None, + type=None, + attachments=None, + suggested_actions=None, + text: str = None, + activity_preview=None, + **kwargs + ) -> None: + super(MessagingExtensionResult, self).__init__(**kwargs) + self.attachment_layout = attachment_layout + self.type = type + self.attachments = attachments + self.suggested_actions = suggested_actions + self.text = text + self.activity_preview = activity_preview + + +class MessagingExtensionSuggestedAction(Model): + """Messaging extension Actions (Only when type is auth or config). + + :param actions: Actions + :type actions: list[~botframework.connector.teams.models.CardAction] + """ + + _attribute_map = { + "actions": {"key": "actions", "type": "[CardAction]"}, + } + + def __init__(self, *, actions=None, **kwargs) -> None: + super(MessagingExtensionSuggestedAction, self).__init__(**kwargs) + self.actions = actions + + +class NotificationInfo(Model): + """Specifies if a notification is to be sent for the mentions. + + :param alert: true if notification is to be sent to the user, false + otherwise. + :type alert: bool + """ + + _attribute_map = { + "alert": {"key": "alert", "type": "bool"}, + } + + def __init__(self, *, alert: bool = None, **kwargs) -> None: + super(NotificationInfo, self).__init__(**kwargs) + self.alert = alert + + +class O365ConnectorCard(Model): + """O365 connector card. + + :param title: Title of the item + :type title: str + :param text: Text for the card + :type text: str + :param summary: Summary for the card + :type summary: str + :param theme_color: Theme color for the card + :type theme_color: str + :param sections: Set of sections for the current card + :type sections: + list[~botframework.connector.teams.models.O365ConnectorCardSection] + :param potential_action: Set of actions for the current card + :type potential_action: + list[~botframework.connector.teams.models.O365ConnectorCardActionBase] + """ + + _attribute_map = { + "title": {"key": "title", "type": "str"}, + "text": {"key": "text", "type": "str"}, + "summary": {"key": "summary", "type": "str"}, + "theme_color": {"key": "themeColor", "type": "str"}, + "sections": {"key": "sections", "type": "[O365ConnectorCardSection]"}, + "potential_action": { + "key": "potentialAction", + "type": "[O365ConnectorCardActionBase]", + }, + } + + def __init__( + self, + *, + title: str = None, + text: str = None, + summary: str = None, + theme_color: str = None, + sections=None, + potential_action=None, + **kwargs + ) -> None: + super(O365ConnectorCard, self).__init__(**kwargs) + self.title = title + self.text = text + self.summary = summary + self.theme_color = theme_color + self.sections = sections + self.potential_action = potential_action + + +class O365ConnectorCardInputBase(Model): + """O365 connector card input for ActionCard action. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__( + self, + *, + type=None, + id: str = None, + is_required: bool = None, + title: str = None, + value: str = None, + **kwargs + ) -> None: + super(O365ConnectorCardInputBase, self).__init__(**kwargs) + self.type = type + self.id = id + self.is_required = is_required + self.title = title + self.value = value + + +class O365ConnectorCardActionBase(Model): + """O365 connector card action base. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + } + + def __init__( + self, *, type=None, name: str = None, id: str = None, **kwargs + ) -> None: + super(O365ConnectorCardActionBase, self).__init__(**kwargs) + self.type = type + self.name = name + self.id = id + + +class O365ConnectorCardActionCard(O365ConnectorCardActionBase): + """O365 connector card ActionCard action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param inputs: Set of inputs contained in this ActionCard whose each item + can be in any subtype of O365ConnectorCardInputBase + :type inputs: + list[~botframework.connector.teams.models.O365ConnectorCardInputBase] + :param actions: Set of actions contained in this ActionCard whose each + item can be in any subtype of O365ConnectorCardActionBase except + O365ConnectorCardActionCard, as nested ActionCard is forbidden. + :type actions: + list[~botframework.connector.teams.models.O365ConnectorCardActionBase] + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "inputs": {"key": "inputs", "type": "[O365ConnectorCardInputBase]"}, + "actions": {"key": "actions", "type": "[O365ConnectorCardActionBase]"}, + } + + def __init__( + self, + *, + type=None, + name: str = None, + id: str = None, + inputs=None, + actions=None, + **kwargs + ) -> None: + super(O365ConnectorCardActionCard, self).__init__( + type=type, name=name, id=id, **kwargs + ) + self.inputs = inputs + self.actions = actions + + +class O365ConnectorCardActionQuery(Model): + """O365 connector card HttpPOST invoke query. + + :param body: The results of body string defined in + IO365ConnectorCardHttpPOST with substituted input values + :type body: str + :param action_id: Action Id associated with the HttpPOST action button + triggered, defined in O365ConnectorCardActionBase. + :type action_id: str + """ + + _attribute_map = { + "body": {"key": "body", "type": "str"}, + "action_id": {"key": "actionId", "type": "str"}, + } + + def __init__(self, *, body: str = None, actionId: str = None, **kwargs) -> None: + super(O365ConnectorCardActionQuery, self).__init__(**kwargs) + self.body = body + # This is how it comes in from Teams + self.action_id = actionId + + +class O365ConnectorCardDateInput(O365ConnectorCardInputBase): + """O365 connector card date input. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param include_time: Include time input field. Default value is false + (date only). + :type include_time: bool + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + "include_time": {"key": "includeTime", "type": "bool"}, + } + + def __init__( + self, + *, + type=None, + id: str = None, + is_required: bool = None, + title: str = None, + value: str = None, + include_time: bool = None, + **kwargs + ) -> None: + super(O365ConnectorCardDateInput, self).__init__( + type=type, + id=id, + is_required=is_required, + title=title, + value=value, + **kwargs + ) + self.include_time = include_time + + +class O365ConnectorCardFact(Model): + """O365 connector card fact. + + :param name: Display name of the fact + :type name: str + :param value: Display value for the fact + :type value: str + """ + + _attribute_map = { + "name": {"key": "name", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, *, name: str = None, value: str = None, **kwargs) -> None: + super(O365ConnectorCardFact, self).__init__(**kwargs) + self.name = name + self.value = value + + +class O365ConnectorCardHttpPOST(O365ConnectorCardActionBase): + """O365 connector card HttpPOST action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param body: Content to be posted back to bots via invoke + :type body: str + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "body": {"key": "body", "type": "str"}, + } + + def __init__( + self, *, type=None, name: str = None, id: str = None, body: str = None, **kwargs + ) -> None: + super(O365ConnectorCardHttpPOST, self).__init__( + type=type, name=name, id=id, **kwargs + ) + self.body = body + + +class O365ConnectorCardImage(Model): + """O365 connector card image. + + :param image: URL for the image + :type image: str + :param title: Alternative text for the image + :type title: str + """ + + _attribute_map = { + "image": {"key": "image", "type": "str"}, + "title": {"key": "title", "type": "str"}, + } + + def __init__(self, *, image: str = None, title: str = None, **kwargs) -> None: + super(O365ConnectorCardImage, self).__init__(**kwargs) + self.image = image + self.title = title + + +class O365ConnectorCardMultichoiceInput(O365ConnectorCardInputBase): + """O365 connector card multiple choice input. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param choices: Set of choices whose each item can be in any subtype of + O365ConnectorCardMultichoiceInputChoice. + :type choices: + list[~botframework.connector.teams.models.O365ConnectorCardMultichoiceInputChoice] + :param style: Choice item rendering style. Default value is 'compact'. + Possible values include: 'compact', 'expanded' + :type style: str or ~botframework.connector.teams.models.enum + :param is_multi_select: Define if this input field allows multiple + selections. Default value is false. + :type is_multi_select: bool + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + "choices": { + "key": "choices", + "type": "[O365ConnectorCardMultichoiceInputChoice]", + }, + "style": {"key": "style", "type": "str"}, + "is_multi_select": {"key": "isMultiSelect", "type": "bool"}, + } + + def __init__( + self, + *, + type=None, + id: str = None, + is_required: bool = None, + title: str = None, + value: str = None, + choices=None, + style=None, + is_multi_select: bool = None, + **kwargs + ) -> None: + super(O365ConnectorCardMultichoiceInput, self).__init__( + type=type, + id=id, + is_required=is_required, + title=title, + value=value, + **kwargs + ) + self.choices = choices + self.style = style + self.is_multi_select = is_multi_select + + +class O365ConnectorCardMultichoiceInputChoice(Model): + """O365O365 connector card multiple choice input item. + + :param display: The text rendered on ActionCard. + :type display: str + :param value: The value received as results. + :type value: str + """ + + _attribute_map = { + "display": {"key": "display", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, *, display: str = None, value: str = None, **kwargs) -> None: + super(O365ConnectorCardMultichoiceInputChoice, self).__init__(**kwargs) + self.display = display + self.value = value + + +class O365ConnectorCardOpenUri(O365ConnectorCardActionBase): + """O365 connector card OpenUri action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param targets: Target os / urls + :type targets: + list[~botframework.connector.teams.models.O365ConnectorCardOpenUriTarget] + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "targets": {"key": "targets", "type": "[O365ConnectorCardOpenUriTarget]"}, + } + + def __init__( + self, *, type=None, name: str = None, id: str = None, targets=None, **kwargs + ) -> None: + super(O365ConnectorCardOpenUri, self).__init__( + type=type, name=name, id=id, **kwargs + ) + self.targets = targets + + +class O365ConnectorCardOpenUriTarget(Model): + """O365 connector card OpenUri target. + + :param os: Target operating system. Possible values include: 'default', + 'iOS', 'android', 'windows' + :type os: str or ~botframework.connector.teams.models.enum + :param uri: Target url + :type uri: str + """ + + _attribute_map = { + "os": {"key": "os", "type": "str"}, + "uri": {"key": "uri", "type": "str"}, + } + + def __init__(self, *, os=None, uri: str = None, **kwargs) -> None: + super(O365ConnectorCardOpenUriTarget, self).__init__(**kwargs) + self.os = os + self.uri = uri + + +class O365ConnectorCardSection(Model): + """O365 connector card section. + + :param title: Title of the section + :type title: str + :param text: Text for the section + :type text: str + :param activity_title: Activity title + :type activity_title: str + :param activity_subtitle: Activity subtitle + :type activity_subtitle: str + :param activity_text: Activity text + :type activity_text: str + :param activity_image: Activity image + :type activity_image: str + :param activity_image_type: Describes how Activity image is rendered. + Possible values include: 'avatar', 'article' + :type activity_image_type: str or + ~botframework.connector.teams.models.enum + :param markdown: Use markdown for all text contents. Default value is + true. + :type markdown: bool + :param facts: Set of facts for the current section + :type facts: + list[~botframework.connector.teams.models.O365ConnectorCardFact] + :param images: Set of images for the current section + :type images: + list[~botframework.connector.teams.models.O365ConnectorCardImage] + :param potential_action: Set of actions for the current section + :type potential_action: + list[~botframework.connector.teams.models.O365ConnectorCardActionBase] + """ + + _attribute_map = { + "title": {"key": "title", "type": "str"}, + "text": {"key": "text", "type": "str"}, + "activity_title": {"key": "activityTitle", "type": "str"}, + "activity_subtitle": {"key": "activitySubtitle", "type": "str"}, + "activity_text": {"key": "activityText", "type": "str"}, + "activity_image": {"key": "activityImage", "type": "str"}, + "activity_image_type": {"key": "activityImageType", "type": "str"}, + "markdown": {"key": "markdown", "type": "bool"}, + "facts": {"key": "facts", "type": "[O365ConnectorCardFact]"}, + "images": {"key": "images", "type": "[O365ConnectorCardImage]"}, + "potential_action": { + "key": "potentialAction", + "type": "[O365ConnectorCardActionBase]", + }, + } + + def __init__( + self, + *, + title: str = None, + text: str = None, + activity_title: str = None, + activity_subtitle: str = None, + activity_text: str = None, + activity_image: str = None, + activity_image_type=None, + markdown: bool = None, + facts=None, + images=None, + potential_action=None, + **kwargs + ) -> None: + super(O365ConnectorCardSection, self).__init__(**kwargs) + self.title = title + self.text = text + self.activity_title = activity_title + self.activity_subtitle = activity_subtitle + self.activity_text = activity_text + self.activity_image = activity_image + self.activity_image_type = activity_image_type + self.markdown = markdown + self.facts = facts + self.images = images + self.potential_action = potential_action + + +class O365ConnectorCardTextInput(O365ConnectorCardInputBase): + """O365 connector card text input. + + :param type: Input type name. Possible values include: 'textInput', + 'dateInput', 'multichoiceInput' + :type type: str or ~botframework.connector.teams.models.enum + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default + value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param is_multiline: Define if text input is allowed for multiple lines. + Default value is false. + :type is_multiline: bool + :param max_length: Maximum length of text input. Default value is + unlimited. + :type max_length: float + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "id": {"key": "id", "type": "str"}, + "is_required": {"key": "isRequired", "type": "bool"}, + "title": {"key": "title", "type": "str"}, + "value": {"key": "value", "type": "str"}, + "is_multiline": {"key": "isMultiline", "type": "bool"}, + "max_length": {"key": "maxLength", "type": "float"}, + } + + def __init__( + self, + *, + type=None, + id: str = None, + is_required: bool = None, + title: str = None, + value: str = None, + is_multiline: bool = None, + max_length: float = None, + **kwargs + ) -> None: + super(O365ConnectorCardTextInput, self).__init__( + type=type, + id=id, + is_required=is_required, + title=title, + value=value, + **kwargs + ) + self.is_multiline = is_multiline + self.max_length = max_length + + +class O365ConnectorCardViewAction(O365ConnectorCardActionBase): + """O365 connector card ViewAction action. + + :param type: Type of the action. Possible values include: 'ViewAction', + 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str or ~botframework.connector.teams.models.enum + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param target: Target urls, only the first url effective for card button + :type target: list[str] + """ + + _attribute_map = { + "type": {"key": "@type", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "id": {"key": "@id", "type": "str"}, + "target": {"key": "target", "type": "[str]"}, + } + + def __init__( + self, *, type=None, name: str = None, id: str = None, target=None, **kwargs + ) -> None: + super(O365ConnectorCardViewAction, self).__init__( + type=type, name=name, id=id, **kwargs + ) + self.target = target + + +class SigninStateVerificationQuery(Model): + """Signin state (part of signin action auth flow) verification invoke query. + + :param state: The state string originally received when the signin web + flow is finished with a state posted back to client via tab SDK + microsoftTeams.authentication.notifySuccess(state) + :type state: str + """ + + _attribute_map = { + "state": {"key": "state", "type": "str"}, + } + + def __init__(self, *, state: str = None, **kwargs) -> None: + super(SigninStateVerificationQuery, self).__init__(**kwargs) + self.state = state + + +class TaskModuleResponseBase(Model): + """Base class for Task Module responses. + + :param type: Choice of action options when responding to the task/submit + message. Possible values include: 'message', 'continue' + :type type: str or ~botframework.connector.teams.models.enum + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(TaskModuleResponseBase, self).__init__(**kwargs) + self.type = type + + +class TaskModuleContinueResponse(TaskModuleResponseBase): + """Task Module Response with continue action. + + :param type: Choice of action options when responding to the task/submit + message. Possible values include: 'message', 'continue' + :type type: str or ~botframework.connector.teams.models.enum + :param value: The JSON for the Adaptive card to appear in the task module. + :type value: ~botframework.connector.teams.models.TaskModuleTaskInfo + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "value": {"key": "value", "type": "TaskModuleTaskInfo"}, + } + + def __init__(self, *, type=None, value=None, **kwargs) -> None: + super(TaskModuleContinueResponse, self).__init__(type=type, **kwargs) + self.value = value + + +class TaskModuleMessageResponse(TaskModuleResponseBase): + """Task Module response with message action. + + :param type: Choice of action options when responding to the task/submit + message. Possible values include: 'message', 'continue' + :type type: str or ~botframework.connector.teams.models.enum + :param value: Teams will display the value of value in a popup message + box. + :type value: str + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "value": {"key": "value", "type": "str"}, + } + + def __init__(self, *, type=None, value: str = None, **kwargs) -> None: + super(TaskModuleMessageResponse, self).__init__(type=type, **kwargs) + self.value = value + + +class TaskModuleRequestContext(Model): + """Current user context, i.e., the current theme. + + :param theme: + :type theme: str + """ + + _attribute_map = { + "theme": {"key": "theme", "type": "str"}, + } + + def __init__(self, *, theme: str = None, **kwargs) -> None: + super(TaskModuleRequestContext, self).__init__(**kwargs) + self.theme = theme + + +class TaskModuleResponse(Model): + """Envelope for Task Module Response. + + :param task: The JSON for the Adaptive card to appear in the task module. + :type task: ~botframework.connector.teams.models.TaskModuleResponseBase + """ + + _attribute_map = { + "task": {"key": "task", "type": "TaskModuleResponseBase"}, + } + + def __init__(self, *, task=None, **kwargs) -> None: + super(TaskModuleResponse, self).__init__(**kwargs) + self.task = task + + +class TaskModuleTaskInfo(Model): + """Metadata for a Task Module. + + :param title: Appears below the app name and to the right of the app icon. + :type title: str + :param height: This can be a number, representing the task module's height + in pixels, or a string, one of: small, medium, large. + :type height: object + :param width: This can be a number, representing the task module's width + in pixels, or a string, one of: small, medium, large. + :type width: object + :param url: The URL of what is loaded as an iframe inside the task module. + One of url or card is required. + :type url: str + :param card: The JSON for the Adaptive card to appear in the task module. + :type card: ~botframework.connector.teams.models.Attachment + :param fallback_url: If a client does not support the task module feature, + this URL is opened in a browser tab. + :type fallback_url: str + :param completion_bot_id: If a client does not support the task module + feature, this URL is opened in a browser tab. + :type completion_bot_id: str + """ + + _attribute_map = { + "title": {"key": "title", "type": "str"}, + "height": {"key": "height", "type": "object"}, + "width": {"key": "width", "type": "object"}, + "url": {"key": "url", "type": "str"}, + "card": {"key": "card", "type": "Attachment"}, + "fallback_url": {"key": "fallbackUrl", "type": "str"}, + "completion_bot_id": {"key": "completionBotId", "type": "str"}, + } + + def __init__( + self, + *, + title: str = None, + height=None, + width=None, + url: str = None, + card=None, + fallback_url: str = None, + completion_bot_id: str = None, + **kwargs + ) -> None: + super(TaskModuleTaskInfo, self).__init__(**kwargs) + self.title = title + self.height = height + self.width = width + self.url = url + self.card = card + self.fallback_url = fallback_url + self.completion_bot_id = completion_bot_id + + +class TeamDetails(Model): + """Details related to a team. + + :param id: Unique identifier representing a team + :type id: str + :param name: Name of team. + :type name: str + :param aad_group_id: Azure Active Directory (AAD) Group Id for the team. + :type aad_group_id: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "aad_group_id": {"key": "aadGroupId", "type": "str"}, + } + + def __init__( + self, *, id: str = None, name: str = None, aad_group_id: str = None, **kwargs + ) -> None: + super(TeamDetails, self).__init__(**kwargs) + self.id = id + self.name = name + self.aad_group_id = aad_group_id + + +class TeamInfo(Model): + """Describes a team. + + :param id: Unique identifier representing a team + :type id: str + :param name: Name of team. + :type name: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + } + + def __init__(self, *, id: str = None, name: str = None, **kwargs) -> None: + super(TeamInfo, self).__init__(**kwargs) + self.id = id + self.name = name + + +class TeamsChannelAccount(ChannelAccount): + """Teams channel account detailing user Azure Active Directory details. + + :param id: Channel id for the user or bot on this channel (Example: + joe@smith.com, or @joesmith or 123456) + :type id: str + :param name: Display friendly name + :type name: str + :param given_name: Given name part of the user name. + :type given_name: str + :param surname: Surname part of the user name. + :type surname: str + :param email: Email Id of the user. + :type email: str + :param user_principal_name: Unique user principal name + :type user_principal_name: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + "name": {"key": "name", "type": "str"}, + "given_name": {"key": "givenName", "type": "str"}, + "surname": {"key": "surname", "type": "str"}, + "email": {"key": "email", "type": "str"}, + "user_principal_name": {"key": "userPrincipalName", "type": "str"}, + } + + def __init__( + self, + *, + id: str = None, + name: str = None, + given_name: str = None, + surname: str = None, + email: str = None, + user_principal_name: str = None, + **kwargs + ) -> None: + super(TeamsChannelAccount, self).__init__(id=id, name=name, **kwargs) + self.given_name = given_name + self.surname = surname + self.email = email + # changing to camel case due to how data comes in off the wire + self.user_principal_name = user_principal_name + + +class TeamsChannelData(Model): + """Channel data specific to messages received in Microsoft Teams. + + :param channel: Information about the channel in which the message was + sent + :type channel: ~botframework.connector.teams.models.ChannelInfo + :param event_type: Type of event. + :type event_type: str + :param team: Information about the team in which the message was sent + :type team: ~botframework.connector.teams.models.TeamInfo + :param notification: Notification settings for the message + :type notification: ~botframework.connector.teams.models.NotificationInfo + :param tenant: Information about the tenant in which the message was sent + :type tenant: ~botframework.connector.teams.models.TenantInfo + """ + + _attribute_map = { + "channel": {"key": "channel", "type": "ChannelInfo"}, + "eventType": {"key": "eventType", "type": "str"}, + "team": {"key": "team", "type": "TeamInfo"}, + "notification": {"key": "notification", "type": "NotificationInfo"}, + "tenant": {"key": "tenant", "type": "TenantInfo"}, + } + + def __init__( + self, + *, + channel=None, + eventType: str = None, + team=None, + notification=None, + tenant=None, + **kwargs + ) -> None: + super(TeamsChannelData, self).__init__(**kwargs) + self.channel = channel + # doing camel case here since that's how the data comes in + self.event_type = eventType + self.team = team + self.notification = notification + self.tenant = tenant + + +class TenantInfo(Model): + """Describes a tenant. + + :param id: Unique identifier representing a tenant + :type id: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + } + + def __init__(self, *, id: str = None, **kwargs) -> None: + super(TenantInfo, self).__init__(**kwargs) + self.id = id diff --git a/libraries/botframework-connector/botframework/connector/auth/__init__.py b/libraries/botframework-connector/botframework/connector/auth/__init__.py index 6d6b0b63c..8d90791bb 100644 --- a/libraries/botframework-connector/botframework/connector/auth/__init__.py +++ b/libraries/botframework-connector/botframework/connector/auth/__init__.py @@ -9,6 +9,7 @@ # regenerated. # -------------------------------------------------------------------------- # pylint: disable=missing-docstring +from .authentication_constants import * from .government_constants import * from .channel_provider import * from .simple_channel_provider import * @@ -19,5 +20,4 @@ from .channel_validation import * from .emulator_validation import * from .jwt_token_extractor import * -from .authentication_constants import * from .authentication_configuration import * diff --git a/libraries/botframework-connector/botframework/connector/auth/channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/channel_validation.py index ee0bc0315..7e9344c79 100644 --- a/libraries/botframework-connector/botframework/connector/auth/channel_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/channel_validation.py @@ -2,7 +2,7 @@ from .authentication_configuration import AuthenticationConfiguration from .verify_options import VerifyOptions -from .constants import Constants +from .authentication_constants import AuthenticationConstants from .jwt_token_extractor import JwtTokenExtractor from .claims_identity import ClaimsIdentity from .credential_provider import CredentialProvider @@ -18,7 +18,7 @@ class ChannelValidation: # TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot # TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions( - issuer=[Constants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER], + issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER], # Audience validation takes place manually in code. audience=None, clock_tolerance=5 * 60, @@ -48,10 +48,8 @@ async def authenticate_channel_token_with_service_url( :return: A valid ClaimsIdentity. :raises Exception: """ - identity = await asyncio.ensure_future( - ChannelValidation.authenticate_channel_token( - auth_header, credentials, channel_id, auth_configuration - ) + identity = await ChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id, auth_configuration ) service_url_claim = identity.get_claim_value( @@ -87,19 +85,17 @@ async def authenticate_channel_token( metadata_endpoint = ( ChannelValidation.open_id_metadata_endpoint if ChannelValidation.open_id_metadata_endpoint - else Constants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL + else AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL ) token_extractor = JwtTokenExtractor( ChannelValidation.TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS, metadata_endpoint, - Constants.ALLOWED_SIGNING_ALGORITHMS, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS, ) - identity = await asyncio.ensure_future( - token_extractor.get_identity_from_auth_header( - auth_header, channel_id, auth_configuration.required_endorsements - ) + identity = await token_extractor.get_identity_from_auth_header( + auth_header, channel_id, auth_configuration.required_endorsements ) return await ChannelValidation.validate_identity(identity, credentials) @@ -123,15 +119,15 @@ async def validate_identity( # Look for the "aud" claim, but only if issued from the Bot Framework if ( - identity.get_claim_value(Constants.ISSUER_CLAIM) - != Constants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER + identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) + != AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER ): # The relevant Audience Claim MUST be present. Not Authorized. raise Exception("Unauthorized. Audience Claim MUST be present.") # The AppId from the claim in the token must match the AppId specified by the developer. # Note that the Bot Framework uses the Audience claim ("aud") to pass the AppID. - aud_claim = identity.get_claim_value(Constants.AUDIENCE_CLAIM) + aud_claim = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM) is_valid_app_id = await asyncio.ensure_future( credentials.is_valid_appid(aud_claim or "") ) diff --git a/libraries/botframework-connector/botframework/connector/auth/constants.py b/libraries/botframework-connector/botframework/connector/auth/constants.py deleted file mode 100644 index 03a95a908..000000000 --- a/libraries/botframework-connector/botframework/connector/auth/constants.py +++ /dev/null @@ -1,31 +0,0 @@ -class Constants: # pylint: disable=too-few-public-methods - """ - TO CHANNEL FROM BOT: Login URL prefix - """ - - TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/" - - """ - TO CHANNEL FROM BOT: Login URL token endpoint path - """ - TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH = "/oauth2/v2.0/token" - - """ - TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication - """ - DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com" - - TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com" - - TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = ( - "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" - ) - TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = ( - "https://login.botframework.com/v1/.well-known/openidconfiguration" - ) - - ALLOWED_SIGNING_ALGORITHMS = ["RS256", "RS384", "RS512"] - - AUTHORIZED_PARTY = "azp" - AUDIENCE_CLAIM = "aud" - ISSUER_CLAIM = "iss" diff --git a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py index 2657e6222..12738f388 100644 --- a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py @@ -8,7 +8,7 @@ from .jwt_token_extractor import JwtTokenExtractor from .verify_options import VerifyOptions -from .constants import Constants +from .authentication_constants import AuthenticationConstants from .credential_provider import CredentialProvider from .claims_identity import ClaimsIdentity from .government_constants import GovernmentConstants @@ -112,13 +112,13 @@ async def authenticate_emulator_token( open_id_metadata = ( GovernmentConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL if is_gov - else Constants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL + else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL ) token_extractor = JwtTokenExtractor( EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS, open_id_metadata, - Constants.ALLOWED_SIGNING_ALGORITHMS, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS, ) identity = await asyncio.ensure_future( @@ -158,7 +158,9 @@ async def authenticate_emulator_token( app_id = app_id_claim elif version_claim == "2.0": # Emulator, "2.0" puts the AppId in the "azp" claim. - app_authz_claim = identity.get_claim_value(Constants.AUTHORIZED_PARTY) + app_authz_claim = identity.get_claim_value( + AuthenticationConstants.AUTHORIZED_PARTY + ) if not app_authz_claim: # No claim around AppID. Not Authorized. raise Exception( diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py index ef080c5d4..c4a0b26e3 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py @@ -27,6 +27,7 @@ async def authenticate_request( auth_header: str, credentials: CredentialProvider, channel_service_or_provider: Union[str, ChannelProvider] = "", + auth_configuration: AuthenticationConfiguration = None, ) -> ClaimsIdentity: """Authenticates the request and sets the service url in the set of trusted urls. :param activity: The incoming Activity from the Bot Framework or the Emulator @@ -34,7 +35,8 @@ async def authenticate_request( :param auth_header: The Bearer token included as part of the request :type auth_header: str :param credentials: The set of valid credentials, such as the Bot Application ID - :param channel_service: String for the channel service + :param channel_service_or_provider: String for the channel service + :param auth_configuration: Authentication configuration :type credentials: CredentialProvider :raises Exception: @@ -55,6 +57,7 @@ async def authenticate_request( channel_service_or_provider, activity.channel_id, activity.service_url, + auth_configuration, ) # On the standard Auth path, we need to trust the URL that was incoming. 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 4191a2a65..180fda6dd 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -1,12 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from datetime import datetime, timedelta from urllib.parse import urlparse + +from adal import AuthenticationContext import requests from msrest.authentication import Authentication -from .constants import Constants +from .authentication_constants import AuthenticationConstants # TODO: Decide to move this to Constants or viceversa (when porting OAuth) AUTH_SETTINGS = { @@ -34,9 +35,9 @@ def __init__(self): def from_json(json_values): result = _OAuthResponse() try: - result.token_type = json_values["token_type"] - result.access_token = json_values["access_token"] - result.expires_in = json_values["expires_in"] + result.token_type = json_values["tokenType"] + result.access_token = json_values["accessToken"] + result.expires_in = json_values["expiresIn"] except KeyError: pass return result @@ -79,15 +80,16 @@ def __init__( tenant = ( channel_auth_tenant if channel_auth_tenant - else Constants.DEFAULT_CHANNEL_AUTH_TENANT + else AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT ) self.oauth_endpoint = ( - Constants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX - + tenant - + Constants.TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH + AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + tenant + ) + self.oauth_scope = ( + oauth_scope or AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER ) - self.oauth_scope = oauth_scope or AUTH_SETTINGS["refreshScope"] self.token_cache_key = app_id + "-cache" if app_id else None + self.authentication_context = AuthenticationContext(self.oauth_endpoint) # pylint: disable=arguments-differ def signed_session(self, session: requests.Session = None) -> requests.Session: @@ -140,19 +142,14 @@ def refresh_token(self) -> _OAuthResponse: """ returns: _OAuthResponse """ - options = { - "grant_type": "client_credentials", - "client_id": self.microsoft_app_id, - "client_secret": self.microsoft_app_password, - "scope": self.oauth_scope, - } - response = requests.post(self.oauth_endpoint, data=options) - response.raise_for_status() + token = self.authentication_context.acquire_token_with_client_credentials( + self.oauth_scope, self.microsoft_app_id, self.microsoft_app_password + ) - oauth_response = _OAuthResponse.from_json(response.json()) + oauth_response = _OAuthResponse.from_json(token) oauth_response.expiration_time = datetime.now() + timedelta( - seconds=(oauth_response.expires_in - 300) + seconds=(int(oauth_response.expires_in) - 300) ) return oauth_response diff --git a/libraries/botframework-connector/botframework/connector/models/__init__.py b/libraries/botframework-connector/botframework/connector/models/__init__.py index 084330d3b..c03adc0f5 100644 --- a/libraries/botframework-connector/botframework/connector/models/__init__.py +++ b/libraries/botframework-connector/botframework/connector/models/__init__.py @@ -10,3 +10,4 @@ # -------------------------------------------------------------------------- from botbuilder.schema import * +from botbuilder.schema.teams import * diff --git a/libraries/botframework-connector/botframework/connector/teams/__init__.py b/libraries/botframework-connector/botframework/connector/teams/__init__.py new file mode 100644 index 000000000..df0cf0a57 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/teams/__init__.py @@ -0,0 +1,17 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .teams_connector_client import TeamsConnectorClient +from .version import VERSION + +__all__ = ["TeamsConnectorClient"] + +__version__ = VERSION diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/__init__.py b/libraries/botframework-connector/botframework/connector/teams/operations/__init__.py new file mode 100644 index 000000000..3e46b2dc2 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/teams/operations/__init__.py @@ -0,0 +1,16 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from .teams_operations import TeamsOperations + +__all__ = [ + "TeamsOperations", +] diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py new file mode 100644 index 000000000..e6a2d909d --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -0,0 +1,147 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.pipeline import ClientRawResponse +from msrest.exceptions import HttpOperationError + +from ... import models + + +class TeamsOperations(object): + """TeamsOperations operations. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + + self.config = config + + def get_teams_channels( + self, team_id, custom_headers=None, raw=False, **operation_config + ): + """Fetches channel list for a given team. + + Fetch the channel list. + + :param team_id: Team Id + :type team_id: str + :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: ConversationList or ClientRawResponse if raw=true + :rtype: ~botframework.connector.teams.models.ConversationList or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + # Construct URL + url = self.get_teams_channels.metadata["url"] + path_format_arguments = { + "teamId": self._serialize.url("team_id", team_id, "str") + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters["Accept"] = "application/json" + if custom_headers: + header_parameters.update(custom_headers) + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise HttpOperationError(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize("ConversationList", response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + get_teams_channels.metadata = {"url": "/v3/teams/{teamId}/conversations"} + + def get_team_details( + self, team_id, custom_headers=None, raw=False, **operation_config + ): + """Fetches details related to a team. + + Fetch details for a team. + + :param team_id: Team Id + :type team_id: str + :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: TeamDetails or ClientRawResponse if raw=true + :rtype: ~botframework.connector.teams.models.TeamDetails or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + # Construct URL + url = self.get_team_details.metadata["url"] + path_format_arguments = { + "teamId": self._serialize.url("team_id", team_id, "str") + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters["Accept"] = "application/json" + if custom_headers: + header_parameters.update(custom_headers) + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise HttpOperationError(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize("TeamDetails", response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + get_team_details.metadata = {"url": "/v3/teams/{teamId}"} diff --git a/libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py b/libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py new file mode 100644 index 000000000..ccf935032 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py @@ -0,0 +1,83 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +from msrest.service_client import SDKClient +from msrest import Configuration, Serializer, Deserializer +from .. import models +from .version import VERSION +from .operations.teams_operations import TeamsOperations + + +class TeamsConnectorClientConfiguration(Configuration): + """Configuration for TeamsConnectorClient + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Subscription credentials which uniquely identify + client subscription. + :type credentials: None + :param str base_url: Service URL + """ + + def __init__(self, credentials, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if not base_url: + base_url = "https://api.botframework.com" + + super(TeamsConnectorClientConfiguration, self).__init__(base_url) + + self.add_user_agent("botframework-connector/{}".format(VERSION)) + + self.credentials = credentials + + +class TeamsConnectorClient(SDKClient): + """The Bot Connector REST API extension for Microsoft Teams allows your bot to perform extended + operations on to Microsoft Teams channel configured in the + [Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses + industry-standard REST and JSON over HTTPS. Client libraries for this REST API are available. See below for a list. + Authentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is + described in detail in the [Connector Authentication](https://docs.botframework.com/en-us/restapi/authentication) + document. + # Client Libraries for the Bot Connector REST API + * [Bot Builder for C#](https://docs.botframework.com/en-us/csharp/builder/sdkreference/) + * [Bot Builder for Node.js](https://docs.botframework.com/en-us/node/builder/overview/) + © 2016 Microsoft + + :ivar config: Configuration for client. + :vartype config: TeamsConnectorClientConfiguration + + :ivar teams: Teams operations + :vartype teams: botframework.connector.teams.operations.TeamsOperations + + :param credentials: Subscription credentials which uniquely identify + client subscription. + :type credentials: None + :param str base_url: Service URL + """ + + def __init__(self, credentials, base_url=None): + + self.config = TeamsConnectorClientConfiguration(credentials, base_url) + super(TeamsConnectorClient, self).__init__(self.config.credentials, self.config) + + client_models = { + k: v for k, v in models.__dict__.items() if isinstance(v, type) + } + self.api_version = "v3" + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.teams = TeamsOperations( + self._client, self.config, self._serialize, self._deserialize + ) diff --git a/libraries/botframework-connector/botframework/connector/teams/version.py b/libraries/botframework-connector/botframework/connector/teams/version.py new file mode 100644 index 000000000..e36069e74 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/teams/version.py @@ -0,0 +1,12 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +VERSION = "v3" diff --git a/libraries/botframework-connector/tests/test_microsoft_app_credentials.py b/libraries/botframework-connector/tests/test_microsoft_app_credentials.py index 900fd927b..c276b8e48 100644 --- a/libraries/botframework-connector/tests/test_microsoft_app_credentials.py +++ b/libraries/botframework-connector/tests/test_microsoft_app_credentials.py @@ -7,7 +7,7 @@ class TestMicrosoftAppCredentials(aiounittest.AsyncTestCase): async def test_app_credentials(self): default_scope_case_1 = MicrosoftAppCredentials("some_app", "some_password") assert ( - AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER == default_scope_case_1.oauth_scope ) @@ -16,7 +16,7 @@ async def test_app_credentials(self): "some_app", "some_password", "some_tenant" ) assert ( - AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER == default_scope_case_2.oauth_scope ) diff --git a/samples/experimental/test-protocol/app.py b/samples/experimental/test-protocol/app.py index ea5669940..e95d2f1be 100644 --- a/samples/experimental/test-protocol/app.py +++ b/samples/experimental/test-protocol/app.py @@ -40,7 +40,7 @@ async def messages(req: Request) -> Response: next_conversation_id = FACTORY.create_skill_conversation_id(current_conversation_id, current_service_url) - await CLIENT.post_activity(None, None, TO_URI, SERVICE_URL, next_conversation_id, inbound_activity) + await CLIENT.post_activity(CONFIG.APP_ID, CONFIG.SKILL_APP_ID, TO_URI, SERVICE_URL, next_conversation_id, inbound_activity) return Response(status=201) APP = web.Application() diff --git a/samples/experimental/test-protocol/config.py b/samples/experimental/test-protocol/config.py index 3821e1e50..9a6ec94ea 100644 --- a/samples/experimental/test-protocol/config.py +++ b/samples/experimental/test-protocol/config.py @@ -15,3 +15,4 @@ class DefaultConfig: APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") NEXT = "http://localhost:3978/api/messages" SERVICE_URL = "http://localhost:3428/api/connector" + SKILL_APP_ID = "" diff --git a/scenarios/activity-update-and-delete/README.md b/scenarios/activity-update-and-delete/README.md new file mode 100644 index 000000000..40e84f525 --- /dev/null +++ b/scenarios/activity-update-and-delete/README.md @@ -0,0 +1,30 @@ +# EchoBot + +Bot Framework v4 echo bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +## Running the sample +- Clone the repository +```bash +git clone https://github.com/Microsoft/botbuilder-python.git +``` +- Activate your desired virtual environment +- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder +- In the terminal, type `pip install -r requirements.txt` +- In the terminal, type `python app.py` + +## Testing the bot using Bot Framework Emulator +[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to bot using Bot Framework Emulator +- Launch Bot Framework Emulator +- Paste this URL in the emulator window - http://localhost:3978/api/messages + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/scenarios/activity-update-and-delete/app.py b/scenarios/activity-update-and-delete/app.py new file mode 100644 index 000000000..166cee39d --- /dev/null +++ b/scenarios/activity-update-and-delete/app.py @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import sys +from datetime import datetime +from types import MethodType + +from flask import Flask, request, Response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + TurnContext, + BotFrameworkAdapter, +) +from botbuilder.schema import Activity, ActivityTypes + +from bots import ActivitiyUpdateAndDeleteBot + +# Create the loop and Flask app +LOOP = asyncio.get_event_loop() +APP = Flask(__name__, instance_relative_config=True) +APP.config.from_object("config.DefaultConfig") + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) +ADAPTER = BotFrameworkAdapter(SETTINGS) + + +# Catch-all for errors. +async def on_error( # pylint: disable=unused-argument + self, context: TurnContext, error: Exception +): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + await context.send_activity( + "To continue to run this bot, please fix the bot source code." + ) + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + + +ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) +ACTIVITY_IDS = [] +# Create the Bot +BOT = ActivitiyUpdateAndDeleteBot(ACTIVITY_IDS) + +# Listen for incoming requests on /api/messages.s +@APP.route("/api/messages", methods=["POST"]) +def messages(): + # Main bot message handler. + if "application/json" in request.headers["Content-Type"]: + body = request.json + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = ( + request.headers["Authorization"] if "Authorization" in request.headers else "" + ) + + try: + task = LOOP.create_task( + ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + ) + LOOP.run_until_complete(task) + return Response(status=201) + except Exception as exception: + raise exception + + +if __name__ == "__main__": + try: + APP.run(debug=False, port=APP.config["PORT"]) # nosec debug + except Exception as exception: + raise exception diff --git a/scenarios/activity-update-and-delete/bots/__init__.py b/scenarios/activity-update-and-delete/bots/__init__.py new file mode 100644 index 000000000..e6c728a12 --- /dev/null +++ b/scenarios/activity-update-and-delete/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .activity_update_and_delete_bot import ActivitiyUpdateAndDeleteBot + +__all__ = ["ActivitiyUpdateAndDeleteBot"] diff --git a/scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py b/scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py new file mode 100644 index 000000000..350cec8c2 --- /dev/null +++ b/scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import MessageFactory, TurnContext, ActivityHandler + + +class ActivitiyUpdateAndDeleteBot(ActivityHandler): + def __init__(self, activity_ids): + self.activity_ids = activity_ids + + async def on_message_activity(self, turn_context: TurnContext): + TurnContext.remove_recipient_mention(turn_context.activity) + if turn_context.activity.text == "delete": + for activity in self.activity_ids: + await turn_context.delete_activity(activity) + + self.activity_ids = [] + else: + await self._send_message_and_log_activity_id( + turn_context, turn_context.activity.text + ) + + for activity_id in self.activity_ids: + new_activity = MessageFactory.text(turn_context.activity.text) + new_activity.id = activity_id + await turn_context.update_activity(new_activity) + + async def _send_message_and_log_activity_id( + self, turn_context: TurnContext, text: str + ): + reply_activity = MessageFactory.text(text) + resource_response = await turn_context.send_activity(reply_activity) + self.activity_ids.append(resource_response.id) diff --git a/scenarios/activity-update-and-delete/config.py b/scenarios/activity-update-and-delete/config.py new file mode 100644 index 000000000..6b5116fba --- /dev/null +++ b/scenarios/activity-update-and-delete/config.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/scenarios/activity-update-and-delete/requirements.txt b/scenarios/activity-update-and-delete/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/scenarios/activity-update-and-delete/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/activity-update-and-delete/teams_app_manifest/color.png b/scenarios/activity-update-and-delete/teams_app_manifest/color.png new file mode 100644 index 0000000000000000000000000000000000000000..48a2de13303e1e8a25f76391f4a34c7c4700fd3d GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCe1|JzX3_D&pSWuFnWfl{x;g|9jrEYf8Vqrkk2Ba|%ol3OT){=#|7ID~|e{ zODQ{kU&ME#@`*-tm%Tukt_gFr+`F?$dx9wg-jad`^gsMn2_%Kh%WH91&SjKq5 zgkdI|!exdOVgw@>>=!Tjnk6q)zV*T8$FdgRFYC{kQ7``NOcl@R(_%_8e5e0E;>v0G zEM9kb)2itgOTSfH7M=b3-S61B?PiazMdwXZwrS)^5UUS#HQjaoua5h_{Gx*_Zz|XK z$tf0mZ&=tpf2!!Q)!A_l&o_$g*|JM$VZa~F^0{x1T{=QFu*x$`=V%~jUW=G`iqqp=lquB-`P{Qjw`=zEu3cMc_x7m2f#9m}uoFBMMQ^+%cOL)F_)N@JZ}Axoxi1y= zeebq`y==e!nl+?cK-PhOec!3%|IupShHrcjW8sSt)F1>NW*{ zW%ljk2)nk%-}+F&?gi=7^$L#VeX3@kp%f{n}fR z`}uZPx$IY~r8R5%gMlrc`jP!L3IloKFoq~sFFH5|cdklX=R08T)}71BhaN8$`AsNf0_ zq>WNhAtCd|-nBlTU=y5zl_vXlXZ~bkuaYENMp>3QSQ_#zuYZ+eQh*OIHRxP~s(}ic zN2J4$u=AQcPt)|>F3zZLsjtP;Tajkugx;NcYED2~JVBlVO>{`uAY?Q4O|AA z=16}CJieK^5P_TKnou!zGR`$!PUC)DqtkO;?!`p!+9v3lP_mu=%Vt3BkoWsq%;FN1sp58w*zfr-z^7tIb*q>!yncCjrzLuOk3N+d&~^Cxd| z=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/conversation-update/teams_app_manifest/color.png b/scenarios/conversation-update/teams_app_manifest/color.png new file mode 100644 index 0000000000000000000000000000000000000000..48a2de13303e1e8a25f76391f4a34c7c4700fd3d GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCe1|JzX3_D&pSWuFnWfl{x;g|9jrEYf8Vqrkk2Ba|%ol3OT){=#|7ID~|e{ zODQ{kU&ME#@`*-tm%Tukt_gFr+`F?$dx9wg-jad`^gsMn2_%Kh%WH91&SjKq5 zgkdI|!exdOVgw@>>=!Tjnk6q)zV*T8$FdgRFYC{kQ7``NOcl@R(_%_8e5e0E;>v0G zEM9kb)2itgOTSfH7M=b3-S61B?PiazMdwXZwrS)^5UUS#HQjaoua5h_{Gx*_Zz|XK z$tf0mZ&=tpf2!!Q)!A_l&o_$g*|JM$VZa~F^0{x1T{=QFu*x$`=V%~jUW=G`iqqp=lquB-`P{Qjw`=zEu3cMc_x7m2f#9m}uoFBMMQ^+%cOL)F_)N@JZ}Axoxi1y= zeebq`y==e!nl+?cK-PhOec!3%|IupShHrcjW8sSt)F1>NW*{ zW%ljk2)nk%-}+F&?gi=7^$L#VeX3@kp%f{n}fR z`}uZPx$IY~r8R5%gMlrc`jP!L3IloKFoq~sFFH5|cdklX=R08T)}71BhaN8$`AsNf0_ zq>WNhAtCd|-nBlTU=y5zl_vXlXZ~bkuaYENMp>3QSQ_#zuYZ+eQh*OIHRxP~s(}ic zN2J4$u=AQcPt)|>F3zZLsjtP;Tajkugx;NcYED2~JVBlVO>{`uAY?Q4O|AA z=16}CJieK^5P_TKnou!zGR`$!PUC)DqtkO;?!`p!+9v3lP_mu=%Vt3BkoWsq%;FN1sp58w*zfr-z^7tIb*q>!yncCjrzLuOk3N+d&~^Cxd| z{turn_context.activity.from_property.name}", + "type": "mention", + } + + mention_object = Mention(**mention_data) + + reply_activity = MessageFactory.text(f"Hello {mention_object.text}") + reply_activity.entities = [mention_object] + await turn_context.send_activity(reply_activity) diff --git a/scenarios/mentions/config.py b/scenarios/mentions/config.py new file mode 100644 index 000000000..6b5116fba --- /dev/null +++ b/scenarios/mentions/config.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/scenarios/mentions/requirements.txt b/scenarios/mentions/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/scenarios/mentions/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/mentions/teams_app_manifest/color.png b/scenarios/mentions/teams_app_manifest/color.png new file mode 100644 index 0000000000000000000000000000000000000000..48a2de13303e1e8a25f76391f4a34c7c4700fd3d GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCe1|JzX3_D&pSWuFnWfl{x;g|9jrEYf8Vqrkk2Ba|%ol3OT){=#|7ID~|e{ zODQ{kU&ME#@`*-tm%Tukt_gFr+`F?$dx9wg-jad`^gsMn2_%Kh%WH91&SjKq5 zgkdI|!exdOVgw@>>=!Tjnk6q)zV*T8$FdgRFYC{kQ7``NOcl@R(_%_8e5e0E;>v0G zEM9kb)2itgOTSfH7M=b3-S61B?PiazMdwXZwrS)^5UUS#HQjaoua5h_{Gx*_Zz|XK z$tf0mZ&=tpf2!!Q)!A_l&o_$g*|JM$VZa~F^0{x1T{=QFu*x$`=V%~jUW=G`iqqp=lquB-`P{Qjw`=zEu3cMc_x7m2f#9m}uoFBMMQ^+%cOL)F_)N@JZ}Axoxi1y= zeebq`y==e!nl+?cK-PhOec!3%|IupShHrcjW8sSt)F1>NW*{ zW%ljk2)nk%-}+F&?gi=7^$L#VeX3@kp%f{n}fR z`}uZ>", + "packageName": "com.teams.sample.conversationUpdate", + "developer": { + "name": "MentionBot", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "MentionBot", + "full": "MentionBot" + }, + "description": { + "short": "MentionBot", + "full": "MentionBot" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "<>", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/scenarios/mentions/teams_app_manifest/outline.png b/scenarios/mentions/teams_app_manifest/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..dbfa9277299d36542af02499e06e3340bc538fe7 GIT binary patch literal 383 zcmV-_0f7FAP)Px$IY~r8R5%gMlrc`jP!L3IloKFoq~sFFH5|cdklX=R08T)}71BhaN8$`AsNf0_ zq>WNhAtCd|-nBlTU=y5zl_vXlXZ~bkuaYENMp>3QSQ_#zuYZ+eQh*OIHRxP~s(}ic zN2J4$u=AQcPt)|>F3zZLsjtP;Tajkugx;NcYED2~JVBlVO>{`uAY?Q4O|AA z=16}CJieK^5P_TKnou!zGR`$!PUC)DqtkO;?!`p!+9v3lP_mu=%Vt3BkoWsq%;FN1sp58w*zfr-z^7tIb*q>!yncCjrzLuOk3N+d&~^Cxd| z Activity: + if not activity_id: + raise TypeError("activity_id is required for ActivityLog.find") + + items = await self._storage.read([activity_id]) + return items[activity_id] if len(items) >= 1 else None diff --git a/scenarios/message-reactions/app.py b/scenarios/message-reactions/app.py new file mode 100644 index 000000000..f92c64c3c --- /dev/null +++ b/scenarios/message-reactions/app.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +from datetime import datetime +from types import MethodType + +from flask import Flask, request, Response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + TurnContext, + BotFrameworkAdapter, + MemoryStorage, +) +from botbuilder.schema import Activity, ActivityTypes +from activity_log import ActivityLog +from bots import MessageReactionBot +from threading_helper import run_coroutine + +# Create the Flask app +APP = Flask(__name__, instance_relative_config=True) +APP.config.from_object("config.DefaultConfig") + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) +ADAPTER = BotFrameworkAdapter(SETTINGS) + + +# Catch-all for errors. +async def on_error( # pylint: disable=unused-argument + self, context: TurnContext, error: Exception +): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + await context.send_activity( + "To continue to run this bot, please fix the bot source code." + ) + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + + +ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) + +MEMORY = MemoryStorage() +ACTIVITY_LOG = ActivityLog(MEMORY) +# Create the Bot +BOT = MessageReactionBot(ACTIVITY_LOG) + +# Listen for incoming requests on /api/messages.s +@APP.route("/api/messages", methods=["POST"]) +def messages(): + # Main bot message handler. + if "application/json" in request.headers["Content-Type"]: + body = request.json + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = ( + request.headers["Authorization"] if "Authorization" in request.headers else "" + ) + + try: + print("about to create task") + print("about to run until complete") + run_coroutine(ADAPTER.process_activity(activity, auth_header, BOT.on_turn)) + print("is now complete") + return Response(status=201) + except Exception as exception: + raise exception + + +if __name__ == "__main__": + try: + APP.run(debug=False, port=APP.config["PORT"]) # nosec debug + except Exception as exception: + raise exception diff --git a/scenarios/message-reactions/bots/__init__.py b/scenarios/message-reactions/bots/__init__.py new file mode 100644 index 000000000..4c417f70c --- /dev/null +++ b/scenarios/message-reactions/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .message_reaction_bot import MessageReactionBot + +__all__ = ["MessageReactionBot"] diff --git a/scenarios/message-reactions/bots/message_reaction_bot.py b/scenarios/message-reactions/bots/message_reaction_bot.py new file mode 100644 index 000000000..ce8c34cea --- /dev/null +++ b/scenarios/message-reactions/bots/message_reaction_bot.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List +from botbuilder.core import MessageFactory, TurnContext, ActivityHandler +from botbuilder.schema import MessageReaction +from activity_log import ActivityLog + + +class MessageReactionBot(ActivityHandler): + def __init__(self, activity_log: ActivityLog): + self._log = activity_log + + async def on_reactions_added( + self, message_reactions: List[MessageReaction], turn_context: TurnContext + ): + for reaction in message_reactions: + activity = await self._log.find(turn_context.activity.reply_to_id) + if not activity: + await self._send_message_and_log_activity_id( + turn_context, + f"Activity {turn_context.activity.reply_to_id} not found in log", + ) + else: + await self._send_message_and_log_activity_id( + turn_context, + f"You added '{reaction.type}' regarding '{activity.text}'", + ) + return + + async def on_reactions_removed( + self, message_reactions: List[MessageReaction], turn_context: TurnContext + ): + for reaction in message_reactions: + activity = await self._log.find(turn_context.activity.reply_to_id) + if not activity: + await self._send_message_and_log_activity_id( + turn_context, + f"Activity {turn_context.activity.reply_to_id} not found in log", + ) + else: + await self._send_message_and_log_activity_id( + turn_context, + f"You removed '{reaction.type}' regarding '{activity.text}'", + ) + return + + async def on_message_activity(self, turn_context: TurnContext): + await self._send_message_and_log_activity_id( + turn_context, f"echo: {turn_context.activity.text}" + ) + + async def _send_message_and_log_activity_id( + self, turn_context: TurnContext, text: str + ): + reply_activity = MessageFactory.text(text) + resource_response = await turn_context.send_activity(reply_activity) + + await self._log.append(resource_response.id, reply_activity) + return diff --git a/scenarios/message-reactions/config.py b/scenarios/message-reactions/config.py new file mode 100644 index 000000000..480b0647b --- /dev/null +++ b/scenarios/message-reactions/config.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "e4c570ca-189d-4fee-a81b-5466be24a557") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "bghqYKJV3709;creKFP8$@@") diff --git a/scenarios/message-reactions/requirements.txt b/scenarios/message-reactions/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/scenarios/message-reactions/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/message-reactions/teams_app_manifest/color.png b/scenarios/message-reactions/teams_app_manifest/color.png new file mode 100644 index 0000000000000000000000000000000000000000..48a2de13303e1e8a25f76391f4a34c7c4700fd3d GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCe1|JzX3_D&pSWuFnWfl{x;g|9jrEYf8Vqrkk2Ba|%ol3OT){=#|7ID~|e{ zODQ{kU&ME#@`*-tm%Tukt_gFr+`F?$dx9wg-jad`^gsMn2_%Kh%WH91&SjKq5 zgkdI|!exdOVgw@>>=!Tjnk6q)zV*T8$FdgRFYC{kQ7``NOcl@R(_%_8e5e0E;>v0G zEM9kb)2itgOTSfH7M=b3-S61B?PiazMdwXZwrS)^5UUS#HQjaoua5h_{Gx*_Zz|XK z$tf0mZ&=tpf2!!Q)!A_l&o_$g*|JM$VZa~F^0{x1T{=QFu*x$`=V%~jUW=G`iqqp=lquB-`P{Qjw`=zEu3cMc_x7m2f#9m}uoFBMMQ^+%cOL)F_)N@JZ}Axoxi1y= zeebq`y==e!nl+?cK-PhOec!3%|IupShHrcjW8sSt)F1>NW*{ zW%ljk2)nk%-}+F&?gi=7^$L#VeX3@kp%f{n}fR z`}uZ>", + "packageName": "com.teams.sample.conversationUpdate", + "developer": { + "name": "MessageReactions", + "websiteUrl": "https://www.microsoft.com", + "privacyUrl": "https://www.teams.com/privacy", + "termsOfUseUrl": "https://www.teams.com/termsofuser" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "MessageReactions", + "full": "MessageReactions" + }, + "description": { + "short": "MessageReactions", + "full": "MessageReactions" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "<>", + "scopes": [ + "groupchat", + "team", + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/scenarios/message-reactions/teams_app_manifest/outline.png b/scenarios/message-reactions/teams_app_manifest/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..dbfa9277299d36542af02499e06e3340bc538fe7 GIT binary patch literal 383 zcmV-_0f7FAP)Px$IY~r8R5%gMlrc`jP!L3IloKFoq~sFFH5|cdklX=R08T)}71BhaN8$`AsNf0_ zq>WNhAtCd|-nBlTU=y5zl_vXlXZ~bkuaYENMp>3QSQ_#zuYZ+eQh*OIHRxP~s(}ic zN2J4$u=AQcPt)|>F3zZLsjtP;Tajkugx;NcYED2~JVBlVO>{`uAY?Q4O|AA z=16}CJieK^5P_TKnou!zGR`$!PUC)DqtkO;?!`p!+9v3lP_mu=%Vt3BkoWsq%;FN1sp58w*zfr-z^7tIb*q>!yncCjrzLuOk3N+d&~^Cxd| z 0 + + try: + pid, status = os.waitpid(expected_pid, 0) + except ChildProcessError: + # The child process is already reaped + # (may happen if waitpid() is called elsewhere). + pid = expected_pid + returncode = 255 + logger.warning( + "Unknown child process pid %d, will report returncode 255", pid + ) + else: + if os.WIFSIGNALED(status): + returncode = -os.WTERMSIG(status) + elif os.WIFEXITED(status): + returncode = os.WEXITSTATUS(status) + else: + returncode = status + + if loop.get_debug(): + logger.debug( + "process %s exited with returncode %s", expected_pid, returncode + ) + + if loop.is_closed(): + logger.warning("Loop %r that handles pid %r is closed", loop, pid) + else: + loop.call_soon_threadsafe(callback, pid, returncode, *args) + + self._threads.pop(expected_pid) + + # add the watcher to the loop policy + asyncio.get_event_loop_policy().set_child_watcher(_Py38ThreadedChildWatcher()) + +__all__ = ["EventLoopThread", "get_event_loop", "stop_event_loop", "run_coroutine"] + +logger = logging.getLogger(__name__) + + +class EventLoopThread(threading.Thread): + loop = None + _count = itertools.count(0) + + def __init__(self): + name = f"{type(self).__name__}-{next(self._count)}" + super().__init__(name=name, daemon=True) + + def __repr__(self): + loop, r, c, d = self.loop, False, True, False + if loop is not None: + r, c, d = loop.is_running(), loop.is_closed(), loop.get_debug() + return ( + f"<{type(self).__name__} {self.name} id={self.ident} " + f"running={r} closed={c} debug={d}>" + ) + + def run(self): + self.loop = loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + loop.run_forever() + finally: + try: + shutdown_asyncgens = loop.shutdown_asyncgens() + except AttributeError: + pass + else: + loop.run_until_complete(shutdown_asyncgens) + loop.close() + asyncio.set_event_loop(None) + + def stop(self): + loop, self.loop = self.loop, None + if loop is None: + return + loop.call_soon_threadsafe(loop.stop) + self.join() + + +_lock = threading.Lock() +_loop_thread = None + + +def get_event_loop(): + global _loop_thread + with _lock: + if _loop_thread is None: + _loop_thread = EventLoopThread() + _loop_thread.start() + return _loop_thread.loop + + +def stop_event_loop(): + global _loop_thread + with _lock: + if _loop_thread is not None: + _loop_thread.stop() + _loop_thread = None + + +def run_coroutine(coro): + return asyncio.run_coroutine_threadsafe(coro, get_event_loop()) diff --git a/scenarios/roster/README.md b/scenarios/roster/README.md new file mode 100644 index 000000000..39f77916c --- /dev/null +++ b/scenarios/roster/README.md @@ -0,0 +1,30 @@ +# RosterBot + +Bot Framework v4 teams roster bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. + +## Running the sample +- Clone the repository +```bash +git clone https://github.com/Microsoft/botbuilder-python.git +``` +- Activate your desired virtual environment +- Bring up a terminal, navigate to `botbuilder-python\samples\roster` folder +- In the terminal, type `pip install -r requirements.txt` +- In the terminal, type `python app.py` + +## Testing the bot using Bot Framework Emulator +[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to bot using Bot Framework Emulator +- Launch Bot Framework Emulator +- Paste this URL in the emulator window - http://localhost:3978/api/messages + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/scenarios/roster/app.py b/scenarios/roster/app.py new file mode 100644 index 000000000..f491845be --- /dev/null +++ b/scenarios/roster/app.py @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import sys +from datetime import datetime +from types import MethodType + +from flask import Flask, request, Response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + TurnContext, + BotFrameworkAdapter, +) +from botbuilder.schema import Activity, ActivityTypes + +from bots import RosterBot + +# Create the loop and Flask app +LOOP = asyncio.get_event_loop() +APP = Flask(__name__, instance_relative_config=True) +APP.config.from_object("config.DefaultConfig") + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) +ADAPTER = BotFrameworkAdapter(SETTINGS) + + +# Catch-all for errors. +async def on_error( # pylint: disable=unused-argument + self, context: TurnContext, error: Exception +): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + await context.send_activity( + "To continue to run this bot, please fix the bot source code." + ) + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + + +ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) + +# Create the Bot +BOT = RosterBot() + +# Listen for incoming requests on /api/messages.s +@APP.route("/api/messages", methods=["POST"]) +def messages(): + # Main bot message handler. + if "application/json" in request.headers["Content-Type"]: + body = request.json + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = ( + request.headers["Authorization"] if "Authorization" in request.headers else "" + ) + + try: + task = LOOP.create_task( + ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + ) + LOOP.run_until_complete(task) + return Response(status=201) + except Exception as exception: + raise exception + + +if __name__ == "__main__": + try: + APP.run(debug=False, port=APP.config["PORT"]) # nosec debug + except Exception as exception: + raise exception diff --git a/scenarios/roster/bots/__init__.py b/scenarios/roster/bots/__init__.py new file mode 100644 index 000000000..a2e035b9f --- /dev/null +++ b/scenarios/roster/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .roster_bot import RosterBot + +__all__ = ["RosterBot"] diff --git a/scenarios/roster/bots/roster_bot.py b/scenarios/roster/bots/roster_bot.py new file mode 100644 index 000000000..0b5661f64 --- /dev/null +++ b/scenarios/roster/bots/roster_bot.py @@ -0,0 +1,66 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List +from botbuilder.core import MessageFactory, TurnContext +from botbuilder.schema import ChannelAccount +from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo + +class RosterBot(TeamsActivityHandler): + async def on_members_added_activity( + self, members_added: [ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity( + "Hello and welcome!" + ) + + async def on_message_activity( + self, turn_context: TurnContext + ): + await turn_context.send_activity(MessageFactory.text(f"Echo: {turn_context.activity.text}")) + + text = turn_context.activity.text.strip() + if "members" in text: + await self._show_members(turn_context) + elif "channels" in text: + await self._show_channels(turn_context) + elif "details" in text: + await self._show_details(turn_context) + else: + await turn_context.send_activity(MessageFactory.text(f"Invalid command. Type \"Show channels\" to see a channel list. Type \"Show members\" to see a list of members in a team. Type \"Show details\" to see team information.")) + + async def _show_members( + self, turn_context: TurnContext + ): + members = await TeamsInfo.get_team_members(turn_context) + reply = MessageFactory.text(f"Total of {len(members)} members are currently in team") + await turn_context.send_activity(reply) + messages = list(map(lambda m: (f'{m.aad_object_id} --> {m.name} --> {m.user_principal_name}'), members)) + await self._send_in_batches(turn_context, messages) + + async def _show_channels( + self, turn_context: TurnContext + ): + channels = TeamsInfo.get_team_channels(turn_context) + reply = MessageFactory.text(f"Total of {len(channels)} channels are currently in team") + await turn_context.send_activity(reply) + messages = list(map(lambda c: (f'{c.id} --> {c.name}'), channels)) + await self._send_in_batches(turn_context, messages) + + async def _show_details(self, turn_context: TurnContext): + team_details = TeamsInfo.get_team_details(turn_context) + reply = MessageFactory.text(f"The team name is {team_details.name}. The team ID is {team_details.id}. The AADGroupID is {team_details.aad_group_id}.") + await turn_context.send_activity(reply) + + async def _send_in_batches(self, turn_context: TurnContext, messages: List[str]): + batch = [] + for msg in messages: + batch.append(msg) + if len(batch) == 10: + await turn_context.send_activity(MessageFactory.text("
".join(batch))) + batch = [] + + if len(batch) > 0: + await turn_context.send_activity(MessageFactory.text("
".join(batch))) \ No newline at end of file diff --git a/scenarios/roster/config.py b/scenarios/roster/config.py new file mode 100644 index 000000000..6b5116fba --- /dev/null +++ b/scenarios/roster/config.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/scenarios/roster/requirements.txt b/scenarios/roster/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/scenarios/roster/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/roster/teams_app_manifest/color.png b/scenarios/roster/teams_app_manifest/color.png new file mode 100644 index 0000000000000000000000000000000000000000..48a2de13303e1e8a25f76391f4a34c7c4700fd3d GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCe1|JzX3_D&pSWuFnWfl{x;g|9jrEYf8Vqrkk2Ba|%ol3OT){=#|7ID~|e{ zODQ{kU&ME#@`*-tm%Tukt_gFr+`F?$dx9wg-jad`^gsMn2_%Kh%WH91&SjKq5 zgkdI|!exdOVgw@>>=!Tjnk6q)zV*T8$FdgRFYC{kQ7``NOcl@R(_%_8e5e0E;>v0G zEM9kb)2itgOTSfH7M=b3-S61B?PiazMdwXZwrS)^5UUS#HQjaoua5h_{Gx*_Zz|XK z$tf0mZ&=tpf2!!Q)!A_l&o_$g*|JM$VZa~F^0{x1T{=QFu*x$`=V%~jUW=G`iqqp=lquB-`P{Qjw`=zEu3cMc_x7m2f#9m}uoFBMMQ^+%cOL)F_)N@JZ}Axoxi1y= zeebq`y==e!nl+?cK-PhOec!3%|IupShHrcjW8sSt)F1>NW*{ zW%ljk2)nk%-}+F&?gi=7^$L#VeX3@kp%f{n}fR z`}uZPx$IY~r8R5%gMlrc`jP!L3IloKFoq~sFFH5|cdklX=R08T)}71BhaN8$`AsNf0_ zq>WNhAtCd|-nBlTU=y5zl_vXlXZ~bkuaYENMp>3QSQ_#zuYZ+eQh*OIHRxP~s(}ic zN2J4$u=AQcPt)|>F3zZLsjtP;Tajkugx;NcYED2~JVBlVO>{`uAY?Q4O|AA z=16}CJieK^5P_TKnou!zGR`$!PUC)DqtkO;?!`p!+9v3lP_mu=%Vt3BkoWsq%;FN1sp58w*zfr-z^7tIb*q>!yncCjrzLuOk3N+d&~^Cxd| z