The most accurate way to know when every call ends on 3cX server in by listening to the Call Data Records (CDR) events.
A detailed guide to setting this up is available here: 3cX Community: How to use CDR .
For my implementation, I decided to set up 3cX server as an Active Socket Client and hosted my app as a server on port 5555 using a TcpListener service present in System.Net.Socket namespace.
My sample project is a .Net Framework Form app and the source code can be found here: GitHub
You'll need to setup an endpoint to receive the CDR information.
Processing CDR Data
CDR information will come in raw form and will need some additional processing to extract data. The data is structured in this format:
The resulting string is broken down in the code below.
//Process the string.
//{"body":"Received: Call 1,00000C0520029E23_12480,,2020/01/21 17:10:12, ,2020/01/21 17:10:21,Failed,Ext.0152,0803420000,0152,08034200000,08034200000,,,,,, ,,\r\n"} 6:10:21 PM
///{"body":"Received: Call 1, historyId
///00000C0520029410_12479, Callid
///00:00:47, duration
///2020/01/21 17:10:09, timestart
///2, time-answered
///2020/01/21 17:11:03, time-end
///TerminatedByDst, reason-terminated
///Ext.0064, from-no
///234803900000, to-no
///0064, from-dn
///10000, to-dn
///08039100000, dial-no
///,
///,
///,
///,
///1,
///0,
///default,
///Chain: Ext.0164;08039100000;\r\n"} 6:11:03 PM
//Assigning indexing
//0= historyId, 1= Callid, 2= duration, 3= timestart, 4= time-answered 5=time-end, 6=reason-terminated, 7=from-no,
//8=to-no, 9=from-dn, 10= to-dn, 11= dial-no, 12= reasonchange, 13= final_number, 14=final-dn, 15=billcode, 16=billrate, 17=billcost, 18 = billname
//19=chain
I am processing the file with a class Call
public class Call
{
public int ID { get; set; }
public int CxID { get; set; }
public string AgentID { get; set; } = string.Empty;
public string CaseID { get; set; } = string.Empty;
public int Happenning { get; set; }
public string Destination { get; set; } = string.Empty;
public int State { get; set; } //investigate
public DateTime AnsweredAt { get; set; }
public DateTime StartedAt { get; set; }
public string MediaID { get; set; } = Guid.NewGuid().ToString();
public DateTime EndedAt { get; set; }
}
And a method to process it
internal static void ProcessCallEnded(string body)
{
try
{
var CallRaw = body.Split(',');
Call call = new Call()
{
// AgentID = CallRaw[9].Trim(),
// Destination = CallRaw[11].Trim(),
// MediaID = CallRaw[1].Trim(),
Happenning = (int)Happenning.Ended,
};
if (!string.IsNullOrEmpty(CallRaw[3]))
{
DateTime.TryParse(CallRaw[3], out DateTime startTime);
call.StartedAt = startTime;
}
if (!string.IsNullOrEmpty(CallRaw[4]))
{
DateTime.TryParse(CallRaw[4], out DateTime dater);
call.AnsweredAt = dater;
}
if (!string.IsNullOrEmpty(CallRaw[5]))
{
DateTime.TryParse(CallRaw[5], out DateTime dater);
call.EndedAt = dater;
}
//Get the agent id
var allExt = CallRaw.Where(d => d.Contains("Ext.")).ToList();
call.AgentID = ChooseAgent(allExt);
//Get the number called
var all11Digits = CallRaw.Where(d => d.Length == 11);
string ChoosenDestination = string.Empty;
foreach (var ele in all11Digits)
{
double.TryParse(ele.Trim(), out double result);
if (result != 0)
ChoosenDestination = ele;
}
call.Destination = ChoosenDestination;
//if (string.IsNullOrEmpty(call.AgentID))
// call.AgentID = CallRaw.FirstOrDefault(d=>d.Length == 4);
//if (string.IsNullOrEmpty(call.Destination))
// call.Destination = CallRaw.FirstOrDefault(d => d.Length == 11);
//if (call.Destination.Contains("Chain"))
//{
// call.AgentID = CallRaw.FirstOrDefault(d => d.Length == 4);
// call.Destination = CallRaw.FirstOrDefault(d => d.Length == 11);
//}
//if (call.AgentID.Contains("ReplacedDst"))
//{
// call.AgentID = CallRaw.FirstOrDefault(d => d.Length == 4);
// call.Destination = CallRaw.FirstOrDefault(d => d.Length == 11);
//}
Logger.Log($"Received Call Ended from CDR via HttpPost by {call.AgentID} to {call.Destination}");
//fetch last call
Call ref_call = Control.FetCallByAgentID(call.AgentID, call.Destination);
if (ref_call != null)
{
if (!string.IsNullOrEmpty(ref_call.MediaID))
{
call.MediaID = ref_call.MediaID;
}
if (!string.IsNullOrEmpty(ref_call.Destination))
{
if (!string.IsNullOrEmpty(ref_call.CaseID))
call.CaseID = ref_call.CaseID;
Control.SaveCallStatus(call);
}
else
{
Control.SaveCallStatus(call);
}
}
else
{
Control.SaveCallStatus(call);
}
//next.
if (!string.IsNullOrEmpty(ref_call.CaseID))
{
var currentCase = Control.FetchCaseById(ref_call.CaseID);
if (currentCase != null)
{
//if there are more than 1 number
int numberCounter = 0;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_1))
numberCounter++;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_2))
numberCounter++;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_3))
numberCounter++;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_4))
numberCounter++;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_5))
numberCounter++;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_6))
numberCounter++;
if (numberCounter > 1)
{
if (!string.IsNullOrEmpty(CallRaw[9])) //call was established.
{
Control.Broadcast(call, Enum.GetName(typeof(Happenning), Happenning.Ended));
}
//there are more than 1 number.
//if (Control.CheckIfEstablishedEverMade(currentCase))
//{
// Control.Broadcast(call, Enum.GetName(typeof(Happenning), Happenning.Ended));
//}
else
{
//initiate Redial
//find the number that hasnt been called at all by fetching the list of the numbers that has been called
List<string> numbersAlreadyCalled = Control.FetchNumbersCalled(currentCase.CASE_CODE_STR);
string newNumberToCall = string.Empty;
if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_1))
{
if (!numbersAlreadyCalled.Contains(currentCase.PHONE_NUM_1))
newNumberToCall = currentCase.PHONE_NUM_1;
}
else if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_2))
{
if (!numbersAlreadyCalled.Contains(currentCase.PHONE_NUM_2))
newNumberToCall = currentCase.PHONE_NUM_2;
}
else if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_3))
{
if (!numbersAlreadyCalled.Contains(currentCase.PHONE_NUM_3))
newNumberToCall = currentCase.PHONE_NUM_3;
}
else if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_4))
{
if (!numbersAlreadyCalled.Contains(currentCase.PHONE_NUM_4))
newNumberToCall = currentCase.PHONE_NUM_4;
}
else if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_5))
{
if (!numbersAlreadyCalled.Contains(currentCase.PHONE_NUM_5))
newNumberToCall = currentCase.PHONE_NUM_5;
}
else if (!string.IsNullOrEmpty(currentCase.PHONE_NUM_6))
{
if (!numbersAlreadyCalled.Contains(currentCase.PHONE_NUM_6))
newNumberToCall = currentCase.PHONE_NUM_6;
}
if (!string.IsNullOrEmpty(newNumberToCall))
{
//send hangup message to Qualco.
Control.BroadcastHangup(call);
//send recall request
Control.RetryDialing(currentCase, call, newNumberToCall);
}
else
{
Control.Broadcast(call, Enum.GetName(typeof(Happenning), Happenning.Ended));
}
}
}
else
{
Control.Broadcast(call, Enum.GetName(typeof(Happenning), Happenning.Ended));
}
}
else
{
Control.Broadcast(call, Enum.GetName(typeof(Happenning), Happenning.Ended));
}
}
else
{
Control.Broadcast(call, Enum.GetName(typeof(Happenning), Happenning.Ended));
}
//Logger.Log(data.body);
}
catch (Exception ex)
{
Logger.Log(ex);
}
// Control.GetNext().Wait();
}