514 lines
26 KiB
Python
514 lines
26 KiB
Python
from datetime import datetime
|
|
import logging
|
|
import config
|
|
import medods
|
|
import aiofiles
|
|
import os
|
|
|
|
|
|
async def calls_to_log(in_dict):
|
|
date_dir = datetime.now().strftime("%Y-%m-%d")
|
|
date_dir_path = os.path.join("log", date_dir)
|
|
if not os.path.exists(date_dir_path):
|
|
os.makedirs(date_dir_path)
|
|
file_name = f"log/{date_dir}/{in_dict.get('Linkedid')}.log"
|
|
async with aiofiles.open(file_name, "a") as f:
|
|
await f.write(f"\n\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
for key, value in in_dict.items():
|
|
await f.write(f"{key}: {value}\n")
|
|
|
|
|
|
def redirect_ids(responsibles):
|
|
if type(responsibles) is not list:
|
|
responsibles = [responsibles]
|
|
resp_list = []
|
|
for resp in responsibles:
|
|
resp_list.extend(config.REDIRECT_IDS[resp])
|
|
return [{"id": int(x)} for x in resp_list]
|
|
|
|
|
|
def phone_number(number: str):
|
|
if len(number) == 6:
|
|
number = f"78162{number}"
|
|
else:
|
|
if number.startswith("8"):
|
|
number = f"7{number[1:]}"
|
|
return number
|
|
|
|
|
|
class CallHandler:
|
|
def __init__(self):
|
|
self.calls = {}
|
|
self.date = datetime.now().date()
|
|
self.finished = []
|
|
|
|
async def handle_event(self, event):
|
|
def check_linkedid(event):
|
|
linkedid = event.get("Linkedid")
|
|
try:
|
|
linkedid_split = linkedid.split(".")
|
|
uniqueid = event.get("Uniqueid")
|
|
uniqueid_split = uniqueid.split(".")
|
|
if int(linkedid_split[1]) <= int(uniqueid_split[1]):
|
|
return True
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
def check_date():
|
|
if self.date != datetime.now().date():
|
|
self.date = datetime.now().date()
|
|
logging.info(f"Date changed to {self.date} and reset calls database")
|
|
if config.DEBUG:
|
|
logging.warning(self.calls)
|
|
self.finished = []
|
|
self.calls = {}
|
|
|
|
def channel_to_responsible(channel: str):
|
|
try:
|
|
if channel.startswith("Local"):
|
|
channel_data = channel.split("@")
|
|
resp = channel_data[0]
|
|
resp_data = resp.split("/")
|
|
responsible = resp_data[-1]
|
|
int(responsible)
|
|
return responsible
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
try:
|
|
if check_linkedid(event):
|
|
if config.DEBUG:
|
|
await calls_to_log(event)
|
|
|
|
linkedid = event.get("Linkedid")
|
|
|
|
if linkedid in self.finished:
|
|
return
|
|
|
|
if linkedid not in self.calls.keys():
|
|
check_date()
|
|
context = event.get("Context")
|
|
if context == "from-internal":
|
|
self.finished.append(linkedid)
|
|
return
|
|
if context != "from-trunk":
|
|
return
|
|
if len(config.AMI_CHANNEL_FILTER) > 0:
|
|
event_channel = event.get("Channel")
|
|
if len(event_channel) < 7:
|
|
return
|
|
for filter in config.AMI_CHANNEL_FILTER:
|
|
if event_channel.startswith(filter):
|
|
await self.incoming_call(event, linkedid)
|
|
return
|
|
self.finished.append(linkedid)
|
|
return
|
|
else:
|
|
await self.incoming_call(event, linkedid)
|
|
return
|
|
else:
|
|
if event.get("ChannelStateDesc") == "Ring":
|
|
if (
|
|
event.get("Context") == "macro-user-callerid"
|
|
and (
|
|
event.get("Event") == "VarSet"
|
|
or (
|
|
event.get("Event") != "Newexten"
|
|
and event.get("Variable") == "MACRO_DEPTH"
|
|
)
|
|
)
|
|
and event.get("Application")
|
|
not in ("ExecIf", "Goto", "Return")
|
|
) or (
|
|
event.get("Variable") == "DIALEDPEERNUMBER"
|
|
and event.get("Exten") in config.OPERATORS
|
|
):
|
|
try:
|
|
uniq = event.get("Uniqueid")
|
|
if uniq != linkedid:
|
|
if (
|
|
uniq
|
|
not in self.calls[linkedid]["records"].values()
|
|
):
|
|
target = "records"
|
|
else:
|
|
target = "records_duble"
|
|
uniq_data = uniq.split(".")
|
|
if int(uniq_data[1]) > int(linkedid.split(".")[1]):
|
|
responsible = channel_to_responsible(
|
|
event.get("Channel")
|
|
)
|
|
if responsible in config.OPERATORS:
|
|
if (
|
|
responsible
|
|
in self.calls[linkedid][
|
|
"records"
|
|
].keys()
|
|
):
|
|
resp_uniq = self.calls[linkedid][
|
|
"records"
|
|
][responsible]
|
|
if int(uniq_data[-1]) < int(
|
|
resp_uniq.split(".")[-1]
|
|
):
|
|
return
|
|
if (
|
|
responsible
|
|
not in self.calls[linkedid][
|
|
"responsibles"
|
|
]
|
|
):
|
|
self.calls[linkedid][
|
|
"responsibles"
|
|
].append(responsible)
|
|
if target == "records":
|
|
if (
|
|
responsible
|
|
in self.calls[linkedid][
|
|
target
|
|
].keys()
|
|
):
|
|
free_uniq = self.calls[linkedid][
|
|
target
|
|
][responsible]
|
|
self.calls[linkedid][target][
|
|
responsible
|
|
] = uniq
|
|
for (
|
|
dub_id,
|
|
dub_uniq,
|
|
) in self.calls[
|
|
linkedid
|
|
]["records_duble"].items():
|
|
if dub_uniq == free_uniq:
|
|
if (
|
|
dub_id
|
|
not in self.calls[
|
|
linkedid
|
|
][target].keys()
|
|
):
|
|
self.calls[linkedid][
|
|
target
|
|
][dub_id] = dub_uniq
|
|
self.calls[linkedid][
|
|
"records_duble"
|
|
].pop(dub_id)
|
|
else:
|
|
self.calls[linkedid][target][
|
|
responsible
|
|
] = uniq
|
|
else:
|
|
if (
|
|
uniq
|
|
not in self.calls[linkedid][
|
|
target
|
|
].values()
|
|
):
|
|
if responsible not in self.calls[
|
|
linkedid
|
|
]["records"].keys() or (
|
|
responsible
|
|
in self.calls[linkedid][
|
|
"records"
|
|
].keys()
|
|
and self.calls[linkedid][
|
|
"records"
|
|
][responsible]
|
|
!= uniq
|
|
):
|
|
self.calls[linkedid][target][
|
|
responsible
|
|
] = uniq
|
|
return
|
|
except:
|
|
return
|
|
if (
|
|
event.get("Context") == "sub-record-check"
|
|
and (
|
|
".wav" in event.get("AppData")
|
|
or "external" in event.get("AppData")
|
|
)
|
|
and event.get("Exten")
|
|
== event.get("Extension")
|
|
== "recordcheck"
|
|
and event.get("Uniqueid") != linkedid
|
|
):
|
|
responsible = channel_to_responsible(event.get("Channel"))
|
|
if (
|
|
responsible in config.OPERATORS
|
|
and responsible
|
|
not in self.calls[linkedid]["responsibles"]
|
|
):
|
|
self.calls[linkedid]["records"][responsible] = (
|
|
event.get("Uniqueid")
|
|
)
|
|
if (
|
|
responsible
|
|
not in self.calls[linkedid]["responsibles"]
|
|
):
|
|
self.calls[linkedid]["responsibles"].append(
|
|
responsible
|
|
)
|
|
if self.calls[linkedid]["started"] is None:
|
|
if (
|
|
(
|
|
event.get("DialStatus") == "ANSWER"
|
|
and event.get("DestChannelStateDesc") == "Up"
|
|
)
|
|
or event.get("Variable")
|
|
in ("BRIDGEPVTCALLID", "BRIDGEPEER")
|
|
or event.get("BridgeTechnology") == "simple_bridge"
|
|
):
|
|
for var in config.ID_VARS:
|
|
answered = event.get(var)
|
|
if answered in self.calls[linkedid]["responsibles"]:
|
|
await self.call_started(linkedid, answered)
|
|
return
|
|
if event.get("Disposition") == "NO ANSWER":
|
|
if len(self.calls[linkedid]["responsibles"]) == 0:
|
|
if event.get("ConnectedLineNum") is not None:
|
|
self.calls[linkedid]["responsibles"].append(
|
|
event.get("ConnectedLineNum")
|
|
)
|
|
await self.call_lost(linkedid)
|
|
return
|
|
else:
|
|
if (
|
|
event.get("BridgeTechnology") == "simple_bridge"
|
|
and event.get("Context") == "from-internal-xfer"
|
|
and event.get("ChannelStateDesc") == "Up"
|
|
):
|
|
self.call_transfered(
|
|
linkedid, event.get("CallerIDNum"), event.get("Exten")
|
|
)
|
|
if (
|
|
event.get("BridgeTechnology")
|
|
== event.get("ToBridgeTechnology")
|
|
== event.get("FromBridgeTechnology")
|
|
== "simple_bridge"
|
|
and event.get("CallerIDNum") not in config.OPERATORS
|
|
and event.get("ChannelStateDesc") == "Up"
|
|
):
|
|
self.call_transfered(
|
|
linkedid,
|
|
event.get("ConnectedLineNum"),
|
|
event.get("CallerIDNum"),
|
|
)
|
|
duration = int(
|
|
(
|
|
datetime.now() - self.calls[linkedid]["started"]
|
|
).total_seconds()
|
|
)
|
|
if (
|
|
(
|
|
(
|
|
"BillableSeconds" in event.keys()
|
|
and event.get("Disposition") == "ANSWERED"
|
|
and (
|
|
event.get("Uniqueid") != linkedid
|
|
or (
|
|
event.get("Cause-txt") == "Normal Clearing"
|
|
or event.get("Context")
|
|
== "macro-hangupcall"
|
|
or event.get("Application") == "Hangup"
|
|
)
|
|
)
|
|
)
|
|
or (
|
|
"TalkTime" in event.keys()
|
|
and event.get("Event") == "VarSet"
|
|
)
|
|
or (
|
|
event.get("Application") == "Hangup"
|
|
and event.get("Disposition") != "NO ANSWER"
|
|
and event.get("ChannelStateDesc") != "Ring"
|
|
and (
|
|
event.get("Uniqueid") != linkedid
|
|
or event.get("Cause-txt") == "Normal Clearing"
|
|
)
|
|
and duration > 1
|
|
)
|
|
or (
|
|
event.get("AppData") == "hangupcall,"
|
|
and event.get("ChannelStateDesc") == "Up"
|
|
and (
|
|
event.get("Uniqueid") != linkedid
|
|
or event.get("Context") == "ext-queues"
|
|
)
|
|
)
|
|
or (
|
|
(
|
|
event.get("Context") == "macro-hangupcall"
|
|
and event.get("ChannelStateDesc") == "Up"
|
|
)
|
|
and (
|
|
event.get("Uniqueid") != linkedid
|
|
or duration > 1
|
|
)
|
|
and (
|
|
(
|
|
event.get("ConnectedLineNum")
|
|
in config.OPERATORS
|
|
or event.get("ConnectedLineNum")
|
|
in self.calls[linkedid]["responsibles"]
|
|
)
|
|
or (
|
|
(
|
|
event.get("CallerIDNum")
|
|
in config.OPERATORS
|
|
or event.get("CallerIDNum")
|
|
in self.calls[linkedid]["responsibles"]
|
|
)
|
|
and event.get("Event") == "BridgeLeave"
|
|
)
|
|
)
|
|
)
|
|
or (event.get("Event") == "Cdr")
|
|
)
|
|
and event.get("Context") != "from-internal-xfer"
|
|
and not event.get("Event").startswith("RTC")
|
|
):
|
|
transfer_duration = None
|
|
if self.calls[linkedid]["transfered"] is not None:
|
|
transfer_duration = int(
|
|
(
|
|
datetime.now()
|
|
- self.calls[linkedid]["transfered"]
|
|
).total_seconds()
|
|
)
|
|
talk_time = 0
|
|
for var in ("BillableSeconds", "TalkTime"):
|
|
if var in event.keys():
|
|
talk_time += int(event.get(var))
|
|
break
|
|
if talk_time > duration:
|
|
duration = talk_time
|
|
if duration >= 1 and (
|
|
transfer_duration is None or transfer_duration > 1
|
|
):
|
|
record_id = None
|
|
if (
|
|
event.get("AppData") == "hangupcall,"
|
|
and event.get("Cause") == "16"
|
|
and event.get("Context") == "ext-local"
|
|
and event.get("ConnectedLineNum")
|
|
in config.OPERATORS
|
|
and event.get("Uniqueid") != linkedid
|
|
):
|
|
record_id = event.get("Uniqueid")
|
|
if record_id is None and duration < 2:
|
|
return
|
|
if event.get("Event") == "AttendedTransfer":
|
|
record_id = event.get("TransfereeUniqueid")
|
|
if (
|
|
event.get("Context") == "macro-hangupcall"
|
|
and event.get("Uniqueid") != linkedid
|
|
and event.get("ConnectedLineNum")
|
|
in config.OPERATORS
|
|
and "BillableSeconds" not in event.keys()
|
|
):
|
|
record_id = event.get("Uniqueid")
|
|
if (
|
|
event.get("Application") == "Hangup"
|
|
and event.get("Membership") == "static"
|
|
and event.get("ConnectedLineNum")
|
|
in config.OPERATORS
|
|
and event.get("Uniqueid") != linkedid
|
|
):
|
|
record_id = event.get("Uniqueid")
|
|
if record_id is None:
|
|
for var in config.ID_VARS:
|
|
answered = event.get(var)
|
|
if (
|
|
answered
|
|
in self.calls[linkedid]["responsibles"]
|
|
):
|
|
try:
|
|
record_id = self.calls[linkedid][
|
|
"records"
|
|
][answered]
|
|
except:
|
|
record_id = self.calls[linkedid][
|
|
"records_duble"
|
|
][answered]
|
|
break
|
|
if record_id is None:
|
|
answered = channel_to_responsible(
|
|
event.get("Channel")
|
|
)
|
|
if answered in self.calls[linkedid]["responsibles"]:
|
|
try:
|
|
record_id = self.calls[linkedid]["records"][
|
|
answered
|
|
]
|
|
except:
|
|
record_id = self.calls[linkedid][
|
|
"records_duble"
|
|
][answered]
|
|
if record_id is None:
|
|
return
|
|
await self.call_finished(linkedid, duration, record_id)
|
|
return
|
|
except:
|
|
pass
|
|
|
|
def call_transfered(self, linkedid, old_responsible, new_responsible):
|
|
try:
|
|
int(new_responsible)
|
|
if new_responsible not in self.calls[linkedid]["responsibles"]:
|
|
if old_responsible in self.calls[linkedid]["records"].keys():
|
|
self.calls[linkedid]["records"][new_responsible] = self.calls[
|
|
linkedid
|
|
]["records"][old_responsible]
|
|
self.calls[linkedid]["responsibles"].append(new_responsible)
|
|
self.calls[linkedid]["transfered"] = datetime.now()
|
|
else:
|
|
if old_responsible in self.calls[linkedid]["records_duble"].keys():
|
|
self.calls[linkedid]["records"][new_responsible] = self.calls[
|
|
linkedid
|
|
]["records_duble"][old_responsible]
|
|
self.calls[linkedid]["responsibles"].append(new_responsible)
|
|
self.calls[linkedid]["transfered"] = datetime.now()
|
|
except:
|
|
pass
|
|
|
|
async def incoming_call(self, event, linkedid):
|
|
self.calls[linkedid] = {
|
|
"responsibles": [],
|
|
"started": None,
|
|
"transfered": None,
|
|
"records": {},
|
|
"records_duble": {},
|
|
}
|
|
exten = event.get("Exten") if event.get("Exten") else event.get("Extension")
|
|
logging.info(
|
|
f"New incoming call: ID={linkedid}, Client={phone_number(event.get('CallerIDNum'))}, Phone={exten}"
|
|
)
|
|
await medods.incoming_call(
|
|
linkedid, phone_number(event.get("CallerIDNum")), exten
|
|
)
|
|
|
|
async def call_started(self, linkedid, responsible):
|
|
logging.info(f"Call started: ID={linkedid}, Responsible={responsible}")
|
|
self.calls[linkedid]["started"] = datetime.now()
|
|
await medods.call_started(linkedid, redirect_ids(responsible))
|
|
|
|
async def call_finished(self, linkedid, duration, record_id):
|
|
logging.info(
|
|
f"Call finished: ID={linkedid}, Duration={duration}, Record ID={record_id}"
|
|
)
|
|
self.finished.append(linkedid)
|
|
self.calls.pop(linkedid)
|
|
await medods.call_finished(linkedid, duration)
|
|
await medods.call_record_file(linkedid, record_id)
|
|
|
|
async def call_lost(self, linkedid):
|
|
logging.info(
|
|
f"Call lost: ID={linkedid}, responsibles: {redirect_ids(self.calls[linkedid]['responsibles'])}"
|
|
)
|
|
await medods.call_lost(
|
|
linkedid, redirect_ids(self.calls[linkedid]["responsibles"])
|
|
)
|