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: bundler
directory: /
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
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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/ruby.yml@main
with:
sample: voice-redirect-loop
6 changes: 4 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
source 'https://rubygems.org'

gem 'bandwidth-sdk', '11.0.0'
gem 'sinatra', '3.1.0'
gem 'bandwidth-sdk', '18.3.1'
gem 'puma', '8.0.2'
gem 'rackup', '2.3.1'
gem 'sinatra', '4.2.1'
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 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
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Voice Redirect Loop

<a href="https://dev.bandwidth.com/docs/voice/quickStart">
<a href="https://dev.bandwidth.com/docs/voice/programmable-voice/quickStart/">
<img src="./icon-voice.svg" title="Voice Quick Start Guide" alt="Voice Quick Start Guide"/>
</a>

Expand All @@ -16,9 +16,9 @@
# Description
This sample app indefinitely redirects an inbound call to itself until an interruption is sent to end the call.

In the Bandwidth Dashboard, set the Application's `Call initiated callback URL` to `<BASE_CALLBACK_URL>/callbacks/inboundCall`. This can also be done via the Dashboard API by setting `CallInitiatedCallbackUrl`. Once configured, inbound calls to your `BW_NUMBER` will be redirected every 30 seconds and ring indefinitely. The `callId` of the inbound call is saved to an `active_calls` array, which can be accessed via a GET request to the `/calls` endpoint provided by the app.
In the Bandwidth Dashboard, set the Application's `Call initiated callback URL` to `<BASE_CALLBACK_URL>/callbacks/inbound/voice`. Also set the `Call status callback URL` to `<BASE_CALLBACK_URL>/callbacks/inbound/voice/disconnect` so calls that hang up on their own are removed from the active list. These can also be done via the Dashboard API by setting `CallInitiatedCallbackUrl` and `CallStatusCallbackUrl`. Once configured, inbound calls to a number associated with that application will be redirected every 30 seconds and ring indefinitely. The `callId` of the inbound call is saved to a list of active calls, which can be accessed via a GET request to the `/calls` endpoint provided by the app.

To stop a call, make a DELETE request to the `/calls/{callId}` endpoint, replacing `{callId}` with that of the call you wish to end. This endpoint will redirect the call to the `/callbacks/callEnded` endpoint, which breaks the redirect loop and speaks a sentence to let you know the call has been ended before hanging up.
To stop a call, make a DELETE request to the `/calls/{callId}` endpoint, replacing `{callId}` with that of the call you wish to end. This endpoint will redirect the call to the `/callbacks/inbound/voice/callEnded` endpoint, which breaks the redirect loop and speaks a sentence to let you know the call has been ended before hanging up.

# Pre-Requisites

Expand All @@ -45,29 +45,29 @@ ruby app.rb
# 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_VOICE_APPLICATION_ID # Your Voice Application Id created in the dashboard
LOCAL_PORT # The port number you wish to run the sample on
BASE_CALLBACK_URL # The public base url
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
BASE_CALLBACK_URL # Your public base url to receive Bandwidth Webhooks. No trailing '/'
LOCAL_PORT # The port number you wish to run the sample on
```

# Callback URLs

For a detailed introduction, check out our [Bandwidth Voice Callbacks](https://dev.bandwidth.com/docs/voice/quickStart#configuring-callback-urls) page.
For a detailed introduction, check out our [Bandwidth Voice Callbacks](https://dev.bandwidth.com/docs/voice/webhooks) page.

Below are the callback paths:
* `/callbacks/inboundCall`
* `/callbacks/callEnded`
* `/callbacks/inbound/voice`
* `/callbacks/inbound/voice/callEnded`
* `/callbacks/inbound/voice/disconnect`

## 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 (`$LOCAL_PORT`)

```cmd
```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.
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 use it as the `BASE_CALLBACK_URL` environmental variable and set it in the voice application created in the [Pre-Requisites](#pre-requisites) section.
97 changes: 56 additions & 41 deletions app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,85 @@
require 'bandwidth-sdk'

begin
BW_USERNAME = ENV.fetch('BW_USERNAME')
BW_PASSWORD = ENV.fetch('BW_PASSWORD')
BW_ACCOUNT_ID = ENV.fetch('BW_ACCOUNT_ID')
BW_VOICE_APPLICATION_ID = ENV.fetch('BW_VOICE_APPLICATION_ID')
LOCAL_PORT = ENV.fetch('LOCAL_PORT')
BW_CLIENT_ID = ENV.fetch('BW_CLIENT_ID')
BW_CLIENT_SECRET = ENV.fetch('BW_CLIENT_SECRET')
BASE_CALLBACK_URL = ENV.fetch('BASE_CALLBACK_URL')
LOCAL_PORT = Integer(ENV.fetch('LOCAL_PORT'))
rescue StandardError
puts 'Please set the environmental variables defined in the README'
exit(-1)
end

set :port, LOCAL_PORT
set :host_authorization, permitted_hosts: []

Bandwidth.configure do |config| # Configure Basic Auth
config.username = BW_USERNAME
config.password = BW_PASSWORD
Bandwidth.configure do |config|
config.client_id = BW_CLIENT_ID
config.client_secret = BW_CLIENT_SECRET
end
set :port, LOCAL_PORT

$active_calls = []
ACTIVE_CALLS = Set.new

post '/callbacks/inboundCall' do
def parse_body
data = JSON.parse(request.body.read)
halt 400 unless data.is_a?(Hash)
data
rescue JSON::ParserError
halt 400
end

$active_calls << data['callId'] if data['eventType'] == 'initiate'
post '/callbacks/inbound/voice' do
data = parse_body
halt 400 unless %w[initiate redirect].include?(data['eventType'])

if (data['eventType'] == 'initiate') || (data['eventType'] == 'redirect')
speak_sentence = Bandwidth::Bxml::SpeakSentence.new('Redirecting call, please wait.')
ring = Bandwidth::Bxml::Ring.new({ duration: 30 })
redirect = Bandwidth::Bxml::Redirect.new({ redirect_url: '/callbacks/inboundCall' })
response = Bandwidth::Bxml::Response.new([speak_sentence, ring, redirect])
ACTIVE_CALLS << data['callId'] if data['eventType'] == 'initiate'

return response.to_bxml
end
speak_sentence = Bandwidth::Bxml::SpeakSentence.new('Redirecting call, please wait.')
ring = Bandwidth::Bxml::Ring.new({ duration: 30 })
redirect = Bandwidth::Bxml::Redirect.new({ redirect_url: '/callbacks/inbound/voice' })

content_type 'application/xml'
Bandwidth::Bxml::Response.new([speak_sentence, ring, redirect]).to_bxml
end

post '/callbacks/callEnded' do
data = JSON.parse(request.body.read)
post '/callbacks/inbound/voice/callEnded' do
data = parse_body
halt 400 unless data['eventType'] == 'redirect'

if data['eventType'] == 'redirect'
speak_sentence = Bandwidth::Bxml::SpeakSentence.new('The call has been ended. Goodbye')
response = Bandwidth::Bxml::Response.new([speak_sentence])

return response.to_bxml
end
speak_sentence = Bandwidth::Bxml::SpeakSentence.new('The call has been ended. Goodbye.')
hangup = Bandwidth::Bxml::Hangup.new

content_type 'application/xml'
Bandwidth::Bxml::Response.new([speak_sentence, hangup]).to_bxml
end

post '/callbacks/inbound/voice/disconnect' do
data = parse_body
halt 400 unless data['eventType'] == 'disconnect'

ACTIVE_CALLS.delete(data['callId'])
status 204
end

get '/calls' do
content_type 'application/json'
ACTIVE_CALLS.to_a.to_json
end

delete '/calls/:call_id' do
call_id = params[:call_id]

if $active_calls.include?(call_id)
call_body = Bandwidth::UpdateCall.new({ redirect_url: "#{BASE_CALLBACK_URL}/callbacks/callEnded" })

calls_api_instance = Bandwidth::CallsApi.new
calls_api_instance.update_call(BW_ACCOUNT_ID, call_id, call_body)
$active_calls.delete(call_id)

return "call #{call_id} will be ended"
else
status 404
return 'call not found'
halt 404 unless ACTIVE_CALLS.include?(call_id)

update_call = Bandwidth::UpdateCall.new(redirect_url: "#{BASE_CALLBACK_URL}/callbacks/inbound/voice/callEnded")

begin
Bandwidth::CallsApi.new.update_call(BW_ACCOUNT_ID, call_id, update_call)
rescue StandardError => e
warn "Failed to update call: #{e.message}"
halt 500
end
end

get '/calls' do
return $active_calls.to_json
ACTIVE_CALLS.delete(call_id)
status 204
end
Loading