From 3cf49a0030127370a1c2ef9e45dc2caa9d752505 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 27 Oct 2026 16:42:36 -0400 Subject: [PATCH] DX-3359 Update Sample --- .github/CODEOWNERS | 2 +- .github/dependabot.yml | 19 ++++++++ .github/workflows/test.yml | 15 ++++++ Gemfile | 6 ++- LICENSE | 2 +- README.md | 28 +++++------ app.rb | 97 ++++++++++++++++++++++---------------- 7 files changed, 110 insertions(+), 59 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/test.yml 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 - + Voice Quick Start Guide @@ -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