diff --git a/scenarios/action-based-messaging-extension-fetch-task/app.py b/scenarios/action-based-messaging-extension-fetch-task/app.py new file mode 100644 index 000000000..103c5f31a --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/app.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import sys +from datetime import datetime + +from aiohttp import web +from aiohttp.web import Request, Response, json_response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + TurnContext, + BotFrameworkAdapter, +) +from botbuilder.schema import Activity, ActivityTypes +from bots import ActionBasedMessagingExtensionFetchTaskBot +from config import DefaultConfig + +CONFIG = DefaultConfig() + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD) +ADAPTER = BotFrameworkAdapter(SETTINGS) + + +# Catch-all for errors. +async def on_error(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 = on_error + +# Create the Bot +BOT = ActionBasedMessagingExtensionFetchTaskBot() + + +# Listen for incoming requests on /api/messages +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) + + activity = Activity().deserialize(body) + auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" + + try: + invoke_response = await ADAPTER.process_activity( + activity, auth_header, BOT.on_turn + ) + if invoke_response: + return json_response( + data=invoke_response.body, status=invoke_response.status + ) + return Response(status=201) + except PermissionError: + return Response(status=401) + except Exception: + return Response(status=500) + + +APP = web.Application() +APP.router.add_post("/api/messages", messages) + +if __name__ == "__main__": + try: + web.run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/scenarios/action-based-messaging-extension-fetch-task/bots/__init__.py b/scenarios/action-based-messaging-extension-fetch-task/bots/__init__.py new file mode 100644 index 000000000..fe9caf948 --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/bots/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .action_based_messaging_extension_fetch_task_bot import ( + ActionBasedMessagingExtensionFetchTaskBot, +) + +__all__ = ["ActionBasedMessagingExtensionFetchTaskBot"] diff --git a/scenarios/action-based-messaging-extension-fetch-task/bots/action_based_messaging_extension_fetch_task_bot.py b/scenarios/action-based-messaging-extension-fetch-task/bots/action_based_messaging_extension_fetch_task_bot.py new file mode 100644 index 000000000..9e9c13fa9 --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/bots/action_based_messaging_extension_fetch_task_bot.py @@ -0,0 +1,229 @@ +# Copyright (c) Microsoft Corp. All rights reserved. +# Licensed under the MIT License. + +from typing import List +import random +from botbuilder.core import ( + CardFactory, + MessageFactory, + TurnContext, +) +from botbuilder.schema import Attachment +from botbuilder.schema.teams import ( + MessagingExtensionAction, + MessagingExtensionActionResponse, + TaskModuleContinueResponse, + MessagingExtensionResult, + TaskModuleTaskInfo, +) +from botbuilder.core.teams import TeamsActivityHandler +from example_data import ExampleData + + +class ActionBasedMessagingExtensionFetchTaskBot(TeamsActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + value = turn_context.activity.value + if value is not None: + # This was a message from the card. + answer = value["Answer"] + choices = value["Choices"] + reply = MessageFactory.text( + f"{turn_context.activity.from_property.name} answered '{answer}' and chose '{choices}'." + ) + await turn_context.send_activity(reply) + else: + # This is a regular text message. + reply = MessageFactory.text( + "Hello from ActionBasedMessagingExtensionFetchTaskBot." + ) + await turn_context.send_activity(reply) + + async def on_teams_messaging_extension_fetch_task( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + card = self._create_adaptive_card_editor() + task_info = TaskModuleTaskInfo( + card=card, height=450, title="Task Module Fetch Example", width=500 + ) + continue_response = TaskModuleContinueResponse(type="continue", value=task_info) + return MessagingExtensionActionResponse(task=continue_response) + + async def on_teams_messaging_extension_submit_action( # pylint: disable=unused-argument + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + question = action.data["Question"] + multi_select = action.data["MultiSelect"] + option1 = action.data["Option1"] + option2 = action.data["Option2"] + option3 = action.data["Option3"] + preview_card = self._create_adaptive_card_preview( + user_text=question, + is_multi_select=multi_select, + option1=option1, + option2=option2, + option3=option3, + ) + + extension_result = MessagingExtensionResult( + type="botMessagePreview", + activity_preview=MessageFactory.attachment(preview_card), + ) + return MessagingExtensionActionResponse(compose_extension=extension_result) + + async def on_teams_messaging_extension_bot_message_preview_edit( # pylint: disable=unused-argument + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + activity_preview = action.bot_activity_preview[0] + content = activity_preview.attachments[0].content + data = self._get_example_data(content) + card = self._create_adaptive_card_editor( + data.question, + data.is_multi_select, + data.option1, + data.option2, + data.option3, + ) + task_info = TaskModuleTaskInfo( + card=card, height=450, title="Task Module Fetch Example", width=500 + ) + continue_response = TaskModuleContinueResponse(type="continue", value=task_info) + return MessagingExtensionActionResponse(task=continue_response) + + async def on_teams_messaging_extension_bot_message_preview_send( # pylint: disable=unused-argument + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + activity_preview = action.bot_activity_preview[0] + content = activity_preview.attachments[0].content + data = self._get_example_data(content) + card = self._create_adaptive_card_preview( + data.question, + data.is_multi_select, + data.option1, + data.option2, + data.option3, + ) + message = MessageFactory.attachment(card) + await turn_context.send_activity(message) + + def _get_example_data(self, content: dict) -> ExampleData: + body = content["body"] + question = body[1]["text"] + choice_set = body[3] + multi_select = "isMultiSelect" in choice_set + option1 = choice_set["choices"][0]["value"] + option2 = choice_set["choices"][1]["value"] + option3 = choice_set["choices"][2]["value"] + return ExampleData(question, multi_select, option1, option2, option3) + + def _create_adaptive_card_editor( + self, + user_text: str = None, + is_multi_select: bool = False, + option1: str = None, + option2: str = None, + option3: str = None, + ) -> Attachment: + return CardFactory.adaptive_card( + { + "actions": [ + { + "data": {"submitLocation": "messagingExtensionFetchTask"}, + "title": "Submit", + "type": "Action.Submit", + } + ], + "body": [ + { + "text": "This is an Adaptive Card within a Task Module", + "type": "TextBlock", + "weight": "bolder", + }, + {"type": "TextBlock", "text": "Enter text for Question:"}, + { + "id": "Question", + "placeholder": "Question text here", + "type": "Input.Text", + "value": user_text, + }, + {"type": "TextBlock", "text": "Options for Question:"}, + {"type": "TextBlock", "text": "Is Multi-Select:"}, + { + "choices": [ + {"title": "True", "value": "true"}, + {"title": "False", "value": "false"}, + ], + "id": "MultiSelect", + "isMultiSelect": "false", + "style": "expanded", + "type": "Input.ChoiceSet", + "value": "true" if is_multi_select else "false", + }, + { + "id": "Option1", + "placeholder": "Option 1 here", + "type": "Input.Text", + "value": option1, + }, + { + "id": "Option2", + "placeholder": "Option 2 here", + "type": "Input.Text", + "value": option2, + }, + { + "id": "Option3", + "placeholder": "Option 3 here", + "type": "Input.Text", + "value": option3, + }, + ], + "type": "AdaptiveCard", + "version": "1.0", + } + ) + + def _create_adaptive_card_preview( + self, + user_text: str = None, + is_multi_select: bool = False, + option1: str = None, + option2: str = None, + option3: str = None, + ) -> Attachment: + return CardFactory.adaptive_card( + { + "actions": [ + { + "type": "Action.Submit", + "title": "Submit", + "data": {"submitLocation": "messagingExtensionSubmit"}, + } + ], + "body": [ + { + "text": "Adaptive Card from Task Module", + "type": "TextBlock", + "weight": "bolder", + }, + {"text": user_text, "type": "TextBlock", "id": "Question"}, + { + "id": "Answer", + "placeholder": "Answer here...", + "type": "Input.Text", + }, + { + "choices": [ + {"title": option1, "value": option1}, + {"title": option2, "value": option2}, + {"title": option3, "value": option3}, + ], + "id": "Choices", + "isMultiSelect": is_multi_select, + "style": "expanded", + "type": "Input.ChoiceSet", + }, + ], + "type": "AdaptiveCard", + "version": "1.0", + } + ) diff --git a/scenarios/action-based-messaging-extension-fetch-task/config.py b/scenarios/action-based-messaging-extension-fetch-task/config.py new file mode 100644 index 000000000..6b5116fba --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/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/action-based-messaging-extension-fetch-task/example_data.py b/scenarios/action-based-messaging-extension-fetch-task/example_data.py new file mode 100644 index 000000000..79dede038 --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/example_data.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class ExampleData(object): + def __init__( + self, + question: str = None, + is_multi_select: bool = False, + option1: str = None, + option2: str = None, + option3: str = None, + ): + self.question = question + self.is_multi_select = is_multi_select + self.option1 = option1 + self.option2 = option2 + self.option3 = option3 diff --git a/scenarios/action-based-messaging-extension-fetch-task/requirements.txt b/scenarios/action-based-messaging-extension-fetch-task/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-color.png b/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-color.png new file mode 100644 index 000000000..48a2de133 Binary files /dev/null and b/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-color.png differ diff --git a/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-outline.png b/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-outline.png new file mode 100644 index 000000000..dbfa92772 Binary files /dev/null and b/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-outline.png differ diff --git a/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/manifest.json b/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/manifest.json new file mode 100644 index 000000000..8c87f9f40 --- /dev/null +++ b/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/manifest.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", + "manifestVersion": "1.5", + "version": "1.0", + "id": "<>", + "packageName": "com.microsoft.teams.samples", + "developer": { + "name": "Microsoft", + "websiteUrl": "https://dev.botframework.com", + "privacyUrl": "https://privacy.microsoft.com", + "termsOfUseUrl": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx" + }, + "icons": { + "color": "icon-color.png", + "outline": "icon-outline.png" + }, + "name": { + "short": "Preview Messaging Extension", + "full": "Microsoft Teams Action Based Messaging Extension with Preview" + }, + "description": { + "short": "Sample demonstrating an Action Based Messaging Extension with Preview", + "full": "Sample Action Messaging Extension built with the Bot Builder SDK demonstrating Preview" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "<>", + "scopes": [ + "team" + ] + } + ], + "composeExtensions": [ + { + "botId": "<>", + "canUpdateConfiguration": false, + "commands": [ + { + "id": "createWithPreview", + "type": "action", + "title": "Create Card", + "description": "Example of creating a Card", + "initialRun": false, + "fetchTask": true, + "context": [ + "commandBox", + "compose", + "message" + ], + "parameters": [ + { + "name": "param", + "title": "param", + "description": "" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file