mirror of
https://github.com/fusionpbx/fusionpbx.git
synced 2026-02-22 10:56:31 +00:00
Websockets (#7393)
* Initial commit of websockets * Move app_menu to the active_calls websockets * Fix hangup function * Remove connection wait-state on web socket server so events can process * Add timestamp and debug level to console for service debug output * Remove debug exit * Fix typo for ws_client instead of ws_server * Update app_config.php * Fix typo and remove empty function * Remove call to empty function * Fix the menu to point to the correct location * Remove Logging Class * Rename service file * Rename service file * Fix the in progress browser request * Fix browser reload and implement 'active_calls' default values * Add apply_filter function * Create new permission_filter object * In progress active calls now use filter * Add invalid_uuid_exception class * add event_key_filter to honor user permissions * add and_link and or_link for filters * Fix disconnected subscriber and add filters to honor permissions * Add $key and $value for filter * define a service name * catch throwable instead of exception * Add $key and $value for filter and allow returning null * Update permission checks when loading page * Add apply_filter function to honor subscriber permissions * Add create_filter_chain_for function to honor subscriber permissions * Add apply_filter function to honor subscriber permissions * Add apply_filter function to honor subscriber permissions * create interface to allow filterable payload * create interface to define functions required for websocket services * Pass in service class when creating a service token * Allow key/name and return null for filter * Adjust subscriber exceptions to return the ID of the subscriber * Add event filter to filter chain * Add command line options for ip and port for websockets and switch * update service to use is_a syntax * initial commit of base class for websockets system services * initial commit of the system cpu status service * remove extra line feed * fix path on active_calls * initial proof of concept for cpu status updated by websockets * Allow returning null * Use default settings to set the interval for cpu status broadcast * Improve the CPU percent function for Linux systems * Show more debug information * Allow child processes to re-connect to the web socket service * Fix websockets as plural instead of singular * Add class name list-row * Update active_calls.php * Update active_calls.php * Update websocket_client.js * Update app_config.php * Update app_menu.php * Update debian-websockets.service * Update debian-active_calls.service --------- Co-authored-by: FusionPBX <markjcrane@gmail.com>
This commit is contained in:
297
app/active_calls/resources/javascript/arrows.js
Normal file
297
app/active_calls/resources/javascript/arrows.js
Normal file
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* FusionPBX
|
||||
* Version: MPL 1.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is FusionPBX
|
||||
*
|
||||
* The Initial Developer of the Original Code is
|
||||
* Mark J Crane <markjcrane@fusionpbx.com>
|
||||
* Portions created by the Initial Developer are Copyright (C) 2008-2025
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Mark J Crane <markjcrane@fusionpbx.com>
|
||||
* Tim Fry <tim@fusionpbx.com>
|
||||
*/
|
||||
|
||||
function create_arrow(direction, color) {
|
||||
switch (direction) {
|
||||
case 'inbound':
|
||||
arrow = create_arrow_inbound(color);
|
||||
break;
|
||||
case 'outbound':
|
||||
arrow = create_arrow_outbound(color);
|
||||
break;
|
||||
case 'local':
|
||||
arrow = create_arrow_local(color);
|
||||
break;
|
||||
case 'voicemail':
|
||||
arrow = create_voicemail_icon(color);
|
||||
break;
|
||||
case 'missed':
|
||||
arrow = create_inbound_missed(color);
|
||||
break;
|
||||
}
|
||||
return arrow;
|
||||
}
|
||||
|
||||
function create_arrow_outbound(color, gridSize = 25) {
|
||||
// Create SVG from SVG Namespace
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
|
||||
// compute how much to scale the original 24-unit grid
|
||||
const scale = gridSize / 25;
|
||||
|
||||
// Set color
|
||||
svg.setAttribute("stroke", color);
|
||||
// Set brush width
|
||||
svg.setAttribute("width", gridSize);
|
||||
svg.setAttribute("height", gridSize);
|
||||
svg.setAttribute("viewBox", `0 0 ${gridSize} ${gridSize}`);
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke-width", 2 * scale);
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
|
||||
// Create line
|
||||
const line = document.createElementNS(SVG_NS, "line");
|
||||
line.setAttribute("x1", (4 * scale).toString());
|
||||
line.setAttribute("y1", (20 * scale).toString());
|
||||
line.setAttribute("x2", (20 * scale).toString());
|
||||
line.setAttribute("y2", (4 * scale).toString());
|
||||
svg.appendChild(line);
|
||||
|
||||
// Create the arrow head (a right-angle triangle)
|
||||
const head = document.createElementNS(SVG_NS, "polygon");
|
||||
// head.setAttribute("points", "20,4 10,9 15,14");
|
||||
head.setAttribute("points", [[20, 4], [10, 9], [15, 14]]
|
||||
.map(([x, y]) => `${x * scale},${y * scale}`).join(" ")
|
||||
);
|
||||
head.setAttribute("fill", color);
|
||||
svg.appendChild(head);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function create_arrow_inbound(color, gridSize = 25) {
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
|
||||
// compute how much to scale the original 24-unit grid
|
||||
const scale = gridSize / 25;
|
||||
|
||||
// size and viewport
|
||||
svg.setAttribute("width", gridSize);
|
||||
svg.setAttribute("height", gridSize);
|
||||
svg.setAttribute("viewBox", `0 0 ${gridSize} ${gridSize}`);
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", color);
|
||||
svg.setAttribute("stroke-width", 2 * scale);
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
|
||||
// scaled line from (4,4) → (20,20)
|
||||
const line = document.createElementNS(SVG_NS, "line");
|
||||
line.setAttribute("x1", (4 * scale).toString());
|
||||
line.setAttribute("y1", (4 * scale).toString());
|
||||
line.setAttribute("x2", (20 * scale).toString());
|
||||
line.setAttribute("y2", (20 * scale).toString());
|
||||
svg.appendChild(line);
|
||||
|
||||
// scaled triangle head: (20,20), (10,15), (15,10)
|
||||
const head = document.createElementNS(SVG_NS, "polygon");
|
||||
head.setAttribute("points", [[20, 20], [10, 15], [15, 10]]
|
||||
.map(([x, y]) => `${x * scale},${y * scale}`).join(" ")
|
||||
);
|
||||
head.setAttribute("fill", color);
|
||||
svg.appendChild(head);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
function create_arrow_local(color, gridSize = 25) {
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
|
||||
// compute how much to scale the original 25-unit grid
|
||||
const scale = gridSize / 25;
|
||||
|
||||
// sizing & styling
|
||||
svg.setAttribute("width", gridSize);
|
||||
svg.setAttribute("height", gridSize);
|
||||
svg.setAttribute("viewBox", `0 0 ${gridSize} ${gridSize}`);
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", color);
|
||||
svg.setAttribute("stroke-width", 2 * scale);
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
|
||||
// shaft
|
||||
const line = document.createElementNS(SVG_NS, "line");
|
||||
line.setAttribute("x1", 6 * scale);
|
||||
line.setAttribute("y1", 12 * scale);
|
||||
line.setAttribute("x2", 18 * scale);
|
||||
line.setAttribute("y2", 12 * scale);
|
||||
svg.appendChild(line);
|
||||
|
||||
// left arrow head
|
||||
const leftHead = document.createElementNS(SVG_NS, "polygon");
|
||||
leftHead.setAttribute("points", [[6,8], [2,12], [6,16]]
|
||||
.map(([x, y]) => `${x * scale},${y * scale}`).join(" ")
|
||||
);
|
||||
leftHead.setAttribute("fill", color);
|
||||
svg.appendChild(leftHead);
|
||||
|
||||
// right arrow head
|
||||
const rightHead = document.createElementNS(SVG_NS, "polygon");
|
||||
rightHead.setAttribute("points", [[18,8], [22,12], [18,16]]
|
||||
.map(([x, y]) => `${x * scale},${y * scale}`).join(" ")
|
||||
);
|
||||
rightHead.setAttribute("fill", color);
|
||||
svg.appendChild(rightHead);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
function create_inbound_missed(color, gridSize = 25) {
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
|
||||
// compute how much to scale the original 25-unit grid
|
||||
const scale = gridSize / 25;
|
||||
|
||||
// size and viewport
|
||||
svg.setAttribute("width", gridSize);
|
||||
svg.setAttribute("height", gridSize);
|
||||
svg.setAttribute("viewBox", `0 0 ${gridSize} ${gridSize}`);
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", color);
|
||||
svg.setAttribute("stroke-width", 2 * scale);
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
|
||||
// 5. Reflective bounce polyline
|
||||
const bounce = document.createElementNS(SVG_NS, 'polyline');
|
||||
bounce.setAttribute('points', [[4, 4], [12, 12], [20, 4]]
|
||||
.map(([x, y]) => `${x * scale},${y * scale}`).join(" ")
|
||||
);
|
||||
bounce.setAttribute('stroke', color);
|
||||
bounce.setAttribute('stroke-width', 2 * scale);
|
||||
bounce.setAttribute('fill', 'none');
|
||||
bounce.setAttribute('stroke-linecap', 'round');
|
||||
bounce.setAttribute('marker-end', 'url(#arrowhead)');
|
||||
svg.appendChild(bounce);
|
||||
|
||||
// scaled triangle head: tip[20,4], left wing[17,5], right wing[19, 7]
|
||||
const head = document.createElementNS(SVG_NS, "polygon");
|
||||
head.setAttribute("points", [[20, 4], [17, 5], [19, 7]]
|
||||
.map(([x, y]) => `${x * scale},${y * scale}`).join(" ")
|
||||
);
|
||||
head.setAttribute("fill", color);
|
||||
svg.appendChild(head);
|
||||
|
||||
// Left earpiece
|
||||
const left = document.createElementNS(SVG_NS, 'ellipse');
|
||||
left.setAttribute("cx", 4 * scale);
|
||||
left.setAttribute("cy", 17 * scale);
|
||||
left.setAttribute("rx", 2 * scale);
|
||||
left.setAttribute("ry", 1 * scale);
|
||||
left.setAttribute("fill", color);
|
||||
svg.appendChild(left);
|
||||
|
||||
// Right earpiece
|
||||
const right = document.createElementNS(SVG_NS, 'ellipse');
|
||||
right.setAttribute("cx", 18 * scale);
|
||||
right.setAttribute("cy", 17 * scale);
|
||||
right.setAttribute("rx", 2 * scale);
|
||||
right.setAttribute("ry", 1 * scale);
|
||||
right.setAttribute("fill", color);
|
||||
svg.appendChild(right);
|
||||
|
||||
// Arc to join left and right
|
||||
const startX = 3 * scale; // left cx + rx
|
||||
const startY = 17 * scale; // cy - ry
|
||||
const endX = 19 * scale; // right cx - rx
|
||||
const endY = startY;
|
||||
|
||||
// choose radii so the handle bows upwards
|
||||
const rx = (endX - startX) / 2; // half the distance
|
||||
const ry = 2.2 * scale; // controls how tall the arc is
|
||||
|
||||
const arc = document.createElementNS(SVG_NS, 'path');
|
||||
// Move to the left‐earpiece top, then arc to the right‐earpiece top
|
||||
const d = `M${startX},${startY} A${rx},${ry} 0 0,1 ${endX},${endY}`;
|
||||
arc.setAttribute('d', d);
|
||||
arc.setAttribute('stroke', color);
|
||||
arc.setAttribute('stroke-width', 2 * scale);
|
||||
arc.setAttribute('stroke-linecap', 'round');
|
||||
|
||||
svg.appendChild(arc);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function create_voicemail_icon(fillColor, gridSize = 25) {
|
||||
// SVG namespace
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
const scale = gridSize / 25;
|
||||
const width = scale * 25;
|
||||
const height = scale * 25;
|
||||
|
||||
// Create the root SVG element
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', width);
|
||||
svg.setAttribute('height', height);
|
||||
svg.setAttribute('viewBox', '0 0 25 25');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Border rectangle (inserted first so it's underneath)
|
||||
const border = document.createElementNS(SVG_NS, 'rect');
|
||||
y = 7;
|
||||
border.setAttribute('x', 1);
|
||||
border.setAttribute('y', y);
|
||||
border.setAttribute('width', 23);
|
||||
border.setAttribute('height', 21 - y);
|
||||
border.setAttribute('fill', 'none');
|
||||
border.setAttribute('stroke', fillColor);
|
||||
border.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(border);
|
||||
|
||||
// Left circle
|
||||
const left_circle = document.createElementNS(SVG_NS, 'circle');
|
||||
left_circle.setAttribute('cx', 7);
|
||||
left_circle.setAttribute('cy', 14);
|
||||
left_circle.setAttribute('r', 3);
|
||||
left_circle.setAttribute('fill', 'none');
|
||||
left_circle.setAttribute('stroke', fillColor);
|
||||
left_circle.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(left_circle);
|
||||
|
||||
// Right circle
|
||||
const right_circle = document.createElementNS(SVG_NS, 'circle');
|
||||
right_circle.setAttribute('cx', 18);
|
||||
right_circle.setAttribute('cy', 14);
|
||||
right_circle.setAttribute('r', 3);
|
||||
right_circle.setAttribute('fill', 'none');
|
||||
right_circle.setAttribute('stroke', fillColor);
|
||||
right_circle.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(right_circle);
|
||||
|
||||
// Connecting line
|
||||
const bar = document.createElementNS(SVG_NS, 'line');
|
||||
bar.setAttribute('x1', 6);
|
||||
bar.setAttribute('y1', 11);
|
||||
bar.setAttribute('x2', 19);
|
||||
bar.setAttribute('y2', 11);
|
||||
bar.setAttribute('stroke', fillColor);
|
||||
bar.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(bar);
|
||||
|
||||
return svg;
|
||||
}
|
||||
136
app/active_calls/resources/javascript/websocket_client.js
Normal file
136
app/active_calls/resources/javascript/websocket_client.js
Normal file
@@ -0,0 +1,136 @@
|
||||
class ws_client {
|
||||
constructor(url, token) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.addEventListener('message', this._onMessage.bind(this));
|
||||
this._nextId = 1;
|
||||
this._pending = new Map();
|
||||
this._eventHandlers = new Map();
|
||||
// The token is submitted on every request
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
// internal message handler called when event occurs on the socket
|
||||
_onMessage(ev) {
|
||||
let message;
|
||||
let switch_event;
|
||||
try {
|
||||
//console.log(ev.data);
|
||||
message = JSON.parse(ev.data);
|
||||
// check for authentication request
|
||||
if (message.status_code === 407) {
|
||||
console.log('Authentication Required');
|
||||
return;
|
||||
}
|
||||
switch_event = message.payload;
|
||||
//console.log('envelope received: ',env);
|
||||
} catch (err) {
|
||||
console.error('Error parsing JSON data:', err);
|
||||
//console.error('Invalid JSON:', ev.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pull out the request_id first
|
||||
const rid = message.request_id ?? null;
|
||||
|
||||
// If this is the response to a pending request
|
||||
if (rid && this._pending.has(rid)) {
|
||||
// Destructure with defaults in case they're missing
|
||||
const {
|
||||
service,
|
||||
topic = '',
|
||||
status = 'ok',
|
||||
code = 200,
|
||||
payload = {}
|
||||
} = message;
|
||||
|
||||
const {resolve, reject} = this._pending.get(rid);
|
||||
this._pending.delete(rid);
|
||||
|
||||
if (status === 'ok' && code >= 200 && code < 300) {
|
||||
resolve({service, topic, payload, code, message});
|
||||
} else {
|
||||
const err = new Error(message || `Error ${code}`);
|
||||
err.code = code;
|
||||
reject(err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise it's a server‑pushed event…
|
||||
// e.g. env.service === 'event' or env.topic is your event name
|
||||
this._dispatchEvent(message.service_name, switch_event);
|
||||
}
|
||||
|
||||
// Send a request to the websocket server using JSON string
|
||||
request(service, topic = null, payload = {}) {
|
||||
const request_id = String(this._nextId++);
|
||||
const env = {
|
||||
request_id: request_id,
|
||||
service,
|
||||
...(topic !== null ? {topic} : {}),
|
||||
token: this.token,
|
||||
payload: payload
|
||||
};
|
||||
const raw = JSON.stringify(env);
|
||||
this.ws.send(raw);
|
||||
return new Promise((resolve, reject) => {
|
||||
this._pending.set(request_id, {resolve, reject});
|
||||
// TODO: get timeout working to reject if no response in X ms
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(topic) {
|
||||
return this.request('active.calls', topic);
|
||||
}
|
||||
|
||||
unsubscribe(topic) {
|
||||
return this.request('active.calls', topic);
|
||||
}
|
||||
|
||||
// register a callback for server-pushes
|
||||
onEvent(topic, handler) {
|
||||
console.log('registering event listener for ' + topic);
|
||||
if (!this._eventHandlers.has(topic)) {
|
||||
this._eventHandlers.set(topic, []);
|
||||
}
|
||||
this._eventHandlers.get(topic).push(handler);
|
||||
}
|
||||
/**
|
||||
* Dispatch a server‑push event envelope to all registered handlers.
|
||||
* @param {object} env
|
||||
*/
|
||||
_dispatchEvent(service, env) {
|
||||
// if service==='event', topic carries the real event name:
|
||||
let event = (typeof env === 'string')
|
||||
? JSON.parse(env)
|
||||
: env;
|
||||
|
||||
// dispatch event handlers
|
||||
if (service === 'active.calls') {
|
||||
const topic = event.event_name;
|
||||
|
||||
let handlers = this._eventHandlers.get(topic) || [];
|
||||
if (handlers.length === 0) {
|
||||
handlers = this._eventHandlers.get('*') || [];
|
||||
}
|
||||
for (const fn of handlers) {
|
||||
try {
|
||||
fn(event);
|
||||
} catch (err) {
|
||||
console.error(`Error in handler for "${topic}":`, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const handlers = this._eventHandlers.get(service) || [];
|
||||
for (const fn of handlers) {
|
||||
try {
|
||||
fn(event.data, event);
|
||||
} catch (err) {
|
||||
console.error(`Error in handler for "${service}":`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user