Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Global rule:
* @Bandwidth-Samples/dx
* @Bandwidth-Samples/band-swi @Bandwidth-Samples/band-devx
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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"
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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.
218 changes: 118 additions & 100 deletions VoiceRecord/Program.cs
Original file line number Diff line number Diff line change
@@ -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<string, string>();
using(var streamReader = new StreamReader(context.Request.Body))
{
var body = await streamReader.ReadToEndAsync();
requestBody = JsonConvert.DeserializeObject<Dictionary<string,string>>(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<Dictionary<string, object>?> ReadCallback(HttpContext context, string expectedEventType)
{
try
{
using var streamReader = new StreamReader(context.Request.Body);
var body = JsonConvert.DeserializeObject<Dictionary<string, object>>(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();
6 changes: 3 additions & 3 deletions VoiceRecord/VoiceRecord.csproj
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bandwidth.Sdk" Version="11.0.0-beta" />
<PackageReference Include="Bandwidth.Sdk" Version="17.3.2" />
</ItemGroup>

<ItemGroup>
<Content Include="Tone.mp3">
<Content Include="tone.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
Expand Down
File renamed without changes.
Loading