From 74e4ed36e501090436e651c203874f070c548de8 Mon Sep 17 00:00:00 2001 From: David Wilkie Date: Tue, 30 Apr 2024 21:46:23 +0700 Subject: [PATCH] Support updating live calls --- components/app/app/jobs/update_call_job.rb | 7 ++ components/app/app/models/outbound_call.rb | 22 +---- components/app/app/web/api.rb | 12 +++ .../app/workflows/bulid_call_properties.rb | 34 +++++++ .../update_call_with_new_url.yml | 58 ++++++++++++ components/app/spec/web/api_spec.rb | 90 +++++++++++++------ 6 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 components/app/app/jobs/update_call_job.rb create mode 100644 components/app/app/workflows/bulid_call_properties.rb create mode 100644 components/app/spec/fixtures/vcr_cassettes/update_call_with_new_url.yml diff --git a/components/app/app/jobs/update_call_job.rb b/components/app/app/jobs/update_call_job.rb new file mode 100644 index 000000000..4bb3b6e60 --- /dev/null +++ b/components/app/app/jobs/update_call_job.rb @@ -0,0 +1,7 @@ +class UpdateCallJob + include SuckerPunch::Job + + def perform(call_controller) + call_controller.run + end +end diff --git a/components/app/app/models/outbound_call.rb b/components/app/app/models/outbound_call.rb index f378c003e..dde555bdf 100644 --- a/components/app/app/models/outbound_call.rb +++ b/components/app/app/models/outbound_call.rb @@ -6,32 +6,16 @@ def initialize(call_params) end def initiate - sip_headers = SIPHeaders.new( - call_sid: call_params.fetch("sid"), - account_sid: call_params.fetch("account_sid") - ) - dial_string = DialString.new(call_params.fetch("routing_parameters")) + call_properties = BuildCallProperties.call(call_params) + sip_headers = call_properties.sip_headers Adhearsion::OutboundCall.originate( dial_string.to_s, from: dial_string.format_number(call_params.fetch("from")), controller: CallController, controller_metadata: { - call_properties: CallProperties.new( - voice_url: call_params.fetch("voice_url"), - voice_method: call_params.fetch("voice_method"), - twiml: call_params["twiml"], - account_sid: call_params.fetch("account_sid"), - auth_token: call_params.fetch("account_auth_token"), - call_sid: call_params.fetch("sid"), - direction: call_params.fetch("direction"), - api_version: call_params.fetch("api_version"), - from: call_params.fetch("from"), - to: call_params.fetch("to"), - default_tts_voice: call_params.fetch("default_tts_voice"), - sip_headers: - ) + call_properties: }, headers: build_call_headers(sip_headers) ) diff --git a/components/app/app/web/api.rb b/components/app/app/web/api.rb index 7851a343b..58458ac96 100644 --- a/components/app/app/web/api.rb +++ b/components/app/app/web/api.rb @@ -19,6 +19,18 @@ class API < Application status 204 end + + patch "/calls/:id" do + call = Adhearsion.active_calls[params[:id]] + return 404 if call.blank? + + call_params = JSON.parse(request.body.read) + call_properties = BuildCallProperties.call(call_params) + + UpdateCallJob.perform_async(CallController.new(call, call_properties:)) + + status 204 + end end end end diff --git a/components/app/app/workflows/bulid_call_properties.rb b/components/app/app/workflows/bulid_call_properties.rb new file mode 100644 index 000000000..75e933077 --- /dev/null +++ b/components/app/app/workflows/bulid_call_properties.rb @@ -0,0 +1,34 @@ +class BuildCallProperties < ApplicationWorkflow + attr_reader :call_params + + def initialize(call_params) + super() + @call_params = call_params + end + + def call + CallProperties.new( + voice_url: call_params.fetch("voice_url"), + voice_method: call_params.fetch("voice_method"), + twiml: call_params["twiml"], + account_sid: call_params.fetch("account_sid"), + auth_token: call_params.fetch("account_auth_token"), + call_sid: call_params.fetch("sid"), + direction: call_params.fetch("direction"), + api_version: call_params.fetch("api_version"), + from: call_params.fetch("from"), + to: call_params.fetch("to"), + default_tts_voice: call_params.fetch("default_tts_voice"), + sip_headers: + ) + end + + private + + def sip_headers + @sip_headers ||= SIPHeaders.new( + call_sid: call_params.fetch("sid"), + account_sid: call_params.fetch("account_sid") + ) + end +end diff --git a/components/app/spec/fixtures/vcr_cassettes/update_call_with_new_url.yml b/components/app/spec/fixtures/vcr_cassettes/update_call_with_new_url.yml new file mode 100644 index 000000000..7e491e940 --- /dev/null +++ b/components/app/spec/fixtures/vcr_cassettes/update_call_with_new_url.yml @@ -0,0 +1,58 @@ +--- +http_interactions: +- request: + method: get + uri: https://demo.twilio.com/docs/voice.xml?AccountSid=sample-account-sid&ApiVersion=2010-04-01&CallSid=sample-call-sid&CallStatus=ringing&Called=%2B85516701721&Caller=2442&Direction=outbound-api&From=2442&To=%2B85516701721 + body: + encoding: ASCII-8BIT + string: '' + headers: + Content-Type: + - application/x-www-form-urlencoded; charset=utf-8 + User-Agent: + - TwilioProxy/1.1 + Accept: + - text/xml, application/xml, text/html + Cache-Control: + - max-age=259200 + X-Twilio-Signature: + - y0Cp1nrrvfLm25MA54H20uizaak= + Connection: + - close + Host: + - demo.twilio.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 30 Apr 2024 14:44:54 GMT + Content-Type: + - text/xml + Content-Length: + - '192' + Connection: + - close + Server: + - nginx + Last-Modified: + - Wed, 06 Dec 2023 17:14:12 GMT + Vary: + - Accept-Encoding + Etag: + - '"6570abe4-c0"' + X-Shenanigans: + - none + Accept-Ranges: + - bytes + body: + encoding: ASCII-8BIT + string: | + + + Thanks for trying our documentation. Enjoy! + http://demo.twilio.com/docs/classic.mp3 + + recorded_at: Tue, 30 Apr 2024 14:44:54 GMT +recorded_with: VCR 6.2.0 diff --git a/components/app/spec/web/api_spec.rb b/components/app/spec/web/api_spec.rb index 0c82e578c..fb630290b 100644 --- a/components/app/spec/web/api_spec.rb +++ b/components/app/spec/web/api_spec.rb @@ -8,63 +8,44 @@ module Web it "requires basic authentication" do basic_authorize "username", "wrong-password" - post "/calls" + post("/calls") expect(last_response.status).to eq(401) end describe "POST /calls" do it "initiates an outbound call" do - outbound_call = instance_double(Adhearsion::OutboundCall, id: "123456") + call_id = SecureRandom.uuid + outbound_call = instance_double(Adhearsion::OutboundCall, id: call_id) allow(Adhearsion::OutboundCall).to receive(:originate).and_return(outbound_call) basic_authorize "adhearsion", "password" post( "/calls", - { - "to" => "+85516701721", - "from" => "2442", - "voice_url" => "https://rapidpro.ngrok.com/handle/33/", - "voice_method" => "GET", - "status_callback_url" => "https://rapidpro.ngrok.com/handle/33/", - "status_callback_method" => "POST", - "sid" => "sample-call-sid", - "account_sid" => "sample-account-sid", - "account_auth_token" => "sample-auth-token", - "direction" => "outbound-api", - "api_version" => "2010-04-01", - "default_tts_voice" => "Basic.Kal", - "routing_parameters" => { - "destination" => "85516701721", - "dial_string_prefix" => nil, - "plus_prefix" => false, - "national_dialing" => false, - "host" => "27.109.112.141", - "username" => nil, - "symmetric_latching" => true - } - }.to_json, + build_call_properties.to_json, { "CONTENT_TYPE" => "application/json" } ) expect(last_response.status).to eq(200) - expect(json_response["id"]).to eq("123456") + expect(json_response["id"]).to eq(call_id) end end describe "DELETE /calls/:id" do it "ends a call" do call = Adhearsion::Call.new - allow(call).to receive(:id).and_return("123456") + call_id = SecureRandom.uuid + allow(call).to receive(:id).and_return(call_id) allow(call).to receive(:hangup) Adhearsion.active_calls << call basic_authorize "adhearsion", "password" delete( - "/calls/123456", + "/calls/#{call_id}", + build_call_properties.to_json, { "CONTENT_TYPE" => "application/json" } @@ -74,6 +55,59 @@ module Web expect(call).to have_received(:hangup) end end + + describe "PATCH /calls/:id" do + it "Redirect an in progress Call to a new URL", :vcr, cassette: :update_call_with_new_url do + call = Adhearsion::Call.new + call_id = SecureRandom.uuid + allow(call).to receive(:id).and_return(call_id) + allow(call).to receive(:hangup) + Adhearsion.active_calls << call + + basic_authorize "adhearsion", "password" + + patch( + "/calls/#{call_id}", + build_call_properties( + "voice_url" => "https://demo.twilio.com/docs/voice.xml", + "voice_method" => "GET" + ).to_json, + { + "CONTENT_TYPE" => "application/json" + } + ) + + expect(last_response.status).to eq(204) + expect(call).to have_received(:hangup) + end + end + + def build_call_properties(**params) + { + "to" => "+85516701721", + "from" => "2442", + "voice_url" => "https://rapidpro.ngrok.com/handle/33/", + "voice_method" => "GET", + "status_callback_url" => "https://rapidpro.ngrok.com/handle/33/", + "status_callback_method" => "POST", + "sid" => "sample-call-sid", + "account_sid" => "sample-account-sid", + "account_auth_token" => "sample-auth-token", + "direction" => "outbound-api", + "api_version" => "2010-04-01", + "default_tts_voice" => "Basic.Kal", + "routing_parameters" => { + "destination" => "85516701721", + "dial_string_prefix" => nil, + "plus_prefix" => false, + "national_dialing" => false, + "host" => "27.109.112.141", + "username" => nil, + "symmetric_latching" => true + }, + **params + } + end end end end