Upload
nguyenanh
View
225
Download
5
Embed Size (px)
Citation preview
Building Your First ARI App
117,612 Calls Later
Mark Ingles Oct 14, 2015 Astricon 2015
Thank You!•
• Joshua Colp – Sr. Dev at Digium
• Nir Simionovich – Greenfield Tech phpari.org
2
About Mark Ingles• 15 years writing PHP (or 1 year 15x)
• Former Sysadmin
• Asterisk User for 8 Years
• Business Owner – Printing - like Vistaprint but for established businesses
3
Already Using ARI?
4
Overview• High Level Ideas And Common Pitfalls
• PHPARI “Basic Stasis Application Template” and Reacting to Events
• Dialing, Bridging, Recording, Sounds In A “Power Dialer” Application
5
High Level Ideas• One Process Reacts To Events • Another To Send Requests
6
Asterisk Emits Events
ARI App Receives
ARI App Processes
ARI App Sends
Requests
Send Requests
Asterisk Processes
7
ws://localhost:8088/ari/events?api_key=asterisk:asterisk&app=hello-world
Looks like you want to add a prefix or /ari in http.conf Not sure? “core set debug 2” from the Asterisk CLI:
HTTP Request URI is /ari/channels/1443501398.rep-mark match request [ari/channels/1443501398.rep-mark] with handler [httpstatus] len 10 match request [ari/channels/1443501398.rep-mark] with handler [static] len 6 match request [ari/channels/1443501398.rep-mark] with handler [ari] len 3 Match made with [ari]
Common Setup Pitfall “prefix” in http.conf
Any Created Resource Gets An ID
8
$response = $channels->originate($ENDPOINT, array( "app" => 'miDialer', "appArgs" => 'IsRep', "callerId“ => $MyCallerID, "timeout" => 10, "channelId" => $NewChannelID, ));
Asterisk can return events with this channel BEFORE it returns a response to this request.
Use A Library• PHPARI
9
All of the heavy lifting is done. • Websockets • Encryption • REST Client • JSON Encoding/Decoding • Logging • Event Handling
High Level Ideas• Dial/Originate goes to Dialplan OR ARI
10
Wiki is updated now. (Oct 2015)
• ARI/Stasis REPLACES dialplan apps
11
Channel Doesn’t Enter ARI Until Answered
12
originate($channelid);
add_to_bridge($bridgeid, $channelid);
This Does NOT work. You have to wait until the StasisStart event, then react to it and add the channel to the bridge.
#1 Pitfall
Reacting To Events
13
$this->Events->on('StasisStart', function ($event) { $channels()->channel_answer($event->channel->id);
}
$this->Events->on('ChannelEnteredBridge', function ($event) {
echo "channel: " . $event->channel->id . " entered bridge: " . $event->bridge->id);
}
Events Happen FAST – no way to get Channel Info on ChannelDestroyed or StasisEnd events
Call Detail Records No “DST” Set
14
DST is from the dialplan.
Solution: Log yourself in whatever format you need. (sql, csv)
INSERT INTO CallLog SET ID=NULL, CallStart=$callstart, ChannelID=$channelid
StasisStart or ChannelEnteredBridge UPDATE CallLog SET CallPickup=$callpickup WHERE ChannelID=$channelid
ChannelDestroyed UPDATE CallLog SET CallEnd=$callend WHERE ChannelID=$channelid
Snoop, Not Chanspy Playbacks Queue
15
Snoop creates a new channel that can listen (spy) or whisper (speak) to another channel.
Playbacks queue a sound file to be played to a channel or bridge.
Record works on all audio in a bridge.
New Sounds Aren’t Updated
16
module reload sounds
If you upload or record new sounds, this has to be run from the CLI, not the ARI.
Our Power Dialer1. Dialing from a CRM 2. Started with “Drag to Dial” 3. Moved To “Click To Dial” 4. Moved To “Dial Me When Ticket Opened”
in CRM
• Rev 1. ARI Dialer Begins Calling When Previous Call Hungup
• Rev 2. ARI Dialer Leaves Pre-Recorded Voicemail When Told By Human
17
Our Power DialerPrerequisites
• Asterisk 13+ Setup With ARI Enabled • Web Server with PHP 5.4+ • Recordings Of Voicemail Messages • MySQL Database For Logging • CRM Access via Database or CURL
18
19
20
Our Power DialerFunctionality
1. Dial Customer Service Rep 2. Call From Highest->Lowest Priority (changes) 3. If Rep Hears VM Greeting, Leave Prerecorded
VM and Dial Next User 4. Log/Record All Calls 5. Notify Rep Of What’s Happening (audio/visual)
21
Our Power DialerResults
Click To Dial (and variations) Avg 30 Calls/Hour Dialer Avg 60 Calls/Hour, Max 90 Calls/Hour
PS – No Dialplan
22
Originate (to ARI App)
How It Works 1 – Call Rep
23
$channels = new channels($phpari); # call the rep- only give the rep 10 seconds to respond $response = $channels->originate( $ENDPOINT, array( "app" => 'miDialer', "appArgs" => 'IsRep, "callerId" => $_SESSION['MyCallNum'], "timeout" => 10, "channelId" => $_SESSION['MyChannelID'], ), array('CALLERID(name)' => 'Phone Dialer') );
Check BridgesHow It Works 2 - Call A Client 1/4
24
$channels = new channels($phpari); $bridges = new bridges($phpari); # check for the rep dialed in, and nobody else in the bridge $repready = 0; $custinbridge = 0; $bd = $bridges->details('bridge-' . $_SESSION['KUsername']); if(is_array($bd)) { foreach($bd['channels'] as $bchannel) { if($bchannel == $_SESSION['MyChannelID']) { $repready = 1; } else { # someone else in bridge $custinbridge = 1; } # if my channel } # foreach channel } # is bridge
Setup VariablesHow It Works 2 - Call A Client 2/4
25
if(0 == $repready OR 1 == $custinbridge) { echo 'Need To Be Dialed In And Hungup With Last Client'; return false; } # get bridged with the rep by the stasis app #$ENDPOINT = 'PJSIP/17275608297@flowroute'; $ENDPOINT = sprintf(OUTBOUNDTECH, $PhoneNum); $callerid = $_SESSION['MyCallNum']; $calleridname = $_SESSION['CIDName']; $newchannelid = time() . '.client-' . $user . '.' . rand();
Originate (to ARI App)How It Works 2 - Call A Client 3/4
26
$response = $channels->originate( $ENDPOINT, array( "app" => 'miDialer', "appArgs " => 'IsOutBound,RepUserName-' . $_SESSION['User'], "callerId" => $callerid, "timeout" => 50, "channelId“ => $newchannelid, ), array('CALLERID(name)' => $calleridname) );
Logging / RingingHow It Works 2 - Call A Client 4/4
27
# log the call - add to call log table $sql = "INSERT INTO CallLog SET ID=NULL, CallStart=:CallStart, ChannelID=:ChannelID, PhoneNum=:PhoneNum, Rep=:Rep, TicketID=:TicketID, CIDNum=:CIDNum"; # finally, play ringing to rep $channels->channel_ringing_start($mychannelid);
SoundsHow It Works - 3 Leave A Voicemail 1/6
28
# leave a prerecorded voicemail for a product # (clipped) get ticket to know which voicemail $sounds = new sounds($phpari); $soundfile = $_SESSION['User'] . '-' . $vmtype; # make sure sound is actually there if(!$sounds->detail($soundfile)) { echo 'Cant Find Voicemail to Play'; return false; }
Stop RecordingHow It Works - 3 Leave A Voicemail 2/6
29
$channels = new channels($phpari); $bridges = new bridges($phpari); $bridgeid = 'bridge-' . $_SESSION['KUsername']; $bdetails = $bridges->details($bridgeid); foreach($bdetails['channels'] as $bchannel) { if($bchannel != $_SESSION['MyChannelID']) { # this must be the cust's channel, since the bridge should only have 2 channels, the rep, and the cust $VMChannelID = $bchannel; # stop recording the rep's bridge, create a new bridge to record both sides of leaving the VM $recordings = new recordings($phpari); $recordings->live(‘stop‘, $VMChannelID); # remove the channel from the rep’s bridge $bridges->remove_channel($bridgeid, $VMChannelID);
TALK_DETECT()How It Works - 3 Leave A Voicemail 3/6
30
$bridges->create( 'mixing', 'bridge-' . $VMChannelID, 'bridge-' . $VMChannelID );
$bridges->addchannel('bridge-' . $VMChannelID, $VMChannelID, ''); # set a variable on the channel of the sound to play $channels->setVariable($VMChannelID, 'miVMtoPlay', $soundfile);
# wait for 2.1 seconds of silence $channels->setVariable($VMChannelID, 'TALK_DETECT(set)', '2100');
ChannelTalkingFinished
How It Works - 3 Leave A Voicemail 4/6
31
# now that talking has stopped, play a voicemail $vmsound = $channels()->getVariable($event->channel->id, 'miVMtoPlay');if($vmsound) { # play it to the bridge so we can record it $bd = $bridges()->details('bridge-' . $event->channel->id); if(FALSE != $bd) { # check to see if we are already playing it sometimes long pauses in greetings will start playback $pbd = $playbacks()->get_playback('miVMtoPlay' . $event->channel->id); if(FALSE != $pbd) { # already playing, restart it- TODO: possibly put a limit on this $playbacks()->control(
'miVMtoPlay' . $event->channel->id, ‘RESTART');
} }
ChannelTalkingFinished
How It Works - 3 Leave A Voicemail 5/6
32
# queue the sound to play on the bridge
$bridges()->playbackstart( 'bridge-' . $event->channel->id, 'sound:custom/' . $vmsound['value'], 'custom', 0, 1000, 'miVMtoPlay' . $event->channel->id);
We need the ID on the Sound so we know what to do when it’s over.
PlaybackFinished
How It Works - 3 Leave A Voicemail 6/6
33
# once we leave the message, hangup the call if('miDialer' == $event->application AND strpos($event->playback->id, 'miVMtoPlay') !== FALSE) { $delchannelid = preg_replace('/bridge:bridge-(.*)/', '$1', $event->playback->target_uri);
# hang up customer channel $channels()->delete($delchannelid); # bridge with the same name as the channel recorded- dump that too $recordings()->live('stop', $delchannelid); $bridges()->delete('bridge-' . $delchannelid);}
ChannelHangupRequest
End Recording When Hungup
34
# The channel is already gone by the time we see the request $repname = preg_replace('/\d+\.rep-(\w+?)\.\d+/', '$1', $event->channel->id);$bd = $bridges()->details('bridge-' . $repname);if(is_array($bd)) { foreach($bd['channels'] as $bchannel) { # hangup anyone else in the bridge- the rep is already gone $this->phpariObject->channels()->channel_delete($bchannel); # stop and store recording $this->phpariObject->recordings()->live(‘stop‘, $bchannel); } # foreach channel } # bridge
ChannelLeftBridge
Audio Notifications
35
# Play a goodbye sound when the client hangs up This is a good place to catch a hangup because we know the channel id and the bridge it left. The channel is probably already destroyed, we don’t have much to work with. If the channel left a client "vm" bridge, we dont need to playbridge-1424374586.client-jessicam.33525 <- sample vm bridge id
if(stripos($event->channel->id, '.CLIENT-') !== FALSE AND stripos($event->bridge->id, '.CLIENT-') === FALSE) { $channels()->channel_playback(
$event->bridge->channels[0], 'sound:goodbye', NULL, NULL, NULL, 'goodbye');
} });
StasisStartCall Comes Into ARI App (Rep or Client)
36
# answer the channel $channels()->answer($event->channel->id);# create the bridge this rep will use to talk to their customers foreach($event->args as $arg) { if(preg_match('/^RepUserName-(.*)$/', $arg, $m)) { $bridgeid = 'bridge-' . $m[1]; }}
# this is an outbound call to a client, just add to bridge if(is_array($event->args) AND in_array('IsOutBound', $event->args)) { $bridges()->addchannel($bridgeid, $event->channel->id, '');}# this is the rep coming in, create bridge, add to bridge if(is_array($event->args) AND in_array('IsRep', $event->args)) { # can check for a bridge instead delete/recreate $bridges()->delete($bridgeid); $bridges()->create('mixing', $bridgeid, $bridgeid); $bridges()->addchannel($bridgeid, $event->channel->id, ''); }
ChannelEnteredBridge
Call Goes Into Bridge (Rep or Client)
37
# stop ringing on other channel. foreach($event->bridge->channels as $bchannel) { if($event->channel->id != $bchannel) { $channels()->channel_ringing_stop($bchannel); }
# if this is a client, start recording the bridge if(stripos($event->channel->id, '.CLIENT-') !== FALSE) {# if it's a new voicemail bridge, save with a diff name to not clobber the old one we still might not have finished recording $recordingname = $event->channel->id . 'vm'; $bridges()->record($event->bridge->id, $recordingname, ‘wav', 0, 0, ‘fail', FALSE, 'none'); # update the call log $sql = "UPDATE CallLog SET CallPickup=$callpickup
WHERE ChannelID=:ChannelID";
ChannelDestroyed
Busy or Bad Number Notification
38
# finish the call log $sql = "UPDATE CallLog SET CallEnd=:CallEnd WHERE ChannelID=:ChannelID";
# stop any ringing - only happens if the far end never picks up $repname = preg_replace('/\d+\.client-(\w+?)\.\d+/', '$1', $event->channel->id);
$channellist = $this->phpariObject->channels()->list();foreach($channellist as $channel) { # stop any ringing if we're dialing out $channels()->channel_ringing_stop($channel['id']); # if its ringing, let the rep know it hung up- TODO: more "causes" if('Ringing' == $event->channel->state OR 'Unallocated (unassigned) number' == $event->cause_txt OR 'User busy' == $event->cause_txt) { $channels()->playback($channel['id'], 'sound:number-not-answering', NULL, NULL, NULL, 'notanswering'); } else { # its connected and recording, stop recording $recordings()->live('stop', $event->channel->id); }}
Resources• Asterisk Docs Wiki • Swagger Interface http://ari.asterisk.org (set allowed_origins=http://ari.asterisk.org in ari.conf)
• #asterisk or #asterisk-ari on Freenode (only about 10 of us in ari)
• Asterisk Users Mailing List • Asterisk App Dev Mailing List • PHPARI.org / github • Astricon Talks • (Half of this year’s talks and 2014 still good)
39
40
41
Everyone here has the sense that right now is one of those moments when we are influencing the future.
Steve Jobs 1985 1970s “Bluebox" Phone Hardware Manufacturer
One More Thing