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..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" 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