Преглед на файлове

Basic CDR and CEL reporting

Hal De преди 4 години
родител
ревизия
07cfe11f76
променени са 2 файла, в които са добавени 151 реда и са изтрити 0 реда
  1. 130 0
      app/app.py
  2. 21 0
      app/utils.py

+ 130 - 0
app/app.py

@@ -5,6 +5,10 @@ import logging
 import os
 import re
 import json
+from datetime import datetime as dt
+from datetime import timedelta as td
+from typing import Any, Optional
+from databases import Database
 from quart import jsonify, request, render_template_string, abort
 from quart_openapi import Pint, Resource
 from http import HTTPStatus
@@ -12,6 +16,24 @@ from panoramisk import Manager, Message
 from utils import *
 from logging.config import dictConfig
 
+class PintDB:
+  def __init__(self, app: Optional[Pint] = None) -> None:
+    self.init_app(app)
+    self._db = Database(app.config["DB_URI"])
+
+  def init_app(self, app: Pint) -> None:
+    app.before_serving(self._before_serving)
+    app.after_serving(self._after_serving)
+
+  async def _before_serving(self) -> None:
+    await self._db.connect()
+
+  async def _after_serving(self) -> None:
+    await self._db.disconnect()
+
+  def __getattr__(self, name: str) -> Any:
+    return getattr(self._db, name)
+
 # One asyncio event loop is used for AMI communication and HTTP requests routing with Quart
 main_loop = asyncio.get_event_loop()
 
@@ -38,6 +60,11 @@ app.config.update({
   'SWAGGER_JS_URL':         os.getenv('APP_SWAGGER_JS_URL', SWAGGER_JS_URL),
   'SWAGGER_CSS_URL':        os.getenv('APP_SWAGGER_CSS_URL', SWAGGER_CSS_URL),
   'STATE_CALLBACK_URL':     os.getenv('APP_STATE_CALLBACK_URL', None),
+  'DB_URI':                 'mysql://{}:{}@{}:{}/{}'.format(os.getenv('MYSQL_USER', 'asterisk'),
+                                                            os.getenv('MYSQL_PASSWORD', 'secret'),
+                                                            os.getenv('MYSQL_SERVER', 'db'),
+                                                            os.getenv('APP_PORT_MYSQL', '3306'),
+                                                            os.getenv('FREEPBX_CDRDBNAME', None)),
   'EXTRA_API_URL':          os.getenv('APP_EXTRA_API_URL', None),
   'STATE_CACHE':           {'user':{},
                             'presence':{}}})
@@ -80,6 +107,7 @@ class AuthMiddleware:
                 'more_body': False})
 
 app.asgi_app = AuthMiddleware(app.asgi_app)
+db = PintDB(app)
 
 @manager.register_event('FullyBooted')
 async def fullyBootedCallback(mngr: Manager, msg: Message):
@@ -107,6 +135,78 @@ async def presenceStatusCallback(mngr: Manager, msg: Message):
     if combinedState != prevState:
       await userStateChangeCallback(user, combinedState, prevState)
 
+async def getCDR(start=None, end=None, **kwargs):
+  cdr = {}
+  if end is None:
+    end = dt.now()
+  if start is None:
+    start=(end - td(days=30))
+  async for row in db.iterate(query='''SELECT linkedid,
+                                              uniqueid,
+                                              calldate,
+                                              did,
+                                              src,
+                                              dst,
+                                              clid,
+                                              dcontext,
+                                              channel,
+                                              dstchannel,
+                                              lastapp,
+                                              duration,
+                                              billsec,
+                                              disposition,
+                                              recordingfile,
+                                              cnum,
+                                              cnam,
+                                              outbound_cnum,
+                                              outbound_cnam,
+                                              dst_cnam,
+                                              peeraccount
+                                       FROM cdr
+                                       WHERE calldate
+                                       BETWEEN :start AND :end
+                                       ORDER BY linkedid,
+                                                calldate,
+                                                uniqueid;''',
+                              values={'start':start,
+                                      'end':end}):
+    call = {_k: str(_v) for _k, _v in row.items() if _k != 'linkedid' and _v != ''}
+    cdr.setdefault(row['linkedid'],[]).append(call)
+  return cdr
+
+async def getCEL(start=None, end=None, **kwargs):
+  cel = {}
+  if end is None:
+    end = dt.now()
+  if start is None:
+    start=(end - td(days=30))
+  async for row in db.iterate(query='''SELECT linkedid,
+                                              uniqueid,
+                                              eventtime,
+                                              eventtype,
+                                              cid_name,
+                                              cid_num,
+                                              cid_ani,
+                                              cid_rdnis,
+                                              cid_dnid,
+                                              exten,
+                                              context,
+                                              channame,
+                                              appname,
+                                              uniqueid,
+                                              linkedid
+                                       FROM cel
+                                       WHERE eventtime
+                                       BETWEEN :start AND :end
+                                       ORDER BY linkedid,
+                                                uniqueid,
+                                                eventtime;''',
+                              values={'start':start,
+                                      'end':end}):
+    event = {_k: str(_v) for _k, _v in row.items() if _k != 'linkedid' and _v != ''}
+    cel.setdefault(row['linkedid'],[]).append(event)
+  return cel
+
 @app.before_first_request
 async def initHttpClient():
   app.config['HTTP_CLIENT'] = aiohttp.ClientSession(loop=main_loop)
@@ -653,5 +753,35 @@ class DeviceUnBind(Resource):
     await setDeviceUser(device, 'none') # Unbind user from device
     return successfullyUnbound(currentUser, device)
 
+@app.route('/cdr')
+class CDR(Resource):
+  @app.param('end', 'End of datetime range. Defaults to now. Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
+  @app.param('start', 'Start of datetime range. Defaults to (start-30 days). Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
+  @app.response(HTTPStatus.OK, 'JSON reply')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self):
+    '''Returns CDR data, groupped by logical call id.
+    All request arguments are optional.
+    '''
+    start = parseDatetime(request.args.get('start'))
+    end = parseDatetime(request.args.get('end'))
+    cdr = await getCDR(start, end)
+    return successReply(cdr)
+
+@app.route('/cel')
+class CEL(Resource):
+  @app.param('end', 'End of datetime range. Defaults to now. Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
+  @app.param('start', 'Start of datetime range. Defaults to (start-30 days). Allowed formats are: timestamp, ISO 8601 and ddmmyyyyHHMMSS', 'query')
+  @app.response(HTTPStatus.OK, 'JSON reply')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self):
+    '''Returns CDR data, groupped by logical call id.
+    All request arguments are optional.
+    '''
+    start = parseDatetime(request.args.get('start'))
+    end = parseDatetime(request.args.get('end'))
+    cel = await getCEL(start, end)
+    return successReply(cel)
+
 manager.connect()
 app.run(loop=main_loop, host='0.0.0.0', port=app.config['PORT'])

+ 21 - 0
app/utils.py

@@ -1,6 +1,8 @@
 #!/usr/bin/env python3
 from dataclasses import dataclass, asdict
 from panoramisk import Message
+from datetime import datetime as dt
+from datetime import timedelta as td
 
 TRUEs = ('true', '1', 'y', 'yes')
 NONEs = (None,'none','')
@@ -50,6 +52,25 @@ SWAGGER_TEMPLATE = '''
     </script>
   </body>'''
 
+def parseDatetime(dateString):
+  _dt = None
+  if dateString is not None:
+    try:
+      _dt = dt.strptime(dateString, '%d%m%Y%H%M%S')
+    except ValueError:
+      pass
+    if _dt is None:
+      try:
+        _dt = dt.fromtimestamp(int(dateString))
+      except ValueError:
+        pass
+    if _dt is None:
+      try:
+        _dt = dt.fromisoformat(dateString)
+      except ValueError:
+        pass
+  return _dt
+
 def followMe2DevState(followMeState):
   if followMeState == 'DIRECT':
     return 'INUSE'