diff --git a/.env b/.env new file mode 100644 index 0000000..01dc55c --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +AMI_HOST=127.0.0.1 +AMI_PORT=5038 +AMI_USER=medods +AMI_PASSWORD=2H4x9#87A%D3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000..ddfecdf --- /dev/null +++ b/logging.conf @@ -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") diff --git a/main.py b/main.py new file mode 100644 index 0000000..f3c8fce --- /dev/null +++ b/main.py @@ -0,0 +1,108 @@ +import logging +import logging.config +import os +import asyncio +from datetime import datetime +import socket +import aiofiles +from dotenv import load_dotenv + +load_dotenv() + +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") + + +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): + 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((AMI_HOST, 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: {AMI_USER}\r\nSecret: {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) + + try: + while True: + event = send_action(s, "") + if event: + await full_log(event) + except (KeyboardInterrupt, SystemExit): + logging.info("Exiting...") + s.close() + except ConnectionResetError: + logging.warning("Connection reset. Restarting...") + await ami_listening() + + + +async def main(): + if not os.path.exists("log"): + os.makedirs("log") + logging.config.fileConfig("logging.conf") + await ami_listening() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..910ea98 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "freepbx-logger" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "aiofiles>=24.1.0", + "python-dotenv>=1.1.1", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e5ff2cd --- /dev/null +++ b/uv.lock @@ -0,0 +1,36 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "freepbx-logger" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=24.1.0" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +]