|
|
@@ -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'])
|