--- /dev/null
+#!.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": <value>,
+ # "Data": <value>,
+ # "Timeout": 10,
+ "CallerID": "Departed",
+ "Variable": f"CALLERID(num)={number}",
+ # "Account": <value>,
+ # "EarlyMedia": <value>,
+ "Async": "true",
+ # "Codecs": <value>,
+ # "ChannelId": <value>,
+ # "OtherChannelId": <value>,
+ # "PreDialGoSub": <value>
+ })
+
+ 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)
--- /dev/null
+<!DOCTYPE html>
+<html lang="">
+ <head>
+ <meta charset="utf-8">
+ <title>Dialer Control Pannel</title>
+ <script src="static/script.js"></script>
+ <script type="module">
+
+import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
+
+// sockets
+
+var socket = io('/');
+socket.on('status', function (data) {
+ console.log(data.number);
+
+ switch (data.status) {
+ case "Disconnected":
+ document.getElementById(data.number).style.backgroundColor = "";
+ break;
+
+ case "Ringing":
+ document.getElementById(data.number).style.backgroundColor = "orange";
+ break;
+
+ case "Connected":
+ document.getElementById(data.number).style.backgroundColor = "green";
+ break;
+ }
+
+ document.getElementById(`${data.number}_status`).innerHTML = data.status;
+
+ let mute = "Unmuted";
+ if (data.mute) {
+ mute = "Muted";
+ }
+ document.getElementById(`${data.number}_mute`).innerHTML = mute;
+});
+
+ </script>
+ <link rel="stylesheet" href="static/style.css">
+ </head>
+ <body>
+ <div class="admin" id="{{ data.admin }}">
+ <span style="text-align: right; padding-right: 0.5em;">Admin status: </span>
+ <span id="{{ data.admin }}_status">Disconnected</span>
+ </div>
+ <div id="sounds">
+
+ {% for sound in data.sounds %}
+ <button id="{{ sound }}" onclick="play('{{ sound }}');">
+ {{ sound }}
+ </button>
+ {% endfor %}
+
+ </div>
+ <div id="players">
+
+ {% for player in data.players %}
+ <div id="{{ player.number }}">
+ <label><b>The {{ player.name|replace("_", " ")|title }}</b></label>
+ {% if "spare" in player.name %}
+ <input type="checkbox" id="{{ player.number }}_enabled" /><br>
+ {% else %}
+ <input type="checkbox" id="{{ player.number }}_enabled" checked="checked" /><br>
+ {% endif %}
+ <p>Status:
+ <span id="{{ player.number }}_status">Disconnected</span>
+ </p>
+ <p id="{{ player.number }}_mute">Muted</p>
+ <button onclick="call('{{ player.number }}');">Call</button>
+ <button onclick="mute('{{ player.number }}');">Mute</button><br>
+ <button onclick="kick('{{ player.number }}');">Kick</button>
+ <button onclick="unmute('{{ player.number }}');">Unmute</button>
+ </div>
+ {% endfor %}
+
+ </div>
+ <div id="bar">
+ <button onclick="callAll();">Call all</button>
+ <button onclick="kick('participants');">Kick all</button>
+ <button onclick="mute('participants');">Mute all</button>
+ <button onclick="unmute('participants');">Unmute all</button>
+ <button onclick="stopAll();">Stop sounds</button>
+ </div>
+ </body>
+</html>