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..8350171
--- /dev/null
+++ b/.github/dependabot.yml
@@ -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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..10d7c0b
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/Gemfile b/Gemfile
index 9147bd1..d4f81ee 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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'
diff --git a/LICENSE b/LICENSE
index 0bb21bd..ddc77f4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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
diff --git a/README.md b/README.md
index 82112dd..f281e6f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Voice Redirect Loop
-
+
@@ -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 `/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 `/callbacks/inbound/voice`. Also set the `Call status callback URL` to `/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
@@ -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.
diff --git a/app.rb b/app.rb
index 0053af9..f18d16b 100644
--- a/app.rb
+++ b/app.rb
@@ -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