заливка базы
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import os
|
||||
import socket
|
||||
import aiofiles
|
||||
import call_handler
|
||||
import config
|
||||
import logging
|
||||
|
||||
|
||||
def send_action(s, action):
|
||||
s.send(action.encode())
|
||||
response = ""
|
||||
while True:
|
||||
chunk = s.recv(4096)
|
||||
try:
|
||||
response += chunk.decode()
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if "\r\n\r\n" in response:
|
||||
break
|
||||
response_parts = response.split("\r\n\r\n", 1)
|
||||
body = response_parts[1]
|
||||
body_dict = {
|
||||
line.split(":")[0].strip(): line.split(":")[1].strip()
|
||||
for line in body.split("\n")
|
||||
if len(line.split(":")) > 1
|
||||
}
|
||||
return body_dict
|
||||
|
||||
|
||||
async def full_log(event):
|
||||
file_name = f"log/{datetime.now().strftime('%Y-%m-%d')}.log"
|
||||
async with aiofiles.open(file_name, "a") as f:
|
||||
await f.write("\n\n" + datetime.now().strftime("%H:%M:%S") + "\n\n")
|
||||
for key, value in event.items():
|
||||
await f.write(f"{key}: {value}\n")
|
||||
|
||||
|
||||
async def ami_listening():
|
||||
# процедура регулярной проверки соединения
|
||||
async def check_connection():
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(3600)
|
||||
s.send(b"Action: Ping\r\n\r\n")
|
||||
await asyncio.sleep(1)
|
||||
except (ConnectionError, ConnectionResetError):
|
||||
if config.DEBUG:
|
||||
logging.warning("Connection lost. Restarting...")
|
||||
await asyncio.sleep(1)
|
||||
return True # возвращаемся в начало функции ami_listening
|
||||
|
||||
# проверка соединения каждые 5 секунд
|
||||
conn_check_task = asyncio.create_task(check_connection())
|
||||
conn_check_task.add_done_callback(lambda t: ami_listening() if t.result() else None)
|
||||
|
||||
while True:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
logging.info("Connecting to Asterisk...")
|
||||
s.connect((config.AMI_HOST, config.AMI_PORT))
|
||||
break
|
||||
except ConnectionRefusedError:
|
||||
logging.warning("Connection refused. Retrying...")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logging.info("Connected to Asterisk")
|
||||
logging.info("Logging in...")
|
||||
|
||||
login_action = f"Action: Login\r\nUsername: {config.AMI_USER}\r\nSecret: {config.AMI_PASSWORD}\r\n\r\n"
|
||||
send_action(s, login_action)
|
||||
|
||||
logging.info("Logged in")
|
||||
logging.info("Listening for events...")
|
||||
|
||||
events_action = "Action: Events\r\nEventMask: on\r\n\r\n"
|
||||
send_action(s, events_action)
|
||||
|
||||
callhandler = call_handler.CallHandler()
|
||||
|
||||
try:
|
||||
while True:
|
||||
event = send_action(s, "")
|
||||
if event:
|
||||
await callhandler.handle_event(event)
|
||||
if config.DEBUG:
|
||||
await full_log(event)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
logging.info("Exiting...")
|
||||
s.close()
|
||||
except ConnectionResetError:
|
||||
logging.warning("Connection reset. Restarting...")
|
||||
await ami_listening()
|
||||
@@ -0,0 +1,513 @@
|
||||
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"])
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Настройки подключения к AMI
|
||||
AMI_HOST = os.environ.get("AMI_HOST")
|
||||
AMI_PORT = int(os.environ.get("AMI_PORT"))
|
||||
AMI_USER = os.environ.get("AMI_USER")
|
||||
AMI_PASSWORD = os.environ.get("AMI_PASSWORD")
|
||||
AMI_CHANNEL_FILTER = os.environ.get("AMI_CHANNEL_FILTER").split(", ")
|
||||
|
||||
RECORDS_SERVER = os.environ.get("RECORDS_SERVER")
|
||||
|
||||
MEDODS_SERVER = os.environ.get("MEDODS_SERVER")
|
||||
MEDODS_AUTH_VERSION = int(os.environ.get("MEDODS_AUTH_VERSION"))
|
||||
MEDODS_V1_TOKEN = os.environ.get("MEDODS_V1_TOKEN")
|
||||
MEDODS_V2_IDENTITY = os.environ.get("MEDODS_V2_IDENTITY")
|
||||
MEDODS_V2_SECRETKEY = os.environ.get("MEDODS_V2_SECRETKEY")
|
||||
|
||||
DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
|
||||
|
||||
REDIRECT_IDS = {}
|
||||
|
||||
with open("redirect.json") as f:
|
||||
REDIRECT_IDS = json.load(f)
|
||||
|
||||
OPERATORS = set(REDIRECT_IDS.keys())
|
||||
ID_VARS = ("ConnectedLineNum", "CallerIDNum", "DestCallerIDNum", "Source")
|
||||
@@ -0,0 +1,23 @@
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[handlers]
|
||||
keys=logconsole
|
||||
|
||||
[formatters]
|
||||
keys=formatter
|
||||
encoding=utf-8
|
||||
|
||||
[logger_root]
|
||||
level=INFO
|
||||
handlers=logconsole
|
||||
|
||||
[formatter_formatter]
|
||||
format=%(asctime)s: [%(levelname)s] %(message)s [%(module)s.%(funcName)s():%(lineno)d]
|
||||
datefmt=%Y-%m-%d %H:%M:%S
|
||||
|
||||
[handler_logconsole]
|
||||
class=logging.StreamHandler
|
||||
level=INFO
|
||||
args=(sys.stdout,)
|
||||
formatter=formatter
|
||||
@@ -0,0 +1,29 @@
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[handlers]
|
||||
keys=logconsole,logfile
|
||||
|
||||
[formatters]
|
||||
keys=formatter
|
||||
encoding=utf-8
|
||||
|
||||
[logger_root]
|
||||
level=INFO
|
||||
handlers=logconsole,logfile
|
||||
|
||||
[formatter_formatter]
|
||||
format=%(asctime)s: [%(levelname)s] %(message)s [%(module)s.%(funcName)s():%(lineno)d]
|
||||
datefmt=%Y-%m-%d %H:%M:%S
|
||||
|
||||
[handler_logconsole]
|
||||
class=logging.StreamHandler
|
||||
level=INFO
|
||||
args=(sys.stdout,)
|
||||
formatter=formatter
|
||||
|
||||
[handler_logfile]
|
||||
class=logging.FileHandler
|
||||
level=INFO
|
||||
formatter=formatter
|
||||
args=("log/medods.log", "a")
|
||||
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.config
|
||||
import os
|
||||
import asterisk_ami
|
||||
import config
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
# Настройки логирования
|
||||
if config.DEBUG:
|
||||
if not os.path.exists("log"):
|
||||
os.makedirs("log")
|
||||
logging_conf = "logging_debug.conf"
|
||||
else:
|
||||
logging_conf = "logging.conf"
|
||||
|
||||
logging.config.fileConfig(logging_conf)
|
||||
|
||||
# Подключение к AMI
|
||||
await asterisk_ami.ami_listening()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
import logging
|
||||
import config
|
||||
import json
|
||||
import jwt
|
||||
import time
|
||||
from aiohttp import ClientSession
|
||||
import aiohttp
|
||||
|
||||
|
||||
def loggingDict(title: str, data: dict) -> None:
|
||||
logging.info(
|
||||
f"{title}: %s",
|
||||
json.dumps(data, indent=4, ensure_ascii=False).encode("utf-8").decode("utf-8"),
|
||||
)
|
||||
|
||||
|
||||
def medods_token():
|
||||
if config.MEDODS_AUTH_VERSION == 1:
|
||||
return config.MEDODS_V1_TOKEN
|
||||
elif config.MEDODS_AUTH_VERSION == 2:
|
||||
iat = int(time.time()) - 10
|
||||
exp = iat + 60
|
||||
payload = {"iss": config.MEDODS_V2_IDENTITY, "iat": iat, "exp": exp}
|
||||
print(payload)
|
||||
token = jwt.encode(payload, config.MEDODS_V2_SECRETKEY, algorithm="HS512")
|
||||
return token
|
||||
|
||||
|
||||
async def send_post_request(body: dict):
|
||||
|
||||
async def post_request(
|
||||
url: str,
|
||||
headers: dict = None,
|
||||
json: dict = None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
try:
|
||||
async with ClientSession() as session:
|
||||
async with session.post(
|
||||
url, json=json, headers=headers, **kwargs
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
return await response.json()
|
||||
except aiohttp.ClientError as e:
|
||||
if config.DEBUG:
|
||||
logging.error(f"Request failed: {str(e)}")
|
||||
return None
|
||||
|
||||
data = {"call": body}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {medods_token()}",
|
||||
}
|
||||
if config.DEBUG:
|
||||
loggingDict("Sending data to Medods", body)
|
||||
r = await post_request(url=config.MEDODS_SERVER, headers=headers, json=data)
|
||||
try:
|
||||
res = {
|
||||
"id": r["call"]["id"],
|
||||
"status": r["call"]["status"],
|
||||
"call_session_id": r["call"]["call_session_id"],
|
||||
}
|
||||
except:
|
||||
res = None
|
||||
loggingDict("Recieved data from Medods:", res)
|
||||
else:
|
||||
await post_request(url=config.MEDODS_SERVER, headers=headers, json=data)
|
||||
|
||||
|
||||
async def incoming_call(call_session_id, contact_phone_number, called_phone_number):
|
||||
body = {
|
||||
"status": "incoming_call",
|
||||
"call_session_id": call_session_id,
|
||||
"contact_phone_number": contact_phone_number,
|
||||
"called_phone_number": called_phone_number,
|
||||
}
|
||||
await send_post_request(body)
|
||||
|
||||
|
||||
async def call_started(call_session_id, responsibles):
|
||||
body = {
|
||||
"status": "call_started",
|
||||
"call_session_id": call_session_id,
|
||||
"responsibles": responsibles,
|
||||
}
|
||||
await send_post_request(body)
|
||||
|
||||
|
||||
async def call_finished(call_session_id, duration):
|
||||
body = {
|
||||
"status": "call_finished",
|
||||
"call_session_id": call_session_id,
|
||||
"duration": duration,
|
||||
}
|
||||
await send_post_request(body)
|
||||
|
||||
|
||||
async def call_lost(call_session_id, responsibles):
|
||||
body = {
|
||||
"status": "call_lost",
|
||||
"call_session_id": call_session_id,
|
||||
"responsibles": responsibles,
|
||||
}
|
||||
await send_post_request(body)
|
||||
|
||||
|
||||
async def call_record_file(call_session_id, uniqueid):
|
||||
body = {
|
||||
"status": "call_record_file",
|
||||
"call_session_id": call_session_id,
|
||||
"file_link": f"{config.RECORDS_SERVER}{uniqueid}",
|
||||
}
|
||||
await send_post_request(body)
|
||||
@@ -0,0 +1,107 @@
|
||||
1. Добавить пользователя в систему Asterisk
|
||||
|
||||
1. В файл /etc/asterisk/manager_custom.conf добавить следующий текст:
|
||||
```
|
||||
[medods]
|
||||
secret = 2H4x9#87A%D3
|
||||
read = all
|
||||
```
|
||||
[medods] - имя пользователя (AMI_USER)
|
||||
secret - пароль пользователя (AMI_PASSWORD)
|
||||
read = all - права на чтение всех событий AMI Asterisk
|
||||
|
||||
2. Применить новые настройки. Выполнить команду:
|
||||
```
|
||||
asterisk -rx "core reload"
|
||||
```
|
||||
|
||||
2. Подготовить файлы конфигурации модуля интеграции
|
||||
|
||||
1. Изменить файл .env при необходимости:
|
||||
```
|
||||
AMI_HOST=127.0.0.1
|
||||
AMI_PORT=5038
|
||||
AMI_USER=medods
|
||||
AMI_PASSWORD=2H4x9#87A%D3
|
||||
# Фильтр для обработки входящих звонков по каналу поступления. Для использования нескольких каналов: вписать через ", " (запятая и пробел)
|
||||
AMI_CHANNEL_FILTER=PJSIP/Megafon_3, PJSIP/rt_769402
|
||||
# Для обработки всех входящих звонков без фильтра по каналу поступления оставить значение переменной пустым
|
||||
# AMI_CHANNEL_FILTER=
|
||||
# Сервер для получения записей звонков в формате mp3. (Дополнительный модуль)
|
||||
RECORDS_SERVER=http://192.168.75.10:3050/
|
||||
MEDODS_SERVER=http://192.168.75.248:3000/api/v2/telephony/common
|
||||
# Версия авторизации на сервере Medods. На 25-08-2024 используется версия "V1".
|
||||
# Указывать только цифровое обозначение версии
|
||||
MEDODS_AUTH_VERSION=1
|
||||
# API Токен для версии V1. Актуален на 25-08-2024
|
||||
MEDODS_V1_TOKEN=NjlmMmYzNThlOWNjYTI5ZGNlYTYzNz
|
||||
# Данные файла apiKey.csv для версии V2. Актуальны на 25-08-2024
|
||||
MEDODS_V2_IDENTITY=ddf28e8a-e6e5-449e-a927-48c0e0cebc13
|
||||
MEDODS_V2_SECRETKEY=b4bd5fafe883069f02c32ce0c9b2ba0c89e5caae42bc43852a058dbfe752ba8d
|
||||
# Включение логирования событий в файлы. Если файлы не требуются: закомментировать параметр или вписать "false"
|
||||
# DEBUG=true
|
||||
```
|
||||
|
||||
2. Отредактировать файл redirect.json при изменении сотрудников регистратуры:
|
||||
```
|
||||
{
|
||||
"10": [99],
|
||||
"12": [99],
|
||||
"13": [99]
|
||||
}
|
||||
```
|
||||
В ковычках записаны номера телефонов из очереди Asterisk
|
||||
В квадратных скобках список ID телефонии сотрудников из списка "Сотрудники" "Медодс", которые могут отвечать на этом номере телефона. Для внесения нескольких значений нужно добавить необходимое количество данных через ", " (запятая и пробел)
|
||||
|
||||
3. Настройки модуля интеграции
|
||||
|
||||
1. Требования:
|
||||
Python версии не менее 3.10, рекомендуемая версия: 3.12
|
||||
|
||||
2. Основной файл для запуска:
|
||||
"main.py"
|
||||
|
||||
3. Логирование
|
||||
|
||||
1. Консоль
|
||||
С выключенным параметром DEBUG в консоль будут фиксировать короткие сообщения по каждому этапу звонка
|
||||
Со включенным параметром DEBUG в консоль будет добавлена информация по отправляемым на сервер Медодс запросам и полученным от него ответам
|
||||
|
||||
2. Файлы
|
||||
С выключенным параметром DEBUG никакие файлы записываться не будут
|
||||
Со включенным параметром DEBUG в папке "log" (будет создана, если отсутствует) будут создаваться следующие файлы:
|
||||
1. medods.log - копия информации, направляемой в консоль
|
||||
2. <дата>.log - вся информация, получаемая от AMI Asterisk
|
||||
3. <дата>/<уникальный номер звонка>.log - информация, получаемая от AMI Asterisk только по уникальному звонку
|
||||
|
||||
4. Linux Service. Для автоматического запуска и перезапуска сервиса модуля интеграции необходимо выполнить следующие действия:
|
||||
1. Создать файл medods.service по пути /etc/systemd/system со следующим содержимым:
|
||||
```
|
||||
[Unit]
|
||||
Description=Medods integration service
|
||||
After=mariadb.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Указать путь к папке и файлу размещения основного файла запуска
|
||||
ExecStart=/medods/venv/bin/python /medods/main.py
|
||||
WorkingDirectory=/medods/
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
KillMode=process
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
2. Выполнить следующие команды:
|
||||
1. systemctl daemon-reload
|
||||
2. systemctl medods enable
|
||||
3. systemctl start medods
|
||||
|
||||
5. Профилактический перезапуск
|
||||
Нужно в файл /etc/crontabs добавить слудующую строку:
|
||||
```
|
||||
0 3 * * * root systemctl restart medods
|
||||
```
|
||||
Это позволит обеспечить бесперебойную работу системы и отложенное до следующего рабочего дня применение правок
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"10": [99],
|
||||
"12": [99],
|
||||
"13": [99]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
requests
|
||||
PyJWT
|
||||
aiohttp
|
||||
aiofiles
|
||||
python-dotenv
|
||||
Reference in New Issue
Block a user