Browse Source

JSON replys redesign and couple more AMI actions

Hal De 4 years ago
parent
commit
638f95f2f8
2 changed files with 174 additions and 86 deletions
  1. 114 75
      app/app.py
  2. 60 11
      app/utils.py

+ 114 - 75
app/app.py

@@ -90,22 +90,22 @@ async def extensionStatusCallback(mngr: Manager, msg: Message):
   user = msg.exten #hint = msg.hint
   user = msg.exten #hint = msg.hint
   state = msg.statustext.lower()
   state = msg.statustext.lower()
   if user in app.config['STATE_CACHE']['user']:
   if user in app.config['STATE_CACHE']['user']:
-    _combinedState = getUserStateCombined(user)
+    prevState = getUserStateCombined(user)
     app.config['STATE_CACHE']['user'][user] = state
     app.config['STATE_CACHE']['user'][user] = state
     combinedState = getUserStateCombined(user)
     combinedState = getUserStateCombined(user)
-    if combinedState != _combinedState:
-      await userStateChangeCallback(user, combinedState)
+    if combinedState != prevState:
+      await userStateChangeCallback(user, combinedState, prevState)
 
 
 @manager.register_event('PresenceStatus')
 @manager.register_event('PresenceStatus')
 async def presenceStatusCallback(mngr: Manager, msg: Message):
 async def presenceStatusCallback(mngr: Manager, msg: Message):
   user = msg.exten #hint = msg.hint
   user = msg.exten #hint = msg.hint
   state = msg.status.lower()
   state = msg.status.lower()
   if user in app.config['STATE_CACHE']['user']:
   if user in app.config['STATE_CACHE']['user']:
-    _combinedState = getUserStateCombined(user)
+    prevState = getUserStateCombined(user)
     app.config['STATE_CACHE']['presence'][user] = state
     app.config['STATE_CACHE']['presence'][user] = state
     combinedState = getUserStateCombined(user)
     combinedState = getUserStateCombined(user)
-    if combinedState != _combinedState:
-      await userStateChangeCallback(user, combinedState)
+    if combinedState != prevState:
+      await userStateChangeCallback(user, combinedState, prevState)
 
 
 @app.before_first_request
 @app.before_first_request
 async def initHttpClient():
 async def initHttpClient():
@@ -158,6 +158,30 @@ async def amiGetVar(variable):
   app.logger.warning('GetVar({})->{}'.format(variable, reply.value))
   app.logger.warning('GetVar({})->{}'.format(variable, reply.value))
   return reply.value
   return reply.value
 
 
+@app.route('/ami/auths')
+async def amiPJSIPShowAuths():
+  auths = {}
+  reply = await manager.send_action({'Action':'PJSIPShowAuths'})
+  if len(reply) >= 2:
+    for message in reply:
+      if ((message.event == 'AuthList') and
+          ('objecttype' in message) and
+          (message.objecttype == 'auth')):
+        auths[message.username] = message.password
+  return successReply(auths)
+
+@app.route('/ami/aors')
+async def amiPJSIPShowAors():
+  aors = {}
+  reply = await manager.send_action({'Action':'PJSIPShowAors'})
+  if len(reply) >= 2:
+    for message in reply:
+      if ((message.event == 'AorList') and
+          ('objecttype' in message) and
+          (message.objecttype == 'aor')):
+        aors[message.objectname] = message.contacts
+  return successReply(aors)
+
 async def amiSetVar(variable, value):
 async def amiSetVar(variable, value):
   '''AMI SetVar
   '''AMI SetVar
   Sets variable using AMI action SetVar to value in background.
   Sets variable using AMI action SetVar to value in background.
@@ -167,15 +191,18 @@ async def amiSetVar(variable, value):
     value (string): Value to set for variable
     value (string): Value to set for variable
 
 
   Returns:
   Returns:
-    boolean: True if DBPut action was successfull, False overwise
+    string: None if SetVar was successfull, error message overwise
   '''
   '''
   reply = await manager.send_action({'Action': 'SetVar',
   reply = await manager.send_action({'Action': 'SetVar',
                                      'Variable': variable,
                                      'Variable': variable,
                                      'Value': value})
                                      'Value': value})
   app.logger.warning('SetVar({}, {})'.format(variable, value))
   app.logger.warning('SetVar({}, {})'.format(variable, value))
-  if (isinstance(reply, Message) and reply.success):
-    return True
-  return False
+  if isinstance(reply, Message):
+    if reply.success:
+      return None
+    else:
+      return reply.message
+  return 'AMI error'
 
 
 async def amiDBGet(family, key):
 async def amiDBGet(family, key):
   '''AMI DBGet
   '''AMI DBGet
@@ -270,14 +297,17 @@ async def amiPresenceState(user):
     user (string): user
     user (string): user
 
 
   Returns:
   Returns:
-    string: One of: not_set, unavailable, available, away, xa, chat or dnd
+    boolean, string: True and state or False and error message
   '''
   '''
   reply = await manager.send_action({'Action': 'PresenceState',
   reply = await manager.send_action({'Action': 'PresenceState',
                                      'Provider': 'CustomPresence:{}'.format(user)})
                                      'Provider': 'CustomPresence:{}'.format(user)})
   app.logger.warning('PresenceState({})'.format(user))
   app.logger.warning('PresenceState({})'.format(user))
-  if (isinstance(reply, Message) and reply.success):
-    return reply.state
-  return None
+  if isinstance(reply, Message):
+    if reply.success:
+      return True, reply.state
+    else:
+      return False, reply.message
+  return False, 'AMI error'
 
 
 async def amiPresenceStateList():
 async def amiPresenceStateList():
   states = {}
   states = {}
@@ -438,34 +468,22 @@ async def refreshStatesCache():
   app.config['STATE_CACHE']['presence'] = await amiPresenceStateList()
   app.config['STATE_CACHE']['presence'] = await amiPresenceStateList()
   return len(app.config['STATE_CACHE']['user'])
   return len(app.config['STATE_CACHE']['user'])
 
 
-async def userStateChangeCallback(user, state):
+async def userStateChangeCallback(user, state, prevState = None):
   reply = None
   reply = None
   if ((app.config['STATE_CALLBACK_URL'] not in NONEs) and
   if ((app.config['STATE_CALLBACK_URL'] not in NONEs) and
       ('HTTP_CLIENT' in app.config)):
       ('HTTP_CLIENT' in app.config)):
     reply = await app.config['HTTP_CLIENT'].post(app.config['STATE_CALLBACK_URL'],
     reply = await app.config['HTTP_CLIENT'].post(app.config['STATE_CALLBACK_URL'],
                                                  json={'user': user,
                                                  json={'user': user,
-                                                       'state': state})
+                                                       'state': state,
+                                                       'prev_state':prevState})
   else:
   else:
     app.logger.warning('{} changed state to: {}'.format(user, state))
     app.logger.warning('{} changed state to: {}'.format(user, state))
   return reply
   return reply
 
 
 def getUserStateCombined(user):
 def getUserStateCombined(user):
-  if user not in app.config['STATE_CACHE']['user']:
-    return None
-  _state = app.config['STATE_CACHE']['user'][user]
-  if (_state == 'idle'):
-    if (user in app.config['STATE_CACHE']['presence']):
-      if app.config['STATE_CACHE']['presence'][user] in ('not_set','available', 'xa', 'chat'):
-        return 'available'
-      else:
-        return app.config['STATE_CACHE']['presence'][user]
-    else:
-      return 'available'
-  else:
-    if _state in ('unavailable', 'ringing'):
-      return _state
-    else:
-      return 'busy'
+  _uCache = app.config['STATE_CACHE']['user']
+  _pCache = app.config['STATE_CACHE']['presence']
+  return combinedStates[_uCache.get(user, 'unavailable')][_pCache.get(user, 'not_set')]
 
 
 def getUsersStatesCombined():
 def getUsersStatesCombined():
   return {user:getUserStateCombined(user) for user in app.config['STATE_CACHE']['user']}
   return {user:getUserStateCombined(user) for user in app.config['STATE_CACHE']['user']}
@@ -474,80 +492,102 @@ def getUsersStatesCombined():
 class AtXfer(Resource):
 class AtXfer(Resource):
   @app.param('userA', 'User initiating the attended transfer', 'path')
   @app.param('userA', 'User initiating the attended transfer', 'path')
   @app.param('userB', 'Transfer destination user', 'path')
   @app.param('userB', 'Transfer destination user', 'path')
-  @app.response(HTTPStatus.OK, 'Successfuly transfered the call')
+  @app.response(HTTPStatus.OK, 'Json reply')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
-  @app.response(HTTPStatus.NOT_FOUND, 'User does not exist, or user is not bound to device, \
-                                       or device is offline, or no current call for user')
-  @app.response(HTTPStatus.BAD_REQUEST, 'Transfer failed somehow or other AMI error')
   async def get(self, userA, userB):
   async def get(self, userA, userB):
     '''Attended call transfer
     '''Attended call transfer
     '''
     '''
     device = await getUserDevice(userA)
     device = await getUserDevice(userA)
     if device in NONEs:
     if device in NONEs:
-      abort(HTTPStatus.NOT_FOUND)
+      return noUserDevice(userA)
     channel = await amiDeviceChannel(device)
     channel = await amiDeviceChannel(device)
     if channel in NONEs:
     if channel in NONEs:
-      abort(HTTPStatus.NOT_FOUND)
+      return noUserChannel(userA)
     reply = await manager.send_action({'Action':'Atxfer',
     reply = await manager.send_action({'Action':'Atxfer',
                                        'Channel':channel,
                                        'Channel':channel,
                                        'async':'false',
                                        'async':'false',
                                        'Exten':userB})
                                        'Exten':userB})
-    if (isinstance(reply, Message) and reply.success):
-      return '', HTTPStatus.OK
-    abort(HTTPStatus.BAD_REQUEST)
+    if isinstance(reply, Message):
+      if reply.success:
+        return successfullyTransfered(userA, userB)
+      else:
+        return errorReply(reply.message)
 
 
 @app.route('/users/states')
 @app.route('/users/states')
 class UsersStates(Resource):
 class UsersStates(Resource):
-  @app.response(HTTPStatus.OK, 'JSON map of form {user1:state1, user2:state2, ...}')
+  @app.response(HTTPStatus.OK, 'JSON reply with user:state map or error message')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   async def get(self):
   async def get(self):
-    '''Returns all users with their states.
+    '''Returns all users with their combined states.
     Possible states are: available, away, dnd, inuse, busy, unavailable, ringing
     Possible states are: available, away, dnd, inuse, busy, unavailable, ringing
     '''
     '''
-    await refreshStatesCache()
-    return getUsersStatesCombined()
+    usersCount = await refreshStatesCache()
+    if usersCount == 0:
+      return stateCacheEmpty()
+    return successReply(getUsersStatesCombined())
+
+@app.route('/user/<user>/state')
+class UserState(Resource):
+  @app.param('user', 'User to query for combined state', 'path')
+  @app.response(HTTPStatus.OK, 'JSON data {"user":user,"state":state}')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self, user):
+    '''Returns user's combined state.
+    One of: available, away, dnd, inuse, busy, unavailable, ringing
+    '''
+    if user not in app.config['STATE_CACHE']['user']:
+      return noUser(user)
+    return successReply({'user':user,'state':getUserStateCombined(user)})
 
 
 @app.route('/user/<user>/presence')
 @app.route('/user/<user>/presence')
-class UserPresenceState(Resource):
+class PresenceState(Resource):
   @app.param('user', 'User to query for presence state', 'path')
   @app.param('user', 'User to query for presence state', 'path')
-  @app.response(HTTPStatus.OK, 'Presence state string')
+  @app.response(HTTPStatus.OK, 'JSON data {"user":user,"state":state}')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
-  @app.response(HTTPStatus.NOT_FOUND, 'User does not exist')
-  @app.response(HTTPStatus.BAD_REQUEST, 'AMI error')
   async def get(self, user):
   async def get(self, user):
     '''Returns user's presence state.
     '''Returns user's presence state.
-    One of: not_set | unavailable | available | away | xa | chat | dnd
+    One of: not_set, unavailable, available, away, xa, chat, dnd
     '''
     '''
-    cidnum = await getUserCID(user) # Check if user exists in astdb
-    if cidnum is None:
-      abort(HTTPStatus.NOT_FOUND)
-    state = await amiPresenceState(user)
-    if state is None:
-      abort(HTTPStatus.BAD_REQUEST)
-    return state
+    if user not in app.config['STATE_CACHE']['user']:
+      return noUser(user)
+    return successReply({'user':user,'state':app.config['STATE_CACHE']['presence'].get(user, 'not_set')})
 
 
 @app.route('/user/<user>/presence/<state>')
 @app.route('/user/<user>/presence/<state>')
-class SetUserPresenceState(Resource):
+class SetPresenceState(Resource):
   @app.param('user', 'Target user to set the presence state', 'path')
   @app.param('user', 'Target user to set the presence state', 'path')
   @app.param('state',
   @app.param('state',
-             'The presence state to set for user, one of: not_set, unavailable, available, away, xa, chat or dnd',
+             'The presence state for user, one of: not_set, unavailable, available, away, xa, chat or dnd',
              'path')
              'path')
-  @app.response(HTTPStatus.OK, 'Successfuly set the presence state')
+  @app.response(HTTPStatus.OK, 'Json reply')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
-  @app.response(HTTPStatus.NOT_FOUND, 'User does not exist')
-  @app.response(HTTPStatus.BAD_REQUEST, 'Wrong state string ot other AMI error')
   async def get(self, user, state):
   async def get(self, user, state):
     '''Sets user's presence state.
     '''Sets user's presence state.
     Allowed states: not_set | unavailable | available | away | xa | chat | dnd
     Allowed states: not_set | unavailable | available | away | xa | chat | dnd
     '''
     '''
-    cidnum = await getUserCID(user) # Check if user exists in astdb
-    if cidnum is None:
-      abort(HTTPStatus.NOT_FOUND)
-    if await amiSetVar('PRESENCE_STATE(CustomPresence:{})'.format(user),
-                       state):
-      return '', HTTPStatus.OK
-    else:
-      abort(HTTPStatus.BAD_REQUEST)
+    if state not in _pstates:
+      return invalidState(state)
+    if user not in app.config['STATE_CACHE']['user']:
+      return noUser(user)
+    result = await amiSetVar('PRESENCE_STATE(CustomPresence:{})'.format(user), state)
+    if result is not None:
+      return errorReply(result)
+    return successfullySetState(user, state)
+
+@app.route('/users/devices')
+class UsersDevices(Resource):
+  @app.response(HTTPStatus.OK, 'JSON reply with user:device map or error message')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self):
+    '''Returns all users with their combined states.
+    Possible states are: available, away, dnd, inuse, busy, unavailable, ringing
+    '''
+    data = {}
+    for user in app.config['STATE_CACHE']['user']:
+      device = await getUserDevice(user)
+      if device in NONEs:
+        device = None
+      data[user]=device
+    return successReply(data)
 
 
 @app.route('/device/<device>/<user>/on')
 @app.route('/device/<device>/<user>/on')
 @app.route('/user/<user>/<device>/on')
 @app.route('/user/<user>/<device>/on')
@@ -562,15 +602,14 @@ class UserDeviceBind(Resource):
     Any device user was previously bound to, is unbound.
     Any device user was previously bound to, is unbound.
     Any user previously bound to device is unbound also.
     Any user previously bound to device is unbound also.
     '''
     '''
-    cidnum = await getUserCID(user) # Check if user exists in astdb
-    if cidnum is None:
+    if user not in app.config['STATE_CACHE']['user']:
       return noUser(user)
       return noUser(user)
     dial = await getDeviceDial(device) # Check if device exists in astdb
     dial = await getDeviceDial(device) # Check if device exists in astdb
     if dial is None:
     if dial is None:
       return noDevice(device)
       return noDevice(device)
     currentUser = await getDeviceUser(device) # Check if any user is already bound to device
     currentUser = await getDeviceUser(device) # Check if any user is already bound to device
     if currentUser == user:
     if currentUser == user:
-      return beenBound(user, device)
+      return alreadyBound(user, device)
     ast = await getGlobalVars()
     ast = await getGlobalVars()
     queues = await amiQueues()
     queues = await amiQueues()
     if currentUser not in NONEs: # If any other user is bound to device, unbind him,
     if currentUser not in NONEs: # If any other user is bound to device, unbind him,
@@ -587,7 +626,7 @@ class UserDeviceBind(Resource):
     await setUserDeviceStates(user, device, queues, ast) # Set device states for users new device
     await setUserDeviceStates(user, device, queues, ast) # Set device states for users new device
     if not (await setUserDevice(user, device)): # Bind device to user
     if not (await setUserDevice(user, device)): # Bind device to user
       return bindError(user, device)
       return bindError(user, device)
-    return beenBound(user, device)
+    return successfullyBound(user, device)
 
 
 @app.route('/device/<device>/off')
 @app.route('/device/<device>/off')
 class DeviceUnBind(Resource):
 class DeviceUnBind(Resource):
@@ -612,7 +651,7 @@ class DeviceUnBind(Resource):
         await setQueueStates(queues, currentUser, device, 'NOT_INUSE')
         await setQueueStates(queues, currentUser, device, 'NOT_INUSE')
       await setUserHint(currentUser, None, ast) # set hints for current user
       await setUserHint(currentUser, None, ast) # set hints for current user
     await setDeviceUser(device, 'none') # Unbind user from device
     await setDeviceUser(device, 'none') # Unbind user from device
-    return beenUnbound(currentUser, device)
+    return successfullyUnbound(currentUser, device)
 
 
 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'])

+ 60 - 11
app/utils.py

@@ -4,6 +4,24 @@ from panoramisk import Message
 
 
 TRUEs = ('true', '1', 'y', 'yes')
 TRUEs = ('true', '1', 'y', 'yes')
 NONEs = (None,'none','')
 NONEs = (None,'none','')
+# userstate presencestate maping:
+_ustates = ['idle',       'inuse', 'busy', 'unavailable', 'ringing', 'inuse&ringing','hold', 'inuse&hold'] #presence:
+_states = [['available',   'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy'],      #not_set
+           ['unavailable', 'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy'],      #unavailable
+           ['available',   'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy'],      #available
+           ['away',        'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy'],      #away
+           ['available',   'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy'],      #xa
+           ['available',   'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy'],      #chat
+           ['dnd',         'busy', 'busy', 'unavailable', 'ringing', 'busy',         'busy', 'busy']]      #dnd
+_pstates = ['not_set',
+            'unavailable',
+            'available',
+            'away',
+            'xa',
+            'chat',
+            'dnd']
+# combinedStates[userstate][presencestate]=combinedState
+combinedStates = {_u: {_p: _states[_pstates.index(_p)][_ustates.index(_u)] for _p in _pstates} for _u in _ustates}
 NO_AUTH_ROUTES = ('/ui','/openapi.json','/favicon.ico')
 NO_AUTH_ROUTES = ('/ui','/openapi.json','/favicon.ico')
 SWAGGER_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.37.2/swagger-ui-bundle.js"
 SWAGGER_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.37.2/swagger-ui-bundle.js"
 SWAGGER_CSS_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.37.2/swagger-ui.min.css"
 SWAGGER_CSS_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.37.2/swagger-ui.min.css"
@@ -65,26 +83,57 @@ class GlobalVars:
   def d(self):
   def d(self):
     return asdict(self)
     return asdict(self)
 
 
-def jsonAPIReply(success, result):
-  return {'success':success, 'result': result}
+def jsonAPIReply(status='success', data=None, message=None):
+  return {'status':status, 'data': data, 'message': message}
+
+def errorReply(message=None):
+  return jsonAPIReply(status='error', data=None, message=message)
+
+def successReply(data=None,message=None):
+  return jsonAPIReply(status='success', data=data, message=message)
 
 
 def noUser(user):
 def noUser(user):
-  return jsonAPIReply(False, 'user {} does not exist'.format(user))
+  return errorReply('User {} does not exist'.format(user))
 
 
 def noDevice(device):
 def noDevice(device):
-  return jsonAPIReply(False, 'device {} does not exist'.format(device))
+  return errorReply('Device {} does not exist'.format(device))
+
+def noUserDevice(user):
+  return errorReply('User {} does not exist or is not bound to device'.format(user))
+
+def noUserChannel(user):
+  return errorReply('User {} does not have active calls'.format(user))
+
+def stateCacheEmpty():
+  return errorReply('Users states cache update failed')
 
 
-def beenBound(user, device):
-  return jsonAPIReply(True, '{} is bound to {}'.format(user, device))
+def invalidState(state):
+  return errorReply('Invalid state "{}" provided'.format(state))
+
+def alreadyBound(user, device):
+  return errorReply('User {} is already bound to device {}'.format(user, device))
 
 
 def bindError(user, device):
 def bindError(user, device):
-  return jsonAPIReply(False, 'Failed binding {} to {}'.format(user, device))
+  return errorReply('Failed binding user {} to device {}'.format(user, device))
 
 
 def hintError(user, device):
 def hintError(user, device):
-  return jsonAPIReply(False, 'Failed setting hint for {}@{}'.format(user, device))
+  return errorReply('Failed setting hint for user {} on device {}'.format(user, device))
 
 
 def noUserBound(device):
 def noUserBound(device):
-  return jsonAPIReply(False, 'no user is bound to {}'.format(device))
+  return errorReply('No user is bound to device {}'.format(device))
+
+def successfullyTransfered(userA, userB):
+  return successReply({'userA':userA,'userB':userB},
+                      'Call was successfully transfered from user {} to user {}'.format(userA, userB))
+
+def successfullyBound(user, device):
+  return successReply({'user':user,'device':device},
+                      'User {} is successfully bound to device {}'.format(user, device))
+
+def successfullyUnbound(user, device):
+  return successReply({'user':user,'device':device},
+                      'User {} was successfully unbound from device {}'.format(user, device))
 
 
-def beenUnbound(user, device):
-  return jsonAPIReply(True, '{} unbound from {}'.format(user, device))
+def successfullySetState(user, state):
+  return successReply({'user':user,'state':state},
+                      'State "{}" was successfully set for user {}'.format(state, user))