Skip to content

Instantly share code, notes, and snippets.

@andrius
Last active October 15, 2019 19:39
Show Gist options
  • Save andrius/232bfb14c58e634f34eb501709bdd798 to your computer and use it in GitHub Desktop.
Save andrius/232bfb14c58e634f34eb501709bdd798 to your computer and use it in GitHub Desktop.
example of custom callbacks
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"
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
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