From: Max Value Date: Thu, 25 Dec 2025 21:46:47 +0000 (+0000) Subject: init X-Git-Url: https://git.ozva.co.uk/?a=commitdiff_plain;h=c1ba41e4edbec0e98a6362ffb0465ca51501a70d;p=conf-dialer init --- c1ba41e4edbec0e98a6362ffb0465ca51501a70d diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..c89f10c --- /dev/null +++ b/config.ini @@ -0,0 +1,4 @@ +[asterisk] +host=127.0.0.1 +username=asterisk +secret=test diff --git a/main.py b/main.py new file mode 100755 index 0000000..3156f21 --- /dev/null +++ b/main.py @@ -0,0 +1,329 @@ +#!.venv/bin/python + +from werkzeug.security import generate_password_hash, check_password_hash +from flask import Flask, request, render_template, jsonify +from requests.auth import HTTPBasicAuth as requests_auth +from panoramisk.call_manager import CallManager +from flask_socketio import SocketIO, emit +from flask_httpauth import HTTPBasicAuth +from panoramisk import Manager +import threading +import requests +import asyncio +import json +import os + +from websockets.sync.client import connect + +app = Flask(__name__) +auth = HTTPBasicAuth() +socketio = SocketIO(app) + + + +# ============================================================================== +# SETUP + +# get the player data ready +with open(f"{app.root_path}/players.json", "r", encoding="utf-8") as f: + players = json.loads(f.read()) + +# get the secrets ready +with open(f"{app.root_path}/secrets.json", "r", encoding="utf-8") as f: + users_raw = json.loads(f.read()) +users = {k: generate_password_hash(v) for (k, v) in users_raw.items()} +basic = requests_auth('controller', users_raw['controller']) + +@auth.verify_password +def verify_password(username, password): + if username in users and \ + check_password_hash(users.get(username), password): + return username + + + +# ============================================================================== +# FRONTEND + +@app.route("/", methods=["GET", "POST"]) +@auth.login_required +def main(): + data = { + "players": players, + "sounds": [x.rsplit(".", maxsplit=1)[0] for x in os.listdir(f"{app.root_path}/sounds")], + "admin": "+447594768180" + } + + return render_template("panel.html", data=data) + + + +# ============================================================================== +# UTILS + +async def get_channel(number): + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + callers = await manager.send_action({ + 'Action': 'ConfbridgeList', + 'Conference': 'blood', + }) + + channel = None + + if type(callers) == list: + for message in callers: + channel_num = message.get("CallerIDNum") #"Channel" + if channel_num == number: + channel = message.channel + + manager.close() + + return channel + + + +# ============================================================================== +# DIALPLAN + +@app.route("/call", methods=["GET", "POST"]) +@auth.login_required +async def call(): + number = request.json["number"] + + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + phonecall = await manager.send_action({ + "Action": "Originate", + "Channel": f"PJSIP/{number}@gotrunk", + "Exten": "blood", + "Context": "from-internal", + "Priority": "1", + # "Application": , + # "Data": , + # "Timeout": 10, + "CallerID": "Departed", + "Variable": f"CALLERID(num)={number}", + # "Account": , + # "EarlyMedia": , + "Async": "true", + # "Codecs": , + # "ChannelId": , + # "OtherChannelId": , + # "PreDialGoSub": + }) + + manager.close() + + return "", 200 + + +@app.route("/kick", methods=["GET", "POST"]) +@auth.login_required +async def kick(): + number = request.json["number"] + + if number == "participants": + channel = number + else: + channel = await get_channel(number) + + if channel is None: + return f"Caller is not connected ({number})", 422 + + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + result = await manager.send_action({ + "Action": "ConfbridgeKick", + "Conference": "blood", + "Channel": channel + }) + + manager.close() + return "", 200 + + +@app.route("/mute", methods=["GET", "POST"]) +@auth.login_required +async def mute(): + number = request.json["number"] + + if number == "participants": + channel = number + else: + channel = await get_channel(number) + + if channel is None: + return f"Caller is not connected ({number})", 422 + + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + result = await manager.send_action({ + "Action": "ConfbridgeMute", + "Conference": "blood", + "Channel": channel + }) + + manager.close() + return "", 200 + + +@app.route("/unmute", methods=["GET", "POST"]) +@auth.login_required +async def unmute(): + number = request.json["number"] + + if number == "participants": + channel = number + else: + channel = await get_channel(number) + + if channel is None: + return "Caller is not connected", 200 + + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + result = await manager.send_action({ + "Action": "ConfbridgeUnmute", + "Conference": "blood", + "Channel": channel + }) + + manager.close() + return "", 200 + + +@app.route("/play", methods=["GET", "POST"]) +@auth.login_required +async def play(): + path = request.json["path"] + + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + phonecall = await manager.send_action({ + "Action": "Originate", + "Channel": "Local/bloodadmin@from-internal", + "Application": "Playback", + "Data": f"/home/will/{path}", + "Async": "true", + "CallerID": f"file_{path}" + }) + + manager.close() + + return "", 200 + + +@app.route("/stopall") +@auth.login_required +async def stopall(): + manager = Manager.from_config(f"{app.root_path}/config.ini") + await manager.connect() + + callers = await manager.send_action({ + 'Action': 'ConfbridgeList', + 'Conference': 'blood', + }) + + if type(callers) == list: + for message in callers: + if message.CallerIDName[:4] == "file": + result = await manager.send_action({ + "Action": "ConfbridgeKick", + "Conference": "blood", + "Channel": message.channel + }) + + manager.close() + return "", 200 + + + +# ============================================================================== +# EVENTS + +watcher = Manager.from_config(f"{app.root_path}/config.ini") + +def start_watch(): + loop = asyncio.new_event_loop() + watcher.loop = loop + + print(" * Event watcher started") + watcher.connect(run_forever=True) + + +def send_emit(data): + with app.test_request_context('/'): + emit("status", data, json=True, broadcast=True, namespace="/") + + +@watcher.register_event('DialState') # Register all events +async def dialstate_callback(manager, message): + send_emit({ + "number": message.DestCallerIDNum, + "status": "Ringing", + "mute": True + }) + + +@watcher.register_event('ConfbridgeJoin') # Register all events +async def confbridgejoin_callback(manager, message): + if message.CallerIDName[:4] == "file": + number = message.CallerIDName[5:] + else: + number = message.CallerIDNum + + send_emit({ + "number": number, + "status": "Connected", + "mute": message.Muted == "yes" + }) + + +@watcher.register_event('ConfbridgeLeave') # Register all events +async def confbridgeleave_callback(manager, message): + if message.CallerIDName[:4] == "file": + number = message.CallerIDName[5:] + else: + number = message.CallerIDNum + + send_emit({ + "number": number, + "status": "Disconnected", + "mute": True + }) + + +@watcher.register_event('ConfbridgeMute') # Register all events +async def confbridgemute_callback(manager, message): + send_emit({ + "number": message.CallerIDNum, + "status": "Connected", + "mute": True + }) + + +@watcher.register_event('ConfbridgeUnmute') # Register all events +async def confbridgeunmute_callback(manager, message): + send_emit({ + "number": message.CallerIDNum, + "status": "Connected", + "mute": False + }) + + + +# ============================================================================== + +if __name__ == "__main__": + # start the event watch manager + thread = threading.Thread(target=start_watch, daemon=True) + thread.start() + + socketio.run(app, host='127.0.0.1', port=8000, debug=True) diff --git a/players.json b/players.json new file mode 100644 index 0000000..ec8c528 --- /dev/null +++ b/players.json @@ -0,0 +1,3 @@ +[ + {"name": "test_name", "number":"+447594768180"} +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..521f904 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask_socketio +flask_httpauth +flask[async] +panoramisk +werkzeug +requests diff --git a/secrets.json b/secrets.json new file mode 100644 index 0000000..e47a7e0 --- /dev/null +++ b/secrets.json @@ -0,0 +1,4 @@ +{ + "controller": "test", + "will": "test" +} diff --git a/sounds/test.sln32 b/sounds/test.sln32 new file mode 100644 index 0000000..99c587c Binary files /dev/null and b/sounds/test.sln32 differ diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..f086d32 --- /dev/null +++ b/static/script.js @@ -0,0 +1,67 @@ +const timeOut = 5000; // ms + +async function play (file) { + response = await fetch("/play", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + + body: JSON.stringify({"path": file}) + } + ); +} + +async function call (num) { + response = await fetch("/call", + { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({"number": num}) + } + ); +} + +async function kick (num) { + response = await fetch("/kick", + { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({"number": num}) + } + ); +} + +async function mute (num) { + response = await fetch("/mute", + { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({"number": num}) + } + ); +} + +async function unmute (num) { + response = await fetch("/unmute", + { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({"number": num}) + } + ); +} + +async function callAll () { + for (e of document.getElementById("players").children) { + if (document.getElementById(`${e.id}_enabled`).checked) { + call(e.id); + } + } +} + + +async function stopAll () { + response = await fetch("/stopall"); +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..54eea35 --- /dev/null +++ b/static/style.css @@ -0,0 +1,72 @@ +.admin, #sounds, #players, #bar { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + display: grid; + overflow: scroll; +} +.admin { + top: 95vh; + padding: 2px; + + margin: 0; + + grid-template-columns: 1fr 1fr; +} +#sounds { + top: 60vh; + bottom: 5vh; + border-bottom: 2px solid gray; + + padding: 2px; + + grid-template-columns: 1fr 1fr 1fr 1fr; +} +#players { + top: 5vh; + bottom: 40vh; + border-bottom: 2px solid gray; + + padding: 2px; + + grid-template-columns: 1fr 1fr 1fr; +} +#bar { + bottom: 95vh; + border-bottom: 2px solid gray; + + padding: 2px; + + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; +} + +#sounds > button { margin: 2px; } + +#players > div { + margin: 2px; + padding: 2px; + + text-align: center; + + border: 2px solid gray; +} + +#players > div > p { + margin: 2px; +} + +#players > div > button { + width: calc(50% - 4px); +} + + + + +#readout { + position: absolute; + left: 0; + bottom: 0; +} diff --git a/templates/panel.html b/templates/panel.html new file mode 100644 index 0000000..134d2e3 --- /dev/null +++ b/templates/panel.html @@ -0,0 +1,87 @@ + + + + + Dialer Control Pannel + + + + + +
+ Admin status: + Disconnected +
+
+ + {% for sound in data.sounds %} + + {% endfor %} + +
+
+ + {% for player in data.players %} +
+ + {% if "spare" in player.name %} +
+ {% else %} +
+ {% endif %} +

Status: + Disconnected +

+

Muted

+ +
+ + +
+ {% endfor %} + +
+
+ + + + + +
+ + diff --git a/test.json b/test.json new file mode 100644 index 0000000..38add34 --- /dev/null +++ b/test.json @@ -0,0 +1,6 @@ +[ + {"name": "dad", "number": "+447976097042"}, + {"name": "mum", "number": "+447759246551"}, + {"name": "grandma", "number": "+447396554079"}, + {"name": "lola", "number": "+447925041171"} +] diff --git a/test.py b/test.py new file mode 100644 index 0000000..f5647a6 --- /dev/null +++ b/test.py @@ -0,0 +1,28 @@ +import asyncio +from panoramisk import Manager + +async def extension_status(): + manager = Manager(loop=asyncio.get_event_loop(), + host='127.0.0.1', port=5038, + username='asterisk', secret='test') + + await manager.connect() + action = { + 'Action': 'ConfbridgeList', + 'Conference': 'blood', + } + + extension = await manager.send_action(action) + print(extension) + manager.close() + + +def main(): + loop = asyncio.get_event_loop() + loop.run_until_complete(extension_status()) + loop.close() + + +if __name__ == '__main__': + main() +