diff --git a/samples/23.facebook-events/README.md b/samples/23.facebook-events/README.md new file mode 100644 index 000000000..01a0f2619 --- /dev/null +++ b/samples/23.facebook-events/README.md @@ -0,0 +1,36 @@ +# Facebook events + +Bot Framework v4 facebook events bot sample + +This bot has been created using [Bot Framework](https://dev.botframework.com), is shows how to integrate and consume Facebook specific payloads, such as postbacks, quick replies and optin events. Since Bot Framework supports multiple Facebook pages for a single bot, we also show how to know the page to which the message was sent, so developers can have custom behavior per page. + +More information about configuring a bot for Facebook Messenger can be found here: [Connect a bot to Facebook](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-facebook) + +## 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\23.facebook-evbents` 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 +- File -> Open Bot +- 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) +- [Facebook Quick Replies](https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies/0) +- [Facebook PostBack](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_postbacks/) +- [Facebook Opt-in](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_optins/) +- [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/samples/23.facebook-events/app.py b/samples/23.facebook-events/app.py new file mode 100644 index 000000000..efd359d67 --- /dev/null +++ b/samples/23.facebook-events/app.py @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import sys +from datetime import datetime + +from flask import Flask, request, Response +from botbuilder.core import ( + BotFrameworkAdapterSettings, + TurnContext, + BotFrameworkAdapter, +) +from botbuilder.schema import Activity, ActivityTypes + +from bots import FacebookBot + +# 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(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 = FacebookBot() + +# Listen for incoming requests on /api/messages +@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/samples/23.facebook-events/bots/__init__.py b/samples/23.facebook-events/bots/__init__.py new file mode 100644 index 000000000..7db4bb27c --- /dev/null +++ b/samples/23.facebook-events/bots/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .facebook_bot import FacebookBot + +__all__ = ["FacebookBot"] diff --git a/samples/23.facebook-events/bots/facebook_bot.py b/samples/23.facebook-events/bots/facebook_bot.py new file mode 100644 index 000000000..7ee4ee609 --- /dev/null +++ b/samples/23.facebook-events/bots/facebook_bot.py @@ -0,0 +1,129 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.dialogs.choices import Choice, ChoiceFactory +from botbuilder.core import ActivityHandler, MessageFactory, TurnContext, CardFactory +from botbuilder.schema import ChannelAccount, CardAction, ActionTypes, HeroCard + +FACEBOOK_PAGEID_OPTION = "Facebook Id" +QUICK_REPLIES_OPTION = "Quick Replies" +POSTBACK_OPTION = "PostBack" + + +class FacebookBot(ActivityHandler): + 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): + if not await self._process_facebook_payload( + turn_context, turn_context.activity.channel_data + ): + await self._show_choices(turn_context) + + async def on_event_activity(self, turn_context: TurnContext): + await self._process_facebook_payload(turn_context, turn_context.activity.value) + + async def _show_choices(self, turn_context: TurnContext): + choices = [ + Choice( + value=QUICK_REPLIES_OPTION, + action=CardAction( + title=QUICK_REPLIES_OPTION, + type=ActionTypes.post_back, + value=QUICK_REPLIES_OPTION, + ), + ), + Choice( + value=FACEBOOK_PAGEID_OPTION, + action=CardAction( + title=FACEBOOK_PAGEID_OPTION, + type=ActionTypes.post_back, + value=FACEBOOK_PAGEID_OPTION, + ), + ), + Choice( + value=POSTBACK_OPTION, + action=CardAction( + title=POSTBACK_OPTION, + type=ActionTypes.post_back, + value=POSTBACK_OPTION, + ), + ), + ] + + message = ChoiceFactory.for_channel( + turn_context.activity.channel_id, + choices, + "What Facebook feature would you like to try? Here are some quick replies to choose from!", + ) + await turn_context.send_activity(message) + + async def _process_facebook_payload(self, turn_context: TurnContext, data) -> bool: + if "postback" in data: + await self._on_facebook_postback(turn_context, data["postback"]) + return True + + if "optin" in data: + await self._on_facebook_optin(turn_context, data["optin"]) + return True + + if "message" in data and "quick_reply" in data["message"]: + await self._on_facebook_quick_reply( + turn_context, data["message"]["quick_reply"] + ) + return True + + if "message" in data and data["message"]["is_echo"]: + await self._on_facebook_echo(turn_context, data["message"]) + return True + + async def _on_facebook_postback( + self, turn_context: TurnContext, facebook_postback: dict + ): + # TODO: Your PostBack handling logic here... + + reply = MessageFactory.text(f"Postback: {facebook_postback}") + await turn_context.send_activity(reply) + await self._show_choices(turn_context) + + async def _on_facebook_quick_reply( + self, turn_context: TurnContext, facebook_quick_reply: dict + ): + # TODO: Your quick reply event handling logic here... + + if turn_context.activity.text == FACEBOOK_PAGEID_OPTION: + reply = MessageFactory.text( + f"This message comes from the following Facebook Page: {turn_context.activity.recipient.id}" + ) + await turn_context.send_activity(reply) + await self._show_choices(turn_context) + elif turn_context.activity.text == POSTBACK_OPTION: + card = HeroCard( + text="Is 42 the answer to the ultimate question of Life, the Universe, and Everything?", + buttons=[ + CardAction(title="Yes", type=ActionTypes.post_back, value="Yes"), + CardAction(title="No", type=ActionTypes.post_back, value="No"), + ], + ) + reply = MessageFactory.attachment(CardFactory.hero_card(card)) + await turn_context.send_activity(reply) + else: + print(facebook_quick_reply) + await turn_context.send_activity("Quick Reply") + await self._show_choices(turn_context) + + async def _on_facebook_optin(self, turn_context: TurnContext, facebook_optin: dict): + # TODO: Your optin event handling logic here... + print(facebook_optin) + await turn_context.send_activity("Opt In") + + async def _on_facebook_echo( + self, turn_context: TurnContext, facebook_message: dict + ): + # TODO: Your echo event handling logic here... + print(facebook_message) + await turn_context.send_activity("Echo") diff --git a/samples/23.facebook-events/config.py b/samples/23.facebook-events/config.py new file mode 100644 index 000000000..e007d0fa9 --- /dev/null +++ b/samples/23.facebook-events/config.py @@ -0,0 +1,15 @@ +#!/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 = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json b/samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..bff8c096d --- /dev/null +++ b/samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,242 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('servicePlanName')]", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('webAppName')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "kind": "app,linux", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/23.facebook-events/requirements.txt b/samples/23.facebook-events/requirements.txt new file mode 100644 index 000000000..a69322ec3 --- /dev/null +++ b/samples/23.facebook-events/requirements.txt @@ -0,0 +1,3 @@ +jsonpickle==1.2 +botbuilder-core>=4.4.0b1 +flask>=1.0.3