cel.py 9.7 KB

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