Browse Source

epoch in user shnge state webhook

Hal De 2 years ago
parent
commit
d04da296b5
1 changed files with 93 additions and 48 deletions
  1. 93 48
      app/app.py

+ 93 - 48
app/app.py

@@ -6,6 +6,7 @@ import logging
 import os
 import os
 import re
 import re
 import json
 import json
+import time
 from datetime import date as date
 from datetime import date as date
 from datetime import datetime as dt
 from datetime import datetime as dt
 from datetime import timedelta as td
 from datetime import timedelta as td
@@ -101,7 +102,8 @@ app.cache = {'devices':{},
              'queues':{},
              'queues':{},
              'calls':{},
              'calls':{},
              'cel_queue_calls':{},
              'cel_queue_calls':{},
-             'cel_calls':{}}
+             'cel_calls':{},
+             'dead_callbacks':{}}
 
 
 manager = Manager(
 manager = Manager(
   loop=main_loop,
   loop=main_loop,
@@ -153,24 +155,26 @@ async def reloadCallback(mngr: Manager, msg: Message):
 async def extensionStatusCallback(mngr: Manager, msg: Message):
 async def extensionStatusCallback(mngr: Manager, msg: Message):
   user = msg.exten
   user = msg.exten
   state = msg.statustext.lower()
   state = msg.statustext.lower()
+  epoch = time.time_ns()
   #app.logger.warning('ExtensionStatus({}, {})'.format(user, state))
   #app.logger.warning('ExtensionStatus({}, {})'.format(user, state))
   if user in app.cache['ustates']:
   if user in app.cache['ustates']:
     prevState = getUserStateCombined(user)
     prevState = getUserStateCombined(user)
     app.cache['ustates'][user] = state
     app.cache['ustates'][user] = state
     combinedState = getUserStateCombined(user)
     combinedState = getUserStateCombined(user)
     if combinedState != prevState:
     if combinedState != prevState:
-      await userStateChangeCallback(user, combinedState, prevState)
+      await userStateChangeCallback(user, combinedState,epoch, 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()
+  epoch = time.time_ns()
   if user in app.cache['ustates']:
   if user in app.cache['ustates']:
     prevState = getUserStateCombined(user)
     prevState = getUserStateCombined(user)
     app.cache['pstates'][user] = state
     app.cache['pstates'][user] = state
     combinedState = getUserStateCombined(user)
     combinedState = getUserStateCombined(user)
     if combinedState != prevState:
     if combinedState != prevState:
-      await userStateChangeCallback(user, combinedState, prevState)
+      await userStateChangeCallback(user, combinedState, epoch, prevState)
 
 
 @manager.register_event('Hangup')
 @manager.register_event('Hangup')
 async def hangupCallback(mngr: Manager, msg: Message):
 async def hangupCallback(mngr: Manager, msg: Message):
@@ -344,6 +348,7 @@ async def celCallback(mngr: Manager, msg: Message):
            if (msg.linkedid in  app.cache['cel_calls']):
            if (msg.linkedid in  app.cache['cel_calls']):
              fillCallbackParameters(_cb,app.cache['cel_calls'][msg.linkedid],cid)
              fillCallbackParameters(_cb,app.cache['cel_calls'][msg.linkedid],cid)
            reply = await doCallback(device, _cb)
            reply = await doCallback(device, _cb)
+           reply = await doCallback('user_calling', _cb)
     if ((msg.Application == 'Queue') and
     if ((msg.Application == 'Queue') and
         (msg.EventName == 'APP_START') and 
         (msg.EventName == 'APP_START') and 
         (firstMessage.Context.startswith('from-internal')) and 
         (firstMessage.Context.startswith('from-internal')) and 
@@ -563,9 +568,9 @@ async def getCallInfo(callid):
     
     
   _q = '''SELECT *
   _q = '''SELECT *
           FROM cdr 
           FROM cdr 
-          WHERE linkedid=:linkedid or linkedid in (select distinct linkedid from cdr where uniqueid in :uniqueids)
+          WHERE  linkedid in (select distinct linkedid from cdr where uniqueid in :uniqueids)
           ORDER BY sequence;'''
           ORDER BY sequence;'''
-  _v = {'linkedid': linkedid, 'uniqueids': uniqueids}
+  _v = {'uniqueids': uniqueids}
   #app.logger.warning('values {}'.format(_v)) 
   #app.logger.warning('values {}'.format(_v)) 
   _f = True
   _f = True
   unique_ids = set()
   unique_ids = set()
@@ -634,20 +639,45 @@ async def getUserCDR(user,
 async def getCEL(start=None, end=None, table='cel', field='eventtime', sort='id'):
 async def getCEL(start=None, end=None, table='cel', field='eventtime', sort='id'):
   return await getCDR(start, end, table, field, sort)
   return await getCDR(start, end, table, field, sort)
 
 
+async def getCallback(callback):
+  row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
+                               values={'device': callback})
+  if row is not None:
+      return row['url']
+  return False
+
+async def replaceCallback(callback, url):
+  await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
+                       values={'device': callback,'url': url})
+  
+async def addCallback(callback, url):
+  await db.execute(query='update callback_urls set url = CONCAT(url,";",:url) where device= :device and  locate(:url,url) in (0,NULL)',
+                       values={'device': callback,'url': url})
+  
 async def doCallback(entity, msg):
 async def doCallback(entity, msg):
+  reply = None
   row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device', values={'device': entity})
   row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device', values={'device': entity})
   if ((row is not None) and (row['url'].startswith('http')) and ('blackhole' not in row['url'])):
   if ((row is not None) and (row['url'].startswith('http')) and ('blackhole' not in row['url'])):
-    app.logger.warning(f'''POST {row['url']} data: {str(msg)}''')
+#    if (row['url'] in app.cache['dead_callbacks']):
+#      if (app.cache['dead_callbacks'][row['url']] > dt.now()):
+#        app.logger.warning(f'''Skipping CALLBACK URL {row['url']} (disabled for {(app.cache['dead_callbacks'][row['url']]-dt.now()).total_seconds()} seconds)''')
+#        return None
+#      else:
+#        del app.cache['dead_callbacks'][row['url']]
+    app.logger.warning(f'''CALLBACK URL {row['url']} ''')
     if not 'HTTP_CLIENT' in app.config:
     if not 'HTTP_CLIENT' in app.config:
       await initHttpClient()
       await initHttpClient()
-    try:
-      reply = await app.config['HTTP_CLIENT'].post(row['url'], json=msg)
-      return reply
-    except Exception as e:
-      app.logger.warning('callback error {}'.format(row['url']))
+    for u in row['url'].split(";"):
+      app.logger.warning(f'''POST {u} data: {str(msg)}''')
+      try:
+        reply = await app.config['HTTP_CLIENT'].post(u, json=msg)
+        #return reply
+      except Exception as e:
+        app.cache['dead_callbacks'][row['url']] = dt.now() + td(minutes=15)
+        app.logger.warning(f'''callback error {u} data: {str(msg)} error {str(e)}''')
   else:
   else:
     app.logger.warning('No callback url defined for {}'.format(entity))
     app.logger.warning('No callback url defined for {}'.format(entity))
-  return None
+  return reply
 
 
 async def doCallbackPostfix(entity,postfix, msg):
 async def doCallbackPostfix(entity,postfix, msg):
   row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device', values={'device': entity})
   row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device', values={'device': entity})
@@ -1207,7 +1237,7 @@ async def rebindLostDevices():
   app.cache['usermap'] = copy.deepcopy(usermap)
   app.cache['usermap'] = copy.deepcopy(usermap)
   app.cache['devicemap'] = copy.deepcopy(devicemap)
   app.cache['devicemap'] = copy.deepcopy(devicemap)
 
 
-async def userStateChangeCallback(user, state, prevState = None):
+async def userStateChangeCallback(user, state, epoch, prevState = None):
   reply = None
   reply = None
   device = None
   device = None
   if (user in app.cache['devicemap']):
   if (user in app.cache['devicemap']):
@@ -1215,12 +1245,14 @@ async def userStateChangeCallback(user, state, prevState = None):
     if device is not None:
     if device is not None:
       _cb = {'webhook_name': 'user_status',
       _cb = {'webhook_name': 'user_status',
              'user': user,
              'user': user,
+             'epoch': epoch,
              'state': state,
              'state': state,
              'prev_state':prevState}
              'prev_state':prevState}
       reply = await doCallback(device, _cb)
       reply = await doCallback(device, _cb)
 
 
   _cb = {'webhook_name': 'user_status',
   _cb = {'webhook_name': 'user_status',
              'user': user,
              'user': user,
+             'epoch': epoch,
              'state': state,
              'state': state,
              'prev_state':prevState}
              'prev_state':prevState}
   reply = await doCallback('user_status', _cb)
   reply = await doCallback('user_status', _cb)
@@ -1769,6 +1801,7 @@ class DeviceCallback(Resource):
     if url is not None:
     if url is not None:
       await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
       await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
                        values={'device': device,'url': url})
                        values={'device': device,'url': url})
+      app.logger.warning(f'''set callback for {device}: {url}''')
     else:
     else:
       row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
       row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
                                values={'device': device})
                                values={'device': device})
@@ -1780,6 +1813,7 @@ class DeviceCallback(Resource):
 class GroupRingingCallback(Resource):
 class GroupRingingCallback(Resource):
   @authRequired
   @authRequired
   @app.param('url', 'used to set the Callback url for the group ringing callback', 'query')
   @app.param('url', 'used to set the Callback url for the group ringing callback', 'query')
+  @app.param('add', 'set to 1 for adding this url instad of replace', 'query')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   async def get(self):
   async def get(self):
@@ -1788,20 +1822,22 @@ class GroupRingingCallback(Resource):
     if not request.admin:
     if not request.admin:
       abort(401)
       abort(401)
     url = request.args.get('url', None)
     url = request.args.get('url', None)
+    add = bool(int(request.args.get('add', 0)))
+    callback = 'groupRinging'
     if url is not None:
     if url is not None:
-      await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
-                       values={'device': 'groupRinging','url': url})
+      if add:
+        await addCallback(callback,url)
+      else:
+        await replaceCallback(callback,url)
     else:
     else:
-      row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
-                               values={'device': 'groupRinging'})
-      if row is not None:
-        url = row['url']
-    return successCommonCallbackURL('groupRinging', url)
+      url = await getCallback(callback)
+    return successCommonCallbackURL(callback, url)
 
 
 @app.route('/group/answered/callback')
 @app.route('/group/answered/callback')
 class GroupAnsweredCallback(Resource):
 class GroupAnsweredCallback(Resource):
   @authRequired
   @authRequired
   @app.param('url', 'used to set the Callback url for the group answered callback', 'query')
   @app.param('url', 'used to set the Callback url for the group answered callback', 'query')
+  @app.param('add', 'set to 1 for adding this url instad of replace', 'query')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   async def get(self):
   async def get(self):
@@ -1810,20 +1846,22 @@ class GroupAnsweredCallback(Resource):
     if not request.admin:
     if not request.admin:
       abort(401)
       abort(401)
     url = request.args.get('url', None)
     url = request.args.get('url', None)
+    add = bool(int(request.args.get('add', 0)))
+    callback = 'groupAnswered'
     if url is not None:
     if url is not None:
-      await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
-                       values={'device': 'groupAnswered','url': url})
+      if add :
+        await addCallback(callback,url)
+      else:
+        await replaceCallback(callback,url)
     else:
     else:
-      row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
-                               values={'device': 'groupAnswered'})
-      if row is not None:
-        url = row['url']
-    return successCommonCallbackURL('groupAnswered', url)
+      url = await getCallback(callback)
+    return  successCommonCallbackURL(callback, url)
 
 
 @app.route('/queue/enter/callback')
 @app.route('/queue/enter/callback')
 class QueueEnterCallback(Resource):
 class QueueEnterCallback(Resource):
   @authRequired
   @authRequired
   @app.param('url', 'used to set the Callback url for the queue enter callback', 'query')
   @app.param('url', 'used to set the Callback url for the queue enter callback', 'query')
+  @app.param('add', 'set to 1 for adding this url instad of replace', 'query')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   async def get(self):
   async def get(self):
@@ -1832,20 +1870,22 @@ class QueueEnterCallback(Resource):
     if not request.admin:
     if not request.admin:
       abort(401)
       abort(401)
     url = request.args.get('url', None)
     url = request.args.get('url', None)
+    add = bool(int(request.args.get('add', 0)))
+    callback = 'queueEnter'
     if url is not None:
     if url is not None:
-      await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
-                       values={'device': 'queueEnter','url': url})
+      if add :
+        await addCallback(callback,url)
+      else:
+        await replaceCallback(callback,url)
     else:
     else:
-      row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
-                               values={'device': 'queueEnter'})
-      if row is not None:
-        url = row['url']
-    return successCommonCallbackURL('queueEnter', url)
+      url = await getCallback(callback)
+    return  successCommonCallbackURL(callback, url)
 
 
 @app.route('/queue/leave/callback')
 @app.route('/queue/leave/callback')
 class QueueLeaveCallback(Resource):
 class QueueLeaveCallback(Resource):
   @authRequired
   @authRequired
   @app.param('url', 'used to set the Callback url for the queue leave callback', 'query')
   @app.param('url', 'used to set the Callback url for the queue leave callback', 'query')
+  @app.param('add', 'set to 1 for adding this url instad of replace', 'query')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   async def get(self):
   async def get(self):
@@ -1854,15 +1894,16 @@ class QueueLeaveCallback(Resource):
     if not request.admin:
     if not request.admin:
       abort(401)
       abort(401)
     url = request.args.get('url', None)
     url = request.args.get('url', None)
+    add = bool(int(request.args.get('add', 0)))
+    callback = 'queueLeave'
     if url is not None:
     if url is not None:
-      await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
-                       values={'device': 'queueLeave','url': url})
+      if add :
+        await addCallback(callback,url)
+      else:
+        await replaceCallback(callback,url)
     else:
     else:
-      row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
-                               values={'device': 'queueLeave'})
-      if row is not None:
-        url = row['url']
-    return successCommonCallbackURL('queueLeave', url)
+      url = await getCallback(callback)
+    return  successCommonCallbackURL(callback, url)
 
 
 
 
 @app.route('/callback/<type>')
 @app.route('/callback/<type>')
@@ -1870,6 +1911,7 @@ class CommonCallback(Resource):
   @authRequired
   @authRequired
   @app.param('type', 'Callback type', 'path')
   @app.param('type', 'Callback type', 'path')
   @app.param('url', 'used to set the Callback url for the autocall', 'query')
   @app.param('url', 'used to set the Callback url for the autocall', 'query')
+  @app.param('add', 'set to 1 for adding this url instad of replace', 'query')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
   async def get(self,type):
   async def get(self,type):
@@ -1878,15 +1920,18 @@ class CommonCallback(Resource):
     if not request.admin:
     if not request.admin:
       abort(401)
       abort(401)
     url = request.args.get('url', None)
     url = request.args.get('url', None)
+    add = bool(int(request.args.get('add', 0)))
+    callback = type
     if url is not None:
     if url is not None:
-      await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
-                       values={'device': type,'url': url})
+      if add :
+        await addCallback(callback,url)
+      else:
+        await replaceCallback(callback,url)
     else:
     else:
-      row = await db.fetch_one(query='SELECT url FROM callback_urls WHERE device = :device',
-                               values={'device': type})
-      if row is not None:
-        url = row['url']
-    return successCommonCallbackURL(type, url)
+      url = await getCallback(callback)
+    return  successCommonCallbackURL(callback, url)
+
+
 
 
 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'])