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"]) )