The magic of CLI

You want to turn something like this:
[bash]
chartsales_messages_es.properties
chartsales_messages.properties
closedpos_messages_es.properties
closedpos_messages.properties
closedproducts_messages_es.properties
closedproducts_messages.properties
customersdiary_messages_es.properties
customersdiary_messages.properties
customers_messages_es.properties
customers_messages.properties
inventoryb_messages_es.properties
inventoryb_messages.properties
inventorydiff_messages_es.properties
inventorydiff_messages.properties
inventory_messages_es.properties
inventory_messages.properties
people_messages_es.properties
people_messages.properties
productlabels_messages_es.properties
productlabels_messages.properties
productsales_messages_es.properties
productsales_messages.properties
productscatalog_messages_es.properties
productscatalog_messages.properties
products_messages_es.properties
products_messages.properties
taxes_messages_es.properties
taxes_messages.properties
usersales_messages_es.properties
usersales_messages.properties
[/bash]

into this:

[bash]
chartsales_messages.properties
chartsales_messages.properties
closedpos_messages.properties
closedpos_messages.properties
closedproducts_messages.properties
closedproducts_messages.properties
customersdiary_messages.properties
customersdiary_messages.properties
customers_messages.properties
customers_messages.properties
inventoryb_messages.properties
inventoryb_messages.properties
inventorydiff_messages.properties
inventorydiff_messages.properties
inventory_messages.properties
inventory_messages.properties
people_messages.properties
people_messages.properties
productlabels_messages.properties
productlabels_messages.properties
productsales_messages.properties
productsales_messages.properties
productscatalog_messages.properties
productscatalog_messages.properties
products_messages.properties
products_messages.properties
taxes_messages.properties
taxes_messages.properties
usersales_messages.properties
usersales_messages.properties
[/bash]

I can’t imagine an easy way to do it with Windows but with bash and any Unix like OS, you get it done in less than a minute.

[bash]
for line in $(ls *.properties)
do
mv $line `echo $line | sed “s/_es.properties/.properties/”`
done;
[/bash]

What I’ve been doing these days

My initial goal when I started this blog was to write at least once a week about the things I did during the week but then I realize that the time I have is not enough or there’s something wrong with the way I set my schedule.

Anyway, the following is a list of the things I’ve been working on:

  1. International numbering plan.
  2. E.164 format specification.
  3. Routing/validation algorithm for E.164 numbers.
  4. Some Asterisk’s stuff.
  5. Openbravo ERP testing.
  6. Openbravo POS code modification to fit Paraguayan business rules.

Hopefully, there will be an independent post for every item on the list since writing a blog is an extraordinary way to document my work 😀

 

Dialer: tracking calls with Asterisk/AMI

Long story short: I was asked to write a real time dialer callable from a web page that takes the number from the parameters received, make the call, and play a pre-recorded message when the called picks up. This post is not about the dialer itself, it’s about tracking the call originated by it, detect the “answer” event, detect the “hangup” event and save a CDR.

The approach I chose involves AMI to generate and track the calls. It offers a really nice interface based on a text protocol and the most important thing, the API can be mapped in almost any programming language like C# (Asterisk.NET).

Some of the protocol internals

  1. The connection is made via TCP/IP sockets using user/password authentication.
  2. Once logged in, you receive every event on the system, even if that event doesn’t belong to you. This could be a real mess because it introduces a lot of noise into the dialer and the software must support some kind of filtering mechanism.
  3. Every event comes with certain data, including a field named Uniqueid which relates the events to a transaction.
  4. The order the events were received not necessarily match the order they were generated.

The native way to generate a call is through the OriginateAction but the problem is that OriginateAction doesn’t reply with the uniqueID of the transaction, it just tells you whether the call was successfully queued or not. Then, you start receiving the events with the uniqueIDs but you have no way to know if that call belongs to you, to another dialer instance, to a human making a call, etc.

Searching the web I found what I thought was the solution: use asynchronous origination.  The usual OriginateAction is a blocking call that returns only if success or failure. With async origination the answer to the request is delivered through the OriginateResponse event and with the parameters you receive, the so wanted uniqueID. I built the dialer on top of this “fix” just to later discover that if the call fails, the uniqueID is empty. Once again, you have no way to know which call failed and for what reason.

I was so pissed off because something as basic as this wasn’t correctly available, or at least, there is no documentation to expose an standard solution. I started considering parallel approaches to abandon AMI but reading the input parameters for OriginateAction a dirty workaround came to my mind.

OriginateAction takes as input a field named CallerID, this field is always available and always filled with information. I used this field to uniquely identify the call whether it succeeded or not. In my particular case, the calls goes to a GSM network so overwriting the callerID has no effect and the call follows its normal destination.

Bellow is part of the dialer class. If you can read C#, it would explain itself.

[csharp]

public class Dialer
{
ManagerConnection _managerConnection;
Timer _timer;
public delegate void CallFinalizedCallback(Dialer sender);
string _uniqueID = string.Empty;
bool _wasOriginated = false;
object _locker = new object();

List<HangupEvent> _hangupList = new List<HangupEvent>();

CallFinalizedCallback _callFinalized;
public CallFinalizedCallback CallFinalized
{
get {
return this._callFinalized;
}
set {
_callFinalized = value;
}
}

CallInfo _callInfo;
public CallInfo CallInfo
{
get
{
return this._callInfo;
}
}

public Dialer()
{
_managerConnection = new ManagerConnection(Configuration.ManagerHost,
Configuration.ManagerPort,
Configuration.ManagerUserName,
Configuration.ManagerPassword);

//_timer = new Timer(Handle_timeoutCallback, Configuration.CallNotificationTimeout);

AddEventHandlers();
}

void Handle_managerConnectionOriginateResponse (object sender, OriginateResponseEvent e)

{
if (!e.CallerIdNum.Equals(_callInfo.CallId.ToString()))
return;

Logger.Print(string.Format(“Call to {0}-{1} was originated”, _callInfo.CallId, e.Exten));

bool succeeded = e.Response.ToLower().Equals(“success”);

if (succeeded)
UpdateDialoutQueue(CallStatus.Established);
else
UpdateDialoutQueue(CallStatus.FinishedWithError);

lock(_locker)
{
_wasOriginated = true;
_uniqueID = _callInfo.CallId.ToString();
}

foreach(HangupEvent hEvent in _hangupList)
{
if (hEvent.CallerIdNum.Equals(e.CallerIdNum)) // the call failed for some reason
{
Handle_managerConnectionHangup(this, hEvent);
break;
}
}
_hangupList.Clear();

}

public Dialer(CallInfo callInfo, CallFinalizedCallback callback) : this()
{
_callInfo = callInfo;

if (callback == null)
throw new ArgumentException(“CallFinalizedCallback cannot be null”);

_callFinalized = callback;
}

void AddEventHandlers()
{
_managerConnection.Hangup += Handle_managerConnectionHangup;
_managerConnection.OriginateResponse += Handle_managerConnectionOriginateResponse;
}

public void DisconnectFromAMI()
{
_managerConnection.Hangup -= Handle_managerConnectionHangup;
_managerConnection.OriginateResponse -= Handle_managerConnectionOriginateResponse;
}

void Handle_timeoutCallback()
{
try
{
Logger.Print(string.Format(“Call {0}-{1} forced to finalize after timeout”, _callInfo.CallId, _callInfo.Extension));

UpdateDialoutQueue(CallStatus.Timedout);
//_timer.CancelAlarm();

if (_callFinalized != null)
_callFinalized(this);

DisconnectFromAMI();
}
catch(Exception ex)
{
Logger.Error(string.Format(“Handle_timeoutCallback: {0}”, ex.Message));
DisconnectFromAMI();
}
}

void Handle_managerConnectionHangup(object sender, HangupEvent e)
{
try
{
lock(_locker)
if (!_wasOriginated || !_uniqueID.Equals(e.CallerIdNum))
{
_hangupList.Add(e); // register this event in case it was fired before the OriginateAction event
return;
}

UpdateDialoutQueue(CallStatus.Finished, e.Cause, e.CauseTxt);
//_timer.CancelAlarm();

Logger.Print(string.Format(“Call {0}-{1} was hung up”, _callInfo.CallId, e.Channel));

if (_callFinalized != null)
_callFinalized(this);

DisconnectFromAMI();
}
catch(Exception ex)
{
Logger.Error(string.Format(“Handle_managerConnectionHangup: {0}”, ex.Message));
DisconnectFromAMI();
}
}

public void Call()
{
Call(_callInfo);
}

public void Call(CallInfo callInfo)
{
try
{
OriginateAction originate = new OriginateAction();

originate.Channel = callInfo.Channel;
originate.Priority = callInfo.Priority;
originate.Context = callInfo.Context;
originate.Exten = callInfo.Extension;
originate.CallerId = callInfo.CallId.ToString(); // <——–
originate.Timeout = 500000;
originate.Async = true;

_managerConnection.Login();

MarkCallAsProcessed();

//_timer.SetAlarm();

UpdateDialoutQueue(CallStatus.Trying);
ManagerResponse r = _managerConnection.SendAction(originate, originate.Timeout);
}
catch(Exception ex)
{
Logger.Error(string.Format(“Call: {0}”, ex.Message));

//_timer.CancelAlarm();

if (ex is Asterisk.NET.Manager.TimeoutException)
{
UpdateDialoutQueue(CallStatus.Timedout);
DisconnectFromAMI();
}
else
UpdateDialoutQueue(CallStatus.FinishedWithError);
}
}
}

[/csharp]