From ee2fd04de203e6a4692d60c542d4d39db04f0a34 Mon Sep 17 00:00:00 2001 From: Cameron Koegel Date: Wed, 23 Mar 2022 16:06:38 -0400 Subject: [PATCH 1/5] DX-2343 Create Sample App --- .gitignore | 2 + LICENSE | 21 +++++++++ README.md | 68 +++++++++++++++++++++++++++- icon-messaging.svg | 1 + main.py | 108 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 icon-messaging.svg create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b81a20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/venv +__pycache__ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c85e706 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Bandwidth Samples + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bc86a66..7f1ef9f 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ -# messaging-auto-respond-python \ No newline at end of file +# Auto-Respond to SMS + + + Messaging Quick Start Guide + + + # Table of Contents + +* [Description](#description) +* [Pre-Requisites](#pre-requisites) +* [Running the Application](#running-the-application) +* [Environmental Variables](#environmental-variables) +* [Callback URLs](#callback-urls) + * [Ngrok](#ngrok) + +# Description + +This app automatically responds to texts sent to the associated Bandwidth number.To use this app, you must check the "Use multiple callback URLs" box on the application page in Dashboard. Then in Dashboard, set the INBOUND CALLBACK to `/callbacks/inbound/messaging` and the STATUS CALLBACK to `/callbacks/outbound/messaging/status`. The same can be accomplished via the Dashboard API by setting InboundCallbackUrl and OutboundCallbackUrl respectively. + +Inbound callbacks are sent notifying you of a received message on a Bandwidth number, this app sends a custom response to those messages based on their content. Outbound callbacks are status updates for messages sent from a Bandwidth number, this app has a dedicated response for each type of status update. + +# Pre-Requisites + +In order to use the Bandwidth API users need to set up the appropriate application at the [Bandwidth Dashboard](https://dashboard.bandwidth.com/) and create API tokens. + +To create an application log into the [Bandwidth Dashboard](https://dashboard.bandwidth.com/) and navigate to the `Applications` tab. Fill out the **New Application** form selecting the service (Messaging or Voice) that the application will be used for. All Bandwidth services require publicly accessible Callback URLs, for more information on how to set one up see [Callback URLs](#callback-urls). + +For more information about API credentials see our [Account Credentials](https://dev.bandwidth.com/docs/account/credentials) page. + +# Running the Application + +Use the following command to run the application: + +```sh +uvicorn main:app --reload # app will automatically reload upon changes. +``` + +# Environmental Variables + +The sample app uses the below environmental variables. + +```sh +BW_ACCOUNT_ID # Your Bandwidth Account Id +BW_USERNAME # Your Bandwidth API Username +BW_PASSWORD # Your Bandwidth API Password +BW_NUMBER # The Bandwidth phone number involved with this application +BW_MESSAGING_APPLICATION_ID # Your Messaging Application Id created in the dashboard +``` + +# Callback URLs + +For a detailed introduction, check out our [Bandwidth Messaging Callbacks](https://dev.bandwidth.com/docs/messaging/webhooks) page. + +Below are the callback paths: +* `/callbacks/outbound/messaging/status` For Outbound Status Callbacks +* `/callbacks/inbound/messaging` For Inbound Message Callbacks + +## Ngrok + +A simple way to set up a local callback URL for testing is to use the free tool [ngrok](https://ngrok.com/). +After you have downloaded and installed `ngrok` run the following command to open a public tunnel to your port (`8000`). `8000` is the default for FastAPI. + +```cmd +ngrok http 8000 +``` + +You can view your public URL at `http://127.0.0.1:4040` after ngrok is running. You can also view the status of the tunnel and requests/responses here. diff --git a/icon-messaging.svg b/icon-messaging.svg new file mode 100644 index 0000000..476ed23 --- /dev/null +++ b/icon-messaging.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..eaa66d8 --- /dev/null +++ b/main.py @@ -0,0 +1,108 @@ +import os +import re + +# TODO: Needs to be changed when the SDK becomes python package. +import sys +sys.path.insert(0, 'C:/Users/ckoegel/Documents/sdks/bandwidth_python') +import bandwidth_python +from bandwidth_python.api.messages_api import MessagesApi +from bandwidth_python.model.message_request import MessageRequest +from bandwidth_python.model.bandwidth_callback_message import BandwidthCallbackMessage +# --------------------------------------------------- + +from fastapi import FastAPI, Request +from pydantic import BaseModel + + +BW_ACCOUNT_ID = os.environ.get('BW_ACCOUNT_ID') +BW_USERNAME = os.environ.get('BW_USERNAME') +BW_PASSWORD = os.environ.get('BW_PASSWORD') +BW_NUMBER = os.environ.get('BW_NUMBER') +BW_MESSAGING_APPLICATION_ID = os.environ.get('BW_MESSAGING_APPLICATION_ID') + + +class CreateBody(BaseModel): # model for the received json body to create a message + to: str + text: str + + +configuration = bandwidth_python.Configuration( # TODO: # Configure HTTP basic authorization: httpBasic + username=BW_USERNAME, + password=BW_PASSWORD +) + + +api_client = bandwidth_python.ApiClient(configuration) # TODO: package name +messages_api_instance = MessagesApi(api_client) # TODO: package name + + +app = FastAPI() + +@app.post('/callbacks/outbound/messaging/status') # This URL handles outbound message status callbacks. +async def handle_outbound_status(request: Request): + status_body_array = await request.json() + status_body = status_body_array[0] + if status_body['type'] == "message-sending": + print("message-sending type is only for MMS.") + elif status_body['type'] == "message-delivered": + print("Your message has been handed off to the Bandwidth's MMSC network, but has not been confirmed at the downstream carrier.") + elif status_body['type'] == "message-failed": + print("For MMS and Group Messages, you will only receive this callback if you have enabled delivery receipts on MMS.") + else: + print("Message type does not match endpoint. This endpoint is used for message status callbacks only.") + + return 200 + + +@app.post('/callbacks/inbound/messaging') # This URL handles inbound message callbacks. +async def handle_inbound(request: Request): + inbound_body_array = await request.json() + inbound_body = BandwidthCallbackMessage._new_from_openapi_data(inbound_body_array[0]) + print(inbound_body.description) + if inbound_body.type == "message-received": + print(f"To: {inbound_body.message.to[0]}\nFrom: {inbound_body.message._from}\nText: {inbound_body.message.text}") + + auto_reponse_message = auto_response(inbound_body.message.text) + + message_body = MessageRequest( + to=[inbound_body.message._from], + _from=BW_NUMBER, + application_id=BW_MESSAGING_APPLICATION_ID, + text=auto_reponse_message + ) + response = messages_api_instance.create_message( + account_id=BW_ACCOUNT_ID, + message_request=message_body, + _return_http_data_only=False + ) + + print("\nSending Auto Response") + print(f"To: {inbound_body.message._from}\nFrom: {inbound_body.message.to[0]}\nText: {auto_reponse_message}") + else: + print("Message type does not match endpoint. This endpoint is used for inbound messages only.\nOutbound message status callbacks should be sent to /callbacks/outbound/messaging/status.") + + return 200 + + +response_map = { + 'stop': "STOP: OK, you'll no longer receive messages from us.", + 'quit': "QUIT: OK, you'll no longer receive messages from us.", + 'help': "Valid words are: STOP, QUIT, HELP, and INFO. Reply STOP or QUIT to opt out.", + 'info': "INFO: This is the test responder service. Reply STOP or QUIT to opt out.", + 'default': "Please respond with a valid word. Reply HELP for help." +} + + +def auto_response(inbound_text): + command = re.search('(.\S+)', inbound_text).group(1).lower() + if command in response_map.keys(): + map_val = response_map[command] + else: + map_val = response_map['default'] + + response = "[Auto Response] " + map_val + return response + + +if __name__ == '__main__': + app.run(host='0.0.0.0') From 3030327716fa7d0a7f77e86839c87b5c2e5a8fea Mon Sep 17 00:00:00 2001 From: Cameron Koegel Date: Thu, 24 Mar 2022 13:53:31 -0400 Subject: [PATCH 2/5] quotes --- main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index eaa66d8..d7a0bad 100644 --- a/main.py +++ b/main.py @@ -85,11 +85,11 @@ async def handle_inbound(request: Request): response_map = { - 'stop': "STOP: OK, you'll no longer receive messages from us.", - 'quit': "QUIT: OK, you'll no longer receive messages from us.", - 'help': "Valid words are: STOP, QUIT, HELP, and INFO. Reply STOP or QUIT to opt out.", - 'info': "INFO: This is the test responder service. Reply STOP or QUIT to opt out.", - 'default': "Please respond with a valid word. Reply HELP for help." + "stop": "STOP: OK, you'll no longer receive messages from us.", + "quit": "QUIT: OK, you'll no longer receive messages from us.", + "help": "Valid words are: STOP, QUIT, HELP, and INFO. Reply STOP or QUIT to opt out.", + "info": "INFO: This is the test responder service. Reply STOP or QUIT to opt out.", + "default": "Please respond with a valid word. Reply HELP for help." } From 75be39d3be4affe1826651d4efab359602552f84 Mon Sep 17 00:00:00 2001 From: Cameron Koegel Date: Thu, 24 Mar 2022 14:13:17 -0400 Subject: [PATCH 3/5] regex --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index d7a0bad..526f1b6 100644 --- a/main.py +++ b/main.py @@ -94,7 +94,7 @@ async def handle_inbound(request: Request): def auto_response(inbound_text): - command = re.search('(.\S+)', inbound_text).group(1).lower() + command = re.search(r"(.\S+)", inbound_text).group(1).lower() if command in response_map.keys(): map_val = response_map[command] else: From d9119d6c381392c4db9320f31f8d6557e8fbc6ed Mon Sep 17 00:00:00 2001 From: Cameron Koegel Date: Thu, 24 Mar 2022 14:15:08 -0400 Subject: [PATCH 4/5] regex --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 526f1b6..1ef1cf8 100644 --- a/main.py +++ b/main.py @@ -94,7 +94,7 @@ async def handle_inbound(request: Request): def auto_response(inbound_text): - command = re.search(r"(.\S+)", inbound_text).group(1).lower() + command = re.search(r"^(.\S+)$", inbound_text).group(1).lower() if command in response_map.keys(): map_val = response_map[command] else: From d0771ebb211d9888e7f0c3fd51ede4c563d29845 Mon Sep 17 00:00:00 2001 From: Cameron Koegel Date: Tue, 29 Mar 2022 11:32:18 -0400 Subject: [PATCH 5/5] requirements readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7f1ef9f..1a3a677 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ For more information about API credentials see our [Account Credentials](https:/ # Running the Application +To install the required packages for this app, run the command: + +```sh +pip install -r requirements.txt +``` + Use the following command to run the application: ```sh