From cc9de497987392809c6f305773e48f484b35c546 Mon Sep 17 00:00:00 2001 From: Eric Dahlvang Date: Fri, 13 Dec 2019 00:24:10 -0800 Subject: [PATCH 1/2] action based messaging extension scenario --- .../action-based-messaging-extension/app.py | 86 ++++++++++++++++++ .../bots/__init__.py | 6 ++ .../teams_messaging_extensions_action_bot.py | 62 +++++++++++++ .../config.py | 13 +++ .../requirements.txt | 2 + .../teams_app_manifest/icon-color.png | Bin 0 -> 1229 bytes .../teams_app_manifest/icon-outline.png | Bin 0 -> 383 bytes .../teams_app_manifest/manifest.json | 78 ++++++++++++++++ 8 files changed, 247 insertions(+) create mode 100644 scenarios/action-based-messaging-extension/app.py create mode 100644 scenarios/action-based-messaging-extension/bots/__init__.py create mode 100644 scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py create mode 100644 scenarios/action-based-messaging-extension/config.py create mode 100644 scenarios/action-based-messaging-extension/requirements.txt create mode 100644 scenarios/action-based-messaging-extension/teams_app_manifest/icon-color.png create mode 100644 scenarios/action-based-messaging-extension/teams_app_manifest/icon-outline.png create mode 100644 scenarios/action-based-messaging-extension/teams_app_manifest/manifest.json diff --git a/scenarios/action-based-messaging-extension/app.py b/scenarios/action-based-messaging-extension/app.py new file mode 100644 index 000000000..bc80e9a89 --- /dev/null +++ b/scenarios/action-based-messaging-extension/app.py @@ -0,0 +1,86 @@ +# 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 TeamsMessagingExtensionsActionBot +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 = TeamsMessagingExtensionsActionBot() + + +# 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: + response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + if response: + return json_response(data=response.value.body, status=response.value.status) + return Response(status=201) + except Exception as exception: + raise exception + +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/bots/__init__.py b/scenarios/action-based-messaging-extension/bots/__init__.py new file mode 100644 index 000000000..daea6bcda --- /dev/null +++ b/scenarios/action-based-messaging-extension/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .teams_messaging_extensions_action_bot import TeamsMessagingExtensionsActionBot + +__all__ = ["TeamsMessagingExtensionsActionBot"] diff --git a/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py b/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py new file mode 100644 index 000000000..1398b48ce --- /dev/null +++ b/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py @@ -0,0 +1,62 @@ +# 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, UserState, ConversationState, PrivateConversationState +from botbuilder.schema import ChannelAccount, HeroCard, CardAction, CardImage +from botbuilder.schema.teams import MessagingExtensionAction, MessagingExtensionActionResponse, MessagingExtensionAttachment, MessagingExtensionResult +from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo +from botbuilder.azure import CosmosDbPartitionedStorage + +class TeamsMessagingExtensionsActionBot(TeamsActivityHandler): + + async def on_teams_messaging_extension_submit_action_dispatch( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + if action.command_id == "createCard": + return await self.create_card_command(turn_context, action) + elif action.command_id == "shareMessage": + return await self.share_message_command(turn_context, action) + + async def create_card_command(self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + title = action.data["title"] + subTitle = action.data["subTitle"] + text = action.data["text"] + + card = HeroCard(title=title, subtitle=subTitle, text=text) + cardAttachment = CardFactory.hero_card(card) + attachment = MessagingExtensionAttachment(content=card, content_type=CardFactory.content_types.hero_card, preview=cardAttachment) + attachments = [attachment] + + extension_result = MessagingExtensionResult(attachment_layout="list", type="result", attachments=attachments) + return MessagingExtensionActionResponse(compose_extension=extension_result) + + async def share_message_command(self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + # The user has chosen to share a message by choosing the 'Share Message' context menu command. + + # TODO: .user is None + title = "Shared Message" #f'{action.message_payload.from_property.user.display_name} orignally sent this message:' + text = action.message_payload.body.content + card = HeroCard(title=title, text=text) + + if not action.message_payload.attachments is None: + # This sample does not add the MessagePayload Attachments. This is left as an + # exercise for the user. + card.subtitle = f'({len(action.message_payload.attachments)} Attachments not included)' + + # This Messaging Extension example allows the user to check a box to include an image with the + # shared message. This demonstrates sending custom parameters along with the message payload. + include_image = action.data["includeImage"] + if include_image == "true": + image = CardImage(url="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU") + card.images = [image] + + cardAttachment = CardFactory.hero_card(card) + attachment = MessagingExtensionAttachment(content=card, content_type=CardFactory.content_types.hero_card, preview=cardAttachment) + attachments = [attachment] + + extension_result = MessagingExtensionResult(attachment_layout="list", type="result", attachments=attachments) + return MessagingExtensionActionResponse(compose_extension=extension_result) diff --git a/scenarios/action-based-messaging-extension/config.py b/scenarios/action-based-messaging-extension/config.py new file mode 100644 index 000000000..6b5116fba --- /dev/null +++ b/scenarios/action-based-messaging-extension/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/requirements.txt b/scenarios/action-based-messaging-extension/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/scenarios/action-based-messaging-extension/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3 diff --git a/scenarios/action-based-messaging-extension/teams_app_manifest/icon-color.png b/scenarios/action-based-messaging-extension/teams_app_manifest/icon-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 Date: Fri, 13 Dec 2019 13:16:41 -0800 Subject: [PATCH 2/2] cleanup and app.py fixes --- .../action-based-messaging-extension/app.py | 13 ++-- .../teams_messaging_extensions_action_bot.py | 60 ++++++++++++++----- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/scenarios/action-based-messaging-extension/app.py b/scenarios/action-based-messaging-extension/app.py index bc80e9a89..4643ee6af 100644 --- a/scenarios/action-based-messaging-extension/app.py +++ b/scenarios/action-based-messaging-extension/app.py @@ -69,12 +69,15 @@ async def messages(req: Request) -> Response: auth_header = req.headers["Authorization"] if "Authorization" in req.headers else "" try: - response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - if response: - return json_response(data=response.value.body, status=response.value.status) + 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 Exception as exception: - raise exception + except PermissionError: + return Response(status=401) + except Exception: + return Response(status=500) + APP = web.Application() APP.router.add_post("/api/messages", messages) diff --git a/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py b/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py index 1398b48ce..aea850e2a 100644 --- a/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py +++ b/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py @@ -3,14 +3,26 @@ from typing import List import random -from botbuilder.core import CardFactory, MessageFactory, TurnContext, UserState, ConversationState, PrivateConversationState +from botbuilder.core import ( + CardFactory, + MessageFactory, + TurnContext, + UserState, + ConversationState, + PrivateConversationState, +) from botbuilder.schema import ChannelAccount, HeroCard, CardAction, CardImage -from botbuilder.schema.teams import MessagingExtensionAction, MessagingExtensionActionResponse, MessagingExtensionAttachment, MessagingExtensionResult +from botbuilder.schema.teams import ( + MessagingExtensionAction, + MessagingExtensionActionResponse, + MessagingExtensionAttachment, + MessagingExtensionResult, +) from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo -from botbuilder.azure import CosmosDbPartitionedStorage +from botbuilder.azure import CosmosDbPartitionedStorage -class TeamsMessagingExtensionsActionBot(TeamsActivityHandler): +class TeamsMessagingExtensionsActionBot(TeamsActivityHandler): async def on_teams_messaging_extension_submit_action_dispatch( self, turn_context: TurnContext, action: MessagingExtensionAction ) -> MessagingExtensionActionResponse: @@ -19,44 +31,62 @@ async def on_teams_messaging_extension_submit_action_dispatch( elif action.command_id == "shareMessage": return await self.share_message_command(turn_context, action) - async def create_card_command(self, turn_context: TurnContext, action: MessagingExtensionAction - ) -> MessagingExtensionActionResponse: + async def create_card_command( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: title = action.data["title"] subTitle = action.data["subTitle"] text = action.data["text"] card = HeroCard(title=title, subtitle=subTitle, text=text) cardAttachment = CardFactory.hero_card(card) - attachment = MessagingExtensionAttachment(content=card, content_type=CardFactory.content_types.hero_card, preview=cardAttachment) + attachment = MessagingExtensionAttachment( + content=card, + content_type=CardFactory.content_types.hero_card, + preview=cardAttachment, + ) attachments = [attachment] - extension_result = MessagingExtensionResult(attachment_layout="list", type="result", attachments=attachments) + extension_result = MessagingExtensionResult( + attachment_layout="list", type="result", attachments=attachments + ) return MessagingExtensionActionResponse(compose_extension=extension_result) - async def share_message_command(self, turn_context: TurnContext, action: MessagingExtensionAction + async def share_message_command( + self, turn_context: TurnContext, action: MessagingExtensionAction ) -> MessagingExtensionActionResponse: # The user has chosen to share a message by choosing the 'Share Message' context menu command. # TODO: .user is None - title = "Shared Message" #f'{action.message_payload.from_property.user.display_name} orignally sent this message:' + title = "Shared Message" # f'{action.message_payload.from_property.user.display_name} orignally sent this message:' text = action.message_payload.body.content card = HeroCard(title=title, text=text) if not action.message_payload.attachments is None: # This sample does not add the MessagePayload Attachments. This is left as an # exercise for the user. - card.subtitle = f'({len(action.message_payload.attachments)} Attachments not included)' + card.subtitle = ( + f"({len(action.message_payload.attachments)} Attachments not included)" + ) # This Messaging Extension example allows the user to check a box to include an image with the # shared message. This demonstrates sending custom parameters along with the message payload. include_image = action.data["includeImage"] if include_image == "true": - image = CardImage(url="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU") + image = CardImage( + url="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU" + ) card.images = [image] - + cardAttachment = CardFactory.hero_card(card) - attachment = MessagingExtensionAttachment(content=card, content_type=CardFactory.content_types.hero_card, preview=cardAttachment) + attachment = MessagingExtensionAttachment( + content=card, + content_type=CardFactory.content_types.hero_card, + preview=cardAttachment, + ) attachments = [attachment] - extension_result = MessagingExtensionResult(attachment_layout="list", type="result", attachments=attachments) + extension_result = MessagingExtensionResult( + attachment_layout="list", type="result", attachments=attachments + ) return MessagingExtensionActionResponse(compose_extension=extension_result)