浏览代码

JSON replys redesign and couple more AMI actions

Hal De 4 年之前
父节点
当前提交
638f95f2f8
共有 2 个文件被更改,包括 174 次插入86 次删除
  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
   state = msg.statustext.lower()
   if user in app.config['STATE_CACHE']['user']:
-    _combinedState = getUserStateCombined(user)
+    prevState = getUserStateCombined(user)
     app.config['STATE_CACHE']['user'][user] = state
     combinedState = getUserStateCombined(user)
-    if combinedState != _combinedState:
-      await userStateChangeCallback(user, combinedState)
+    if combinedState != prevState:
+      await userStateChangeCallback(user, combinedState, prevState)
 
 @manager.register_event('PresenceStatus')
 async def presenceStatusCallback(mngr: Manager, msg: Message):
   user = msg.exten #hint = msg.hint
   state = msg.status.lower()
   if user in app.config['STATE_CACHE']['user']:
-    _combinedState = getUserStateCombined(user)
+    prevState = getUserStateCombined(user)
     app.config['STATE_CACHE']['presence'][user] = state
     combinedState = getUserStateCombined(user)
-    if combinedState != _combinedState:
-      await userStateChangeCallback(user, combinedState)
+    if combinedState != prevState:
+      await userStateChangeCallback(user, combinedState, prevState)
 
 @app.before_first_request
 async def initHttpClient():
@@ -158,6 +158,30 @@ async def amiGetVar(variable):
   app.logger.warning('GetVar({})->{}'.format(variable, 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):
   '''AMI SetVar
   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
 
   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',
                                      'Variable': variable,
                                      'Value': 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):
   '''AMI DBGet
@@ -270,14 +297,17 @@ async def amiPresenceState(user):
     user (string): user
 
   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',
                                      'Provider': 'CustomPresence:{}'.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():
   states = {}
@@ -438,34 +468,22 @@ async def refreshStatesCache():
   app.config['STATE_CACHE']['presence'] = await amiPresenceStateList()
   return len(app.config['STATE_CACHE']['user'])
 
-async def userStateChangeCallback(user, state):
+async def userStateChangeCallback(user, state, prevState = None):
   reply = None
   if ((app.config['STATE_CALLBACK_URL'] not in NONEs) and
       ('HTTP_CLIENT' in app.config)):
     reply = await app.config['HTTP_CLIENT'].post(app.config['STATE_CALLBACK_URL'],
                                                  json={'user': user,
-                                                       'state': state})
+                                                       'state': state,
+                                                       'prev_state':prevState})
   else:
     app.logger.warning('{} changed state to: {}'.format(user, state))
   return reply
 
 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():
   return {user:getUserStateCombined(user) for user in app.config['STATE_CACHE']['user']}
@@ -474,80 +492,102 @@ def getUsersStatesCombined():
 class AtXfer(Resource):
   @app.param('userA', 'User initiating the attended transfer', '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.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):
     '''Attended call transfer
     '''
     device = await getUserDevice(userA)
     if device in NONEs:
-      abort(HTTPStatus.NOT_FOUND)
+      return noUserDevice(userA)
     channel = await amiDeviceChannel(device)
     if channel in NONEs:
-      abort(HTTPStatus.NOT_FOUND)
+      return noUserChannel(userA)
     reply = await manager.send_action({'Action':'Atxfer',
                                        'Channel':channel,
                                        'async':'false',
                                        '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')
 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')
   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
     '''
-    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')
-class UserPresenceState(Resource):
+class PresenceState(Resource):
   @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.NOT_FOUND, 'User does not exist')
-  @app.response(HTTPStatus.BAD_REQUEST, 'AMI error')
   async def get(self, user):
     '''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>')
-class SetUserPresenceState(Resource):
+class SetPresenceState(Resource):
   @app.param('user', 'Target user to set the presence state', 'path')
   @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')
-  @app.response(HTTPStatus.OK, 'Successfuly set the presence state')
+  @app.response(HTTPStatus.OK, 'Json reply')
   @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):
     '''Sets user's presence state.
     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('/user/<user>/<device>/on')
@@ -562,15 +602,14 @@ class UserDeviceBind(Resource):
     Any device user was previously bound to, is unbound.
     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)
     dial = await getDeviceDial(device) # Check if device exists in astdb
     if dial is None:
       return noDevice(device)
     currentUser = await getDeviceUser(device) # Check if any user is already bound to device
     if currentUser == user:
-      return beenBound(user, device)
+      return alreadyBound(user, device)
     ast = await getGlobalVars()
     queues = await amiQueues()
     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
     if not (await setUserDevice(user, device)): # Bind device to user
       return bindError(user, device)
-    return beenBound(user, device)
+    return successfullyBound(user, device)
 
 @app.route('/device/<device>/off')
 class DeviceUnBind(Resource):
@@ -612,7 +651,7 @@ class DeviceUnBind(Resource):
         await setQueueStates(queues, currentUser, device, 'NOT_INUSE')
       await setUserHint(currentUser, None, ast) # set hints for current user
     await setDeviceUser(device, 'none') # Unbind user from device
-    return beenUnbound(currentUser, device)
+    return successfullyUnbound(currentUser, device)
 
 manager.connect()
 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')
 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')
 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"
@@ -65,26 +83,57 @@ class GlobalVars:
   def d(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):
-  return jsonAPIReply(False, 'user {} does not exist'.format(user))
+  return errorReply('User {} does not exist'.format(user))
 
 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):
-  return jsonAPIReply(False, 'Failed binding {} to {}'.format(user, device))
+  return errorReply('Failed binding user {} to device {}'.format(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):
-  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))