From 850ce760bf74ee157fab500266c479c2a064b8fe Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 29 Oct 2026 16:41:28 -0400 Subject: [PATCH] DX-3359 Update Sample --- .github/CODEOWNERS | 2 +- .github/dependabot.yml | 19 +++ .github/workflows/test.yml | 16 +++ .../AutoRespond.csproj | 4 +- AutoRespond/Program.cs | 119 +++++++++++++++++ .../Properties/launchSettings.json | 2 - .../appsettings.Development.json | 0 .../appsettings.json | 0 LICENSE | 2 +- README.md | 61 ++++----- SendAutoRespond/Program.cs | 122 ------------------ icon-messaging.svg | 4 +- messaging-auto-respond-csharp.sln | 2 +- 13 files changed, 193 insertions(+), 160 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/test.yml rename SendAutoRespond/SendAutoRespond.csproj => AutoRespond/AutoRespond.csproj (59%) create mode 100644 AutoRespond/Program.cs rename {SendAutoRespond => AutoRespond}/Properties/launchSettings.json (83%) rename {SendAutoRespond => AutoRespond}/appsettings.Development.json (100%) rename {SendAutoRespond => AutoRespond}/appsettings.json (100%) delete mode 100644 SendAutoRespond/Program.cs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index beb9074..12776e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Global rule: -* @Bandwidth-Samples/dx +* @Bandwidth-Samples/band-swi @Bandwidth-Samples/band-devx diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5a29fba --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 + +updates: + - package-ecosystem: nuget + directory: /AutoRespond + schedule: + interval: monthly + open-pull-requests-limit: 5 + ignore: + - dependency-name: "Bandwidth.Sdk" + update-types: + - version-update:semver-minor + - version-update:semver-patch + groups: + dependencies: + patterns: + - "*" + exclude-patterns: + - "Bandwidth.Sdk" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5c5263c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: Test + +on: + pull_request: + branches: + - main + schedule: + - cron: "0 9 * * 1" + +jobs: + contract-test: + name: Contract Test + uses: Bandwidth-Samples/sample-app-tooling/.github/workflows/csharp.yml@main + with: + sample: messaging-auto-respond + working-directory: AutoRespond diff --git a/SendAutoRespond/SendAutoRespond.csproj b/AutoRespond/AutoRespond.csproj similarity index 59% rename from SendAutoRespond/SendAutoRespond.csproj rename to AutoRespond/AutoRespond.csproj index 4b5a048..fd311df 100644 --- a/SendAutoRespond/SendAutoRespond.csproj +++ b/AutoRespond/AutoRespond.csproj @@ -1,13 +1,13 @@ - net7.0 + net8.0 enable enable - + diff --git a/AutoRespond/Program.cs b/AutoRespond/Program.cs new file mode 100644 index 0000000..46c4cb4 --- /dev/null +++ b/AutoRespond/Program.cs @@ -0,0 +1,119 @@ +using Bandwidth.Standard.Api; +using Bandwidth.Standard.Client; +using Bandwidth.Standard.Model; +using Newtonsoft.Json; + +//Setting up environment variables +string? BW_ACCOUNT_ID = Environment.GetEnvironmentVariable("BW_ACCOUNT_ID"); +string? BW_CLIENT_ID = Environment.GetEnvironmentVariable("BW_CLIENT_ID"); +string? BW_CLIENT_SECRET = Environment.GetEnvironmentVariable("BW_CLIENT_SECRET"); +string? BW_MESSAGING_APPLICATION_ID = Environment.GetEnvironmentVariable("BW_MESSAGING_APPLICATION_ID"); +string? BW_NUMBER = Environment.GetEnvironmentVariable("BW_NUMBER"); +string? LOCAL_PORT = Environment.GetEnvironmentVariable("LOCAL_PORT"); + +if (BW_ACCOUNT_ID == null || BW_CLIENT_ID == null || BW_CLIENT_SECRET == null || BW_MESSAGING_APPLICATION_ID == null + || BW_NUMBER == null || !int.TryParse(LOCAL_PORT, out int port)) +{ + Console.WriteLine("Please set the environmental variables defined in the README"); + Environment.Exit(1); + return; +} + +var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseUrls($"http://localhost:{port}"); +var app = builder.Build(); + +Configuration configuration = new Configuration(); +configuration.OAuthClientId = BW_CLIENT_ID; +configuration.OAuthClientSecret = BW_CLIENT_SECRET; + +var statusMessages = new Dictionary +{ + [StatusCallbackTypeEnum.Sending] = "message-sending type is only for MMS.", + [StatusCallbackTypeEnum.Delivered] = "Your message has been handed off to the Bandwidth's MMSC network, but has not been confirmed at the downstream carrier.", + [StatusCallbackTypeEnum.Failed] = "For MMS and Group Messages, you will only receive this callback if you have enabled delivery receipts on MMS." +}; + +var replies = new Dictionary +{ + ["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." +}; +const string DEFAULT_REPLY = "Please respond with a valid word. Reply HELP for help."; + +async Task ReadBody(HttpContext context) +{ + try + { + using var streamReader = new StreamReader(context.Request.Body); + return JsonConvert.DeserializeObject(await streamReader.ReadToEndAsync()); + } + catch (JsonException) + { + return default; + } +} + +string AutoReply(string? text) +{ + return replies.GetValueOrDefault((text ?? "").Trim().ToLower(), DEFAULT_REPLY); +} + +async Task SendReply(string to, string text) +{ + MessageRequest messageRequest = new MessageRequest( + applicationId: BW_MESSAGING_APPLICATION_ID, + to: new List { to }, + from: BW_NUMBER, + text: text + ); + + try + { + MessagesApi apiInstance = new MessagesApi(configuration); + Message message = await apiInstance.CreateMessageAsync(BW_ACCOUNT_ID, messageRequest); + Console.WriteLine($"Sent reply {message.Id}\nTo: {to}\nText: {text}"); + } + catch (Exception e) + { + Console.WriteLine($"Failed to send reply: {e.Message}"); + } +} + +app.MapPost("/callbacks/outbound/messaging/status", async (HttpContext context) => +{ + var callbacks = await ReadBody>(context); + if (callbacks == null || !callbacks.All(callback => statusMessages.ContainsKey(callback.Type))) + { + return Results.BadRequest(); + } + + foreach (var callback in callbacks) + { + Console.WriteLine(statusMessages[callback.Type]); + } + + return Results.NoContent(); +}); + +app.MapPost("/callbacks/inbound/messaging", async (HttpContext context) => +{ + var callbacks = await ReadBody>(context); + if (callbacks == null || !callbacks.All(callback => callback.Type == InboundCallbackTypeEnum.MessageReceived)) + { + return Results.BadRequest(); + } + + foreach (var callback in callbacks) + { + var message = callback.Message; + Console.WriteLine($"{callback.Description}\nTo: {message.To[0]}\nFrom: {message.From}\nText: {message.Text}"); + await SendReply(message.From, AutoReply(message.Text)); + } + + return Results.NoContent(); +}); + +app.Run(); diff --git a/SendAutoRespond/Properties/launchSettings.json b/AutoRespond/Properties/launchSettings.json similarity index 83% rename from SendAutoRespond/Properties/launchSettings.json rename to AutoRespond/Properties/launchSettings.json index a6cf4a4..46a8914 100644 --- a/SendAutoRespond/Properties/launchSettings.json +++ b/AutoRespond/Properties/launchSettings.json @@ -12,7 +12,6 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -21,7 +20,6 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7278;http://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/SendAutoRespond/appsettings.Development.json b/AutoRespond/appsettings.Development.json similarity index 100% rename from SendAutoRespond/appsettings.Development.json rename to AutoRespond/appsettings.Development.json diff --git a/SendAutoRespond/appsettings.json b/AutoRespond/appsettings.json similarity index 100% rename from SendAutoRespond/appsettings.json rename to AutoRespond/appsettings.json diff --git a/LICENSE b/LICENSE index c85e706..ddc77f4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Bandwidth Samples +Copyright (c) 2026 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 diff --git a/README.md b/README.md index 3fdc41f..703bf39 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,56 @@ -# Send Auto Response C# +# Messaging Auto Respond - + Messaging Quick Start Guide - # Table of Contents +# Table of Contents -- [Send Auto Response C#](#send-auto-response-c) -- [Table of Contents](#table-of-contents) -- [Description](#description) -- [Pre-Requisites](#pre-requisites) -- [Environmental Variables](#environmental-variables) -- [Running the Application](#running-the-application) -- [Callback URLs](#callback-urls) - - [Ngrok](#ngrok) +* [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. Any texts sent to the `BW_NUMBER` will be auto-responded to using the auto response map. Valid words are: `STOP`, `QUIT`, `HELP`, and `INFO`. +This sample app automatically replies to texts sent to your Bandwidth number. The reply depends on the text received: `STOP`, `QUIT`, `HELP` and `INFO` each get their own reply, and anything else gets a prompt to reply `HELP`. Keywords are matched against the whole message and are not case sensitive. -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. +The `STOP` and `QUIT` replies are for demonstration only. This app doesn't keep track of opt-outs, so it will keep replying to a number that has sent `STOP`. -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. +In order to use the correct endpoints, 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 prints the phone numbers involved and the text received, then sends the reply. 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 that the application will be used for (this sample app uses a messaging application). All Bandwidth services require publicly accessible Callback URLs, for more information on how to set one up see [Callback URLs](#callback-urls). +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. -# Environmental Variables +# Running the Application -The sample app uses the below environmental variables. +Use the following command to run the application: ```sh -BW_ACCOUNT_ID # Your Bandwidth Account Id -BW_USERNAME # Your Bandwidth API Username -BW_PASSWORD # Your Bandwidth API Password -BW_MESSAGING_APPLICATION_ID # Your Messaging Application Id created in the dashboard +cd AutoRespond/ +dotnet run ``` -# Running the Application +# Environmental Variables -Use the following command to run the application: +The sample app uses the below environmental variables. ```sh -cd SendAutoRespond/ -dotnet run +BW_ACCOUNT_ID # Your Bandwidth Account Id +BW_CLIENT_ID # Your Bandwidth API OAuth Client Id +BW_CLIENT_SECRET # Your Bandwidth API OAuth Client Secret +BW_NUMBER # The Bandwidth phone number involved with this application +BW_MESSAGING_APPLICATION_ID # Your Messaging Application Id created in the dashboard +LOCAL_PORT # The port number you wish to run the sample on ``` # Callback URLs @@ -62,11 +64,10 @@ Below are the callback paths: ## 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 (in this case port 5001). +After you have downloaded and installed `ngrok` run the following command to open a public tunnel to your port (`$LOCAL_PORT`) -```cmd -ngrok http 5001 +```sh +ngrok http $LOCAL_PORT ``` -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. -*Note: If you would like to change your port number feel free to do so. However, if you do change the port you will also need to change the number appended to the application URL in the `launchSettings.json` file located in `SendAutoRespond/Properties/`* +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. Once your public ngrok url has been created, use it as the base of the callback URLs in the messaging application created in the [Pre-Requisites](#pre-requisites) section. diff --git a/SendAutoRespond/Program.cs b/SendAutoRespond/Program.cs deleted file mode 100644 index 33773cc..0000000 --- a/SendAutoRespond/Program.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Bandwidth.Standard.Api; -using Bandwidth.Standard.Client; -using Bandwidth.Standard.Model; -using Newtonsoft.Json; - -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -string BW_USERNAME; -string BW_PASSWORD; -string BW_MESSAGING_APPLICATION_ID; -string BW_ACCOUNT_ID; - -//Setting up environment variables -try -{ - BW_USERNAME = Environment.GetEnvironmentVariable("BW_USERNAME"); - BW_PASSWORD = Environment.GetEnvironmentVariable("BW_PASSWORD"); - BW_MESSAGING_APPLICATION_ID = Environment.GetEnvironmentVariable("BW_MESSAGING_APPLICATION_ID"); - BW_ACCOUNT_ID = Environment.GetEnvironmentVariable("BW_ACCOUNT_ID"); -} -catch (Exception) -{ - Console.WriteLine("Please set the environmental variables defined in the README"); - throw; -} - -Configuration configuration = new Configuration(); -configuration.Username = BW_USERNAME; -configuration.Password = BW_PASSWORD; - -app.MapPost("/callbacks/inbound/messaging", async (HttpContext context) => -{ - var requestBody = new List(); - using(var streamReader = new StreamReader(context.Request.Body)) - { - var body = await streamReader.ReadToEndAsync(); - requestBody = JsonConvert.DeserializeObject>(body); - } - - var type = (string)((dynamic)requestBody[0]).type; - - if(type.Equals("message-received")) - { - var from = (string)((dynamic)requestBody[0]).message.from; - var to = (string)((dynamic)requestBody[0]).message.to[0]; - var text = (string)((dynamic)requestBody[0]).message.text; - Console.WriteLine($"Message received from '{from}' to '{to}' with text '{text}'."); - - string autoResponseMessage = autoResponse(text); - - MessageRequest request = new MessageRequest( - applicationId: BW_MESSAGING_APPLICATION_ID, - to: new List { from }, - from: to, - text: autoResponseMessage - ); - - MessagesApi apiInstance = new MessagesApi(configuration); - try - { - // Send a message - var result = await apiInstance.CreateMessageAsync(BW_ACCOUNT_ID, request); - Console.WriteLine($"\nSending Auto Response\nTo: {from}\nFrom: {to}\nText: {autoResponseMessage}"); - } - catch (ApiException e) - { - Console.WriteLine("Exception when calling MessagesApi.CreateMessage: " + e.Message); - } - } - else - { - Console.WriteLine("Message type does not match endpoint. This endpoint is used for inbound messages only."); - Console.WriteLine("Outbound message callbacks should be sent to /callbacks/outbound/messaging."); - } -}); - -app.MapPost("/callbacks/outbound/messaging/status", async (HttpContext context) => -{ - var requestBody = new List(); - using(var streamReader = new StreamReader(context.Request.Body)) - { - var body = await streamReader.ReadToEndAsync(); - requestBody = JsonConvert.DeserializeObject>(body); - } - - var type = (string)((dynamic)requestBody[0]).type; - - switch (type) - { - case "message-sending": - Console.WriteLine("message-sending type is only for MMS."); - break; - case "message-delivered": - Console.WriteLine("Your message has been handed off to the Bandwidth's MMSC network, but has not been confirmed at the downstream carrier."); - break; - case "message-failed": - Console.WriteLine("For MMS and Group Messages, you will only receive this callback if you have enabled delivery receipts on MMS."); - break; - default: - Console.WriteLine("Message type does not match endpoint. This endpoint is used for message status callbacks only."); - break; - } -}); - -String autoResponse(String inboundText) -{ - var command = inboundText.ToLower(); - - Dictionary responseMap = new Dictionary(); - responseMap.Add("stop","STOP: OK, you'll no longer receive messages from us."); - responseMap.Add("quit","QUIT: OK, you'll no longer receive messages from us."); - responseMap.Add("help","Valid words are: STOP, QUIT, HELP, and INFO. Reply STOP or QUIT to opt out."); - responseMap.Add("info","INFO: This is the test responder service. Reply STOP or QUIT to opt out."); - responseMap.Add("default","Please respond with a valid word. Reply HELP for help."); - - var mapVal = responseMap.GetValueOrDefault(command, responseMap["default"]); - - return "[Auto Response] " + mapVal; -} - -app.Run(); diff --git a/icon-messaging.svg b/icon-messaging.svg index 476ed23..fa130bd 100644 --- a/icon-messaging.svg +++ b/icon-messaging.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/messaging-auto-respond-csharp.sln b/messaging-auto-respond-csharp.sln index 808ee0a..639f82e 100644 --- a/messaging-auto-respond-csharp.sln +++ b/messaging-auto-respond-csharp.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SendAutoRespond", "SendAutoRespond\SendAutoRespond.csproj", "{E505815B-66CF-45B1-A91A-5B237CAD666B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoRespond", "AutoRespond\AutoRespond.csproj", "{E505815B-66CF-45B1-A91A-5B237CAD666B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution