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
@@ -205,7 +205,7 @@ class event_message implements filterable_payload {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a websocket_message_event object from a json string
|
* Creates a websocket_message_event object from a json string
|
||||||
* @param type $json_string
|
* @param string $json_string
|
||||||
* @return self|null
|
* @return self|null
|
||||||
*/
|
*/
|
||||||
public static function create_from_json($json_string) {
|
public static function create_from_json($json_string) {
|
||||||
@@ -229,7 +229,7 @@ class event_message implements filterable_payload {
|
|||||||
*
|
*
|
||||||
* @return self
|
* @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
|
// Set the options from the flags passed
|
||||||
$swap_api_name_with_event_name = ($flags & self::EVENT_SWAP_API) !== 0;
|
$swap_api_name_with_event_name = ($flags & self::EVENT_SWAP_API) !== 0;
|
||||||
|
|||||||
@@ -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> ";
|
||||||
|
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"; ?>
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -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.";
|
||||||
@@ -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++;
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 347 B |
|
After Width: | Height: | Size: 813 B |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 830 B |
|
After Width: | Height: | Size: 413 B |
|
After Width: | Height: | Size: 257 B |
@@ -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 server‑pushed 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 server‑push 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -34,29 +34,30 @@
|
|||||||
$apps[$x]['menu'][$y]['groups'][] = "superadmin";
|
$apps[$x]['menu'][$y]['groups'][] = "superadmin";
|
||||||
$apps[$x]['menu'][$y]['groups'][] = "admin";
|
$apps[$x]['menu'][$y]['groups'][] = "admin";
|
||||||
$y++;
|
$y++;
|
||||||
$apps[$x]['menu'][$y]['title']['en-us'] = "Conference Centers";
|
$apps[$x]['menu'][$y]['title']['en-us'] = "Conference Rooms";
|
||||||
$apps[$x]['menu'][$y]['title']['ar-eg'] = "مراكز المؤتمرات";
|
$apps[$x]['menu'][$y]['title']['en-gb'] = "Conference Rooms";
|
||||||
$apps[$x]['menu'][$y]['title']['de-at'] = "Konferenz Zentrale";
|
$apps[$x]['menu'][$y]['title']['ar-eg'] = "غرف المؤتمرات";
|
||||||
$apps[$x]['menu'][$y]['title']['de-ch'] = "Konferenzzentren";
|
$apps[$x]['menu'][$y]['title']['de-at'] = "Konferenzräume";
|
||||||
$apps[$x]['menu'][$y]['title']['de-de'] = "Konferenz Zentrale";
|
$apps[$x]['menu'][$y]['title']['de-ch'] = "Konferenzräume";
|
||||||
$apps[$x]['menu'][$y]['title']['es-cl'] = "Cent. de Conferencias";
|
$apps[$x]['menu'][$y]['title']['de-de'] = "Konferenzräume";
|
||||||
$apps[$x]['menu'][$y]['title']['es-mx'] = "Conference Centers";
|
$apps[$x]['menu'][$y]['title']['es-cl'] = "Salas de Conferencias";
|
||||||
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Centres de conférences";
|
$apps[$x]['menu'][$y]['title']['es-mx'] = "Salas de Conferencias";
|
||||||
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Centre de Conférences";
|
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Salles de conférences";
|
||||||
$apps[$x]['menu'][$y]['title']['he-il'] = "מרכזי כנסים";
|
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Salles de Conférences";
|
||||||
$apps[$x]['menu'][$y]['title']['it-it'] = "Centro Conferenze";
|
$apps[$x]['menu'][$y]['title']['he-il'] = "חדרי כנסים";
|
||||||
$apps[$x]['menu'][$y]['title']['ka-ge'] = "კონფერენც-ცენტრები";
|
$apps[$x]['menu'][$y]['title']['it-it'] = "Sale Conferenze";
|
||||||
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Conferentie centra";
|
$apps[$x]['menu'][$y]['title']['ka-ge'] = "კონფერენციის ოთახები";
|
||||||
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Centrum Konferencyjne";
|
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Conferentie kamers";
|
||||||
$apps[$x]['menu'][$y]['title']['pt-br'] = "Centro de Conferência";
|
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Sale Konferencyjne";
|
||||||
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Conferencias";
|
$apps[$x]['menu'][$y]['title']['pt-br'] = "Salas de Conferência";
|
||||||
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Centre de conferințe";
|
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Salas de Conferências";
|
||||||
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференц-центр";
|
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Săli de conferințe";
|
||||||
$apps[$x]['menu'][$y]['title']['sv-se'] = "Konferenscenter";
|
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференц-залы";
|
||||||
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Конференц-центр";
|
$apps[$x]['menu'][$y]['title']['sv-se'] = "Konferensrum";
|
||||||
$apps[$x]['menu'][$y]['title']['zh-cn'] = "会议中心";
|
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Конференц-зали";
|
||||||
$apps[$x]['menu'][$y]['title']['ja-jp'] = "カンファレンスセンター";
|
$apps[$x]['menu'][$y]['title']['zh-cn'] = "会议室";
|
||||||
$apps[$x]['menu'][$y]['title']['ko-kr'] = "컨퍼런스 센터";
|
$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]['uuid'] = "b99cb768-ca19-4374-a954-02e344313d84";
|
||||||
$apps[$x]['menu'][$y]['parent_uuid'] = "fd29e39c-c936-f5fc-8e2b-611681b266b5";
|
$apps[$x]['menu'][$y]['parent_uuid'] = "fd29e39c-c936-f5fc-8e2b-611681b266b5";
|
||||||
$apps[$x]['menu'][$y]['category'] = "internal";
|
$apps[$x]['menu'][$y]['category'] = "internal";
|
||||||
|
|||||||
@@ -40,16 +40,24 @@
|
|||||||
$text = $language->get();
|
$text = $language->get();
|
||||||
|
|
||||||
//set additional variables
|
//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
|
//set from session variables
|
||||||
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
|
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
|
||||||
|
|
||||||
//get the http post data
|
//get the http post data
|
||||||
if (!empty($_POST['conference_rooms'])) {
|
if (!empty($_POST['conference_rooms'])) {
|
||||||
$action = $_POST['action'];
|
|
||||||
$toggle_field = $_POST['toggle_field'];
|
|
||||||
$search = $_POST['search'] ?? '';
|
|
||||||
$conference_rooms = $_POST['conference_rooms'];
|
$conference_rooms = $_POST['conference_rooms'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,10 +456,10 @@
|
|||||||
}
|
}
|
||||||
echo " <td class='no-link no-wrap center'>\n";
|
echo " <td class='no-link no-wrap center'>\n";
|
||||||
if (permission_exists('conference_interactive_view')) {
|
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')) {
|
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')) {
|
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";
|
echo " <a href='/app/conference_cdr/conference_cdr.php?id=".urlencode($row['conference_room_uuid'])."'>".$text['button-cdr']."</a>\n";
|
||||||
|
|||||||
@@ -40,15 +40,23 @@
|
|||||||
$text = $language->get();
|
$text = $language->get();
|
||||||
|
|
||||||
//set additional variables
|
//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
|
//set from session variables
|
||||||
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
|
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
|
||||||
|
|
||||||
//get posted data
|
//get posted data
|
||||||
if (!empty($_POST['conferences'])) {
|
if (!empty($_POST['conferences'])) {
|
||||||
$action = $_POST['action'];
|
|
||||||
$search = $_POST['search'] ?? '';
|
|
||||||
$conferences = $_POST['conferences'];
|
$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='heading'><b>".$text['title-conferences']."</b><div class='count'>".number_format($num_rows)."</div></div>\n";
|
||||||
echo " <div class='actions'>\n";
|
echo " <div class='actions'>\n";
|
||||||
if (permission_exists('conference_active_view')) {
|
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')) {
|
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']);
|
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'])." </td>\n";
|
echo " <td class='center'>".escape($row['conference_order'])." </td>\n";
|
||||||
echo " <td class='no-link center'>\n";
|
echo " <td class='no-link center'>\n";
|
||||||
if (permission_exists('conference_interactive_view')) {
|
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')) {
|
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 {
|
else {
|
||||||
echo " &nsbp;\n";
|
echo " &nsbp;\n";
|
||||||
|
|||||||
@@ -144,11 +144,15 @@ abstract class base_websocket_system_service extends service implements websocke
|
|||||||
$ws_client->disconnect();
|
$ws_client->disconnect();
|
||||||
}, $this->ws_client);
|
}, $this->ws_client);
|
||||||
|
|
||||||
|
// Call the register topics in the child classes
|
||||||
$this->register_topics();
|
$this->register_topics();
|
||||||
|
|
||||||
// Register the authenticate request
|
// Register the authenticate request
|
||||||
$this->on_topic('authenticate', [$this, 'on_authenticate']);
|
$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
|
// Track the WebSocket Server Error Message so it doesn't flood the system logs
|
||||||
$suppress_ws_message = false;
|
$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
|
// stream_select will update $read so re-check it
|
||||||
if (!empty($read)) {
|
if (!empty($read)) {
|
||||||
$this->debug("Received event");
|
//$this->debug("Received event");
|
||||||
// Iterate over each socket event
|
// Iterate over each socket event
|
||||||
foreach ($read as $resource) {
|
foreach ($read as $resource) {
|
||||||
// Web socket event
|
// Web socket event
|
||||||
@@ -250,7 +254,8 @@ abstract class base_websocket_system_service extends service implements websocke
|
|||||||
// Disable the stream blocking
|
// Disable the stream blocking
|
||||||
$this->ws_client->set_blocking(false);
|
$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) {
|
} catch (\RuntimeException $re) {
|
||||||
//unable to connect
|
//unable to connect
|
||||||
return false;
|
return false;
|
||||||
@@ -258,6 +263,34 @@ abstract class base_websocket_system_service extends service implements websocke
|
|||||||
return true;
|
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
|
* 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
|
// Nothing to do
|
||||||
if ($json_string === null) {
|
if ($json_string === null) {
|
||||||
$this->warn('Message received from Websocket is empty');
|
$this->warning('Message received from Websocket is empty');
|
||||||
return;
|
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
|
// Get the web socket message as an object
|
||||||
$message = websocket_message::create_from_json_message($json_string);
|
$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) {
|
protected function on_authenticate(websocket_message $websocket_message) {
|
||||||
$this->info("Authenticating with websocket server");
|
$this->info("Authenticating with websocket server");
|
||||||
// Create a service token
|
// 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
|
// Request authentication as a service
|
||||||
$this->ws_client->authenticate($token_name, $token_hash);
|
$this->ws_client->authenticate($token_name, $token_hash);
|
||||||
|
|||||||
@@ -184,6 +184,13 @@ class subscriber {
|
|||||||
*/
|
*/
|
||||||
private $authenticated;
|
private $authenticated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this subscriber has requested debug subscribe all mode
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $debug_subscribe_all = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User information
|
* User information
|
||||||
*
|
*
|
||||||
@@ -603,6 +610,10 @@ class subscriber {
|
|||||||
$this->service = is_a($this->service_class, 'websocket_service_interface', true);
|
$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) . ")");
|
//self::$logger->debug("Permission count(".count($this->permissions) . ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +648,15 @@ class subscriber {
|
|||||||
return $this;
|
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
|
* Sets the domain UUID and name
|
||||||
*
|
*
|
||||||
@@ -650,7 +670,7 @@ class subscriber {
|
|||||||
*/
|
*/
|
||||||
public function set_domain(string $uuid, string $name): self {
|
public function set_domain(string $uuid, string $name): self {
|
||||||
if (is_uuid($uuid)) {
|
if (is_uuid($uuid)) {
|
||||||
$this->uuid = $uuid;
|
$this->domain_uuid = $uuid;
|
||||||
} else {
|
} else {
|
||||||
throw new invalid_uuid_exception("UUID is not valid");
|
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 $token Standard token issued from the token object
|
||||||
* @param array $services A simple array list of service names to subscribe to
|
* @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 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()
|
* @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
|
// Store the currently logged in user when available
|
||||||
//
|
//
|
||||||
$array['user'] = $_SESSION['user'] ?? [];
|
$array['user'] = $_SESSION['user'] ?? [];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Store additional options
|
||||||
|
//
|
||||||
|
$array['options'] = $options;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Store the token service and events
|
// Store the token service and events
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class websocket_message extends base_message {
|
|||||||
*
|
*
|
||||||
* @param string $service_name
|
* @param string $service_name
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return $this|string
|
||||||
*/
|
*/
|
||||||
public function service_name($service_name = null) {
|
public function service_name($service_name = null) {
|
||||||
if (func_num_args() > 0) {
|
if (func_num_args() > 0) {
|
||||||
@@ -125,7 +125,7 @@ class websocket_message extends base_message {
|
|||||||
*
|
*
|
||||||
* @param array $permissions
|
* @param array $permissions
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return array|$this
|
||||||
*/
|
*/
|
||||||
public function permissions($permissions = []) {
|
public function permissions($permissions = []) {
|
||||||
if (func_num_args() > 0) {
|
if (func_num_args() > 0) {
|
||||||
@@ -135,6 +135,15 @@ class websocket_message extends base_message {
|
|||||||
return $this->permissions;
|
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.
|
* Applies a filter to the payload of this message.
|
||||||
* When a filter returns null then the payload is set to null
|
* When a filter returns null then the payload is set to null
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class websocket_service extends service {
|
|||||||
/**
|
/**
|
||||||
* Subscriber Objects
|
* Subscriber Objects
|
||||||
*
|
*
|
||||||
* @var subscriber
|
* @var array<subscriber>
|
||||||
*/
|
*/
|
||||||
protected $subscribers;
|
protected $subscribers;
|
||||||
|
|
||||||
@@ -225,8 +225,9 @@ class websocket_service extends service {
|
|||||||
$subscriber->send(websocket_message::request_authenticated($message->request_id, $message->service));
|
$subscriber->send(websocket_message::request_authenticated($message->request_id, $message->service));
|
||||||
// Check for service authenticated
|
// Check for service authenticated
|
||||||
if ($subscriber->is_service()) {
|
if ($subscriber->is_service()) {
|
||||||
$this->info("Service $subscriber->id authenticated");
|
$service_name = $subscriber->service_name();
|
||||||
$this->services[$subscriber->service_name()] = $subscriber;
|
$this->info("Service $service_name authenticated using id $subscriber->id");
|
||||||
|
$this->services[$service_name] = $subscriber;
|
||||||
} else {
|
} else {
|
||||||
// Subscriber authenticated
|
// Subscriber authenticated
|
||||||
$this->info("Client $subscriber->id authenticated");
|
$this->info("Client $subscriber->id authenticated");
|
||||||
@@ -237,12 +238,21 @@ class websocket_service extends service {
|
|||||||
$class_name = $subscriber_service->service_class();
|
$class_name = $subscriber_service->service_class();
|
||||||
// Make sure we can call the 'create_filter_chain_for' method
|
// Make sure we can call the 'create_filter_chain_for' method
|
||||||
if (is_a($class_name, 'websocket_service_interface', true)) {
|
if (is_a($class_name, 'websocket_service_interface', true)) {
|
||||||
// Call the service class method to validate the subscriber
|
try {
|
||||||
$filter = $class_name::create_filter_chain_for($subscriber);
|
// Call the service class method to validate the subscriber
|
||||||
if ($filter !== null) {
|
$filter = $class_name::create_filter_chain_for($subscriber);
|
||||||
// Log the filter has been set for the subscriber
|
if ($filter !== null) {
|
||||||
$this->info("Set filter for " . $subscriber->id());
|
// Log the filter has been set for the subscriber
|
||||||
$subscriber->set_filter($filter);
|
$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());
|
$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
|
// Ensure we have something to do
|
||||||
if ($message === null) {
|
if ($message === null) {
|
||||||
$this->warn("Unable to broadcast empty message");
|
$this->warning("Unable to broadcast empty message");
|
||||||
return;
|
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");
|
$this->debug("Broadcasting message '" . $message->topic() . "' for service '" . $message->service_name . "' to subscriber $subscriber->id");
|
||||||
$subscriber->send_message($message);
|
$subscriber->send_message($message);
|
||||||
} catch (subscriber_token_expired_exception $ste) {
|
} 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
|
// Subscriber token has expired so disconnect them
|
||||||
$this->handle_disconnect($subscriber->socket_id());
|
$this->handle_disconnect($subscriber->socket_id());
|
||||||
}
|
}
|
||||||
@@ -315,7 +325,7 @@ class websocket_service extends service {
|
|||||||
// Remove the resource_id from the message
|
// Remove the resource_id from the message
|
||||||
$message->resource_id('');
|
$message->resource_id('');
|
||||||
// TODO: Fix removal of request_id
|
// TODO: Fix removal of request_id
|
||||||
$message->request_id('');
|
//$message->request_id('');
|
||||||
// Return the requested results back to the subscriber
|
// Return the requested results back to the subscriber
|
||||||
$subscriber->send_message($message);
|
$subscriber->send_message($message);
|
||||||
}
|
}
|
||||||
@@ -448,7 +458,7 @@ class websocket_service extends service {
|
|||||||
$message = websocket_message::create_from_json_message($json_array);
|
$message = websocket_message::create_from_json_message($json_array);
|
||||||
|
|
||||||
if ($message === null) {
|
if ($message === null) {
|
||||||
$this->warn("Message is empty");
|
$this->warning("Message is empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -954,6 +954,18 @@ abstract class service {
|
|||||||
self::log($message, LOG_DEBUG);
|
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.
|
* Logs a message at the INFO level.
|
||||||
*
|
*
|
||||||
|
|||||||