Listening to 3cX Call-Ended Events using CDR

Listening to 3cX Call-Ended Events using CDR

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: cdr1.PNG

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();
        }