|
|
@@ -114,7 +114,8 @@ class AuthMiddleware:
|
|
|
return await self.app(scope, receive, send)
|
|
|
# Paths "/openapi.json" and "/ui" do not require auth
|
|
|
if (('path' in scope) and
|
|
|
- (scope['path'] in NO_AUTH_ROUTES)):
|
|
|
+ ((scope['path'] in NO_AUTH_ROUTES) or
|
|
|
+ (scope['path'].startswith('/static/records')))):
|
|
|
return await self.app(scope, receive, send)
|
|
|
return await self.error_response(receive, send)
|
|
|
async def error_response(self, receive, send):
|
|
|
@@ -189,6 +190,50 @@ async def getCDR(start=None,
|
|
|
cdr.append(_cdr[_id])
|
|
|
return cdr
|
|
|
|
|
|
+async def getUserCDR(user,
|
|
|
+ start=None,
|
|
|
+ end=None,
|
|
|
+ direction=None,
|
|
|
+ limit=None,
|
|
|
+ offset=None,
|
|
|
+ order='ASC'):
|
|
|
+ _q = f'''SELECT * FROM cdr AS c INNER JOIN (SELECT linkedid FROM cdr WHERE'''
|
|
|
+ direction=direction.lower()
|
|
|
+ if direction in ('in', True, '1', 'incoming', 'inbound'):
|
|
|
+ direction = 'inbound'
|
|
|
+ _q += f''' dst="{user}"'''
|
|
|
+ elif direction in ('out', False, '0', 'outgoing', 'outbound'):
|
|
|
+ direction = 'outbound'
|
|
|
+ _q += f''' src="{user}"'''
|
|
|
+ else:
|
|
|
+ direction = None
|
|
|
+ _q += f''' (src="{user}" or dst="{user}")'''
|
|
|
+ if end is None:
|
|
|
+ end = dt.now()
|
|
|
+ if start is None:
|
|
|
+ start=(end - td(hours=24))
|
|
|
+ _q += f''' AND calldate BETWEEN "{start}" AND "{end}" GROUP BY linkedid'''
|
|
|
+ if None not in (limit, offset):
|
|
|
+ _q += f''' LIMIT {offset},{limit}'''
|
|
|
+ _q += f''') AS c2 ON c.linkedid = c2.linkedid;'''
|
|
|
+ app.logger.warning('SQL: {}'.format(_q))
|
|
|
+ _cdr = {}
|
|
|
+ async for row in db.iterate(query=_q):
|
|
|
+ if row['linkedid'] in _cdr:
|
|
|
+ _cdr[row['linkedid']].events.add(row)
|
|
|
+ else:
|
|
|
+ _cdr[row['linkedid']]=CdrUserCall(user, row)
|
|
|
+ cdr = []
|
|
|
+ for _id in sorted(_cdr.keys(), reverse = True if (order.lower() == 'desc') else False):
|
|
|
+ record = _cdr[_id].simple
|
|
|
+ if (direction is not None) and (record['src'] == record['dst']) and (record['direction'] != direction):
|
|
|
+ record['direction'] = direction
|
|
|
+ if record['file'] is not None:
|
|
|
+ record['file'] = '/static/records/{d.year}/{d.month:02}/{d.day:02}/{filename}'.format(d=record['start'],
|
|
|
+ filename=record['file'])
|
|
|
+ cdr.append(record)
|
|
|
+ return cdr
|
|
|
+
|
|
|
async def getCEL(start=None, end=None, table='cel', field='eventtime', sort='id'):
|
|
|
return await getCDR(start, end, table, field, sort)
|
|
|
|
|
|
@@ -501,6 +546,9 @@ async def setQueueStates(user, device, state):
|
|
|
async def getDeviceUser(device):
|
|
|
return await amiDBGet('DEVICE', '{}/user'.format(device))
|
|
|
|
|
|
+async def getDeviceType(device):
|
|
|
+ return await amiDBGet('DEVICE', '{}/type'.format(device))
|
|
|
+
|
|
|
async def getDeviceDial(device):
|
|
|
return await amiDBGet('DEVICE', '{}/dial'.format(device))
|
|
|
|
|
|
@@ -570,7 +618,8 @@ async def rebindLostDevices():
|
|
|
ast = await getGlobalVars()
|
|
|
for device in app.cache['devices']:
|
|
|
user = await getDeviceUser(device)
|
|
|
- if (user != 'none') and (user in app.cache['ustates'].keys()):
|
|
|
+ deviceType = await getDeviceType(device)
|
|
|
+ if (deviceType != 'fixed') and (user != 'none') and (user in app.cache['ustates'].keys()):
|
|
|
_device = await getUserDevice(user)
|
|
|
if _device != device:
|
|
|
app.logger.warning('Fixing bind user {} to device {}'.format(user, device))
|
|
|
@@ -704,9 +753,13 @@ class SetPresenceState(Resource):
|
|
|
return invalidState(state)
|
|
|
if user not in app.cache['ustates']:
|
|
|
return noUser(user)
|
|
|
+ if (state.lower() in ('available','not_set','away','xa','chat')) and (getUserStateCombined(user) == 'dnd'):
|
|
|
+ result = await amiDBDel('DND', '{}'.format(user))
|
|
|
result = await amiSetVar('PRESENCE_STATE(CustomPresence:{})'.format(user), state)
|
|
|
if result is not None:
|
|
|
return errorReply(result)
|
|
|
+ if state.lower() == 'dnd':
|
|
|
+ result = await amiDBPut('DND', '{}'.format(user), 'YES')
|
|
|
return successfullySetState(user, state)
|
|
|
|
|
|
@app.route('/users/devices')
|
|
|
@@ -845,5 +898,30 @@ class Calls(Resource):
|
|
|
calls.append(_call)
|
|
|
return successReply(calls)
|
|
|
|
|
|
+@app.route('/user/<user>/calls')
|
|
|
+class UserCalls(Resource):
|
|
|
+ @app.param('user', 'User to query for call stats', 'path')
|
|
|
+ @app.param('end', 'End of datetime range. Defaults to now. Allowed formats are: timestamp, ISO 8601 and YYYYMMDDhhmmss', 'query')
|
|
|
+ @app.param('start', 'Start of datetime range. Defaults to end-24h. Allowed formats are: timestamp, ISO 8601 and YYYYMMDDhhmmss', 'query')
|
|
|
+ @app.param('direction', 'Calls direction, in or out. If not specified both are returned', 'query')
|
|
|
+ @app.param('limit', 'Max number of returned records, defaults to unlimited. Use offset parameter together with limit', 'query')
|
|
|
+ @app.param('offset', 'If limit is specified use offset parameter to request more results', 'query')
|
|
|
+ @app.param('order', 'Calls sort order for datetime field. ASC or DESC. Defaults to ASC', 'query')
|
|
|
+ @app.response(HTTPStatus.OK, 'JSON data {"user":user,"state":state}')
|
|
|
+ @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
|
|
|
+ async def get(self, user):
|
|
|
+ '''Returns user's call stats.
|
|
|
+ '''
|
|
|
+ if user not in app.cache['ustates']:
|
|
|
+ return noUser(user)
|
|
|
+ cdr = await getUserCDR(user,
|
|
|
+ parseDatetime(request.args.get('start')),
|
|
|
+ parseDatetime(request.args.get('end')),
|
|
|
+ request.args.get('direction', None),
|
|
|
+ request.args.get('limit', None),
|
|
|
+ request.args.get('offset', None),
|
|
|
+ request.args.get('order', 'ASC'))
|
|
|
+ return successReply(cdr)
|
|
|
+
|
|
|
manager.connect()
|
|
|
app.run(loop=main_loop, host='0.0.0.0', port=app.config['PORT'])
|