-
-
Save andrius/232bfb14c58e634f34eb501709bdd798 to your computer and use it in GitHub Desktop.
example of custom callbacks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Voiceapp | |
# Methods shared by outbound actions (`Dial` and `Originate_`) | |
module Outbound | |
# Destination indlude data for outbound call: endpoint and trunk details. | |
# In ML spec it could be defined as follows: | |
# ``` | |
# { | |
# :destination => { | |
# :endpoint => { | |
# :type => "string", | |
# :description => "phone number or named resource to dial on trunk", | |
# }, | |
# :trunk => { | |
# :type => "object", | |
# :description => "trunk IP address or hostname reachable by kamailio and it's internal ID within Unic for tracking", | |
# :properties => { | |
# :id => {:type => "string", | |
# :description => "Trunk ID", | |
# }, | |
# :address => {:type => "string", | |
# :description => "IP address or hostname of the trunk", | |
# }, | |
# }, | |
# }, | |
# }, | |
# } | |
# ``` | |
struct Destination | |
include JSON::Serializable | |
property endpoint : String | |
property trunk : Trunk | |
end | |
struct Trunk | |
include JSON::Serializable | |
property id : String | |
property address : String | |
end | |
# build dial endpoint based on source data from the ML action | |
def endpoint : String | |
if sip_endpoint | |
sip_endpoint.not_nil! | |
else | |
dest = destination.not_nil! | |
"SIP/#{dest.endpoint}@#{dest.trunk.address}" | |
end | |
end | |
# dial observers | |
private getter channel_events_name : String = UUID.random.to_s | |
private getter other_channel_events_name : String = UUID.random.to_s | |
private getter channel_state_event_name : String = UUID.random.to_s | |
private getter dial_status_event_name : String = UUID.random.to_s | |
private getter left_bridge_event_name : String = UUID.random.to_s | |
private getter other_left_bridge_event_name : String = UUID.random.to_s | |
# originate observers | |
private getter originated_channel_events_name : String = UUID.random.to_s | |
private getter originated_channel_destroyed_event_name : String = UUID.random.to_s | |
private getter originated_channel_state_event_name : String = UUID.random.to_s | |
private def remove_handlers! | |
[ channel_events_name, | |
other_channel_events_name, | |
channel_state_event_name, | |
dial_status_event_name, | |
dial_status_event_name, | |
left_bridge_event_name, | |
other_left_bridge_event_name, | |
originated_channel_events_name, | |
originated_channel_destroyed_event_name, | |
originated_channel_state_event_name | |
].each { |cb| ari.remove_handler cb } | |
end | |
# only for debugging: catch all the events with `call.id` | |
private def channel_events(channel_id) | |
JSON.parse(%({"channel": {"id": "#{channel_id}"}})) | |
end | |
end | |
end | |
require "./outbound_dial.cr" | |
require "./outbound_originate.cr" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Voiceapp | |
# It dial destination: either `sip_endpoint` or `destination` | |
class Dial < ML::Action | |
include Voice | |
include Outbound | |
ml_spec( | |
call_id: String, | |
# sip_endpoint OR destination is required | |
sip_endpoint: {type: String, nilable: true}, | |
destination: {type: Outbound::Destination, nilable: true}, | |
# dial duration | |
dial_timeout: {type: Int32, default: 32} | |
) | |
# create dial channel | |
@other_call : Asterisk::ARI::Channels::Channel? = nil | |
def other_call | |
@other_call ||= ari.channels.create( | |
originator: call.id, | |
endpoint: endpoint, | |
app: config.asterisk_ari_appname, | |
app_args: { role: "follower", | |
action: "dial", | |
call_type: "Outbound", | |
parent_action: "dial", | |
operation: "hold", | |
leader_call_id: call.id, | |
service: service, | |
media_server_id: media_server_id, | |
action_id: action_id, | |
workflow_id: workflow_id, | |
tenant_id: tenant_id | |
}.to_json | |
).as(Asterisk::ARI::Channels::Channel) | |
end | |
def other_call! | |
other_call | |
# if endpoint is built based on destination, trunk information should be | |
# added to the SIP headers | |
if destination | |
dest = destination.not_nil! | |
# add X-trunk-id | |
ari.channels.set_channel_var channel_id: other_call.id, | |
variable: "SIPADDHEADER01", | |
value: "X-trunk-id: #{dest.trunk.id}" | |
end | |
# logger.debug "#{self.class}: dialed channel, aka 'other_call' was created #{other_call.pretty_inspect}" | |
other_call | |
end | |
def run! | |
logger.debug "#{self.class} dialing to the endpoint (destination) #{endpoint}" | |
other_call! | |
if logger.debug? | |
ari.on channel_events_name, channel_events(channel_id: call.id) do |event_json| | |
event_json = JSON.parse(event_json) | |
event_name = event_json["type"].as_s | |
call_id = event_json["channel"]["id"].as_s | |
channel_state = event_json["channel"]["state"].as_s | |
logger.debug "Event '#{event_name}' on leader call (#{call_id}). channel_state: #{channel_state}" | |
end | |
ari.on other_channel_events_name, channel_events(channel_id: other_call.id) do |event_json| | |
event_json = JSON.parse(event_json) | |
event_name = event_json["type"].as_s | |
call_id = event_json["channel"]["id"].as_s | |
channel_state = event_json["channel"]["state"].as_s | |
# logger.debug "Event '#{event_name}' on 'other_call': #{event_json.inspect}" | |
logger.debug "Event '#{event_name}' on 'other_call' (#{call_id}). channel_state: #{channel_state}" | |
end | |
end | |
ari.bridges.add_channel bridge_id: call.bridge.id, channel: call.id | |
ari.bridges.add_channel bridge_id: call.bridge.id, channel: other_call.id | |
dial_state = Channel(String).new | |
# After follower channel get bridged and answered, dial action is completed | |
ari.on dial_status_event_name, dial_status_event do |event_json| | |
event = Asterisk::ARI::Events::ChannelStateChange.from_json(event_json) | |
logger.debug "#{self.class}: ChannelStateChange on 'other_call', state: #{event.channel.state}" | |
remove_handlers! | |
dial_state.send "Connected" | |
end | |
# After follower channel get left bridge, dial action is completed | |
ari.on other_left_bridge_event_name, other_left_bridge_event do |event_json| | |
event = Asterisk::ARI::Events::ChannelLeftBridge.from_json(event_json) | |
logger.debug "#{self.class}: ChannelLeftBridge event on 'other_call'" | |
remove_handlers! | |
dial_state.send "RemoteHangup" | |
end | |
# We should terminate handlers and other_call if "this" channel | |
# (leader) will gone | |
ari.on channel_state_event_name, channel_state_event do |event_json| | |
event = Asterisk::ARI::Events::ChannelStateChange.from_json(event_json) | |
# logger.debug "#{self.class}: ChannelStateChange\n#{event.pretty_inspect}" | |
logger.debug "#{self.class}: ChannelStateChange state: #{event.channel.state}" | |
end | |
# After leader channel get left bridge, dial action is completed | |
ari.on left_bridge_event_name, left_bridge_event do |event_json| | |
event = Asterisk::ARI::Events::ChannelLeftBridge.from_json(event_json) | |
logger.debug "#{self.class}: ChannelLeftBridge event" | |
remove_handlers! | |
ari.channels.hangup channel_id: other_call.id | |
dial_state.send "Hangup" | |
end | |
# dial | |
ari.channels.dial channel_id: other_call.id, | |
caller: "TEST", | |
timeout: dial_timeout | |
logger.debug "#{self.class}: dial initiated, waiting for ChannelStateChange event for 'other_call'" | |
dial_status = dial_state.receive | |
logger.debug "#{self.class}: received dial_status: #{dial_status}" | |
ML::Response.new status: "completed", | |
bridge_id: call.bridge.id, | |
other_call_id: other_call.id, | |
dial_status: dial_status | |
ensure | |
remove_handlers! | |
end | |
private def channel_state_event | |
JSON.parse(%({"type": "ChannelStateChange", "channel": {"id": "#{call.id}"}})) | |
end | |
private def dial_status_event | |
JSON.parse(%({"type": "ChannelStateChange", "channel": {"id": "#{other_call.id}"}})) | |
end | |
private def left_bridge_event | |
JSON.parse(%({"type": "ChannelLeftBridge", "channel": {"id": "#{call.id}"}})) | |
end | |
private def other_left_bridge_event | |
JSON.parse(%({"type": "ChannelLeftBridge", "channel": {"id": "#{other_call.id}"}})) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "./spec_helper" | |
describe Voiceapp do | |
it "should start and terminate dial due to leader hangup while ringing" do | |
# dialed destination | |
destination = "answer" | |
with_ari do |ami, ari| | |
# logger.level = Logger::DEBUG | |
bindaddress = get_bindaddress() | |
channel = Channel(String).new | |
channel_id = "" | |
# dial_channel = Channel(String).new | |
ari.on_stasis_start do |event| | |
call = Voiceapp::Call.new(event.channel) | |
if call.call_type.inbound? | |
logger.debug "Leader entered into stasis" | |
channel.send call.id | |
spawn do | |
sleep 0.2 | |
ari.channels.hangup channel_id: call.id | |
end | |
elsif call.app_data["parent_action"]? == "dial" && call.app_data["role"]? == "follower" | |
logger.debug "Follower entered into stasis" | |
# ari.channels.answer channel_id: call.id | |
# dial_channel.send call.id | |
end | |
end | |
trigger_stasis_call(application: "Wait", app_data: "90") | |
# receive channel id (from StasisStart) | |
channel_id = channel.receive | |
# action 'dial' | |
action_json = %({ | |
"action": "dial", | |
"action_id": "action:#{UUID.random.to_s}", | |
"service": "unic.service.asterisk", | |
"workflow_id": "workflow:#{UUID.random.to_s}", | |
"tenant_id": "#{UUID.random.to_s}", | |
"call_id": "#{channel_id}", | |
"sip_endpoint": "SIP/c972d4cf257a4ae1874516e8c68af9a1-#{destination}@#{bindaddress}" | |
}) | |
response = ML::Action.run(action_json) | |
logger.debug "Response received:\n#{response.pretty_inspect}" | |
response.status.should eq("completed") | |
Voiceapp::Call.destroy(channel_id) | |
end | |
end | |
############################################################################## | |
it "should start and terminate dial due to call reject or congestion at remotre party" do | |
# dialed destination | |
destination = "hangup" | |
with_ari do |ami, ari| | |
# logger.level = Logger::DEBUG | |
bindaddress = get_bindaddress() | |
channel = Channel(String).new | |
# dial_channel = Channel(String).new | |
ari.on_stasis_start do |event| | |
call = Voiceapp::Call.new(event.channel) | |
if call.call_type.inbound? | |
logger.debug "Leader entered into stasis" | |
channel.send call.id | |
elsif call.app_data["parent_action"]? == "dial" && call.app_data["role"]? == "follower" | |
logger.debug "Follower entered into stasis" | |
# dial_channel.send call.id | |
end | |
end | |
trigger_stasis_call(application: "Wait", app_data: "90") | |
# receive channel id (from StasisStart) | |
channel_id = channel.receive | |
# action 'dial' | |
action_json = %({ | |
"action": "dial", | |
"action_id": "action:#{UUID.random.to_s}", | |
"service": "unic.service.asterisk", | |
"workflow_id": "workflow:#{UUID.random.to_s}", | |
"tenant_id": "#{UUID.random.to_s}", | |
"call_id": "#{channel_id}", | |
"sip_endpoint": "SIP/c972d4cf257a4ae1874516e8c68af9a1-#{destination}@#{bindaddress}" | |
}) | |
response = ML::Action.run(action_json) | |
logger.debug "Response received:\n#{response.pretty_inspect}" | |
response.status.should eq("completed") | |
Voiceapp::Call.destroy(channel_id) | |
end | |
end | |
############################################################################## | |
it "should dial destination and get it connected" do | |
# dialed destination | |
destination = "answer" | |
with_ari do |ami, ari| | |
logger.level = Logger::DEBUG | |
bindaddress = get_bindaddress() | |
channel = Channel(String).new | |
channel_id = "" | |
# dial_channel = Channel(String).new | |
ari.on_stasis_start do |event| | |
call = Voiceapp::Call.new(event.channel) | |
if call.call_type.inbound? | |
logger.debug "Leader entered into stasis" | |
channel.send call.id | |
elsif call.app_data["parent_action"]? == "dial" && call.app_data["role"]? == "follower" | |
logger.debug "Follower entered into stasis" | |
# dial_channel.send call.id | |
end | |
end | |
trigger_stasis_call(application: "Wait", app_data: "90") | |
# receive channel id (from StasisStart) | |
channel_id = channel.receive | |
# action 'dial' | |
action_json = %({ | |
"action": "dial", | |
"action_id": "action:#{UUID.random.to_s}", | |
"service": "unic.service.asterisk", | |
"workflow_id": "workflow:#{UUID.random.to_s}", | |
"tenant_id": "#{UUID.random.to_s}", | |
"call_id": "#{channel_id}", | |
"sip_endpoint": "SIP/c972d4cf257a4ae1874516e8c68af9a1-#{destination}@#{bindaddress}" | |
}) | |
response = ML::Action.run(action_json) | |
logger.debug "Response received:\n#{response.pretty_inspect}" | |
response.status.should eq("completed") | |
response["dial_status"].should eq("Connected") | |
sleep 0.02 | |
Voiceapp::Call.destroy(channel_id) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment