cel.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. #!/usr/bin/env python3
  2. import json
  3. import re
  4. from datetime import datetime
  5. from datetime import timedelta as td
  6. fieldsFilter = ('id', 'linkedid', 'appdata')
  7. class CdrChannel:
  8. def __init__(self, chanData):
  9. self._str = chanData
  10. try:
  11. (self.tech,
  12. self.peer,
  13. self.context,
  14. self.id,
  15. self.leg) = re.match(r'(\w+)/([_\w\.-]+)(?:@([\w-]+))?-([0-9a-f]{8})(?:;([0-9]+))?', chanData).groups()
  16. except:
  17. self.id=None
  18. pass
  19. def __repr__(self):
  20. return self._str
  21. class CdrEvent:
  22. def __init__(self, eventData):
  23. eventData = dict(eventData)
  24. for key, value in eventData.items():
  25. if ((key not in fieldsFilter) and (value is not None) and (str(value) != '')):
  26. if key == 'extra':
  27. setattr(self, key, json.loads(value))
  28. elif key in ('channame','channel','dstchannel'):
  29. setattr(self, key, CdrChannel(value))
  30. else:
  31. setattr(self, key, value)
  32. def __getattr__(self, attr):
  33. return None
  34. class CdrEvents:
  35. def __init__(self):
  36. self._events = []
  37. self._channels = {}
  38. self._dstchannels = {}
  39. self.start = 0
  40. self._waitingchannel = None
  41. def add(self, event):
  42. if not isinstance(event, CdrEvent):
  43. event = CdrEvent(event)
  44. if not self.start:
  45. self.start = event.calldate
  46. if (event.lastapp == 'ExecIf'):
  47. return
  48. if (event.lastapp == 'Return' and (event.billsec==0 or not event.dstchannel)):
  49. return
  50. if (event.lastapp == 'Dial' and (event.dcontext.startswith("from-internal-c2c") or event.dcontext == 'followme-check')):
  51. return
  52. if (event.lastapp == 'Return'):
  53. event.lastapp='Dial'
  54. event.duration=event.billsec
  55. event.cnum = event.src
  56. if (event.channel and event.channel.id not in self._channels):
  57. self._channels[event.channel.id] = event
  58. if (event.dstchannel and event.dstchannel.id not in self._dstchannels):
  59. self._dstchannels[event.dstchannel.id] = event.dst
  60. event.waiting = max(0,event.duration - event.billsec)
  61. #when were transfer - count duration till transfer
  62. if (event.lastapp=='Dial' and event.disposition=='ANSWERED' and event.transfer_time and event.transfer_time>event.calldate):
  63. event.duration = (event.transfer_time - event.calldate).total_seconds()
  64. event.billsec = event.duration - event.waiting
  65. if (self._waitingchannel is not None) and (self._waitingchannel.id == event.channel.id): # if dial event after queue event - set queue event timing using dial events
  66. for e in self._events:
  67. if (e.lastapp == 'Queue') and (e.dstchannel.id == event.channel.id):
  68. e.duration = (event.calldate-e.calldate).total_seconds() + event.duration #waiting is dial waiting+difference between queue and dial start
  69. e.billsec = event.billsec
  70. self._waitingchannel = None
  71. if (event.lastapp == 'Queue'):
  72. event.billsec = 0
  73. queueevent = event
  74. for e in self._events:
  75. if (e.lastapp == 'Queue') and (e.uniqueid == event.uniqueid):#find first queue event
  76. queueevent = e
  77. if (event.disposition == 'ANSWERED'): #if it answered - fill duration and waitng based on answered dial event
  78. queueevent.disposition = 'ANSWERED'
  79. queueevent.dstchannel = event.dstchannel
  80. if (event.dstchannel and event.dstchannel.id in self._channels): # if dial event before queue event
  81. dialevent = self._channels[event.dstchannel.id]
  82. queueevent.duration = (dialevent.calldate-queueevent.calldate).total_seconds() + dialevent.duration
  83. queueevent.billsec = dialevent.billsec
  84. else:
  85. self._waitingchannel = event.dstchannel # wait for dial event to fill timing
  86. else:
  87. if queueevent.disposition != 'ANSWERED': #count total queue duration
  88. queueevent.duration = (event.calldate-queueevent.calldate).total_seconds() + event.duration
  89. if (event != queueevent):
  90. return; # add only forst queue event
  91. if (event.lastapp == 'Dial' and event.dstchannel and event.dstchannel.id in self._dstchannels):
  92. event.dst = self._dstchannels[event.dstchannel.id] #get dst from fisrt event with rthis channel. for transfer
  93. if (event.lastapp == 'Dial' and event.dstchannel and event.dstchannel.id in self._channels):
  94. event.dst = self._channels[event.dstchannel.id].dst #for transfer to queue
  95. self.end = event.calldate + td(seconds=event.duration)
  96. self._events.append(event)
  97. @property
  98. def all(self):
  99. return self._events
  100. @property
  101. def first(self):
  102. return self._events[0] if len(self._events) > 0 else None
  103. @property
  104. def second(self):
  105. return self._events[1] if len(self._events) > 1 else None
  106. @property
  107. def last(self):
  108. return self._events[-1] if len(self._events) > 0 else None
  109. def filter(self, key, value, func='__eq__'):
  110. result = CdrEvents()
  111. for event in self._events:
  112. if getattr(str(getattr(event, key)), func)(value):
  113. result.add(event)
  114. return result
  115. def has(self, value, key='disposition'):
  116. for event in self._events:
  117. if getattr(event, key) == value:
  118. return True
  119. return False
  120. def __len__(self):
  121. return len(self._events)
  122. def simple(self):
  123. res = []
  124. for event in self._events:
  125. simple_event = {'start': event.calldate,
  126. 'answered': event.disposition=='ANSWERED',
  127. 'duration': event.billsec,
  128. 'waiting': max(0,event.duration - event.billsec),
  129. 'application': event.lastapp,
  130. 'caller': event.cnum,
  131. 'dst': event.dst,
  132. 'uniqueid': event.uniqueid,
  133. 'file': event.recordingfile}
  134. res.append(simple_event)
  135. return res
  136. class CdrUserEvents(CdrEvents):
  137. def __init__(self, user):
  138. self._user = user
  139. self._events = []
  140. self.start = 0
  141. self.answer = 0
  142. self.end = 0
  143. self.recordingfile = ''
  144. def add(self, event):
  145. if not isinstance(event, CdrEvent):
  146. event = CdrEvent(event)
  147. if (event.billsec > event.duration):
  148. event.billsec = event.duration
  149. if self._user in (event.src, event.dst, event.cnum):
  150. if (self.answer ==0) and (event.disposition == 'ANSWERED'):
  151. self.answer = event.calldate + td(seconds=event.duration - event.billsec)
  152. self.recordingfile = event.recordingfile
  153. if (self.recordingfile == ''):
  154. self.recordingfile = event.recordingfile
  155. if len(self._events) == 0:
  156. self.start = event.calldate
  157. self.end = event.calldate + td(seconds=event.duration)
  158. self._events.append(event)
  159. else:
  160. if (event.calldate + td(seconds=event.duration)) > self.end:
  161. self.end = event.calldate + td(seconds=event.duration)
  162. if event.calldate < self.start:
  163. self.start = event.calldate
  164. lo = 0
  165. hi = len(self._events)
  166. while lo < hi:
  167. mid = (lo+hi)//2
  168. if event.calldate <= self._events[mid].calldate:
  169. hi = mid
  170. else:
  171. lo = mid+1
  172. self._events.insert(lo, event)
  173. def simple(self):
  174. res = []
  175. for event in self._events:
  176. simple_event = {'start': event.calldate,
  177. 'answered': event.disposition=='ANSWERED',
  178. 'duration': event.duration,
  179. #'waiting': event.waiting,
  180. 'application': event.lastapp,
  181. 'dst': event.dst,
  182. 'uniqueid': event.uniqueid,
  183. 'file': event.recordingfile}
  184. res.append(simple_event)
  185. return res
  186. class CelEvents(CdrEvents):
  187. def has(self, value, key='eventtype'):
  188. return super().has(value, key)
  189. class CdrCall:
  190. def __init__(self, event=None):
  191. self.events = CdrEvents()
  192. if event is not None:
  193. self.events.add(event)
  194. self.linkedid = event['linkedid']
  195. else:
  196. self.linkedid = None
  197. @property
  198. def start(self):
  199. return self.events.first.calldate
  200. @property
  201. def disposition(self):
  202. return self.events.first.disposition
  203. @property
  204. def file(self):
  205. return self.events.first.recordingfile
  206. @property
  207. def src(self):
  208. return self.events.first.cnum
  209. @property
  210. def direction(self):
  211. if self.events.first.dcontext == 'from-internal':
  212. if self.events.first.outbound_cnum is not None:
  213. return 'outbound'
  214. else:
  215. return 'local'
  216. else:
  217. return 'inbound'
  218. @property
  219. def dst(self):
  220. # placeholder
  221. # TODO: determine last dst based on disposition
  222. if self.direction == 'outbound':
  223. return self.events.first.dst
  224. elif self.direction == 'local':
  225. return self.events.first.dst
  226. else:
  227. if self.events.first.dstchannel is not None:
  228. return self.events.first.dstchannel.peer
  229. else:
  230. return self.events.first.dst
  231. @property
  232. def did(self):
  233. if self.direction == 'inbound':
  234. return self.events.first.did
  235. else:
  236. return None
  237. @property
  238. def duration(self):
  239. if not self.isAnswered:
  240. return 0
  241. else:
  242. if len(self.events) > 1:
  243. return self.events.second.billsec
  244. else:
  245. return self.events.first.billsec
  246. @property
  247. def waiting(self):
  248. if not self.isAnswered:
  249. return self.events.first.duration
  250. else:
  251. # TODO: exclude time on ivr
  252. return self.events.first.duration - self.duration
  253. @property
  254. def isAnswered(self):
  255. return self.disposition == 'ANSWERED';
  256. class CdrUserCall(CdrCall):
  257. def __init__(self, user, event=None):
  258. self.user = user
  259. self.events = CdrUserEvents(user)
  260. if event is not None:
  261. self.events.add(event)
  262. self.linkedid = event['linkedid']
  263. else:
  264. self.linkedid = None
  265. @property
  266. def file(self):
  267. return self.events.recordingfile
  268. @property
  269. def src(self):
  270. return self.events.first.cnum
  271. @property
  272. def direction(self):
  273. if self.user in (self.events.first.src, self.events.first.cnum):
  274. return 'outbound'
  275. else:
  276. return 'inbound'
  277. @property
  278. def dst(self):
  279. return self.events.first.dst
  280. @property
  281. def did(self):
  282. if self.direction == 'inbound':
  283. return self.events.first.did
  284. else:
  285. return None
  286. @property
  287. def duration(self):
  288. if self.events.answer == 0:
  289. return 0
  290. else:
  291. return (self.events.end - self.events.answer).total_seconds()
  292. @property
  293. def waiting(self):
  294. if self.events.answer == 0:
  295. return (self.events.end - self.events.start).total_seconds()
  296. else:
  297. # TODO: exclude time on ivr
  298. return (self.events.answer - self.events.start).total_seconds()
  299. @property
  300. def isAnswered(self):
  301. return self.events.answer != 0;
  302. @property
  303. def simple(self):
  304. return {'start': self.start,
  305. 'direction': self.direction,
  306. 'answered': self.isAnswered,
  307. 'duration': self.duration,
  308. 'waiting': self.waiting,
  309. 'src': self.src,
  310. 'dst': self.dst,
  311. 'did': self.did,
  312. 'linkedid': self.linkedid,
  313. 'file': self.file,
  314. 'transfer_from': self.events.first.transfer_from
  315. }#'events': self.events.simple()}
  316. class CelCall:
  317. def __init__(self, event=None):
  318. self.events = CelEvents()
  319. if event is not None:
  320. self.events.add(event)
  321. self.linkedid = event['linkedid']
  322. else:
  323. self.linkedid = None
  324. @property
  325. def start(self):
  326. return self.events.first.eventtime