#!/usr/bin/env python3 import json import re from datetime import datetime from datetime import timedelta as td fieldsFilter = ('id', 'linkedid', 'appdata', 'lastdata') class CdrChannel: def __init__(self, chanData): self._str = chanData try: (self.tech, self.peer, self.context, self.id, self.leg) = re.match(r'(\w+)/([_\w\.-]+)(?:@([\w-]+))?-([0-9a-f]{8})(?:;([0-9]+))?', chanData).groups() except: pass def __repr__(self): return self._str class CdrEvent: def __init__(self, eventData): eventData = dict(eventData) for key, value in eventData.items(): if ((key not in fieldsFilter) and (value is not None) and (str(value) != '')): if key == 'extra': setattr(self, key, json.loads(value)) elif key in ('channame','channel','dstchannel'): setattr(self, key, CdrChannel(value)) else: setattr(self, key, value) def __getattr__(self, attr): return None class CdrEvents: def __init__(self): self._events = [] self._channels = {} self._dstchannels = {} self.start = 0 self._waitingchannel = None def add(self, event): if not isinstance(event, CdrEvent): event = CdrEvent(event) if not self.start: self.start = event.calldate if (event.lastapp == 'Return' and event.billsec==0): return if (event.lastapp == 'Return'): event.lastapp='Dial' event.duration=event.billsec event.cnum = event.src if (event.channel and event.channel.id not in self._channels): self._channels[event.channel.id] = event if (event.dstchannel and event.dstchannel.id not in self._dstchannels): self._dstchannels[event.dstchannel.id] = event.dst if (self._waitingchannel is not None) and (self._waitingchannel.id == event.channel.id): # if dial event after queue event for e in self._events: if (e.lastapp == 'Queue') and (e.dstchannel.id == event.channel.id): e.duration = (event.calldate-e.calldate).total_seconds() + event.duration e.billsec = event.billsec self._waitingchannel = None if (event.lastapp == 'Queue'): event.billsec = 0 queueevent = event for e in self._events: if (e.lastapp == 'Queue') and (e.uniqueid == event.uniqueid):#find first queue event queueevent = e if (event.disposition == 'ANSWERED'): #if it answered - fill duration and waitng based on answered dial event queueevent.disposition = 'ANSWERED' queueevent.dstchannel = event.dstchannel if (event.dstchannel and event.dstchannel.id in self._channels): # if dial event before queue event dialevent = self._channels[event.dstchannel.id] queueevent.duration = (dialevent.calldate-queueevent.calldate).total_seconds() + dialevent.duration queueevent.billsec = dialevent.billsec else: self._waitingchannel = event.dstchannel # wait for dial event else: if queueevent.disposition != 'ANSWERED': #count total queue duration queueevent.duration = (event.calldate-queueevent.calldate).total_seconds() + event.duration if (event != queueevent): return; # add only forst queue event if (event.lastapp == 'Dial' and event.dstchannel and event.dstchannel.id in self._dstchannels): event.dst = self._dstchannels[event.dstchannel.id] #get dst from fisrt event with rthis channel. for transfer if (event.lastapp == 'Dial' and event.dstchannel and event.dstchannel.id in self._channels): event.dst = self._channels[event.dstchannel.id].dst #for transfer to queue self.end = event.calldate + td(seconds=event.duration) self._events.append(event) @property def all(self): return self._events @property def first(self): return self._events[0] if len(self._events) > 0 else None @property def second(self): return self._events[1] if len(self._events) > 1 else None @property def last(self): return self._events[-1] if len(self._events) > 0 else None def filter(self, key, value, func='__eq__'): result = CdrEvents() for event in self._events: if getattr(str(getattr(event, key)), func)(value): result.add(event) return result def has(self, value, key='disposition'): for event in self._events: if getattr(event, key) == value: return True return False def __len__(self): return len(self._events) def simple(self): res = [] for event in self._events: simple_event = {'start': event.calldate, 'answered': event.disposition=='ANSWERED', 'duration': event.billsec, 'waiting': event.duration - event.billsec, 'application': event.lastapp, 'caller': event.cnum, 'dst': event.dst, 'uniqueid': event.uniqueid, 'file': event.recordingfile} res.append(simple_event) return res class CdrUserEvents(CdrEvents): def __init__(self, user): self._user = user self._events = [] self.start = 0 self.answer = 0 self.end = 0 self.recordingfile = '' def add(self, event): if not isinstance(event, CdrEvent): event = CdrEvent(event) if (event.billsec > event.duration): event.billsec = event.duration if self._user in (event.src, event.dst, event.cnum): if (self.answer ==0) and (event.disposition == 'ANSWERED'): self.answer = event.calldate + td(seconds=event.duration - event.billsec) self.recordingfile = event.recordingfile if (self.recordingfile == ''): self.recordingfile = event.recordingfile if len(self._events) == 0: self.start = event.calldate self.end = event.calldate + td(seconds=event.duration) self._events.append(event) else: if (event.calldate + td(seconds=event.duration)) > self.end: self.end = event.calldate + td(seconds=event.duration) if event.calldate < self.start: self.start = event.calldate lo = 0 hi = len(self._events) while lo < hi: mid = (lo+hi)//2 if event.calldate <= self._events[mid].calldate: hi = mid else: lo = mid+1 self._events.insert(lo, event) def simple(self): res = [] for event in self._events: simple_event = {'start': event.calldate, 'answered': event.disposition=='ANSWERED', 'duration': event.duration, #'waiting': event.waiting, 'application': event.lastapp, 'dst': event.dst, 'uniqueid': event.uniqueid, 'file': event.recordingfile} res.append(simple_event) return res class CelEvents(CdrEvents): def has(self, value, key='eventtype'): return super().has(value, key) class CdrCall: def __init__(self, event=None): self.events = CdrEvents() if event is not None: self.events.add(event) self.linkedid = event['linkedid'] else: self.linkedid = None @property def start(self): return self.events.first.calldate @property def disposition(self): return self.events.first.disposition @property def file(self): return self.events.first.recordingfile @property def src(self): return self.events.first.cnum @property def direction(self): if self.events.first.dcontext == 'from-internal': if self.events.first.outbound_cnum is not None: return 'outbound' else: return 'local' else: return 'inbound' @property def dst(self): # placeholder # TODO: determine last dst based on disposition if self.direction == 'outbound': return self.events.first.dst elif self.direction == 'local': return self.events.first.dst else: if self.events.first.dstchannel is not None: return self.events.first.dstchannel.peer else: return self.events.first.dst @property def did(self): if self.direction == 'inbound': return self.events.first.did else: return None @property def duration(self): if not self.isAnswered: return 0 else: if len(self.events) > 1: return self.events.second.billsec else: return self.events.first.billsec @property def waiting(self): if not self.isAnswered: return self.events.first.duration else: # TODO: exclude time on ivr return self.events.first.duration - self.duration @property def isAnswered(self): return self.disposition == 'ANSWERED'; class CdrUserCall(CdrCall): def __init__(self, user, event=None): self.user = user self.events = CdrUserEvents(user) if event is not None: self.events.add(event) self.linkedid = event['linkedid'] else: self.linkedid = None @property def file(self): return self.events.recordingfile @property def src(self): return self.events.first.cnum @property def direction(self): if self.user in (self.events.first.src, self.events.first.cnum): return 'outbound' else: return 'inbound' @property def dst(self): return self.events.first.dst @property def did(self): if self.direction == 'inbound': return self.events.first.did else: return None @property def duration(self): if self.events.answer == 0: return 0 else: return (self.events.end - self.events.answer).total_seconds() @property def waiting(self): if self.events.answer == 0: return (self.events.end - self.events.start).total_seconds() else: # TODO: exclude time on ivr return (self.events.answer - self.events.start).total_seconds() @property def isAnswered(self): return self.events.answer != 0; @property def simple(self): return {'start': self.start, 'direction': self.direction, 'answered': self.isAnswered, 'duration': self.duration, 'waiting': self.waiting, 'src': self.src, 'dst': self.dst, 'did': self.did, 'linkedid': self.linkedid, 'file': self.file, 'transfer_from': self.events.first.transfer_from }#'events': self.events.simple()} class CelCall: def __init__(self, event=None): self.events = CelEvents() if event is not None: self.events.add(event) self.linkedid = event['linkedid'] else: self.linkedid = None @property def start(self): return self.events.first.eventtime