From 2efee3f199fdc220e642d4e9e710ac06c6501069 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 16 Oct 2026 17:02:21 -0400 Subject: [PATCH 1/4] DX-3359 Update Sample App --- .github/workflows/test.yml | 16 +++ README.md | 23 +-- VoiceRecord/Program.cs | 218 ++++++++++++++++------------- VoiceRecord/VoiceRecord.csproj | 6 +- VoiceRecord/{Tone.mp3 => tone.mp3} | Bin 5 files changed, 149 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/test.yml rename VoiceRecord/{Tone.mp3 => tone.mp3} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6e2b817 --- /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: voice-record + working-directory: VoiceRecord diff --git a/README.md b/README.md index 30982bc..314b9c3 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ This sample app demonstrates how to record a message from an inbound call, similar to how a voicemail machine would work. -When the user calls the Bandwidth phone number associated with this application, the user will hear an audio prompt to leave a message, then a beep. Once this happens, the call will begin recording. +When the user calls the Bandwidth phone number associated with this application, the user will hear an audio prompt to leave a message, then a beep. Once this happens, the call will begin recording, for up to 30 seconds. -When the call ends, Bandwidth will start to process the recording. Once this processing is complete, Bandwidth will send a webhook to the `/recordingAvailableCallback` endpoint on this application. Once received, the application will download the recording via Bandwidth's voice recording API. +When the call ends, Bandwidth will start to process the recording. Once this processing is complete, Bandwidth will send a webhook to the `/callbacks/inbound/voice/recordingAvailable` endpoint on this application. Once received, the application will download the recording via Bandwidth's voice recording API. # Pre-Requisites @@ -40,8 +40,9 @@ For more information about API credentials see [here](https://dev.bandwidth.com/ The sample app uses the below environmental variables. ```sh BW_ACCOUNT_ID # Your Bandwidth Account Id -BW_USERNAME # Your Bandwidth API Token -BW_PASSWORD # Your Bandwidth API Secret +BW_CLIENT_ID # Your Bandwidth API OAuth Client Id +BW_CLIENT_SECRET # Your Bandwidth API OAuth Client Secret +LOCAL_PORT # The port number you wish to run the sample on ``` # Running the Application @@ -58,16 +59,16 @@ dotnet run For a detailed introduction to Bandwidth Callbacks see https://dev.bandwidth.com/guides/callbacks/callbacks.html Below are the callback paths: -* `/callbacks/callInitiatedCallback` -* `/callbacks/recordingAvailableCallback` +* `/callbacks/inbound/voice` +* `/callbacks/inbound/voice/recordingAvailable` + +The app also serves the prompt tone at `/tone.mp3`, which is not a callback. ## 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 (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 +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 `SendReceiveMMS/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, you can set it, followed by `/callbacks/inbound/voice`, as the callback URL of the voice application created in the [Pre-Requisites](#pre-requisites) section. diff --git a/VoiceRecord/Program.cs b/VoiceRecord/Program.cs index fa9c1b9..4269400 100644 --- a/VoiceRecord/Program.cs +++ b/VoiceRecord/Program.cs @@ -1,100 +1,118 @@ -using System.Reflection; -using Bandwidth.Standard.Api; -using Bandwidth.Standard.Client; -using Bandwidth.Standard.Model.Bxml; -using Bandwidth.Standard.Model.Bxml.Verbs; -using Newtonsoft.Json; - -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -string BW_USERNAME; -string BW_PASSWORD; -string BW_ACCOUNT_ID; - -//Setting up environment variables -try -{ - BW_USERNAME = Environment.GetEnvironmentVariable("BW_USERNAME"); - BW_PASSWORD = Environment.GetEnvironmentVariable("BW_PASSWORD"); - 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/callInitiatedCallback", async (HttpContext context) => - { - SpeakSentence unavailableSpeakSentence = new SpeakSentence() - { - Text = "You have reached Vandelay Industries, Kal Varnsen is unavailable at this time." - }; - SpeakSentence messageSpeakSentence = new SpeakSentence() - { - Text = "At the tone, please record your message, when you have finished recording, you may hang up." - }; - PlayAudio playAudio = new PlayAudio() - { - AudioUri = "Tone.mp3" - }; - Record record = new Record() - { - RecordingAvailableUrl = "/callbacks/recordingAvailableCallback" - }; - - - IVerb[] verbs = new IVerb[] - { - unavailableSpeakSentence, - messageSpeakSentence, - playAudio, - record - }; - Response response = new Response(verbs); - string bxml = response.ToBXML(); - - return bxml; - } -); - -app.MapGet("callbacks/Tone.mp3", async (HttpContext context) => -{ - var path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Tone.mp3"); - using var fileStream = new FileStream(path, FileMode.Open); - await fileStream.CopyToAsync(context.Response.Body); -}); - -app.MapPost("/callbacks/recordingAvailableCallback", async (HttpContext context) => -{ - var requestBody = new Dictionary(); - using(var streamReader = new StreamReader(context.Request.Body)) - { - var body = await streamReader.ReadToEndAsync(); - requestBody = JsonConvert.DeserializeObject>(body); - } - - var eventType = requestBody["eventType"]; - - if (eventType == "recordingAvailable") - { - RecordingsApi recordingsApi = new RecordingsApi(configuration); - - var fileFormat = requestBody["fileFormat"]; - var callId = requestBody["callId"]; - var recordingId = requestBody["recordingId"]; - - var recording = recordingsApi.DownloadCallRecording(BW_ACCOUNT_ID, callId, recordingId); - using (var fileStream = File.Create("MyRecording." + fileFormat)) - { - recording.CopyTo(fileStream); - } - } -}); - -app.Run(); +using Bandwidth.Standard.Api; +using Bandwidth.Standard.Client; +using Bandwidth.Standard.Model.Bxml; +using Bandwidth.Standard.Model.Bxml.Verbs; +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? LOCAL_PORT = Environment.GetEnvironmentVariable("LOCAL_PORT"); + +if (BW_ACCOUNT_ID == null || BW_CLIENT_ID == null || BW_CLIENT_SECRET == 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; + +async Task?> ReadCallback(HttpContext context, string expectedEventType) +{ + try + { + using var streamReader = new StreamReader(context.Request.Body); + var body = JsonConvert.DeserializeObject>(await streamReader.ReadToEndAsync()); + if (body != null && body.TryGetValue("eventType", out var eventType) && eventType?.ToString() == expectedEventType) + { + return body; + } + } + catch (JsonException) + { + } + return null; +} + +app.MapPost("/callbacks/inbound/voice", async (HttpContext context) => + { + if (await ReadCallback(context, "initiate") == null) + { + return Results.BadRequest(); + } + + SpeakSentence unavailableSpeakSentence = new SpeakSentence() + { + Text = "You have reached Vandelay Industries, Kal Varnsen is unavailable at this time." + }; + SpeakSentence messageSpeakSentence = new SpeakSentence() + { + Text = "At the tone, please record your message, when you have finished recording, you may hang up." + }; + PlayAudio playAudio = new PlayAudio() + { + AudioUri = "/tone.mp3" + }; + Record record = new Record() + { + RecordingAvailableUrl = "/callbacks/inbound/voice/recordingAvailable", + MaxDuration = 30 + }; + + + IVerb[] verbs = new IVerb[] + { + unavailableSpeakSentence, + messageSpeakSentence, + playAudio, + record + }; + Response response = new Response(verbs); + string bxml = response.ToBXML(); + + return Results.Content(bxml, "application/xml"); + } +); + +app.MapGet("/tone.mp3", () => Results.File(Path.Combine(AppContext.BaseDirectory, "tone.mp3"), "audio/mpeg")); + +app.MapPost("/callbacks/inbound/voice/recordingAvailable", async (HttpContext context) => +{ + var requestBody = await ReadCallback(context, "recordingAvailable"); + if (requestBody == null) + { + return Results.BadRequest(); + } + + try + { + RecordingsApi recordingsApi = new RecordingsApi(configuration); + + var fileFormat = requestBody["fileFormat"]; + var callId = requestBody["callId"].ToString(); + var recordingId = requestBody["recordingId"]; + + var recording = recordingsApi.DownloadCallRecording(BW_ACCOUNT_ID, callId, recordingId.ToString()); + using (var fileStream = File.Create($"{recordingId}.{fileFormat}")) + { + recording.CopyTo(fileStream); + } + } + catch (Exception e) + { + Console.WriteLine($"Failed to download recording: {e.Message}"); + return Results.StatusCode(500); + } + + return Results.NoContent(); +}); + +app.Run(); diff --git a/VoiceRecord/VoiceRecord.csproj b/VoiceRecord/VoiceRecord.csproj index 16030fd..8ad4cd9 100644 --- a/VoiceRecord/VoiceRecord.csproj +++ b/VoiceRecord/VoiceRecord.csproj @@ -1,17 +1,17 @@ - net7.0 + net8.0 enable enable - + - + Always diff --git a/VoiceRecord/Tone.mp3 b/VoiceRecord/tone.mp3 similarity index 100% rename from VoiceRecord/Tone.mp3 rename to VoiceRecord/tone.mp3 From e65cbf784a0879d7d20c5189357a60a702d46d08 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 16 Oct 2026 17:07:27 -0400 Subject: [PATCH 2/4] update codeowners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index beb9074..d67a43f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Global rule: -* @Bandwidth-Samples/dx +* @Bandwidth-Samples/band-swi From 2f0a2422556b938741d0f212d926aa5c100d2262 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 16 Oct 2026 17:30:20 -0400 Subject: [PATCH 3/4] dx --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d67a43f..12776e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Global rule: -* @Bandwidth-Samples/band-swi +* @Bandwidth-Samples/band-swi @Bandwidth-Samples/band-devx From c2663c96383d9b2d8baf0d3c1ffadc32514b2579 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Mon, 19 Oct 2026 14:05:50 -0400 Subject: [PATCH 4/4] dependabot --- .github/dependabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..909aabe --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 + +updates: + - package-ecosystem: nuget + directory: /VoiceRecord + 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"