Hal De 3 rokov pred
rodič
commit
66c6c38b7b
1 zmenil súbory, kde vykonal 501 pridanie a 61 odobranie
  1. 501 61
      app0/app.py

+ 501 - 61
app0/app.py

@@ -1,11 +1,15 @@
 #!/usr/bin/env python3
+
 import asyncio
+import aiohttp
 import logging
 import os
 import re
 import json
+from datetime import date as date
 from datetime import datetime as dt
 from datetime import timedelta as td
+from datetime import timezone as dtz
 from typing import Any, Optional
 from functools import wraps
 from secrets import compare_digest
@@ -20,11 +24,13 @@ from cel import *
 from logging.config import dictConfig
 from pprint import pformat
 from inspect import getmembers
+from aiohttp.resolver import AsyncResolver
+import copy
 
 class ApiJsonEncoder(JSONEncoder):
   def default(self, o):
     if isinstance(o, dt):
-      return o.isoformat()
+      return o.astimezone(dtz.utc).replace(tzinfo=None).isoformat() + 'Z'
     if isinstance(o, CdrChannel):
       return str(o)
     if isinstance(o, CdrEvent):
@@ -146,7 +152,7 @@ async def reloadCallback(mngr: Manager, msg: Message):
 async def extensionStatusCallback(mngr: Manager, msg: Message):
   user = msg.exten
   state = msg.statustext.lower()
-  app.logger.warning('ExtensionStatus({}, {})'.format(user, state))
+  #app.logger.warning('ExtensionStatus({}, {})'.format(user, state))
   if user in app.cache['ustates']:
     prevState = getUserStateCombined(user)
     app.cache['ustates'][user] = state
@@ -170,9 +176,18 @@ async def hangupCallback(mngr: Manager, msg: Message):
   if msg.uniqueid in app.cache['calls']:
     del app.cache['calls'][msg.uniqueid]
 
+@manager.register_event('VarSet')
+async def VarSetCallback(mngr: Manager, msg: Message):
+  m = re.search(r"(.*savedb_)(.*)", msg.variable)
+  if (m):
+    varname = m.group(2)
+    value = msg.value
+    app.logger.warning('insert into call_values {}, {}, {}'.format(msg.linkedid,varname,value))
+    await db.execute(query='insert into call_values (linkedid,name,value) values (:linkedid,:name,:value);',values={'linkedid': msg.linkedid,'name': varname,'value':value})
+
 @manager.register_event('Newchannel')
 async def newchannelCallback(mngr: Manager, msg: Message):
-  if (msg.channelstate == '4'):
+  if (msg.channelstate == '4') and ('HTTP_CLIENT' in app.config):
     did = None
     cid = None
     user = None
@@ -204,70 +219,168 @@ async def newchannelCallback(mngr: Manager, msg: Message):
              'callerId': cid,
              'did': did,
              'callId': uid}
-      if ('WebCallId' in app.cache['calls'][msg.linkedid]):
-        _cb['WebCallId'] = app.cache['calls'][msg.linkedid]['WebCallId']
-      reply = await doCallback(device, _cb)
+      if (msg.linkedid in  app.cache['cel_calls']):
+        fillCallbackParameters(_cb,app.cache['cel_calls'][msg.linkedid],cid)
+      #reply = await doCallback(device, _cb)
+
+def fillCallbackParameters(cb,call,cid):
+    if ('WebCallId' in call):
+      cb['WebCallId'] = call['WebCallId']
+    if ('CallerNumber' in call):
+      cb['CallerNumber'] = call['CallerNumber']
+    elif ('AlertInfo' in call):
+      cb['CallerNumber'] = call['AlertInfo']
+    if  ('BNumber' in call):
+      cb['BNumber'] = call['BNumber']
+    elif ('AlertInfo' in call):
+      cb['BNumber'] = call['AlertInfo']
+    if ('ANumber' in call):
+      cb['ANumber'] = call['ANumber']
+    elif call.Context.startswith('from-pstn'):
+      cb['ANumber'] = cid
+
+async def getVariableFromPJSIP(callid,channel,variable_source,variable_destination=None):
+    if (variable_destination == None):
+      variable_destination = variable_source
+    value = await amiChannelGetVar(channel,"PJSIP_HEADER(read,{})".format(variable_source))
+    if (value):
+      app.logger.warning('set {}={} for {} from header'.format(variable_destination,value,callid))
+      app.cache['cel_calls'][callid][variable_destination] = value
+        
+async def getVariableFromChannel(callid,channel,variable_source,variable_destination=None):
+    if (variable_destination == None):
+      variable_destination = variable_source
+    value = await amiChannelGetVar(channel,variable_source)
+    if (value):
+      app.logger.warning('set {}={} for {} from channel'.format(variable_destination,value,callid))
+      app.cache['cel_calls'][callid][variable_destination] = value
 
 @manager.register_event('CEL')
 async def celCallback(mngr: Manager, msg: Message):
-  app.logger.warning('CEL {}'.format(msg))
+  #app.logger.warning('CEL {}'.format(msg))
   lid = msg.LinkedID
+  if (msg.EventName == 'ATTENDEDTRANSFER'):
+    #app.logger.warning('CEL {}'.format(msg))
+    extra = json.loads(msg.Extra)
+    if ('transferee_channel_uniqueid' in extra) and ('channel2_uniqueid' in extra):
+      first = extra['transferee_channel_uniqueid']; #unique
+      second = extra['channel2_uniqueid']   #linked
+      firstname = extra['transferee_channel_name']
+      secondname = extra['channel2_name']
+      if (True or msg.CallerIDrdnis == '78124254209'):
+        res = await amiStopMixMonitor(firstname);
+        filename = "transfer-{}-{}-{}".format(msg.Exten, msg.CallerIDnum, msg.LinkedID);
+        res = await amiStartMixMonitor(firstname,filename);#2022/03/11/external-2534-1934-20220311-122726-1647001646.56557.wav
+        res = await amiChannelSetVar(firstname,"CDR(recordingfile)","{}.wav".format(filename))
+        #await amiStopMixMonitor(secondname);
+      app.cache['cel_calls'][lid]['transfers'].append((first,second)) #no cdr in db here
+      #app.logger.warning('first {} {}'.format(first,second))
+    
   if ((msg.EventName == 'CHAN_START') and (lid == msg.UniqueID)):    #save first msg
     app.cache['cel_calls'][lid] = msg
     app.cache['cel_calls'][lid]['current_channels'] = {}
     app.cache['cel_calls'][lid]['all_channels'] = {}
+    app.cache['cel_calls'][lid]['transfers'] = []
+    #app.cache['cel_calls'][lid]['mix_monitors'] = []
+    if (msg.Context=='from-internal'):
+      sip_call_id = await amiChannelGetVar(msg.Channel,"PJSIP_HEADER(read,UniqueId)")
+      #if( False and   not sip_call_id):
+      #  sip_call_id = await amiChannelGetVar(msg.Channel,"PJSIP_HEADER(read,Call-ID)")
+      if (sip_call_id):
+        await amiChannelSetVar(msg.Channel,"CDR(userfield)",sip_call_id)
+      await getVariableFromPJSIP(lid,msg.Channel,"WebCallId")
+      await getVariableFromPJSIP(lid,msg.Channel,"CallerNumber","ANumber")
+      #await getVariableFromPJSIP(lid,msg.Channel,"CallerNumber")
+      await getVariableFromPJSIP(lid,msg.Channel,"BNumber")
+    else:
+      await getVariableFromChannel(lid,msg.Channel,"ALERT_INFO","AlertInfo")
+    
   if (lid in app.cache['cel_calls']):
     firstMessage = app.cache['cel_calls'][lid]
     cid = firstMessage.CallerIDnum
     if firstMessage.CallerIDnum in app.cache['usermap']:
         cid = app.cache['usermap'][firstMessage.CallerIDnum]
     uid = firstMessage.LinkedID
+    if (msg.EventName == 'CHAN_START') and (lid != msg.UniqueID) and (not msg.Channel.startswith('Local/')):
+        if (msg.CallerIDnum!=''): # or (firstMessage.Context == 'from-pstn') or (firstMessage.get('groupCall',False))):#all calls 
+           device = msg.CallerIDnum
+           user = device
+           if device in app.cache['usermap']:
+             user = app.cache['usermap'][device]
+           did = firstMessage.Exten
+           _cb = {'webhook_name': 'user_calling',
+             'user': user,
+             'device': device,
+             'state': 'ringing',
+             'callerId': cid,
+             'did': did,
+             'callId': uid} 
+           if ('queueName' in app.cache['cel_calls'][msg.linkedid]):
+             _cb['queue'] = app.cache['cel_calls'][msg.linkedid]['queueName']  
+           if (msg.linkedid in  app.cache['cel_calls']):
+             fillCallbackParameters(_cb,app.cache['cel_calls'][msg.linkedid],cid)
+           reply = await doCallback(device, _cb)
     if ((msg.Application == 'Queue') and
         (msg.EventName == 'APP_START') and 
-        (firstMessage.Context == 'from-internal')):
+        (firstMessage.Context == 'from-internal') and 
+        (cid is not None) and (len(cid) < 7)):
       app.cache['cel_calls'][lid]['groupCall'] = True
+      app.cache['cel_calls'][lid]['queueName'] = msg.Exten
     if ((msg.Application == 'Queue') and
         (msg.EventName == 'APP_END') and 
-        (firstMessage.Context == 'from-internal')):
+        (firstMessage.get('groupCall',False))):
       app.cache['cel_calls'][lid]['groupCall'] = False
-    if (cid is not None) and (len(cid) < 7):                         #for local calls only
-      if msg.Context in ('from-queue'):
+      app.cache['cel_calls'][lid]['queueName'] = False
+    if (firstMessage.get('groupCall',False)):                         #for local calls only
+      if msg.Channel.startswith('PJSIP/'):
+        called = msg.CallerIDnum
+        if called in app.cache['usermap']:
+          called = app.cache['usermap'][called]
         if ((msg.EventName == 'CHAN_START') or
             ((msg.EventName == 'CHAN_END') and ('answered' not in firstMessage))):
           old_count = len(app.cache['cel_calls'][lid]['current_channels'])
-          channel = msg.Channel.split(';')[0]
+          channel = msg.Channel
           if msg.EventName == 'CHAN_START':                          #start dial
-            app.cache['cel_calls'][lid]['current_channels'][channel] = msg.Exten
-            app.cache['cel_calls'][lid]['all_channels'][channel] = msg.Exten
+            app.cache['cel_calls'][lid]['current_channels'][channel] = called
+            app.cache['cel_calls'][lid]['all_channels'][channel] = called
+            _cb = {'webhook_name':'group_user_start_calling',
+                 'user': called,
+                 'state': 'group_start_ringing',
+                 'callerId': cid,
+                 'callId': msg.UniqueID}
           else:                                                      #end dial
             app.cache['cel_calls'][uid]['current_channels'].pop(channel, False)
-          if old_count != len(app.cache['cel_calls'][lid]['current_channels']):
-            _cb = {'users': list(app.cache['cel_calls'][uid]['current_channels'].values()),
-                 'state': 'group_ringing',
+            _cb = {'webhook_name':'group_user_stop_calling',
+                 'user': called,
+                 'state': 'group_end_ringing',
                  'callerId': cid,
-                 'callId': uid}
-            if ('WebCallId' in app.cache['cel_calls'][msg.linkedid]):
-              _cb['WebCallId'] = app.cache['cel_calls'][msg.linkedid]['WebCallId']
-            reply = await doCallback('groupRinging', _cb)
-      if ((msg.EventName == 'ANSWER') and
+                 'callId': msg.UniqueID}
+
+          if (msg.linkedid in  app.cache['cel_calls']):
+            fillCallbackParameters(_cb,app.cache['cel_calls'][msg.linkedid],cid)
+          reply = await doCallback('groupRinging', _cb)
+        if ((msg.EventName == 'ANSWER') and
           (msg.Application == 'AppDial') and
-          firstMessage.get('groupCall',False) and 
           (lid in app.cache['cel_calls'])):
-        called = msg.Exten
-        app.cache['cel_calls'][lid]['answered'] = True
-        _cb = {'user': called,
-               'users': list(app.cache['cel_calls'][uid]['all_channels'].keys()),
+          #called = msg.Exten
+          app.cache['cel_calls'][lid]['answered'] = True
+          _cb = {'webhook_name':'group_user_answered',
+               'user': called,
+               'users': list(app.cache['cel_calls'][uid]['all_channels'].values()),
                'state': 'group_answer',
                'callerId': cid,
-               'callId': uid}
-        if ('WebCallId' in app.cache['cel_calls'][msg.linkedid]):
-          _cb['WebCallId'] = app.cache['cel_calls'][msg.linkedid]['WebCallId']
-        reply = await doCallback('groupAnswered', _cb)
+               'callId': msg.UniqueID}
+          if (msg.linkedid in  app.cache['cel_calls']):
+            fillCallbackParameters(_cb,app.cache['cel_calls'][msg.linkedid],cid)
+          reply = await doCallback('groupAnswered', _cb)
+          
     if ((msg.Application == 'Queue') and
-        (firstMessage.Context == 'from-pstn')):
+        firstMessage.Context.startswith('from-pstn')):
       if (msg.EventName == 'APP_START'):
         app.cache['cel_queue_calls'][lid] = {'caller': msg.CallerIDnum, 'start': parseDatetime(msg.EventTime).isoformat()}
-        _cb = {'callid': lid, 
+        app.cache['cel_calls'][lid]['queueName'] = msg.Exten
+        _cb = {'webhook_name':'queue_enter',
+            'callid': lid, 
             'caller': msg.CallerIDnum, 
             'start': parseDatetime(msg.EventTime).isoformat(),
             'callerfrom': firstMessage.Exten,
@@ -276,21 +389,68 @@ async def celCallback(mngr: Manager, msg: Message):
         reply = await doCallback('queueEnter', _cb)
       if (msg.EventName in ('APP_END', 'BRIDGE_ENTER')):
         call = app.cache['cel_queue_calls'].pop(lid,False)
+        app.cache['cel_calls'][lid]['queueName'] = False
         queue_changed = (call != None)
         if queue_changed :
-          _cb = {'callid': lid, 
+          _cb = {'webhook_name':'queue_leave',
+              'callid': lid, 
               'queue': msg.Exten,
               'agents': [q.user for q in app.cache['queues'][msg.Exten]]}
           reply = await doCallback('queueLeave', _cb)
+          
+    #if ((msg.EventName == 'APP_START')  and (msg.Application == 'MixMonitor')):
+    #  app.cache['cel_calls'][lid]['mix_monitors'].append(msg.Channel)
+    #if ((msg.EventName == 'APP_END')  and (msg.Application == 'MixMonitor')):
+    #  app.cache['cel_calls'][lid]['mix_monitors'].remove(msg.Channel)
+    if (msg.EventName == "ANSWER" and msg.Application == 'AppDial' and not "answer_time" in app.cache['cel_calls'][lid]):
+      app.cache['cel_calls'][lid]["answer_time"]=msg.EventTime
     if (msg.EventName == 'LINKEDID_END'):
+      for t in firstMessage['transfers']:
+        #app.logger.warning('first {}'.format(t))
+        (f,s) = t
+        await db.execute(query='update cdr set transfer_from=(select distinct linkedid from cdr where uniqueid=:first) where linkedid=:second;',values={'first': f,'second': s})
+      if ("c2c_start_time" in app.cache['cel_calls'][lid]):
+        call = app.cache['cel_calls'][lid]
+        _cb = {'webhook_name': "c2c_end",
+               'asteriskCallId':msg.LinkedID,
+               'start_time': call["c2c_start_time"],
+               'end_time': msg.EventTime,
+               'ANumber': call["c2c_user"],
+               'BNumber': call["c2c_phone"]}
+        if "answer_time" in call:
+         _cb["answer_time"]=call["answer_time"]
+        app.logger.warning('c2c cb {}'.format(_cb))
       app.cache['cel_calls'].pop(lid, False)
       app.cache['cel_queue_calls'].pop(lid, False)
+      
     if (msg.EventName == 'USER_DEFINED') and (msg.UserDefType == 'SETVARIABLE'):
       varname, value = msg.AppData.split(',')[1].split('=')[0:2]
       app.cache['cel_calls'][lid][varname]=value
+      app.logger.warning('set {} = {} for  {}'.format(varname,value,lid))
       if (lid in app.cache['calls']):
         app.cache['calls'][lid][varname]=value
 
+    if (msg.EventName == 'USER_DEFINED') and (msg.UserDefType == 'CALLBACK_POSTFIX'):
+      callback_type = msg.AppData.split(',')[1]
+      postfix = msg.AppData.split(',')[2]
+      values = msg.AppData.split(',')[3:]
+      _cb = {'asteriskCallId':msg.LinkedID}
+      for value in values:
+        vn,vv = value.split('=')
+        _cb[vn] = vv
+      reply = await doCallbackPostfix(callback_type,postfix, _cb)
+    if (msg.EventName == 'USER_DEFINED') and (msg.UserDefType == 'START_C2C'):
+        app.logger.warning('c2c event {}'.format(msg))
+        app.cache['cel_calls'][lid]["c2c_start_time"]=msg.EventTime
+        app.cache['cel_calls'][lid]["c2c_user"] = msg.CallerIDnum
+        app.cache['cel_calls'][lid]["c2c_phone"] = msg.Exten
+        _cb = {'webhook_name': "c2c_start",
+             'asteriskCallId':msg.LinkedID,
+             'start_time': msg.EventTime,
+             'ANumber': msg.CallerIDnum,
+             'BNumber': msg.Exten}
+        app.logger.warning('c2c cb {}'.format(_cb))
+
 async def getCDR(start=None,
                  end=None,
                  table='cdr',
@@ -322,6 +482,49 @@ async def getCDR(start=None,
     cdr.append(_cdr[_id])
   return cdr
 
+async def getCallInfo(callid):
+  pattern = re.compile("^[0-9]+\.[0-9]+")
+  #app.logger.warning('callid {}'.format(callid)) 
+  if (pattern.match(callid)):
+    linkedid=callid
+  else:
+    row = await db.fetch_one(query='SELECT linkedid FROM cdr WHERE userfield = :callid', values={'callid': callid})
+    if row:
+      linkedid = row['linkedid']
+    else:
+      return "{}"
+  #app.logger.warning('linkedid {}'.format(linkedid)) 
+  _q = '''SELECT *
+          FROM cel 
+          WHERE linkedid=:linkedid and eventtype = :eventtype'''
+  _v = {'linkedid': linkedid, 'eventtype': 'ATTENDEDTRANSFER'}
+  _f = True
+  uniqueids = set((linkedid,)) #get ids of transferred calls
+  async for row in db.iterate(query=_q, values=_v):
+    extra  = json.loads(row['extra'])
+    uniqueids.add(extra['channel2_uniqueid'])
+    
+  _q = '''SELECT *
+          FROM cdr 
+          WHERE linkedid=:linkedid or linkedid in (select distinct linkedid from cdr where uniqueid in :uniqueids)
+          ORDER BY sequence;'''
+  _v = {'linkedid': linkedid, 'uniqueids': uniqueids}
+  #app.logger.warning('values {}'.format(_v)) 
+  _f = True
+  unique_ids = set()
+  events = CdrEvents()
+  async for row in db.iterate(query=_q, values=_v):
+    row = dict(row)
+    #app.logger.warning('row {}'.format(row)) 
+    if row['recordingfile'] is not None and row['recordingfile']  != '':
+      row['recordingfile'] = '/static/records/{d.year}/{d.month:02}/{d.day:02}/{filename}'.format(d=row['calldate'],
+                                                                                            filename=row['recordingfile'])
+    #app.logger.warning('event row {}'.format(row))   
+    events.add(row)
+  record = events.simple()                                                     
+  return record
+
+    
 async def getUserCDR(user,
                      start=None,
                      end=None,
@@ -337,10 +540,10 @@ async def getUserCDR(user,
     _q += f''' dst="{user}"'''
   elif direction in ('out', False, '0', 'outgoing', 'outbound'):
     direction = 'outbound'
-    _q += f''' src="{user}"'''
+    _q += f''' cnum="{user}"'''
   else:
     direction = None
-    _q += f''' (src="{user}" or dst="{user}")'''
+    _q += f''' (cnum="{user}" or dst="{user}")'''
   if end is None:
     end = dt.now()
   if start is None:
@@ -349,7 +552,7 @@ async def getUserCDR(user,
   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))
+  #app.logger.warning('SQL: {}'.format(_q))
   _cdr = {}
   async for row in db.iterate(query=_q):
     if (row['disposition']=='FAILED' and row['lastapp']=='Queue'):
@@ -361,7 +564,7 @@ async def getUserCDR(user,
   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):
+    if (direction is not None) and (record['cnum'] == 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'],
@@ -374,12 +577,42 @@ async def getCEL(start=None, end=None, table='cel', field='eventtime', sort='id'
 
 async def doCallback(entity, msg):
   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')):
+  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)}''')
+    return None
+    if not 'HTTP_CLIENT' in app.config:
+      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']))
   else:
     app.logger.warning('No callback url defined for {}'.format(entity))
   return None
 
+async def doCallbackPostfix(entity,postfix, msg):
+  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'])):
+    url = row["url"]+'/'+postfix
+    app.logger.warning(f'''POST {url} data: {str(msg)}''')
+    return None
+    if not 'HTTP_CLIENT' in app.config:
+      await initHttpClient()
+    try:
+      reply = await app.config['HTTP_CLIENT'].post(url, json=msg)
+      return reply
+    except Exception as e:
+      app.logger.warning('callback error {}'.format(url))
+  else:
+    app.logger.warning('No callback url defined for {}'.format(entity))
+  return None
+
+@app.before_first_request
+async def initHttpClient():
+  app.config['HTTP_CLIENT'] = aiohttp.ClientSession(loop=main_loop,
+                                                    connector=aiohttp.TCPConnector(verify_ssl=False,
+                                                                                   resolver=AsyncResolver(nameservers=['192.168.171.10','1.1.1.1'])))
 
 @app.route('/openapi.json')
 async def openapi():
@@ -407,11 +640,34 @@ async def ui():
                                       js_url=app.config['SWAGGER_JS_URL'],
                                       css_url=app.config['SWAGGER_CSS_URL'])
 
+@app.route('/ami/action', methods=['POST'])
 async def action():
   _payload = await request.get_data()
   reply = await manager.send_action(json.loads(_payload))
+  if (isinstance(reply, list) and
+      (len(reply) > 1)):
+    for message in reply:
+      if (message.event == 'DBGetResponse'):
+        return message.val
   return str(reply)
 
+async def amiChannelGetVar(channel,variable):
+  '''AMI GetVar
+  Gets variable using AMI action GetVar to value in background.
+
+  Parameters:
+    channel (string)
+    variable (string): Variable to get
+
+  Returns:
+    string: value if GetVar was successfull, error message overwise
+  '''
+  reply = await manager.send_action({'Action': 'GetVar',
+                                     'Variable': variable,
+                                     'Channel': channel})
+  app.logger.warning('GetVar({},{}={})'.format(channel,variable,reply.value))
+  return reply.value
+
 async def amiGetVar(variable):
   '''AMI GetVar
   Returns value of requested variable using AMI action GetVar in background.
@@ -424,7 +680,7 @@ async def amiGetVar(variable):
   '''
   reply = await manager.send_action({'Action': 'GetVar',
                                      'Variable': variable})
-  app.logger.warning('GetVar({})->{}'.format(variable, reply.value))
+  #app.logger.warning('GetVar({})->{}'.format(variable, reply.value))
   return reply.value
 
 @app.route('/ami/auths')
@@ -455,6 +711,52 @@ async def amiPJSIPShowAors():
     app.logger.warning('AorsList: {}'.format(','.join(aors.keys())))
   return successReply(aors)
 
+
+async def amiStartMixMonitor(channel,filename):
+  '''AMI MixMonitor
+  Parameters:
+    channel (string):channel to start mixmonitor
+    filename (string): file name
+
+  Returns:
+    string: None if SetVar was successfull, error message overwise
+  '''
+  d = date.today()
+  year = d.strftime("%Y")
+  month = d.strftime("%m")
+  day = d.strftime("%d")
+  fullname = "{}/{}/{}/{}.wav".format(year,month,day,filename)
+  reply = await manager.send_action({'Action': 'MixMonitor',
+                                     'Channel': channel,
+                                     'options': 'ai(LOCAL_MIXMON_ID)',
+                                     'Command': "/etc/asterisk/scripts/wav2mp3.sh {} {} {} {}".format(year,month,day,filename),
+                                     'File': fullname})
+  #app.logger.warning('MixMonitor({}, {})'.format(channel, fullname))
+  if isinstance(reply, Message):
+    if reply.success:
+      return None
+    else:
+      return reply.message
+  return 'AMI error'
+  
+async def amiStopMixMonitor(channel):
+  '''AMI StopMixMonitor
+  Parameters:
+    channel (string):channel to stop mixmonitor
+
+  Returns:
+    string: None if SetVar was successfull, error message overwise
+  '''
+  reply = await manager.send_action({'Action': 'StopMixMonitor',
+                                     'Channel': channel})
+  #app.logger.warning('StopMixMonitor({})'.format(channel))
+  if isinstance(reply, Message):
+    if reply.success:
+      return None
+    else:
+      return reply.message
+  return 'AMI error'
+  
 async def amiUserEvent(name, data):
   '''AMI UserEvent
   Generates AMI Event using AMI action UserEvent with name and data supplied.
@@ -469,7 +771,31 @@ async def amiUserEvent(name, data):
   reply = await manager.send_action({**{'Action': 'UserEvent',
                                         'UserEvent': name},
                                      **data})
-  app.logger.warning('UserEvent({})'.format(name))
+  #app.logger.warning('UserEvent({})'.format(name))
+  if isinstance(reply, Message):
+    if reply.success:
+      return None
+    else:
+      return reply.message
+  return 'AMI error'
+
+async def amiChannelSetVar(channel,variable, value):
+  '''AMI SetVar
+  Sets variable using AMI action SetVar to value in background.
+
+  Parameters:
+    channel (string)
+    variable (string): Variable to set
+    value (string): Value to set for variable
+
+  Returns:
+    string: None if SetVar was successfull, error message overwise
+  '''
+  reply = await manager.send_action({'Action': 'SetVar',
+                                     'Variable': variable,
+                                     'Channel': channel,
+                                     'Value': value})
+  app.logger.warning('SetVar({},{}={})'.format(channel,variable, value))
   if isinstance(reply, Message):
     if reply.success:
       return None
@@ -491,7 +817,7 @@ async def amiSetVar(variable, value):
   reply = await manager.send_action({'Action': 'SetVar',
                                      'Variable': variable,
                                      'Value': value})
-  app.logger.warning('SetVar({}, {})'.format(variable, value))
+  #app.logger.warning('SetVar({}, {})'.format(variable, value))
   if isinstance(reply, Message):
     if reply.success:
       return None
@@ -517,7 +843,7 @@ async def amiDBGet(family, key):
       (len(reply) > 1)):
     for message in reply:
       if (message.event == 'DBGetResponse'):
-        app.logger.warning('DBGet(/{}/{})->{}'.format(family, key, message.val))
+        #app.logger.warning('DBGet(/{}/{})->{}'.format(family, key, message.val))
         return message.val
   app.logger.warning('DBGet(/{}/{})->Error!'.format(family, key))
   return None
@@ -596,7 +922,7 @@ async def amiPresenceState(user):
   '''
   reply = await manager.send_action({'Action': 'PresenceState',
                                      'Provider': 'CustomPresence:{}'.format(user)})
-  app.logger.warning('PresenceState({})'.format(user))
+  #app.logger.warning('PresenceState({})'.format(user))
   if isinstance(reply, Message):
     if reply.success:
       return True, reply.state
@@ -795,8 +1121,8 @@ async def refreshQueuesCache():
   return len(app.cache['queues'])
 
 async def rebindLostDevices():
-  app.cache['usermap'] = {}
-  app.cache['devicemap'] = {}
+  usermap = {}
+  devicemap = {}
   ast = await getGlobalVars()
   for device in app.cache['devices']:
     user = await getDeviceUser(device)
@@ -809,13 +1135,26 @@ async def rebindLostDevices():
         await setUserHint(user, dial, ast)           # Set hints for user on new device
         await setUserDeviceStates(user, device, ast) # Set device states for users device
         await setUserDevice(user, device)            # Bind device to user
-    app.cache['usermap'][device] = user
+    usermap[device] = user
     if user != 'none':
-      app.cache['devicemap'][user] = device
+      devicemap[user] = device
+  app.cache['usermap'] = copy.deepcopy(usermap)
+  app.cache['devicemap'] = copy.deepcopy(devicemap)
 
 async def userStateChangeCallback(user, state, prevState = None):
-  app.logger.warning('{} changed state to: {}'.format(user, state))
-  return ''
+  reply = None
+  device = None
+  if (user in app.cache['devicemap']):
+    device = app.cache['devicemap'][user]
+    if device is not None:
+      _cb = {'webhook_name': 'user_status',
+             'user': user,
+             'state': state,
+             'prev_state':prevState}
+      reply = await doCallback(device, _cb)
+
+  #app.logger.warning('{} changed state to: {}'.format(user, state))
+  return reply
 
 def getUserStateCombined(user):
   _uCache = app.cache['ustates']
@@ -893,18 +1232,82 @@ class Originate(Resource):
     device = device.replace('{}&'.format(user), '')
     _act = { 'Action':'Originate',
              'Channel':'PJSIP/{}'.format(device),
-             'Context':'from-internal',
+             'Context':'from-internal-c2c',
              'Exten':number,
              'Priority': '1',
-             'async':'false',
-             'Callerid': '{} <{}>'.format(user, user)}
+             'Async':'true',
+             'Callerid': '{} <{}>'.format(user, device)}
     app.logger.warning(_act)
-    reply = await manager.send_action(_act)
-    if isinstance(reply, Message):
-      if reply.success:
-        return successfullyOriginated(user, number)
-      else:
-        return errorReply(reply.message)
+    await manager.send_action(_act)
+    return successfullyOriginated(user, number)
+    #reply = await manager.send_action(_act)
+    #if isinstance(reply, Message):
+    #  if reply.success:
+    #    return successfullyOriginated(user, number)
+    #  else:
+    #    return errorReply(reply.message)
+
+@app.route('/originate_test/<user>/<number>')
+class Originate_test(Resource):
+  @authRequired
+  @app.param('user', 'User initiating the call', 'path')
+  @app.param('number', 'Destination number', 'path')
+  @app.response(HTTPStatus.OK, 'Json reply')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self, user, number):
+    '''Originate call
+    '''
+    if (user != request.user) and (not request.admin):
+      abort(401)
+    device = await getUserDevice(user)
+    if device in NONEs:
+      return noUserDevice(user)
+    device = device.replace('{}&'.format(user), '')
+    _act = { 'Action':'Originate',
+             'Channel':'PJSIP/{}'.format(device),
+             'Context':'form-internal-c2c',
+             'Exten':number,
+             'Priority': '1',
+             'Async':'true',
+             'Callerid': '{} <{}>'.format(user, device)}
+    app.logger.warning(_act)
+    await manager.send_action(_act)
+    return successfullyOriginated(user, number)
+    #reply = await manager.send_action(_act)
+    #if isinstance(reply, Message):
+    #  if reply.success:
+    #    return successfullyOriginated(user, number)
+    #  else:
+    #    return errorReply(reply.message)
+
+@app.route('/autocall/<numberA>/<numberB>')
+class Autocall(Resource):
+  @authRequired
+  @app.param('numberA', 'User calling first', 'path')
+  @app.param('numberB', 'user calling after numberA enter DTMF 2', 'path')
+  @app.param('callid', 'callid to be returned in callback', 'query')
+  @app.response(HTTPStatus.OK, 'Json reply')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self, numberA, numberB):
+    '''Originate autocall
+    '''
+    if  (not request.admin):
+      abort(401)
+    numberA = re.sub('\D', '', numberA);
+    numberB = re.sub('\D', '', numberB);
+    callid = request.args.get('callid', None)
+    _act = { 'Action':'Originate',
+             'Channel':'Local/{}@autocall-legA'.format(numberA),
+             'Context':'autocall-legB',
+             'Exten':numberB,
+             'Priority': '1',
+             'Async':'true',
+             'Callerid': '{} <{}>'.format(1000, 1000),
+             'Variable': '__callid={}'.format(callid)
+             }
+    app.logger.warning(_act)
+    await manager.send_action(_act)
+    return successfullyOriginated(numberA, numberB)
 
 @app.route('/hangup/<user>')
 class Hangup(Resource):
@@ -1018,7 +1421,8 @@ class SetPresenceState(Resource):
     if user not in app.cache['ustates']:
       return noUser(user)
     # app.logger.warning('state={}, getUserStateCombined({})={}'.format(state, user, getUserStateCombined(user)))
-    if (state.lower() in ('available','away','not_set','xa','chat')) and (getUserStateCombined(user) in ('dnd')):
+    # if (state.lower() in ('available','away','not_set','xa','chat')) and (getUserStateCombined(user) in ('dnd')):
+    if (state.lower() not in ('dnd')):
       result = await amiDBDel('DND', '{}'.format(user))
     result = await amiSetVar('PRESENCE_STATE(CustomPresence:{})'.format(user), state)
     if result is not None:
@@ -1182,7 +1586,7 @@ class Calls(Resource):
       _call = {'id':c.linkedid,
                'start':c.start,
                'type': c.direction,
-               'numberA': c.src,
+               'numberA': c.cnum,
                'numberB': c.dst,
                'line': c.did,
                'duration': c.duration,
@@ -1219,6 +1623,18 @@ class UserCalls(Resource):
                            request.args.get('offset', None),
                            request.args.get('order', 'ASC'))
     return successReply(cdr)
+    
+@app.route('/call/<call_id>')
+class CallInfo(Resource):
+  @authRequired
+  @app.param('call_id', 'call_id for ', 'path')
+  @app.response(HTTPStatus.OK, 'JSON data {"status":status,"data":data,"message":message}')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self, call_id):
+    '''Returns call info.'''
+
+    call = await getCallInfo(call_id)
+    return successReply(call)
 
 @app.route('/device/<device>/callback')
 class DeviceCallback(Resource):
@@ -1331,5 +1747,29 @@ class QueueLeaveCallback(Resource):
         url = row['url']
     return successCommonCallbackURL('queueLeave', url)
 
+
+@app.route('/callback/<type>')
+class CommonCallback(Resource):
+  @authRequired
+  @app.param('type', 'Callback type', 'path')
+  @app.param('url', 'used to set the Callback url for the autocall', 'query')
+  @app.response(HTTPStatus.OK, 'JSON data {"url":url}')
+  @app.response(HTTPStatus.UNAUTHORIZED, 'Authorization required')
+  async def get(self,type):
+    '''Returns and sets Autocall callback url.
+    '''
+    if not request.admin:
+      abort(401)
+    url = request.args.get('url', None)
+    if url is not None:
+      await db.execute(query='REPLACE INTO callback_urls (device, url) VALUES (:device, :url)',
+                       values={'device': type,'url': url})
+    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)
+
 manager.connect()
 app.run(loop=main_loop, host='0.0.0.0', port=app.config['PORT'])