заливка базы

This commit is contained in:
Dev PC
2025-07-03 01:35:09 +03:00
parent e7da06dcc1
commit ccde82adf3
25 changed files with 565320 additions and 0 deletions
BIN
View File
Binary file not shown.
View File
+94
View File
@@ -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()
+513
View File
@@ -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"])
)
+30
View File
@@ -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")
+23
View File
@@ -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
+29
View File
@@ -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")
+26
View File
@@ -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())
+114
View File
@@ -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)
+107
View File
@@ -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
```
Это позволит обеспечить бесперебойную работу системы и отложенное до следующего рабочего дня применение правок
+5
View File
@@ -0,0 +1,5 @@
{
"10": [99],
"12": [99],
"13": [99]
}
+5
View File
@@ -0,0 +1,5 @@
requests
PyJWT
aiohttp
aiofiles
python-dotenv