Active conferences (#7684)

* Add active conferences with web sockets

* Buttons mostly working

* Convert all methods, function, variable, const to snake case instead of standards.

* Add default settings for customized control

* Add customizable settings

* More debugging default settings added

* Add better authentication handling for websocket connections
These methods were added:
- on_ws_authenticated can be overridden in the child class if there are tasks that need to be done after authentication.
- handle_ws_authenticated was added in the parent class

Handle methods are called by the this class and then their respective 'on_ws_' method is then called.

* Mute All now working

* Add PHPDoc block comments

* More PHPDoc to better describe class and variables

* Fix accidental removal of function during PHPDoc block edits

* Remove the variable type declaration for PHP 7.1 compatibility

* Update conferences with more websocket communication to replace AJAX calls.

* Ensure interface is loaded when no members

* Move color settings to theme category

* Update page view to default settings changes
This commit is contained in:
frytimo
2025-12-30 00:30:08 -05:00
committed by GitHub
parent c36e6262b1
commit 46d3eb18ea
28 changed files with 5730 additions and 59 deletions

View File

@@ -205,7 +205,7 @@ class event_message implements filterable_payload {
/**
* Creates a websocket_message_event object from a json string
* @param type $json_string
* @param string $json_string
* @return self|null
*/
public static function create_from_json($json_string) {
@@ -229,7 +229,7 @@ class event_message implements filterable_payload {
*
* @return self
*/
public static function create_from_switch_event($raw_event, filter $filter = null, $flags = 3): self {
public static function create_from_switch_event($raw_event, ?filter $filter = null, ?int $flags = 3): self {
// Set the options from the flags passed
$swap_api_name_with_event_name = ($flags & self::EVENT_SWAP_API) !== 0;

View File

@@ -0,0 +1,273 @@
<?php
/*
* 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>
*/
//includes files
require_once dirname(__DIR__, 2) . "/resources/require.php";
require_once "resources/check_auth.php";
//check permissions
if (!permission_exists('conference_interactive_view')) {
echo "access denied";
exit;
}
//show intended global variables
global $domain_uuid, $user_uuid, $settings, $database, $config;
//get the domain uuid
if (empty($domain_uuid)) {
$domain_uuid = $_SESSION['domain_uuid'] ?? '';
}
//get the user uuid
if (empty($user_uuid)) {
$user_uuid = $_SESSION['user_uuid'] ?? '';
}
//load the config
if (!($config instanceof config)) {
$config = config::load();
}
//load the database
if (!($database instanceof database)) {
$database = new database;
}
//load the settings
if (!($settings instanceof settings)) {
$settings = new settings(['database' => $database, 'domain_uuid' => $domain_uuid, 'user_uuid' => $user_uuid]);
}
//add multi-lingual support
$language = new text;
$text = $language->get();
//get the http get or post and set it as php variables
if (!empty($_REQUEST["c"]) && is_numeric($_REQUEST["c"])) {
$conference_id = $_REQUEST["c"];
}
elseif (!empty($_REQUEST["c"]) && is_uuid($_REQUEST["c"])) {
$conference_id = $_REQUEST["c"];
}
else {
//exit if the conference id is invalid
exit;
}
//replace the space with underscore
$conference_name = $conference_id.'@'.$_SESSION['domain_name'];
//get and prepare the conference display name
$conference_display_name = str_replace("-", " ", $conference_id);
$conference_display_name = str_replace("_", " ", $conference_display_name);
//create token
$token = (new token())->create($_SERVER['PHP_SELF']);
// Pass the token to the subscriber class so that when this subscriber makes a websocket
// connection, the subscriber object can validate the information.
subscriber::save_token($token, ['active.conferences']);
//show the header
$document['title'] = $text['label-interactive'];
require_once dirname(__DIR__, 2) . "/resources/header.php";
//break the caching
$version = md5(file_get_contents(__DIR__ . '/resources/javascript/websocket_client.js'));
//build permissions object for client-side checks
$user_permissions = [
'lock' => permission_exists('conference_interactive_lock'),
'mute' => permission_exists('conference_interactive_mute'),
'deaf' => permission_exists('conference_interactive_deaf'),
'kick' => permission_exists('conference_interactive_kick'),
'energy' => permission_exists('conference_interactive_energy'),
'volume' => permission_exists('conference_interactive_volume'),
'gain' => permission_exists('conference_interactive_gain'),
'video' => permission_exists('conference_interactive_video'),
];
//get websocket settings from default settings
$ws_settings = [
'reconnect_delay' => (int)$settings->get('active_conferences', 'reconnect_delay', 2000),
'ping_interval' => (int)$settings->get('active_conferences', 'ping_interval', 30000),
'auth_timeout' => (int)$settings->get('active_conferences', 'auth_timeout', 10000),
'pong_timeout' => (int)$settings->get('active_conferences', 'pong_timeout', 10000),
'refresh_interval' => (int)$settings->get('active_conferences', 'refresh_interval', 0),
'max_reconnect_delay' => (int)$settings->get('active_conferences', 'max_reconnect_delay', 30000),
'pong_timeout_max_retries' => (int)$settings->get('active_conferences', 'pong_timeout_max_retries', 3),
];
//get theme colors for status indicator
$status_colors = [
'connected' => $settings->get('theme', 'active_conference_status_connected', '#28a745'),
'warning' => $settings->get('theme', 'active_conference_status_warning', '#ffc107'),
'disconnected' => $settings->get('theme', 'active_conference_status_disconnected', '#dc3545'),
'connecting' => $settings->get('theme', 'active_conference_status_connecting', '#6c757d'),
];
//get status indicator mode and icons
$status_indicator_mode = $settings->get('theme', 'active_conference_status_indicator_mode', 'color');
$status_icons = [
'connected' => $settings->get('theme', 'active_conference_status_icon_connected', 'fa-solid fa-plug-circle-check'),
'warning' => $settings->get('theme', 'active_conference_status_icon_warning', 'fa-solid fa-plug-circle-exclamation'),
'disconnected' => $settings->get('theme', 'active_conference_status_icon_disconnected', 'fa-solid fa-plug-circle-xmark'),
'connecting' => $settings->get('theme', 'active_conference_status_icon_connecting', 'fa-solid fa-plug fa-fade'),
];
//get status tooltips from translations
$status_tooltips = [
'connected' => $text['status-connected'],
'warning' => $text['status-warning'],
'disconnected' => $text['status-disconnected'],
'connecting' => $text['status-connecting'],
];
?>
<script type="text/javascript">
//user permissions for client-side checks
const user_permissions = <?= json_encode($user_permissions) ?>;
//websocket configuration from server settings
const ws_config = <?= json_encode($ws_settings) ?>;
//status indicator colors from theme settings
const status_colors = <?= json_encode($status_colors) ?>;
//status indicator icons from settings
const status_icons = <?= json_encode($status_icons) ?>;
//status tooltips from translations
const status_tooltips = <?= json_encode($status_tooltips) ?>;
//status indicator mode: 'color' or 'icon'
const status_indicator_mode = <?= json_encode($status_indicator_mode) ?>;
//translations
const text = <?= json_encode($text) ?>;
//send action via WebSocket
function send_action(action, options = {}, skip_refresh = false) {
if (!ws || !ws.ws || ws.ws.readyState !== WebSocket.OPEN) {
console.error('WebSocket not connected');
return Promise.reject('Not connected');
}
const payload = {
action: action,
conference_name: conference_name,
domain_name: '<?= $_SESSION['domain_name'] ?>',
...options
};
console.log('Sending action:', action, payload);
return ws.request('active.conferences', 'action', payload)
.then(response => {
console.log('Action response:', response);
const result = response.payload || response;
if (!result.success) {
console.error('Action failed:', result.message);
}
// Refresh data after action (unless skip_refresh is true)
if (!skip_refresh) {
load_conference_data();
}
return result;
})
.catch(err => {
console.error('Action error:', err);
throw err;
});
}
//conference control functions
function conference_action(action, member_id, uuid, direction) {
return send_action(action, {
member_id: member_id || '',
uuid: uuid || '',
direction: direction || ''
});
}
var record_count = 0;
</script>
<?php
$ws_client_file = __DIR__ . '/resources/javascript/websocket_client.js';
$ws_client_hash = file_exists($ws_client_file) ? md5_file($ws_client_file) : $version;
$ac_js_file = __DIR__ . '/resources/javascript/active_conferences.js';
$ac_js_hash = file_exists($ac_js_file) ? md5_file($ac_js_file) : $version;
?>
<script src="resources/javascript/websocket_client.js?v=<?= $ws_client_hash ?>"></script>
<script src="resources/javascript/active_conferences.js?v=<?= $ac_js_hash ?>"></script>
<?php
//page header
echo "<div class='action_bar' id='action_bar'>\n";
echo "<div class='heading'><b>".$text['label-interactive']."</b>&nbsp;";
if ($status_indicator_mode === 'icon') {
echo "<span id='connection_status' class='".$status_icons['connecting']."' style='color: ".$status_colors['connecting'].";' title='".$status_tooltips['connecting']."'></span>";
} else {
echo "<div id='connection_status' class='count' style='display: inline-block; min-width: 12px; height: 12px; vertical-align: middle; background: ".$status_colors['connecting'].";' title='".$status_tooltips['connecting']."'></div>";
}
echo "</div>\n";
echo "<div class='actions'>\n";
echo "</div>\n";
echo "<div style='clear: both;'></div>\n";
echo "</div>\n";
echo $text['description-interactive']."\n";
echo "<br /><br />\n";
//show the content
echo "<div id='conference_container'></div>\n";
echo "<br /><br />\n";
?>
<script>
const token = {
name: '<?= $token['name'] ?>',
hash: '<?= $token['hash'] ?>'
};
const conference_name = <?= json_encode($conference_name) ?>;
const conference_id = <?= json_encode($conference_id) ?>;
const domain_name = '<?= $_SESSION['domain_name'] ?>';
// Start websocket connection
connect_websocket();
render_conference_room();
</script>
<?php require_once "resources/footer.php"; ?>

View File

@@ -0,0 +1,174 @@
<?php
/*
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-2024
the Initial Developer. All Rights Reserved.
Contributor(s):
Mark J Crane <markjcrane@fusionpbx.com>
James Rose <james.o.rose@gmail.com>
*/
//includes files
require_once dirname(__DIR__, 2) . "/resources/require.php";
require_once "resources/check_auth.php";
//check permissions
if (!permission_exists('conference_active_view')) {
echo "access denied";
exit;
}
//add multi-lingual support
$language = new text;
$text = $language->get();
//create token
$token = (new token())->create($_SERVER['PHP_SELF']);
//pass the token to the subscriber class so that when this subscriber makes a websocket
//connection, the subscriber object can validate the information.
subscriber::save_token($token, ['active.conferences']);
//include the header
$document['title'] = $text['title-active_conferences'];
require_once "resources/header.php";
//break the caching
$version = md5(file_get_contents(__DIR__ . '/resources/javascript/websocket_client.js'));
//get websocket settings from default settings
$ws_settings = [
'reconnect_delay' => (int)$settings->get('active_conferences', 'reconnect_delay', 2000),
'ping_interval' => (int)$settings->get('active_conferences', 'ping_interval', 30000),
'auth_timeout' => (int)$settings->get('active_conferences', 'auth_timeout', 10000),
'pong_timeout' => (int)$settings->get('active_conferences', 'pong_timeout', 10000),
'refresh_interval' => (int)$settings->get('active_conferences', 'refresh_interval', 0),
'max_reconnect_delay' => (int)$settings->get('active_conferences', 'max_reconnect_delay', 30000),
'pong_timeout_max_retries' => (int)$settings->get('active_conferences', 'pong_timeout_max_retries', 3),
];
//get theme colors for status indicator
$status_colors = [
'connected' => $settings->get('theme', 'active_conference_status_connected', '#28a745'),
'warning' => $settings->get('theme', 'active_conference_status_warning', '#ffc107'),
'disconnected' => $settings->get('theme', 'active_conference_status_disconnected', '#dc3545'),
'connecting' => $settings->get('theme', 'active_conference_status_connecting', '#6c757d'),
];
//get status indicator mode and icons
$status_indicator_mode = $settings->get('theme', 'active_conference_status_indicator_mode', 'color');
$status_icons = [
'connected' => $settings->get('theme', 'active_conference_status_icon_connected', 'fa-solid fa-plug-circle-check'),
'warning' => $settings->get('theme', 'active_conference_status_icon_warning', 'fa-solid fa-plug-circle-exclamation'),
'disconnected' => $settings->get('theme', 'active_conference_status_icon_disconnected', 'fa-solid fa-plug-circle-xmark'),
'connecting' => $settings->get('theme', 'active_conference_status_icon_connecting', 'fa-solid fa-plug fa-fade'),
];
//get status tooltips from translations
$status_tooltips = [
'connected' => $text['status-connected'],
'warning' => $text['status-warning'],
'disconnected' => $text['status-disconnected'],
'connecting' => $text['status-connecting'],
];
?>
<script type="text/javascript">
//websocket configuration from server settings
const ws_config = <?= json_encode($ws_settings) ?>;
//status indicator colors from theme settings
const status_colors = <?= json_encode($status_colors) ?>;
//status indicator icons from settings
const status_icons = <?= json_encode($status_icons) ?>;
//status tooltips from translations
const status_tooltips = <?= json_encode($status_tooltips) ?>;
//status indicator mode: 'color' or 'icon'
const status_indicator_mode = <?= json_encode($status_indicator_mode) ?>;
//translations
const text = <?= json_encode($text) ?>;
//permissions
const permissions = {
conference_interactive_view: <?= permission_exists('conference_interactive_view') ? 'true' : 'false' ?>,
list_row_edit_button: <?= $settings->get('theme', 'list_row_edit_button', false) ? 'true' : 'false' ?>
};
//button icon
const button_icon_view = '<?= $settings->get('theme', 'button_icon_view') ?>';
</script>
<?php
$ws_client_file = __DIR__ . '/resources/javascript/websocket_client.js';
$ws_client_hash = file_exists($ws_client_file) ? md5_file($ws_client_file) : $version;
$ac_js_file = __DIR__ . '/resources/javascript/active_conferences.js';
$ac_js_hash = file_exists($ac_js_file) ? md5_file($ac_js_file) : $version;
?>
<script src="resources/javascript/websocket_client.js?v=<?= $ws_client_hash ?>"></script>
<script src="resources/javascript/active_conferences.js?v=<?= $ac_js_hash ?>"></script>
<script type="text/javascript">
const token = {
name: '<?= $token['name'] ?>',
hash: '<?= $token['hash'] ?>'
};
// Domain name for filtering
const domain_name = '<?= $_SESSION['domain_name'] ?>';
// Start websocket connection
connect_websocket();
</script>
<?php
//page header
echo "<div class='action_bar' id='action_bar'>\n";
echo " <div class='heading'><b>".$text['title-active_conferences']."</b>";
if ($status_indicator_mode === 'icon') {
echo "<span id='connection_status' class='".$status_icons['connecting']."' style='color: ".$status_colors['connecting'].";' title='".$status_tooltips['connecting']."'></span>";
} else {
echo "<div id='connection_status' class='count'><span id='conference_count'>0</span></div>";
}
echo "</div>\n";
echo " <div class='actions'>\n";
echo " </div>\n";
echo " <div style='clear: both;'></div>\n";
echo "</div>\n";
echo $text['description-active']."\n";
echo "<br /><br />\n";
//show the content
echo "<div id='conferences_container'></div>"; // Replaced ajax_response
echo "<br><br>";
//include the footer
require_once "resources/footer.php";
?>

View File

@@ -0,0 +1,237 @@
<?php
//application details
$apps[$x]['name'] = "Conferences Active";
$apps[$x]['uuid'] = "c168c943-833a-c29c-7ef9-d1ee78810b71";
$apps[$x]['category'] = "Switch";;
$apps[$x]['subcategory'] = "";
$apps[$x]['version'] = "1.0";
$apps[$x]['license'] = "Mozilla Public License 1.1";
$apps[$x]['url'] = "http://www.fusionpbx.com";
$apps[$x]['description']['en-us'] = "A real-time active conference viewer and moderator tool.";
$apps[$x]['description']['en-gb'] = "A real-time active conference viewer and moderator tool.";
$apps[$x]['description']['ar-eg'] = "";
$apps[$x]['description']['de-at'] = "";
$apps[$x]['description']['de-ch'] = "";
$apps[$x]['description']['de-de'] = "";
$apps[$x]['description']['es-cl'] = "";
$apps[$x]['description']['es-mx'] = "";
$apps[$x]['description']['fr-ca'] = "";
$apps[$x]['description']['fr-fr'] = "";
$apps[$x]['description']['he-il'] = "";
$apps[$x]['description']['it-it'] = "";
$apps[$x]['description']['ka-ge'] = "";
$apps[$x]['description']['nl-nl'] = "";
$apps[$x]['description']['pl-pl'] = "";
$apps[$x]['description']['pt-br'] = "";
$apps[$x]['description']['pt-pt'] = "";
$apps[$x]['description']['ro-ro'] = "";
$apps[$x]['description']['ru-ru'] = "";
$apps[$x]['description']['sv-se'] = "";
$apps[$x]['description']['uk-ua'] = "";
//permission details
$y=0;
$apps[$x]['permissions'][$y]['name'] = "conference_active_view";
$apps[$x]['permissions'][$y]['menu']['uuid'] = "2d857bbb-43b9-b8f7-a138-642868e0453a";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_view";
$apps[$x]['permissions'][$y]['groups'][] = "user";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_lock";
$apps[$x]['permissions'][$y]['groups'][] = "user";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_kick";
$apps[$x]['permissions'][$y]['groups'][] = "user";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_energy";
//$apps[$x]['permissions'][$y]['groups'][] = "user";
//$apps[$x]['permissions'][$y]['groups'][] = "admin";
//$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_volume";
//$apps[$x]['permissions'][$y]['groups'][] = "user";
//$apps[$x]['permissions'][$y]['groups'][] = "admin";
//$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_gain";
//$apps[$x]['permissions'][$y]['groups'][] = "user";
//$apps[$x]['permissions'][$y]['groups'][] = "admin";
//$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_mute";
$apps[$x]['permissions'][$y]['groups'][] = "user";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_deaf";
$apps[$x]['permissions'][$y]['groups'][] = "user";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_video";
$apps[$x]['permissions'][$y]['groups'][] = "user";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
//default settings
$y = 0;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "0242ad2b-72c7-42f8-b8fe-8716654e0c99";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "reconnect_delay";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "2000";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Base delay in milliseconds before attempting to reconnect after disconnect.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "9451b1a9-4f81-4819-bfe4-47cc468cd095";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "ping_interval";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "15000";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Interval in milliseconds between keepalive ping requests.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "605791c3-f20a-438c-98ec-869b600d27ea";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "auth_timeout";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "3000";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Timeout in milliseconds waiting for WebSocket authentication before redirecting to login.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "ea18eab7-9772-4ade-b318-a70a2ed40906";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "pong_timeout";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "500";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Timeout in milliseconds waiting for pong response before reloading the page.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "31b5f0e3-aec7-403b-be3d-94dbf3b8a59e";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "refresh_interval";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "0";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Optional interval in milliseconds to periodically refresh conference data. Set to 0 to disable (rely on WebSocket events only).";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "3a757df6-37ce-4351-b046-d3ba2eaffd18";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "max_reconnect_delay";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "15000";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Maximum delay in milliseconds between reconnection attempts (exponential backoff cap).";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "79c34bee-725a-4b85-86eb-5d9ceabda6a4";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "pong_timeout_max_retries";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "3";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Number of pong timeouts allowed before reloading the page. During retries, status indicator shows warning color.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "28babdbb-9155-4ff9-928d-5e9d7bac94c4";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_connected";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#28a745";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when connected and receiving pong responses.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "8a447132-799a-4ae7-8d21-137df917b66a";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_warning";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#ffc107";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when ping sent but pong not yet received (warning state).";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "809954c0-48b9-4828-8843-a046235515b6";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_disconnected";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#dc3545";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when disconnected or not authenticated.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "522d2274-b4ea-4f25-9bb3-43d7669defe2";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_connecting";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#6c757d";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when connecting or authenticating.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "2c16616d-49d8-4921-b6bf-112f0f2fb4e8";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_indicator_mode";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "color";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Status indicator display mode: 'color' for colored circle, 'icon' for Font Awesome icons.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "51357753-3782-4b23-9e31-ea5d5c649890";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_connected";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug-circle-check";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for connected status.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "86c2ef42-2988-4d6d-9c92-3fc697a171ba";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_warning";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug-circle-exclamation";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for warning status (ping sent, awaiting pong).";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "51e4cff5-de0d-4ecb-b4e6-33eac9140b4b";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_disconnected";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug-circle-xmark";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for disconnected status.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "1b5a4a79-9b15-44ff-8a7c-d4cc773151f2";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_connecting";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug fa-fade";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for connecting status.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "7fb8a315-38ff-4170-9b3c-bd7eae513d35";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "debug_show_permissions_mode";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "off";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "false";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "When running in debug mode, Permissions can be shown as: 'bytes' for bytes only, 'full' for detailed permission checks, or 'off' to suppress showing permissions.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "9b8853db-244c-40a6-a8fe-77c5b133c6b0";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "debug_show_switch_event";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "boolean";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "false";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "When running in debug mode, show the raw switch event message for conference maintenance events.";
$y++;
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "f84c37ed-0469-42d1-9f20-cb3ba00a9d7b";
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "websocket_enabled";
$apps[$x]['default_settings'][$y]['default_setting_name'] = "boolean";
$apps[$x]['default_settings'][$y]['default_setting_value'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Enable or disable the use of websockets for active conferences.";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
<?php
$y=0;
$apps[$x]['menu'][$y]['title']['en-us'] = "Active Conferences";
$apps[$x]['menu'][$y]['title']['en-gb'] = "Active Conferences";
$apps[$x]['menu'][$y]['title']['ar-eg'] = "المؤتمرات النشطة";
$apps[$x]['menu'][$y]['title']['de-at'] = "Aktive Konferenzen";
$apps[$x]['menu'][$y]['title']['de-ch'] = "Aktive Konferenzen";
$apps[$x]['menu'][$y]['title']['de-de'] = "Aktive Konferenzen";
$apps[$x]['menu'][$y]['title']['es-cl'] = "Conferencias Activas";
$apps[$x]['menu'][$y]['title']['es-mx'] = "Conferencias activas";
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Conférences actives";
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Conférences en cours";
$apps[$x]['menu'][$y]['title']['he-il'] = "כנסים פעילים";
$apps[$x]['menu'][$y]['title']['it-it'] = "Conferenze attive";
$apps[$x]['menu'][$y]['title']['ka-ge'] = "აქტიური კონფერენციები";
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Aktieve conferenties";
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Aktywne rozmowy konferencyjne";
$apps[$x]['menu'][$y]['title']['pt-br'] = "Conferência ativa";
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Conferencias Activas";
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Conferințe active";
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференции Активные";
$apps[$x]['menu'][$y]['title']['sv-se'] = "Aktiva Konferenser";
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Активні конференції";
$apps[$x]['menu'][$y]['title']['zh-cn'] = "活动会议";
$apps[$x]['menu'][$y]['title']['ja-jp'] = "アクティブな会議";
$apps[$x]['menu'][$y]['title']['ko-kr'] = "활성 회의";
$apps[$x]['menu'][$y]['uuid'] = "b3cdd852-1382-4d7f-9676-f25b133971f2";
$apps[$x]['menu'][$y]['parent_uuid'] = "0438b504-8613-7887-c420-c837ffb20cb1";
$apps[$x]['menu'][$y]['category'] = "internal";
$apps[$x]['menu'][$y]['icon'] = "";
$apps[$x]['menu'][$y]['path'] = "/app/active_conferences/active_conferences.php";
$apps[$x]['menu'][$y]['order'] = "";
$apps[$x]['menu'][$y]['groups'][] = "admin";
$apps[$x]['menu'][$y]['groups'][] = "superadmin";
$y++;

View File

@@ -0,0 +1,364 @@
<?php
require_once dirname(__DIR__, 2) . '/resources/require.php';
// Create the token
$token = (new token)->create($_SERVER['PHP_SELF']);
// Save the token
subscriber::save_token($token, [active_conferences_service::get_service_name()]);
//break the caching
$version = md5(file_get_contents(__DIR__ . '/resources/javascript/websocket_client.js'));
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Event Logger</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.websocket-container {
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 20px;
}
.websocket-header {
background-color: #f5f5f5;
padding: 10px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #ddd;
}
.websocket-header:hover {
background-color: #e9e9e9;
}
.toggle-icon {
transition: transform 0.2s;
}
.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.websocket-content {
padding: 15px;
background-color: #fff;
}
.event-log {
max-height: 400px;
overflow-y: auto;
border: 1px solid #eee;
padding: 10px;
background-color: #f9f9f9;
font-family: monospace;
font-size: 12px;
}
.event-item {
margin-bottom: 8px;
padding: 5px;
border-radius: 3px;
background-color: #fff;
border-left: 3px solid #007bff;
}
.event-item:hover {
background-color: #f0f8ff;
}
.event-timestamp {
color: #666;
font-size: 11px;
}
.event-type {
font-weight: bold;
color: #007bff;
}
.event-data {
margin-top: 5px;
word-break: break-all;
}
.connection-status {
margin-bottom: 10px;
padding: 10px;
border-radius: 3px;
}
.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.controls {
margin-bottom: 20px;
}
button {
padding: 8px 15px;
margin-right: 10px;
cursor: pointer;
border: none;
border-radius: 3px;
}
.connect-btn {
background-color: #28a745;
color: white;
}
.disconnect-btn {
background-color: #dc3545;
color: white;
}
.clear-btn {
background-color: #6c757d;
color: white;
}
</style>
<script>
const token = {
name: '<?= $token['name'] ?>',
hash: '<?= $token['hash'] ?>'
}
</script>
<script src="resources/javascript/websocket_client.js?v=<?= $version ?>"></script>
</head>
<body>
<h1>WebSocket Event Logger</h1>
<div class="controls">
<button class="connect-btn" onclick="connect_websocket()">Connect</button>
<button class="disconnect-btn" onclick="disconnect_websocket()">Disconnect</button>
<button class="clear-btn" onclick="clear_log()">Clear Log</button>
</div>
<div id="connection-status" class="connection-status disconnected">
Disconnected
</div>
<div class="websocket-container" id="websocket-container">
<div class="websocket-header" onclick="toggle_collapse()">
<span>WebSocket Events <span id="event-count">(0 events)</span></span>
<span class="toggle-icon">▼</span>
</div>
<div class="websocket-content" id="websocket-content">
<div class="event-log" id="event-log">
<div id="placeholder">No events received yet. Connect to WebSocket to start logging.</div>
</div>
</div>
</div>
<script>
let ws = null;
let event_count = 0;
let is_collapsed = false;
let reconnect_attempts = 0;
const max_reconnect_delay = 30000; // 30 seconds
const base_reconnect_delay = 1000; // 1 second
function connect_websocket() {
// Replace with your WebSocket server URL
const ws_url = `wss://${window.location.hostname}/websockets/`; // Update this URL
try {
ws = new ws_client(ws_url, token);
ws.on_event('authenticated', authenticated);
// CONNECTED
ws.ws.addEventListener("open", () => {
console.log('WebSocket connection opened');
reconnect_attempts = 0;
});
// DISCONNECTED - handle reconnection
ws.ws.addEventListener("close", (event) => {
console.warn('WebSocket disconnected:', event.code, event.reason);
update_connection_status('Disconnected - reconnecting...', 'disconnected');
// Exponential backoff for reconnection
reconnect_attempts++;
const delay = Math.min(base_reconnect_delay * Math.pow(2, reconnect_attempts - 1), max_reconnect_delay);
console.log(`Reconnecting in ${delay}ms (attempt ${reconnect_attempts})`);
setTimeout(() => {
connect_websocket();
}, delay);
});
// ERROR
ws.ws.addEventListener("error", (error) => {
console.error('WebSocket error:', error);
});
} catch (error) {
console.error('Failed to connect to WebSocket:', error);
update_connection_status('Connection failed: ' + error.message, 'disconnected');
}
}
function authenticated(message) {
console.log('WebSocket connected');
update_connection_status('Connected', 'connected');
// Log the authenticated event to the UI
log_event(JSON.stringify({event_name: 'authenticated', message: message}));
// Register wildcard handler to catch ALL events
ws.on_event('*', on_any_event);
// Subscribe to all events using wildcard
ws.subscribe('*');
}
function on_any_event(event) {
console.log('Event received:', event.event_name, event);
log_event(JSON.stringify(event));
}
function disconnect_websocket() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
}
function log_event(data) {
event_count++;
update_event_count();
const event_log = document.getElementById('event-log');
// Remove placeholder if it exists
const placeholder = document.getElementById('placeholder');
if (placeholder) {
placeholder.remove();
}
let event_data;
try {
event_data = JSON.parse(data);
} catch (e) {
event_data = data;
}
// Check for payload object (events wrapped in websocket_message)
const payload = event_data.payload || event_data;
// Get UUID from unique_id field (with fallbacks)
const event_uuid = payload.unique_id || event_data.unique_id || payload.uuid || event_data.uuid || '';
// Get action from Action field, falling back to event_name
const event_action = payload.action || event_data.action || event_data.event_name || payload.event_name || 'Unknown Event';
const event_item = document.createElement('div');
event_item.className = 'event-item collapsible';
const timestamp = new Date().toLocaleString();
// Format display: show action, optionally with UUID if present
const display_text = event_uuid ? `${event_action} (${event_uuid})` : event_action;
event_item.innerHTML = `
<div class="event-header" onclick="toggle_event(this)">
<span class="event-timestamp">${timestamp}</span>
<span class="event-type">${display_text}</span>
<span class="toggle-icon">▼</span>
</div>
<div class="event-data" style="display: none;">
${format_data(event_data)}
</div>
`;
// Insert at the top of the list (newest first)
event_log.insertBefore(event_item, event_log.firstChild);
}
function toggle_event(element) {
const event_data = element.nextElementSibling;
const icon = element.querySelector('.toggle-icon');
if (event_data.style.display === 'none' || event_data.style.display === '') {
event_data.style.display = 'block';
icon.textContent = '▲';
} else {
event_data.style.display = 'none';
icon.textContent = '▼';
}
}
function format_data(data) {
if (typeof data === 'object') {
return JSON.stringify(data, null, 2);
}
return String(data);
}
function update_connection_status(message, status_class) {
const status_element = document.getElementById('connection-status');
status_element.textContent = message;
status_element.className = 'connection-status ' + status_class;
}
function update_event_count() {
document.getElementById('event-count').textContent = `(${event_count} events)`;
}
function clear_log() {
const event_log = document.getElementById('event-log');
event_log.innerHTML = '<div id="placeholder">No events received yet. Connect to WebSocket to start logging.</div>';
event_count = 0;
update_event_count();
}
function toggle_collapse() {
const container = document.getElementById('websocket-container');
const content = document.getElementById('websocket-content');
const icon = document.querySelector('.toggle-icon');
is_collapsed = !is_collapsed;
if (is_collapsed) {
container.classList.add('collapsed');
content.style.display = 'none';
icon.textContent = '▶';
} else {
container.classList.remove('collapsed');
content.style.display = 'block';
icon.textContent = '▼';
}
}
// Auto-connect on page load (optional)
// window.addEventListener('load', connect_websocket);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
<?php
/*
* 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>
*/
/**
* Filters events based on event type permissions.
*
* When the 'event_name' or 'action' key is encountered, this filter checks
* if the subscriber has permission to receive this event type. If not,
* returns null to drop the entire message.
*
* @author FusionPBX
*/
class event_type_permission_filter implements filter {
/**
* Map of event types to required permissions
* @var array
*/
private $event_permission_map;
/**
* The subscriber's permissions
* @var array
*/
private $permissions;
/**
* Whether permission check has been performed for this message
* @var bool
*/
private $checked = false;
/**
* Constructor
*
* @param array $event_permission_map Map of event types to required permissions
* @param array $permissions The subscriber's permissions
*/
public function __construct(array $event_permission_map, array $permissions) {
$this->event_permission_map = $event_permission_map;
$this->permissions = $permissions;
}
/**
* Check if the subscriber has permission to receive this event type.
*
* When invoked with the 'event_name' or 'action' key, checks the event type
* against the permission map. Returns null to drop the message if subscriber
* doesn't have permission.
*
* @param string $key The key from the payload
* @param mixed $value The value from the payload
*
* @return bool|null True if permitted or not an event type key, null to drop message
*/
public function __invoke(string $key, $value): ?bool {
// Only check event_name or action keys (first one wins)
if ($this->checked || ($key !== 'event_name' && $key !== 'action')) {
return true;
}
$this->checked = true;
// Normalize the event name (replace hyphens with underscores)
$event_type = str_replace('-', '_', $value);
// Look up required permission for this event type
$required_permission = $this->event_permission_map[$event_type] ?? null;
// If event is not in map, allow by default (base view permission already checked)
if ($required_permission === null) {
return true;
}
// Check if subscriber has the required permission
if (isset($this->permissions[$required_permission])) {
return true;
}
// Subscriber doesn't have permission - drop the entire message
return null;
}
/**
* Reset the filter for reuse with a new message
*/
public function reset(): void {
$this->checked = false;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,199 @@
class ws_client {
constructor(url, token) {
this.ws = new WebSocket(url);
this.ws.addEventListener('message', this._on_message.bind(this));
this._next_id = 1;
this._pending = new Map();
this._event_handlers = new Map();
// The token is submitted on every request
this.token = token;
}
authenticate() {
//
// Authentication is with websockets not the service, so we need to send a special
// request for authentication and specify the service that will be handling our
// future messages. This means the service is authentication and the topic is the
// service that will handle our future messages. This is a special case because we
// must authenticate with websockets, not the service. The service is only used to
// handle future messages.
//
// service = 'authentication'
// topic = active_conferences_service::get_service_name()
// payload = token
//
this.request('authentication', 'active.conferences', { token: this.token });
}
// internal message handler called when event occurs on the socket
_on_message(ev) {
let message;
let switch_event;
try {
console.log('Raw message received:', ev.data);
message = JSON.parse(ev.data);
// check for authentication request
if (message.status_code === 407) {
console.log('Authentication Required');
this.authenticate();
return;
}
switch_event = message.payload;
if (message.topic === 'authenticated') {
console.log('Authenticated');
this._dispatch_event('active.conferences', {event_name: 'authenticated'});
return; // Don't process further after authenticated
}
//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_name = '',
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) {
console.log('Response received:', {service_name, topic, payload, code});
resolve({service_name, topic, payload, code, message});
// Also dispatch as an event so handlers get notified
// Use topic from message as event_name if payload doesn't have one
const event_data = (typeof switch_event === 'object' && switch_event !== null)
? { ...switch_event, event_name: switch_event.event_name || topic }
: { event_name: topic, data: switch_event };
this._dispatch_event(service_name, event_data);
} else {
const err = new Error(message || `Error ${code}`);
err.code = code;
reject(err);
}
return;
}
// Otherwise it's a serverpushed event…
// e.g. env.service === 'event' or env.topic is your event name
console.log('Server-pushed event - service_name:', message.service_name, 'service:', message.service, 'topic:', message.topic, 'payload:', switch_event);
// Use service_name, or fall back to service, or default to 'active.conferences'
const service = message.service_name || message.service || 'active.conferences';
// Ensure event has event_name set from topic if not in payload
// IMPORTANT: Also preserve the topic as the action since that's what the PHP service sends
const event_data = (typeof switch_event === 'object' && switch_event !== null)
? { ...switch_event, event_name: switch_event.event_name || message.topic, topic: message.topic }
: { event_name: message.topic, topic: message.topic, data: switch_event };
console.log('Dispatching event to handlers:', event_data);
this._dispatch_event(service, event_data);
}
// Send a request to the websocket server using JSON string
request(service, topic = null, payload = {}) {
const request_id = String(this._next_id++);
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.conferences', topic);
}
unsubscribe(topic) {
return this.request('active.conferences', topic);
}
// register a callback for server-pushes
on_event(topic, handler) {
console.log('registering event listener for ' + topic);
if (!this._event_handlers.has(topic)) {
this._event_handlers.set(topic, []);
}
this._event_handlers.get(topic).push(handler);
}
/**
* Dispatch a serverpush event envelope to all registered handlers.
* @param {object} env
*/
_dispatch_event(service, env) {
console.log('_dispatch_event called with service:', service, 'env:', env);
// if service==='event', topic carries the real event name:
let event = (typeof env === 'string')
? JSON.parse(env)
: env;
console.log('Parsed event:', event);
console.log('Registered handlers:', Array.from(this._event_handlers.keys()));
// dispatch event handlers
if (service === 'active.conferences') {
const topic = event.event_name;
console.log('Looking for handlers for topic:', topic);
// Get specific handlers for this topic
const handlers = this._event_handlers.get(topic) || [];
// Always get wildcard handlers too
const wildcard_handlers = this._event_handlers.get('*') || [];
console.log('Found handlers:', handlers.length, 'wildcard:', wildcard_handlers.length);
// Call specific handlers
for (const fn of handlers) {
try {
fn(event);
} catch (err) {
console.error(`Error in handler for "${topic}":`, err);
}
}
// Always call wildcard handlers for all events
for (const fn of wildcard_handlers) {
try {
fn(event);
} catch (err) {
console.error(`Error in wildcard handler:`, err);
}
}
} else {
const handlers = this._event_handlers.get(service) || [];
for (const fn of handlers) {
try {
if (fn === '*') {
event(event.data, event);
} else {
fn(event.data, event);
}
} catch (err) {
console.error(`Error in handler for "${service}":`, err);
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env php
<?php
/*
* 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>
*/
/**
* Active Conferences Service
* Polls conference data and broadcasts via websockets
*/
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
die("This script requires PHP 7.1.0 or higher. You are running " . PHP_VERSION . "\n");
}
require_once dirname(__DIR__, 4) . '/resources/require.php';
define('SERVICE_NAME', active_conferences_service::get_service_name());
try {
$active_conferences_service = active_conferences_service::create();
// Exit using whatever status run returns
exit($active_conferences_service->run());
} catch (Exception $ex) {
echo "Error occurred in " . $ex->getFile() . ' (' . $ex->getLine() . '):' . $ex->getMessage();
// Exit with error code
exit($ex->getCode());
}

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Active Conferences Websocket Service
[Service]
WorkingDirectory=/var/www/fusionpbx
ExecStart=/usr/bin/php /var/www/fusionpbx/app/active_conferences/resources/service/active_conferences.php
RuntimeDirectory=fusionpbx
RuntimeDirectoryMode=0755
RuntimeDirectoryPreserve=yes
User=www-data
Group=www-data
Restart=always
RestartSec=5
StartLimitInterval=0
[Install]
WantedBy=multi-user.target

View File

@@ -34,29 +34,30 @@
$apps[$x]['menu'][$y]['groups'][] = "superadmin";
$apps[$x]['menu'][$y]['groups'][] = "admin";
$y++;
$apps[$x]['menu'][$y]['title']['en-us'] = "Conference Centers";
$apps[$x]['menu'][$y]['title']['ar-eg'] = "مراكز المؤتمرات";
$apps[$x]['menu'][$y]['title']['de-at'] = "Konferenz Zentrale";
$apps[$x]['menu'][$y]['title']['de-ch'] = "Konferenzzentren";
$apps[$x]['menu'][$y]['title']['de-de'] = "Konferenz Zentrale";
$apps[$x]['menu'][$y]['title']['es-cl'] = "Cent. de Conferencias";
$apps[$x]['menu'][$y]['title']['es-mx'] = "Conference Centers";
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Centres de conférences";
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Centre de Conférences";
$apps[$x]['menu'][$y]['title']['he-il'] = "מרכזי כנסים";
$apps[$x]['menu'][$y]['title']['it-it'] = "Centro Conferenze";
$apps[$x]['menu'][$y]['title']['ka-ge'] = "კონფერენც-ცენტრები";
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Conferentie centra";
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Centrum Konferencyjne";
$apps[$x]['menu'][$y]['title']['pt-br'] = "Centro de Conferência";
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Conferencias";
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Centre de conferințe";
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференц-центр";
$apps[$x]['menu'][$y]['title']['sv-se'] = "Konferenscenter";
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Конференц-центр";
$apps[$x]['menu'][$y]['title']['zh-cn'] = "会议中心";
$apps[$x]['menu'][$y]['title']['ja-jp'] = "カンファレンスセンター";
$apps[$x]['menu'][$y]['title']['ko-kr'] = "컨퍼런스 센터";
$apps[$x]['menu'][$y]['title']['en-us'] = "Conference Rooms";
$apps[$x]['menu'][$y]['title']['en-gb'] = "Conference Rooms";
$apps[$x]['menu'][$y]['title']['ar-eg'] = "غرف المؤتمرات";
$apps[$x]['menu'][$y]['title']['de-at'] = "Konferenzräume";
$apps[$x]['menu'][$y]['title']['de-ch'] = "Konferenzräume";
$apps[$x]['menu'][$y]['title']['de-de'] = "Konferenzräume";
$apps[$x]['menu'][$y]['title']['es-cl'] = "Salas de Conferencias";
$apps[$x]['menu'][$y]['title']['es-mx'] = "Salas de Conferencias";
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Salles de conférences";
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Salles de Conférences";
$apps[$x]['menu'][$y]['title']['he-il'] = "חדרי כנסים";
$apps[$x]['menu'][$y]['title']['it-it'] = "Sale Conferenze";
$apps[$x]['menu'][$y]['title']['ka-ge'] = "კონფერენციის ოთახები";
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Conferentie kamers";
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Sale Konferencyjne";
$apps[$x]['menu'][$y]['title']['pt-br'] = "Salas de Conferência";
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Salas de Conferências";
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Săli de conferințe";
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференц-залы";
$apps[$x]['menu'][$y]['title']['sv-se'] = "Konferensrum";
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Конференц-зали";
$apps[$x]['menu'][$y]['title']['zh-cn'] = "会议室";
$apps[$x]['menu'][$y]['title']['ja-jp'] = "会議室";
$apps[$x]['menu'][$y]['title']['ko-kr'] = "컨퍼런스 룸";
$apps[$x]['menu'][$y]['uuid'] = "b99cb768-ca19-4374-a954-02e344313d84";
$apps[$x]['menu'][$y]['parent_uuid'] = "fd29e39c-c936-f5fc-8e2b-611681b266b5";
$apps[$x]['menu'][$y]['category'] = "internal";

View File

@@ -40,16 +40,24 @@
$text = $language->get();
//set additional variables
$search = $_GET["search"] ?? null;
$show = $_REQUEST["show"] ?? '';
$action = $_REQUEST['action'] ?? '';
$search = $_REQUEST['search'] ?? '';
$toggle_field = $_REQUEST['toggle_field'] ?? '';
//check if we are using websockets for the conference view
if ($settings->get('active_conferences', 'websockets_enabled', true)) {
$conference_view_page = '/app/active_conferences/active_conference_room.php';
}
else {
$conference_view_page = '/app/conferences_active/conferences_active.php';
}
//set from session variables
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
//get the http post data
if (!empty($_POST['conference_rooms'])) {
$action = $_POST['action'];
$toggle_field = $_POST['toggle_field'];
$search = $_POST['search'] ?? '';
$conference_rooms = $_POST['conference_rooms'];
}
@@ -448,10 +456,10 @@
}
echo " <td class='no-link no-wrap center'>\n";
if (permission_exists('conference_interactive_view')) {
echo " <a href='".PROJECT_PATH."/app/conferences_active/conference_interactive.php?c=".urlencode($row['conference_room_uuid'])."'>".$text['label-view']."</a>\n";
echo " <a href='".PROJECT_PATH.$conference_view_page."?c=".urlencode($row['conference_room_uuid'])."'>".$text['label-view']."</a>\n";
}
else if (permission_exists('conference_active_view')) {
echo " <a href='".PROJECT_PATH."/app/conferences_active/conferences_active.php'>".$text['label-view']."</a>\n";
echo " <a href='".PROJECT_PATH.$conference_view_page."'>".$text['label-view']."</a>\n";
}
if (permission_exists('conference_cdr_view')) {
echo " <a href='/app/conference_cdr/conference_cdr.php?id=".urlencode($row['conference_room_uuid'])."'>".$text['button-cdr']."</a>\n";

View File

@@ -40,15 +40,23 @@
$text = $language->get();
//set additional variables
$show = $_GET["show"] ?? '';
$show = $_REQUEST["show"] ?? '';
$action = $_REQUEST['action'] ?? '';
$search = $_REQUEST['search'] ?? '';
//check if we are using websockets for the conference view
if ($settings->get('active_conferences', 'websocket_enabled', true)) {
$conference_view_page = '/app/active_conferences/active_conferences.php';
}
else {
$conference_view_page = '/app/conferences_active/conferences_active.php';
}
//set from session variables
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
//get posted data
if (!empty($_POST['conferences'])) {
$action = $_POST['action'];
$search = $_POST['search'] ?? '';
$conferences = $_POST['conferences'];
}
@@ -180,7 +188,7 @@
echo " <div class='heading'><b>".$text['title-conferences']."</b><div class='count'>".number_format($num_rows)."</div></div>\n";
echo " <div class='actions'>\n";
if (permission_exists('conference_active_view')) {
echo button::create(['type'=>'button','label'=>$text['button-view_active'],'icon'=>'comments','style'=>'margin-right: 15px;','link'=>PROJECT_PATH.'/app/conferences_active/conferences_active.php']);
echo button::create(['type'=>'button','label'=>$text['button-view_active'],'icon'=>'comments','style'=>'margin-right: 15px;','link'=>PROJECT_PATH.$conference_view_page]);
}
if (permission_exists('conference_add')) {
echo button::create(['type'=>'button','label'=>$text['button-add'],'icon'=>$settings->get('theme', 'button_icon_add'),'id'=>'btn_add','link'=>'conference_edit.php']);
@@ -286,10 +294,10 @@
echo " <td class='center'>".escape($row['conference_order'])."&nbsp;</td>\n";
echo " <td class='no-link center'>\n";
if (permission_exists('conference_interactive_view')) {
echo " <a href='".PROJECT_PATH."/app/conferences_active/conference_interactive.php?c=".urlencode($row['conference_extension'])."'>".$text['label-view']."</a>\n";
echo " <a href='".PROJECT_PATH."$conference_view_page?c=".urlencode($row['conference_extension'])."'>".$text['label-view']."</a>\n";
}
else if (permission_exists('conference_active_view')) {
echo " <a href='".PROJECT_PATH."/app/conferences_active/conferences_active.php'>".$text['label-view']."</a>\n";
echo " <a href='".PROJECT_PATH."$conference_view_page'>".$text['label-view']."</a>\n";
}
else {
echo " &nsbp;\n";

View File

@@ -144,11 +144,15 @@ abstract class base_websocket_system_service extends service implements websocke
$ws_client->disconnect();
}, $this->ws_client);
// Call the register topics in the child classes
$this->register_topics();
// Register the authenticate request
$this->on_topic('authenticate', [$this, 'on_authenticate']);
// Register the authenticated response handler
$this->on_topic('authenticated', [$this, 'handle_ws_authenticated']);
// Track the WebSocket Server Error Message so it doesn't flood the system logs
$suppress_ws_message = false;
@@ -184,7 +188,7 @@ abstract class base_websocket_system_service extends service implements websocke
}
// stream_select will update $read so re-check it
if (!empty($read)) {
$this->debug("Received event");
//$this->debug("Received event");
// Iterate over each socket event
foreach ($read as $resource) {
// Web socket event
@@ -250,7 +254,8 @@ abstract class base_websocket_system_service extends service implements websocke
// Disable the stream blocking
$this->ws_client->set_blocking(false);
$this->debug(self::class . " RESOURCE ID: " . $this->ws_client->socket());
// Call the on connected event function
$this->handle_ws_connected();
} catch (\RuntimeException $re) {
//unable to connect
return false;
@@ -258,6 +263,34 @@ abstract class base_websocket_system_service extends service implements websocke
return true;
}
private function handle_ws_connected(): void {
$this->info("Websocket connection established to server");
$this->debug(static::class . " RESOURCE ID: " . $this->ws_client->socket());
$this->on_ws_connected();
}
/**
* This is called when the web socket is first connected
*
* @return void
*/
protected function on_ws_connected(): void {
// Override in child class if needed
}
private function handle_ws_authenticated(websocket_message $websocket_message): void {
$this->info("Successfully authenticated with websocket server");
$this->on_ws_authenticated();
}
/**
* Called when the service has successfully authenticated with the websocket server.
* Override in child class to perform actions after authentication.
*/
protected function on_ws_authenticated(): void {
// Override in child class if needed
}
/**
* Handles the message from the web socket client and triggers the appropriate requested topic event
*
@@ -269,11 +302,11 @@ abstract class base_websocket_system_service extends service implements websocke
// Nothing to do
if ($json_string === null) {
$this->warn('Message received from Websocket is empty');
$this->warning('Message received from Websocket is empty');
return;
}
$this->debug("Received message on websocket: $json_string (" . strlen($json_string) . " bytes)");
//$this->debug("Received message on websocket: $json_string (" . strlen($json_string) . " bytes)");
// Get the web socket message as an object
$message = websocket_message::create_from_json_message($json_string);
@@ -314,7 +347,9 @@ abstract class base_websocket_system_service extends service implements websocke
protected function on_authenticate(websocket_message $websocket_message) {
$this->info("Authenticating with websocket server");
// Create a service token
[$token_name, $token_hash] = websocket_client::create_service_token(active_calls_service::get_service_name(), static::class);
$service_name = static::get_service_name();
$class_name = static::class;
[$token_name, $token_hash] = websocket_client::create_service_token($service_name, $class_name);
// Request authentication as a service
$this->ws_client->authenticate($token_name, $token_hash);

View File

@@ -184,6 +184,13 @@ class subscriber {
*/
private $authenticated;
/**
* Whether this subscriber has requested debug subscribe all mode
*
* @var bool
*/
private $debug_subscribe_all = false;
/**
* User information
*
@@ -603,6 +610,10 @@ class subscriber {
$this->service = is_a($this->service_class, 'websocket_service_interface', true);
}
// Load options (e.g., debug_subscribe_all)
$options = $array['options'] ?? [];
$this->debug_subscribe_all = !empty($options['debug_subscribe_all']);
//self::$logger->debug("Permission count(".count($this->permissions) . ")");
}
@@ -637,6 +648,15 @@ class subscriber {
return $this;
}
/**
* Returns whether this subscriber has requested debug subscribe all mode.
*
* @return bool
*/
public function has_debug_subscribe_all(): bool {
return $this->debug_subscribe_all;
}
/**
* Sets the domain UUID and name
*
@@ -650,7 +670,7 @@ class subscriber {
*/
public function set_domain(string $uuid, string $name): self {
if (is_uuid($uuid)) {
$this->uuid = $uuid;
$this->domain_uuid = $uuid;
} else {
throw new invalid_uuid_exception("UUID is not valid");
}
@@ -877,16 +897,22 @@ class subscriber {
* @param array $token Standard token issued from the token object
* @param array $services A simple array list of service names to subscribe to
* @param int $time_limit_in_minutes Set a token time limit. Setting to zero will disable the time limit
* @param array $options Optional additional options (e.g., debug_subscribe_all)
*
* @see token::create()
*/
public static function save_token(array $token, array $services, int $time_limit_in_minutes = 0) {
public static function save_token(array $token, array $services, int $time_limit_in_minutes = 0, array $options = []) {
//
// Store the currently logged in user when available
//
$array['user'] = $_SESSION['user'] ?? [];
//
// Store additional options
//
$array['options'] = $options;
//
// Store the token service and events
//

View File

@@ -110,7 +110,7 @@ class websocket_message extends base_message {
*
* @param string $service_name
*
* @return $this
* @return $this|string
*/
public function service_name($service_name = null) {
if (func_num_args() > 0) {
@@ -125,7 +125,7 @@ class websocket_message extends base_message {
*
* @param array $permissions
*
* @return $this
* @return array|$this
*/
public function permissions($permissions = []) {
if (func_num_args() > 0) {
@@ -135,6 +135,15 @@ class websocket_message extends base_message {
return $this->permissions;
}
/**
* Returns the array of permissions
*
* @return array
*/
public function get_permissions(): array {
return $this->permissions;
}
/**
* Applies a filter to the payload of this message.
* When a filter returns null then the payload is set to null

View File

@@ -93,7 +93,7 @@ class websocket_service extends service {
/**
* Subscriber Objects
*
* @var subscriber
* @var array<subscriber>
*/
protected $subscribers;
@@ -225,8 +225,9 @@ class websocket_service extends service {
$subscriber->send(websocket_message::request_authenticated($message->request_id, $message->service));
// Check for service authenticated
if ($subscriber->is_service()) {
$this->info("Service $subscriber->id authenticated");
$this->services[$subscriber->service_name()] = $subscriber;
$service_name = $subscriber->service_name();
$this->info("Service $service_name authenticated using id $subscriber->id");
$this->services[$service_name] = $subscriber;
} else {
// Subscriber authenticated
$this->info("Client $subscriber->id authenticated");
@@ -237,12 +238,21 @@ class websocket_service extends service {
$class_name = $subscriber_service->service_class();
// Make sure we can call the 'create_filter_chain_for' method
if (is_a($class_name, 'websocket_service_interface', true)) {
// Call the service class method to validate the subscriber
$filter = $class_name::create_filter_chain_for($subscriber);
if ($filter !== null) {
// Log the filter has been set for the subscriber
$this->info("Set filter for " . $subscriber->id());
$subscriber->set_filter($filter);
try {
// Call the service class method to validate the subscriber
$filter = $class_name::create_filter_chain_for($subscriber);
if ($filter !== null) {
// Log the filter has been set for the subscriber
$this->info("Set filter for " . $subscriber->id());
$subscriber->set_filter($filter);
}
} catch (subscriber_missing_permission_exception $smpe) {
// Subscriber requested debug mode but service is not in debug mode
// Disconnect them for security
$this->warning("Client $subscriber->id denied: " . $smpe->getMessage());
$subscriber->send(websocket_message::request_unauthorized($message->request_id, $message->service));
$this->handle_disconnect($subscriber->socket_id());
return;
}
}
$this->info("Set permissions for $subscriber->id for service " . $subscriber_service->service_name());
@@ -271,7 +281,7 @@ class websocket_service extends service {
// Ensure we have something to do
if ($message === null) {
$this->warn("Unable to broadcast empty message");
$this->warning("Unable to broadcast empty message");
return;
}
@@ -301,7 +311,7 @@ class websocket_service extends service {
$this->debug("Broadcasting message '" . $message->topic() . "' for service '" . $message->service_name . "' to subscriber $subscriber->id");
$subscriber->send_message($message);
} catch (subscriber_token_expired_exception $ste) {
$this->info("Subscriber $ste->id token expired");
$this->info("Subscriber $ste->subscriber_id token expired");
// Subscriber token has expired so disconnect them
$this->handle_disconnect($subscriber->socket_id());
}
@@ -315,7 +325,7 @@ class websocket_service extends service {
// Remove the resource_id from the message
$message->resource_id('');
// TODO: Fix removal of request_id
$message->request_id('');
//$message->request_id('');
// Return the requested results back to the subscriber
$subscriber->send_message($message);
}
@@ -448,7 +458,7 @@ class websocket_service extends service {
$message = websocket_message::create_from_json_message($json_array);
if ($message === null) {
$this->warn("Message is empty");
$this->warning("Message is empty");
return;
}

View File

@@ -954,6 +954,18 @@ abstract class service {
self::log($message, LOG_DEBUG);
}
/**
* Checks if the service is running in debug mode (LOG_DEBUG level).
*
* This is useful for security checks where certain features should only
* be available when the service is explicitly started with debug logging.
*
* @return bool True if the service is running at LOG_DEBUG level
*/
public static function is_debug_mode(): bool {
return self::$log_level === LOG_DEBUG;
}
/**
* Logs a message at the INFO level.
*