|
@@ -5,6 +5,10 @@ import logging
|
|
|
import os
|
|
import os
|
|
|
import re
|
|
import re
|
|
|
import json
|
|
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 import jsonify, request, render_template_string, abort
|
|
|
from quart_openapi import Pint, Resource
|
|
from quart_openapi import Pint, Resource
|
|
|
from http import HTTPStatus
|
|
from http import HTTPStatus
|
|
@@ -12,6 +16,24 @@ from panoramisk import Manager, Message
|
|
|
from utils import *
|
|
from utils import *
|
|
|
from logging.config import dictConfig
|
|
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
|
|
# One asyncio event loop is used for AMI communication and HTTP requests routing with Quart
|
|
|
main_loop = asyncio.get_event_loop()
|
|
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_JS_URL': os.getenv('APP_SWAGGER_JS_URL', SWAGGER_JS_URL),
|
|
|
'SWAGGER_CSS_URL': os.getenv('APP_SWAGGER_CSS_URL', SWAGGER_CSS_URL),
|
|
'SWAGGER_CSS_URL': os.getenv('APP_SWAGGER_CSS_URL', SWAGGER_CSS_URL),
|
|
|
'STATE_CALLBACK_URL': os.getenv('APP_STATE_CALLBACK_URL', None),
|
|
'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),
|
|
'EXTRA_API_URL': os.getenv('APP_EXTRA_API_URL', None),
|
|
|
'STATE_CACHE': {'user':{},
|
|
'STATE_CACHE': {'user':{},
|
|
|
'presence':{}}})
|
|
'presence':{}}})
|
|
@@ -80,6 +107,7 @@ class AuthMiddleware:
|
|
|
'more_body': False})
|
|
'more_body': False})
|
|
|
|
|
|
|
|
app.asgi_app = AuthMiddleware(app.asgi_app)
|
|
app.asgi_app = AuthMiddleware(app.asgi_app)
|
|
|
|
|
+db = PintDB(app)
|
|
|
|
|
|
|
|
@manager.register_event('FullyBooted')
|
|
@manager.register_event('FullyBooted')
|
|
|
async def fullyBootedCallback(mngr: Manager, msg: Message):
|
|
async def fullyBootedCallback(mngr: Manager, msg: Message):
|
|
@@ -107,6 +135,78 @@ async def presenceStatusCallback(mngr: Manager, msg: Message):
|
|
|
if combinedState != prevState:
|
|
if combinedState != prevState:
|
|
|
await userStateChangeCallback(user, 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
|
|
@app.before_first_request
|
|
|
async def initHttpClient():
|
|
async def initHttpClient():
|
|
|
app.config['HTTP_CLIENT'] = aiohttp.ClientSession(loop=main_loop)
|
|
app.config['HTTP_CLIENT'] = aiohttp.ClientSession(loop=main_loop)
|
|
@@ -653,5 +753,35 @@ class DeviceUnBind(Resource):
|
|
|
await setDeviceUser(device, 'none') # Unbind user from device
|
|
await setDeviceUser(device, 'none') # Unbind user from device
|
|
|
return successfullyUnbound(currentUser, 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()
|
|
manager.connect()
|
|
|
app.run(loop=main_loop, host='0.0.0.0', port=app.config['PORT'])
|
|
app.run(loop=main_loop, host='0.0.0.0', port=app.config['PORT'])
|