Skip to content

Commit

Permalink
Handle custom parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
dwilkie committed Mar 20, 2024
1 parent 3dc57b8 commit ffb4ae0
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 38 deletions.
88 changes: 83 additions & 5 deletions components/app/app/models/twiml/connect_verb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,88 @@

module TwiML
class ConnectVerb < TwiMLNode
class StreamNoun < TwiMLNode
class Parser < TwiML::NodeParser
VALID_NOUNS = ["Parameter"].freeze

def parse(node)
super.merge(parameters: build_parameters)
end

private

def valid?
validate_nested_nouns
super
end

def validate_nested_nouns
return if nested_nodes.all? { |nested_node| VALID_NOUNS.include?(nested_node.name) }

errors.add("<Stream> must only contain <Parameter> nouns")
end

def build_parameters
nested_nodes.each_with_object({}) do |nested_node, result|
parameter_noun = ParameterNoun.parse(nested_node)
result[parameter_noun.name] = parameter_noun.value
end
end
end

class << self
def parse(node)
super(node, parser: Parser.new)
end
end

attr_reader :parameters

def initialize(parameters:, **options)
super(**options)
@parameters = parameters
end

def url
attributes.fetch("url")
end
end

class ParameterNoun < TwiMLNode
class Parser < TwiML::NodeParser
private

def valid?
validate_attributes
super
end

def validate_attributes
return if noun.attributes["name"].present? && noun.attributes["value"].present?

errors.add("<Parameter> must have a 'name' and 'value' attribute")
end

def noun
@noun ||= TwiMLNode.parse(node)
end
end

class << self
def parse(node)
super(node, parser: Parser.new)
end
end

def name
attributes.fetch("name")
end

def value
attributes.fetch("value")
end
end

class Parser < TwiML::NodeParser
VALID_NOUNS = ["Stream"].freeze

Expand Down Expand Up @@ -37,12 +119,8 @@ def url_scheme(url)
URI(url.to_s).scheme
end

def nested_nodes
node.children
end

def stream_noun
@stream_noun ||= TwiMLNode.parse(nested_nodes.first)
@stream_noun ||= StreamNoun.parse(nested_nodes.first)
end
end

Expand Down
4 changes: 0 additions & 4 deletions components/app/app/models/twiml/dial_verb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ def validate_nested_nouns
invalid_node = nested_nodes.find { |v| VALID_NOUNS.exclude?(v.name) }
errors.add("<#{invalid_node.name}> is not allowed within <Dial>")
end

def nested_nodes
node.children
end
end

class << self
Expand Down
4 changes: 0 additions & 4 deletions components/app/app/models/twiml/gather_verb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ def validate_nested_verbs
invalid_node = nested_nodes.find { |v| VALID_NESTED_VERBS.exclude?(v.name) }
errors.add("<#{invalid_node.name}> is not allowed within <Gather>")
end

def nested_nodes
node.children
end
end

class << self
Expand Down
4 changes: 4 additions & 0 deletions components/app/app/models/twiml/node_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ def parse(node)
def valid?
errors.empty?
end

def nested_nodes
node.children.reject(&:comment?)
end
end
end
16 changes: 9 additions & 7 deletions components/app/app/workflows/execute_connect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@ class ExecuteConnect < ExecuteTwiMLVerb
def call
answer!

url = verb.stream_noun.attributes.fetch("url")
response = create_audio_stream(url:)
component = build_component(stream_sid: response.id, url:)
url = verb.stream_noun.url
custom_parameters = verb.stream_noun.parameters
response = create_audio_stream(url:, custom_parameters:)
component = build_component(stream_sid: response.id, url:, custom_parameters:)
context.execute_component_and_await_completion(component)
end

private

def create_audio_stream(url:)
call_platform_client.create_audio_stream(phone_call_id: call_properties.call_sid, url:)
def create_audio_stream(**params)
call_platform_client.create_audio_stream(phone_call_id: call_properties.call_sid, **params)
end

def build_component(url:, stream_sid:)
def build_component(url:, stream_sid:, custom_parameters:)
Rayo::Component::TwilioStream::Start.new(
uuid: phone_call.id,
url:,
metadata: {
call_sid: call_properties.call_sid,
account_sid: call_properties.account_sid,
stream_sid:
stream_sid:,
custom_parameters:
}.to_json
)
end
Expand Down
38 changes: 35 additions & 3 deletions components/app/spec/call_controllers/connect_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@

it "connects to a websockets stream" do
controller = build_controller(
stub_voice_commands: :play_audio,
call_properties: {
call_sid: "6f362591-ab86-4d1a-b39b-40c87e7929fc"
}
Expand All @@ -45,7 +44,6 @@
<Connect>
<Stream url="wss://mystream.ngrok.io/audiostream" />
</Connect>
<Play>http://api.twilio.com/cowbell.mp3</Play>
</Response>
TWIML

Expand All @@ -61,7 +59,41 @@
"stream_sid" => be_present
)
end
expect(controller).not_to have_received(:play_audio)
end

it "handles custom parameters" do
controller = build_controller(
stub_voice_commands: :play_audio,
call_properties: {
call_sid: "6f362591-ab86-4d1a-b39b-40c87e7929fc"
}
)
allow(controller).to receive(:execute_component_and_await_completion).and_raise(Adhearsion::Call::Hangup)

stub_twiml_request(controller, response: <<~TWIML)
<Response>
<Play>https://api.twilio.com/cowbell.mp3</Play>
<Connect>
<Stream url="wss://mystream.ngrok.io/audiostream">
<Parameter name="aCustomParameter" value="aCustomValue that was set in TwiML" />
<Parameter name="bCustomParameter" value="bCustomValue that was set in TwiML" />
</Stream>
</Connect>
</Response>
TWIML

expect { controller.run }.to raise_error(Adhearsion::Call::Hangup)

expect(controller).to have_received(:play_audio).with("https://api.twilio.com/cowbell.mp3")
expect(controller).to have_received(:execute_component_and_await_completion) do |component|
metadata = JSON.parse(component.metadata)
expect(metadata).to include(
"custom_parameters" => {
"aCustomParameter" => "aCustomValue that was set in TwiML",
"bCustomParameter" => "bCustomValue that was set in TwiML"
}
)
end
end
end
end
Expand Down
18 changes: 9 additions & 9 deletions components/app/spec/fixtures/vcr_cassettes/audio_stream.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 65 additions & 6 deletions components/app/spec/models/twiml/connect_verb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,91 @@
module TwiML
RSpec.describe ConnectVerb do
describe ".parse" do
it "handles invalid nested nouns" do
it "parses a <Connect> verb" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Room>DailyStandup</Room>
<!-- This is a comment -->
<Stream url="wss://mystream.ngrok.io/audiostream" />
</Connect>
</Response>
TWIML
connect_node = TwiMLDocument.new(twiml).twiml.first

expect { ConnectVerb.parse(connect_node) }.to raise_error(::Errors::TwiMLError, "<Connect> must contain exactly one of the following nouns: <Stream>")
result = ConnectVerb.parse(connect_node)

expect(result.stream_noun.url).to eq("wss://mystream.ngrok.io/audiostream")
end

it "handles invalid stream URLs" do
it "handles nested <Stream> <Parameter> nouns" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="https://mystream.ngrok.io/audiostream" />
<Stream url="wss://mystream.ngrok.io/audiostream">
<Parameter name="aCustomParameter" value="aCustomValue that was set in TwiML" />
<Parameter name="bCustomParameter" value="bCustomValue that was set in TwiML" />
<!-- Parameter name="bCustomParameter" value="bCustomValue that was set in TwiML" /-->
</Stream>
</Connect>
</Response>
TWIML
connect_node = TwiMLDocument.new(twiml).twiml.first

expect { ConnectVerb.parse(connect_node) }.to raise_error(::Errors::TwiMLError, "<Stream> must contain a valid wss 'url' attribute")
result = ConnectVerb.parse(connect_node)

expect(result.stream_noun.parameters).to eq(
"aCustomParameter" => "aCustomValue that was set in TwiML",
"bCustomParameter" => "bCustomValue that was set in TwiML"
)
end

it "handles invalid nested <Stream> nouns" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://mystream.ngrok.io/audiostream">
<Parameter name="aCustomParameter" value="aCustomValue that was set in TwiML" />
<Foobar name="bCustomParameter" value="bCustomValue that was set in TwiML" />
</Stream>
</Connect>
</Response>
TWIML
connect_node = TwiMLDocument.new(twiml).twiml.first

expect { ConnectVerb.parse(connect_node) }.to raise_error(::Errors::TwiMLError, "<Stream> must only contain <Parameter> nouns")
end

it "handles invalid <Parameter> nouns" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://mystream.ngrok.io/audiostream">
<Parameter foobar="aCustomParameter" value="aCustomValue that was set in TwiML" />
</Stream>
</Connect>
</Response>
TWIML
connect_node = TwiMLDocument.new(twiml).twiml.first

expect { ConnectVerb.parse(connect_node) }.to raise_error(::Errors::TwiMLError, "<Parameter> must have a 'name' and 'value' attribute")
end

it "handles invalid nested nouns" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Room>DailyStandup</Room>
</Connect>
</Response>
TWIML
connect_node = TwiMLDocument.new(twiml).twiml.first

expect { ConnectVerb.parse(connect_node) }.to raise_error(::Errors::TwiMLError, "<Connect> must contain exactly one of the following nouns: <Stream>")
end
end
end
Expand Down
20 changes: 20 additions & 0 deletions components/app/spec/models/twiml/dial_verb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
module TwiML
RSpec.describe DialVerb do
describe ".parse" do
it "parses a <Dial> verb" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<Dial>
<Number>85516701721</Number>
<!-- Number>855715100860</Number -->
</Dial>
</Response>
TWIML
dial_node = TwiMLDocument.new(twiml).twiml.first

result = DialVerb.parse(dial_node)

expect(result.nested_nouns.size).to eq(1)
expect(result.nested_nouns.first).to have_attributes(
content: "85516701721"
)
end

it "handles invalid nested verbs" do
twiml = <<~TWIML
<?xml version="1.0" encoding="UTF-8" ?>
Expand Down
Loading

0 comments on commit ffb4ae0

Please sign in to comment.