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
|
||||
* @param type $json_string
|
||||
* @param string $json_string
|
||||
* @return self|null
|
||||
*/
|
||||
public static function create_from_json($json_string) {
|
||||
@@ -229,7 +229,7 @@ class event_message implements filterable_payload {
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function create_from_switch_event($raw_event, filter $filter = null, $flags = 3): self {
|
||||
public static function create_from_switch_event($raw_event, ?filter $filter = null, ?int $flags = 3): self {
|
||||
|
||||
// Set the options from the flags passed
|
||||
$swap_api_name_with_event_name = ($flags & self::EVENT_SWAP_API) !== 0;
|
||||
|
||||
273
app/active_conferences/active_conference_room.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/*
|
||||
* FusionPBX
|
||||
* Version: MPL 1.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is FusionPBX
|
||||
*
|
||||
* The Initial Developer of the Original Code is
|
||||
* Mark J Crane <markjcrane@fusionpbx.com>
|
||||
* Portions created by the Initial Developer are Copyright (C) 2008-2025
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Mark J Crane <markjcrane@fusionpbx.com>
|
||||
* Tim Fry <tim@fusionpbx.com>
|
||||
*/
|
||||
|
||||
//includes files
|
||||
require_once dirname(__DIR__, 2) . "/resources/require.php";
|
||||
require_once "resources/check_auth.php";
|
||||
|
||||
//check permissions
|
||||
if (!permission_exists('conference_interactive_view')) {
|
||||
echo "access denied";
|
||||
exit;
|
||||
}
|
||||
|
||||
//show intended global variables
|
||||
global $domain_uuid, $user_uuid, $settings, $database, $config;
|
||||
|
||||
//get the domain uuid
|
||||
if (empty($domain_uuid)) {
|
||||
$domain_uuid = $_SESSION['domain_uuid'] ?? '';
|
||||
}
|
||||
|
||||
//get the user uuid
|
||||
if (empty($user_uuid)) {
|
||||
$user_uuid = $_SESSION['user_uuid'] ?? '';
|
||||
}
|
||||
|
||||
//load the config
|
||||
if (!($config instanceof config)) {
|
||||
$config = config::load();
|
||||
}
|
||||
|
||||
//load the database
|
||||
if (!($database instanceof database)) {
|
||||
$database = new database;
|
||||
}
|
||||
|
||||
//load the settings
|
||||
if (!($settings instanceof settings)) {
|
||||
$settings = new settings(['database' => $database, 'domain_uuid' => $domain_uuid, 'user_uuid' => $user_uuid]);
|
||||
}
|
||||
|
||||
//add multi-lingual support
|
||||
$language = new text;
|
||||
$text = $language->get();
|
||||
|
||||
//get the http get or post and set it as php variables
|
||||
if (!empty($_REQUEST["c"]) && is_numeric($_REQUEST["c"])) {
|
||||
$conference_id = $_REQUEST["c"];
|
||||
}
|
||||
elseif (!empty($_REQUEST["c"]) && is_uuid($_REQUEST["c"])) {
|
||||
$conference_id = $_REQUEST["c"];
|
||||
}
|
||||
else {
|
||||
//exit if the conference id is invalid
|
||||
exit;
|
||||
}
|
||||
|
||||
//replace the space with underscore
|
||||
$conference_name = $conference_id.'@'.$_SESSION['domain_name'];
|
||||
|
||||
//get and prepare the conference display name
|
||||
$conference_display_name = str_replace("-", " ", $conference_id);
|
||||
$conference_display_name = str_replace("_", " ", $conference_display_name);
|
||||
|
||||
//create token
|
||||
$token = (new token())->create($_SERVER['PHP_SELF']);
|
||||
|
||||
// Pass the token to the subscriber class so that when this subscriber makes a websocket
|
||||
// connection, the subscriber object can validate the information.
|
||||
subscriber::save_token($token, ['active.conferences']);
|
||||
|
||||
//show the header
|
||||
$document['title'] = $text['label-interactive'];
|
||||
require_once dirname(__DIR__, 2) . "/resources/header.php";
|
||||
|
||||
//break the caching
|
||||
$version = md5(file_get_contents(__DIR__ . '/resources/javascript/websocket_client.js'));
|
||||
|
||||
//build permissions object for client-side checks
|
||||
$user_permissions = [
|
||||
'lock' => permission_exists('conference_interactive_lock'),
|
||||
'mute' => permission_exists('conference_interactive_mute'),
|
||||
'deaf' => permission_exists('conference_interactive_deaf'),
|
||||
'kick' => permission_exists('conference_interactive_kick'),
|
||||
'energy' => permission_exists('conference_interactive_energy'),
|
||||
'volume' => permission_exists('conference_interactive_volume'),
|
||||
'gain' => permission_exists('conference_interactive_gain'),
|
||||
'video' => permission_exists('conference_interactive_video'),
|
||||
];
|
||||
|
||||
//get websocket settings from default settings
|
||||
$ws_settings = [
|
||||
'reconnect_delay' => (int)$settings->get('active_conferences', 'reconnect_delay', 2000),
|
||||
'ping_interval' => (int)$settings->get('active_conferences', 'ping_interval', 30000),
|
||||
'auth_timeout' => (int)$settings->get('active_conferences', 'auth_timeout', 10000),
|
||||
'pong_timeout' => (int)$settings->get('active_conferences', 'pong_timeout', 10000),
|
||||
'refresh_interval' => (int)$settings->get('active_conferences', 'refresh_interval', 0),
|
||||
'max_reconnect_delay' => (int)$settings->get('active_conferences', 'max_reconnect_delay', 30000),
|
||||
'pong_timeout_max_retries' => (int)$settings->get('active_conferences', 'pong_timeout_max_retries', 3),
|
||||
];
|
||||
|
||||
//get theme colors for status indicator
|
||||
$status_colors = [
|
||||
'connected' => $settings->get('theme', 'active_conference_status_connected', '#28a745'),
|
||||
'warning' => $settings->get('theme', 'active_conference_status_warning', '#ffc107'),
|
||||
'disconnected' => $settings->get('theme', 'active_conference_status_disconnected', '#dc3545'),
|
||||
'connecting' => $settings->get('theme', 'active_conference_status_connecting', '#6c757d'),
|
||||
];
|
||||
|
||||
//get status indicator mode and icons
|
||||
$status_indicator_mode = $settings->get('theme', 'active_conference_status_indicator_mode', 'color');
|
||||
$status_icons = [
|
||||
'connected' => $settings->get('theme', 'active_conference_status_icon_connected', 'fa-solid fa-plug-circle-check'),
|
||||
'warning' => $settings->get('theme', 'active_conference_status_icon_warning', 'fa-solid fa-plug-circle-exclamation'),
|
||||
'disconnected' => $settings->get('theme', 'active_conference_status_icon_disconnected', 'fa-solid fa-plug-circle-xmark'),
|
||||
'connecting' => $settings->get('theme', 'active_conference_status_icon_connecting', 'fa-solid fa-plug fa-fade'),
|
||||
];
|
||||
|
||||
//get status tooltips from translations
|
||||
$status_tooltips = [
|
||||
'connected' => $text['status-connected'],
|
||||
'warning' => $text['status-warning'],
|
||||
'disconnected' => $text['status-disconnected'],
|
||||
'connecting' => $text['status-connecting'],
|
||||
];
|
||||
|
||||
?>
|
||||
|
||||
<script type="text/javascript">
|
||||
//user permissions for client-side checks
|
||||
const user_permissions = <?= json_encode($user_permissions) ?>;
|
||||
|
||||
//websocket configuration from server settings
|
||||
const ws_config = <?= json_encode($ws_settings) ?>;
|
||||
|
||||
//status indicator colors from theme settings
|
||||
const status_colors = <?= json_encode($status_colors) ?>;
|
||||
|
||||
//status indicator icons from settings
|
||||
const status_icons = <?= json_encode($status_icons) ?>;
|
||||
|
||||
//status tooltips from translations
|
||||
const status_tooltips = <?= json_encode($status_tooltips) ?>;
|
||||
|
||||
//status indicator mode: 'color' or 'icon'
|
||||
const status_indicator_mode = <?= json_encode($status_indicator_mode) ?>;
|
||||
|
||||
//translations
|
||||
const text = <?= json_encode($text) ?>;
|
||||
|
||||
//send action via WebSocket
|
||||
function send_action(action, options = {}, skip_refresh = false) {
|
||||
if (!ws || !ws.ws || ws.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket not connected');
|
||||
return Promise.reject('Not connected');
|
||||
}
|
||||
|
||||
const payload = {
|
||||
action: action,
|
||||
conference_name: conference_name,
|
||||
domain_name: '<?= $_SESSION['domain_name'] ?>',
|
||||
...options
|
||||
};
|
||||
|
||||
console.log('Sending action:', action, payload);
|
||||
|
||||
return ws.request('active.conferences', 'action', payload)
|
||||
.then(response => {
|
||||
console.log('Action response:', response);
|
||||
const result = response.payload || response;
|
||||
if (!result.success) {
|
||||
console.error('Action failed:', result.message);
|
||||
}
|
||||
// Refresh data after action (unless skip_refresh is true)
|
||||
if (!skip_refresh) {
|
||||
load_conference_data();
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Action error:', err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
//conference control functions
|
||||
function conference_action(action, member_id, uuid, direction) {
|
||||
return send_action(action, {
|
||||
member_id: member_id || '',
|
||||
uuid: uuid || '',
|
||||
direction: direction || ''
|
||||
});
|
||||
}
|
||||
|
||||
var record_count = 0;
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$ws_client_file = __DIR__ . '/resources/javascript/websocket_client.js';
|
||||
$ws_client_hash = file_exists($ws_client_file) ? md5_file($ws_client_file) : $version;
|
||||
|
||||
$ac_js_file = __DIR__ . '/resources/javascript/active_conferences.js';
|
||||
$ac_js_hash = file_exists($ac_js_file) ? md5_file($ac_js_file) : $version;
|
||||
?>
|
||||
<script src="resources/javascript/websocket_client.js?v=<?= $ws_client_hash ?>"></script>
|
||||
<script src="resources/javascript/active_conferences.js?v=<?= $ac_js_hash ?>"></script>
|
||||
|
||||
<?php
|
||||
|
||||
//page header
|
||||
echo "<div class='action_bar' id='action_bar'>\n";
|
||||
echo "<div class='heading'><b>".$text['label-interactive']."</b> ";
|
||||
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"; ?>
|
||||
174
app/active_conferences/active_conferences.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/*
|
||||
FusionPBX
|
||||
Version: MPL 1.1
|
||||
|
||||
The contents of this file are subject to the Mozilla Public License Version
|
||||
1.1 (the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
http://www.mozilla.org/MPL/
|
||||
|
||||
Software distributed under the License is distributed on an "AS IS" basis,
|
||||
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
for the specific language governing rights and limitations under the
|
||||
License.
|
||||
|
||||
The Original Code is FusionPBX
|
||||
|
||||
The Initial Developer of the Original Code is
|
||||
Mark J Crane <markjcrane@fusionpbx.com>
|
||||
Portions created by the Initial Developer are Copyright (C) 2008-2024
|
||||
the Initial Developer. All Rights Reserved.
|
||||
|
||||
Contributor(s):
|
||||
Mark J Crane <markjcrane@fusionpbx.com>
|
||||
James Rose <james.o.rose@gmail.com>
|
||||
*/
|
||||
|
||||
//includes files
|
||||
require_once dirname(__DIR__, 2) . "/resources/require.php";
|
||||
require_once "resources/check_auth.php";
|
||||
|
||||
//check permissions
|
||||
if (!permission_exists('conference_active_view')) {
|
||||
echo "access denied";
|
||||
exit;
|
||||
}
|
||||
|
||||
//add multi-lingual support
|
||||
$language = new text;
|
||||
$text = $language->get();
|
||||
|
||||
//create token
|
||||
$token = (new token())->create($_SERVER['PHP_SELF']);
|
||||
|
||||
//pass the token to the subscriber class so that when this subscriber makes a websocket
|
||||
//connection, the subscriber object can validate the information.
|
||||
subscriber::save_token($token, ['active.conferences']);
|
||||
|
||||
//include the header
|
||||
$document['title'] = $text['title-active_conferences'];
|
||||
require_once "resources/header.php";
|
||||
|
||||
//break the caching
|
||||
$version = md5(file_get_contents(__DIR__ . '/resources/javascript/websocket_client.js'));
|
||||
|
||||
//get websocket settings from default settings
|
||||
$ws_settings = [
|
||||
'reconnect_delay' => (int)$settings->get('active_conferences', 'reconnect_delay', 2000),
|
||||
'ping_interval' => (int)$settings->get('active_conferences', 'ping_interval', 30000),
|
||||
'auth_timeout' => (int)$settings->get('active_conferences', 'auth_timeout', 10000),
|
||||
'pong_timeout' => (int)$settings->get('active_conferences', 'pong_timeout', 10000),
|
||||
'refresh_interval' => (int)$settings->get('active_conferences', 'refresh_interval', 0),
|
||||
'max_reconnect_delay' => (int)$settings->get('active_conferences', 'max_reconnect_delay', 30000),
|
||||
'pong_timeout_max_retries' => (int)$settings->get('active_conferences', 'pong_timeout_max_retries', 3),
|
||||
];
|
||||
|
||||
//get theme colors for status indicator
|
||||
$status_colors = [
|
||||
'connected' => $settings->get('theme', 'active_conference_status_connected', '#28a745'),
|
||||
'warning' => $settings->get('theme', 'active_conference_status_warning', '#ffc107'),
|
||||
'disconnected' => $settings->get('theme', 'active_conference_status_disconnected', '#dc3545'),
|
||||
'connecting' => $settings->get('theme', 'active_conference_status_connecting', '#6c757d'),
|
||||
];
|
||||
|
||||
//get status indicator mode and icons
|
||||
$status_indicator_mode = $settings->get('theme', 'active_conference_status_indicator_mode', 'color');
|
||||
$status_icons = [
|
||||
'connected' => $settings->get('theme', 'active_conference_status_icon_connected', 'fa-solid fa-plug-circle-check'),
|
||||
'warning' => $settings->get('theme', 'active_conference_status_icon_warning', 'fa-solid fa-plug-circle-exclamation'),
|
||||
'disconnected' => $settings->get('theme', 'active_conference_status_icon_disconnected', 'fa-solid fa-plug-circle-xmark'),
|
||||
'connecting' => $settings->get('theme', 'active_conference_status_icon_connecting', 'fa-solid fa-plug fa-fade'),
|
||||
];
|
||||
|
||||
//get status tooltips from translations
|
||||
$status_tooltips = [
|
||||
'connected' => $text['status-connected'],
|
||||
'warning' => $text['status-warning'],
|
||||
'disconnected' => $text['status-disconnected'],
|
||||
'connecting' => $text['status-connecting'],
|
||||
];
|
||||
|
||||
?>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
//websocket configuration from server settings
|
||||
const ws_config = <?= json_encode($ws_settings) ?>;
|
||||
|
||||
//status indicator colors from theme settings
|
||||
const status_colors = <?= json_encode($status_colors) ?>;
|
||||
|
||||
//status indicator icons from settings
|
||||
const status_icons = <?= json_encode($status_icons) ?>;
|
||||
|
||||
//status tooltips from translations
|
||||
const status_tooltips = <?= json_encode($status_tooltips) ?>;
|
||||
|
||||
//status indicator mode: 'color' or 'icon'
|
||||
const status_indicator_mode = <?= json_encode($status_indicator_mode) ?>;
|
||||
|
||||
//translations
|
||||
const text = <?= json_encode($text) ?>;
|
||||
|
||||
//permissions
|
||||
const permissions = {
|
||||
conference_interactive_view: <?= permission_exists('conference_interactive_view') ? 'true' : 'false' ?>,
|
||||
list_row_edit_button: <?= $settings->get('theme', 'list_row_edit_button', false) ? 'true' : 'false' ?>
|
||||
};
|
||||
|
||||
//button icon
|
||||
const button_icon_view = '<?= $settings->get('theme', 'button_icon_view') ?>';
|
||||
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$ws_client_file = __DIR__ . '/resources/javascript/websocket_client.js';
|
||||
$ws_client_hash = file_exists($ws_client_file) ? md5_file($ws_client_file) : $version;
|
||||
|
||||
$ac_js_file = __DIR__ . '/resources/javascript/active_conferences.js';
|
||||
$ac_js_hash = file_exists($ac_js_file) ? md5_file($ac_js_file) : $version;
|
||||
?>
|
||||
<script src="resources/javascript/websocket_client.js?v=<?= $ws_client_hash ?>"></script>
|
||||
<script src="resources/javascript/active_conferences.js?v=<?= $ac_js_hash ?>"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
const token = {
|
||||
name: '<?= $token['name'] ?>',
|
||||
hash: '<?= $token['hash'] ?>'
|
||||
};
|
||||
|
||||
// Domain name for filtering
|
||||
const domain_name = '<?= $_SESSION['domain_name'] ?>';
|
||||
|
||||
// Start websocket connection
|
||||
connect_websocket();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
//page header
|
||||
echo "<div class='action_bar' id='action_bar'>\n";
|
||||
echo " <div class='heading'><b>".$text['title-active_conferences']."</b>";
|
||||
if ($status_indicator_mode === 'icon') {
|
||||
echo "<span id='connection_status' class='".$status_icons['connecting']."' style='color: ".$status_colors['connecting'].";' title='".$status_tooltips['connecting']."'></span>";
|
||||
} else {
|
||||
echo "<div id='connection_status' class='count'><span id='conference_count'>0</span></div>";
|
||||
}
|
||||
echo "</div>\n";
|
||||
echo " <div class='actions'>\n";
|
||||
echo " </div>\n";
|
||||
echo " <div style='clear: both;'></div>\n";
|
||||
echo "</div>\n";
|
||||
|
||||
echo $text['description-active']."\n";
|
||||
echo "<br /><br />\n";
|
||||
|
||||
//show the content
|
||||
echo "<div id='conferences_container'></div>"; // Replaced ajax_response
|
||||
echo "<br><br>";
|
||||
|
||||
//include the footer
|
||||
require_once "resources/footer.php";
|
||||
|
||||
?>
|
||||
237
app/active_conferences/app_config.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
//application details
|
||||
$apps[$x]['name'] = "Conferences Active";
|
||||
$apps[$x]['uuid'] = "c168c943-833a-c29c-7ef9-d1ee78810b71";
|
||||
$apps[$x]['category'] = "Switch";;
|
||||
$apps[$x]['subcategory'] = "";
|
||||
$apps[$x]['version'] = "1.0";
|
||||
$apps[$x]['license'] = "Mozilla Public License 1.1";
|
||||
$apps[$x]['url'] = "http://www.fusionpbx.com";
|
||||
$apps[$x]['description']['en-us'] = "A real-time active conference viewer and moderator tool.";
|
||||
$apps[$x]['description']['en-gb'] = "A real-time active conference viewer and moderator tool.";
|
||||
$apps[$x]['description']['ar-eg'] = "";
|
||||
$apps[$x]['description']['de-at'] = "";
|
||||
$apps[$x]['description']['de-ch'] = "";
|
||||
$apps[$x]['description']['de-de'] = "";
|
||||
$apps[$x]['description']['es-cl'] = "";
|
||||
$apps[$x]['description']['es-mx'] = "";
|
||||
$apps[$x]['description']['fr-ca'] = "";
|
||||
$apps[$x]['description']['fr-fr'] = "";
|
||||
$apps[$x]['description']['he-il'] = "";
|
||||
$apps[$x]['description']['it-it'] = "";
|
||||
$apps[$x]['description']['ka-ge'] = "";
|
||||
$apps[$x]['description']['nl-nl'] = "";
|
||||
$apps[$x]['description']['pl-pl'] = "";
|
||||
$apps[$x]['description']['pt-br'] = "";
|
||||
$apps[$x]['description']['pt-pt'] = "";
|
||||
$apps[$x]['description']['ro-ro'] = "";
|
||||
$apps[$x]['description']['ru-ru'] = "";
|
||||
$apps[$x]['description']['sv-se'] = "";
|
||||
$apps[$x]['description']['uk-ua'] = "";
|
||||
|
||||
//permission details
|
||||
$y=0;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_active_view";
|
||||
$apps[$x]['permissions'][$y]['menu']['uuid'] = "2d857bbb-43b9-b8f7-a138-642868e0453a";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_view";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_lock";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_kick";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_energy";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_volume";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_gain";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
//$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_mute";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_deaf";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
$apps[$x]['permissions'][$y]['name'] = "conference_interactive_video";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "user";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
|
||||
|
||||
//default settings
|
||||
$y = 0;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "0242ad2b-72c7-42f8-b8fe-8716654e0c99";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "reconnect_delay";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "2000";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Base delay in milliseconds before attempting to reconnect after disconnect.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "9451b1a9-4f81-4819-bfe4-47cc468cd095";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "ping_interval";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "15000";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Interval in milliseconds between keepalive ping requests.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "605791c3-f20a-438c-98ec-869b600d27ea";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "auth_timeout";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "3000";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Timeout in milliseconds waiting for WebSocket authentication before redirecting to login.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "ea18eab7-9772-4ade-b318-a70a2ed40906";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "pong_timeout";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "500";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Timeout in milliseconds waiting for pong response before reloading the page.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "31b5f0e3-aec7-403b-be3d-94dbf3b8a59e";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "refresh_interval";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "0";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Optional interval in milliseconds to periodically refresh conference data. Set to 0 to disable (rely on WebSocket events only).";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "3a757df6-37ce-4351-b046-d3ba2eaffd18";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "max_reconnect_delay";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "15000";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Maximum delay in milliseconds between reconnection attempts (exponential backoff cap).";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "79c34bee-725a-4b85-86eb-5d9ceabda6a4";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "pong_timeout_max_retries";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "numeric";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "3";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Number of pong timeouts allowed before reloading the page. During retries, status indicator shows warning color.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "28babdbb-9155-4ff9-928d-5e9d7bac94c4";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_connected";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#28a745";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when connected and receiving pong responses.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "8a447132-799a-4ae7-8d21-137df917b66a";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_warning";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#ffc107";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when ping sent but pong not yet received (warning state).";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "809954c0-48b9-4828-8843-a046235515b6";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_disconnected";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#dc3545";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when disconnected or not authenticated.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "522d2274-b4ea-4f25-9bb3-43d7669defe2";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_connecting";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "#6c757d";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Color of the status indicator when connecting or authenticating.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "2c16616d-49d8-4921-b6bf-112f0f2fb4e8";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_indicator_mode";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "color";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Status indicator display mode: 'color' for colored circle, 'icon' for Font Awesome icons.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "51357753-3782-4b23-9e31-ea5d5c649890";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_connected";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug-circle-check";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for connected status.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "86c2ef42-2988-4d6d-9c92-3fc697a171ba";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_warning";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug-circle-exclamation";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for warning status (ping sent, awaiting pong).";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "51e4cff5-de0d-4ecb-b4e6-33eac9140b4b";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_disconnected";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug-circle-xmark";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for disconnected status.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "1b5a4a79-9b15-44ff-8a7c-d4cc773151f2";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "active_conference_status_icon_connecting";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fa-solid fa-plug fa-fade";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for connecting status.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "7fb8a315-38ff-4170-9b3c-bd7eae513d35";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "debug_show_permissions_mode";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "off";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "false";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "When running in debug mode, Permissions can be shown as: 'bytes' for bytes only, 'full' for detailed permission checks, or 'off' to suppress showing permissions.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "9b8853db-244c-40a6-a8fe-77c5b133c6b0";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "debug_show_switch_event";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "boolean";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "false";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "When running in debug mode, show the raw switch event message for conference maintenance events.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "f84c37ed-0469-42d1-9f20-cb3ba00a9d7b";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "active_conferences";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "websocket_enabled";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "boolean";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Enable or disable the use of websockets for active conferences.";
|
||||
1433
app/active_conferences/app_languages.php
Normal file
36
app/active_conferences/app_menu.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
$y=0;
|
||||
$apps[$x]['menu'][$y]['title']['en-us'] = "Active Conferences";
|
||||
$apps[$x]['menu'][$y]['title']['en-gb'] = "Active Conferences";
|
||||
$apps[$x]['menu'][$y]['title']['ar-eg'] = "المؤتمرات النشطة";
|
||||
$apps[$x]['menu'][$y]['title']['de-at'] = "Aktive Konferenzen";
|
||||
$apps[$x]['menu'][$y]['title']['de-ch'] = "Aktive Konferenzen";
|
||||
$apps[$x]['menu'][$y]['title']['de-de'] = "Aktive Konferenzen";
|
||||
$apps[$x]['menu'][$y]['title']['es-cl'] = "Conferencias Activas";
|
||||
$apps[$x]['menu'][$y]['title']['es-mx'] = "Conferencias activas";
|
||||
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Conférences actives";
|
||||
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Conférences en cours";
|
||||
$apps[$x]['menu'][$y]['title']['he-il'] = "כנסים פעילים";
|
||||
$apps[$x]['menu'][$y]['title']['it-it'] = "Conferenze attive";
|
||||
$apps[$x]['menu'][$y]['title']['ka-ge'] = "აქტიური კონფერენციები";
|
||||
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Aktieve conferenties";
|
||||
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Aktywne rozmowy konferencyjne";
|
||||
$apps[$x]['menu'][$y]['title']['pt-br'] = "Conferência ativa";
|
||||
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Conferencias Activas";
|
||||
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Conferințe active";
|
||||
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференции Активные";
|
||||
$apps[$x]['menu'][$y]['title']['sv-se'] = "Aktiva Konferenser";
|
||||
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Активні конференції";
|
||||
$apps[$x]['menu'][$y]['title']['zh-cn'] = "活动会议";
|
||||
$apps[$x]['menu'][$y]['title']['ja-jp'] = "アクティブな会議";
|
||||
$apps[$x]['menu'][$y]['title']['ko-kr'] = "활성 회의";
|
||||
$apps[$x]['menu'][$y]['uuid'] = "b3cdd852-1382-4d7f-9676-f25b133971f2";
|
||||
$apps[$x]['menu'][$y]['parent_uuid'] = "0438b504-8613-7887-c420-c837ffb20cb1";
|
||||
$apps[$x]['menu'][$y]['category'] = "internal";
|
||||
$apps[$x]['menu'][$y]['icon'] = "";
|
||||
$apps[$x]['menu'][$y]['path'] = "/app/active_conferences/active_conferences.php";
|
||||
$apps[$x]['menu'][$y]['order'] = "";
|
||||
$apps[$x]['menu'][$y]['groups'][] = "admin";
|
||||
$apps[$x]['menu'][$y]['groups'][] = "superadmin";
|
||||
$y++;
|
||||
364
app/active_conferences/debug.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/resources/require.php';
|
||||
|
||||
// Create the token
|
||||
$token = (new token)->create($_SERVER['PHP_SELF']);
|
||||
|
||||
// Save the token
|
||||
subscriber::save_token($token, [active_conferences_service::get_service_name()]);
|
||||
|
||||
//break the caching
|
||||
$version = md5(file_get_contents(__DIR__ . '/resources/javascript/websocket_client.js'));
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket Event Logger</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.websocket-container {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.websocket-header {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.websocket-header:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.collapsed .toggle-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.websocket-content {
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.event-log {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
margin-bottom: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
border-left: 3px solid #007bff;
|
||||
}
|
||||
|
||||
.event-item:hover {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.event-timestamp {
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.event-data {
|
||||
margin-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.connected {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 15px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.connect-btn {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const token = {
|
||||
name: '<?= $token['name'] ?>',
|
||||
hash: '<?= $token['hash'] ?>'
|
||||
}
|
||||
</script>
|
||||
<script src="resources/javascript/websocket_client.js?v=<?= $version ?>"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>WebSocket Event Logger</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button class="connect-btn" onclick="connect_websocket()">Connect</button>
|
||||
<button class="disconnect-btn" onclick="disconnect_websocket()">Disconnect</button>
|
||||
<button class="clear-btn" onclick="clear_log()">Clear Log</button>
|
||||
</div>
|
||||
|
||||
<div id="connection-status" class="connection-status disconnected">
|
||||
Disconnected
|
||||
</div>
|
||||
|
||||
<div class="websocket-container" id="websocket-container">
|
||||
<div class="websocket-header" onclick="toggle_collapse()">
|
||||
<span>WebSocket Events <span id="event-count">(0 events)</span></span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</div>
|
||||
<div class="websocket-content" id="websocket-content">
|
||||
<div class="event-log" id="event-log">
|
||||
<div id="placeholder">No events received yet. Connect to WebSocket to start logging.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
let event_count = 0;
|
||||
let is_collapsed = false;
|
||||
let reconnect_attempts = 0;
|
||||
const max_reconnect_delay = 30000; // 30 seconds
|
||||
const base_reconnect_delay = 1000; // 1 second
|
||||
|
||||
function connect_websocket() {
|
||||
// Replace with your WebSocket server URL
|
||||
const ws_url = `wss://${window.location.hostname}/websockets/`; // Update this URL
|
||||
|
||||
try {
|
||||
ws = new ws_client(ws_url, token);
|
||||
|
||||
ws.on_event('authenticated', authenticated);
|
||||
|
||||
// CONNECTED
|
||||
ws.ws.addEventListener("open", () => {
|
||||
console.log('WebSocket connection opened');
|
||||
reconnect_attempts = 0;
|
||||
});
|
||||
|
||||
// DISCONNECTED - handle reconnection
|
||||
ws.ws.addEventListener("close", (event) => {
|
||||
console.warn('WebSocket disconnected:', event.code, event.reason);
|
||||
update_connection_status('Disconnected - reconnecting...', 'disconnected');
|
||||
|
||||
// Exponential backoff for reconnection
|
||||
reconnect_attempts++;
|
||||
const delay = Math.min(base_reconnect_delay * Math.pow(2, reconnect_attempts - 1), max_reconnect_delay);
|
||||
console.log(`Reconnecting in ${delay}ms (attempt ${reconnect_attempts})`);
|
||||
|
||||
setTimeout(() => {
|
||||
connect_websocket();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
// ERROR
|
||||
ws.ws.addEventListener("error", (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to WebSocket:', error);
|
||||
update_connection_status('Connection failed: ' + error.message, 'disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
function authenticated(message) {
|
||||
console.log('WebSocket connected');
|
||||
update_connection_status('Connected', 'connected');
|
||||
|
||||
// Log the authenticated event to the UI
|
||||
log_event(JSON.stringify({event_name: 'authenticated', message: message}));
|
||||
|
||||
// Register wildcard handler to catch ALL events
|
||||
ws.on_event('*', on_any_event);
|
||||
|
||||
// Subscribe to all events using wildcard
|
||||
ws.subscribe('*');
|
||||
}
|
||||
|
||||
function on_any_event(event) {
|
||||
console.log('Event received:', event.event_name, event);
|
||||
log_event(JSON.stringify(event));
|
||||
}
|
||||
|
||||
function disconnect_websocket() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
function log_event(data) {
|
||||
event_count++;
|
||||
update_event_count();
|
||||
|
||||
const event_log = document.getElementById('event-log');
|
||||
|
||||
// Remove placeholder if it exists
|
||||
const placeholder = document.getElementById('placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
let event_data;
|
||||
try {
|
||||
event_data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
event_data = data;
|
||||
}
|
||||
|
||||
// Check for payload object (events wrapped in websocket_message)
|
||||
const payload = event_data.payload || event_data;
|
||||
|
||||
// Get UUID from unique_id field (with fallbacks)
|
||||
const event_uuid = payload.unique_id || event_data.unique_id || payload.uuid || event_data.uuid || '';
|
||||
|
||||
// Get action from Action field, falling back to event_name
|
||||
const event_action = payload.action || event_data.action || event_data.event_name || payload.event_name || 'Unknown Event';
|
||||
|
||||
const event_item = document.createElement('div');
|
||||
event_item.className = 'event-item collapsible';
|
||||
|
||||
const timestamp = new Date().toLocaleString();
|
||||
|
||||
// Format display: show action, optionally with UUID if present
|
||||
const display_text = event_uuid ? `${event_action} (${event_uuid})` : event_action;
|
||||
|
||||
event_item.innerHTML = `
|
||||
<div class="event-header" onclick="toggle_event(this)">
|
||||
<span class="event-timestamp">${timestamp}</span>
|
||||
<span class="event-type">${display_text}</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</div>
|
||||
<div class="event-data" style="display: none;">
|
||||
${format_data(event_data)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at the top of the list (newest first)
|
||||
event_log.insertBefore(event_item, event_log.firstChild);
|
||||
}
|
||||
|
||||
function toggle_event(element) {
|
||||
const event_data = element.nextElementSibling;
|
||||
const icon = element.querySelector('.toggle-icon');
|
||||
|
||||
if (event_data.style.display === 'none' || event_data.style.display === '') {
|
||||
event_data.style.display = 'block';
|
||||
icon.textContent = '▲';
|
||||
} else {
|
||||
event_data.style.display = 'none';
|
||||
icon.textContent = '▼';
|
||||
}
|
||||
}
|
||||
|
||||
function format_data(data) {
|
||||
if (typeof data === 'object') {
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
function update_connection_status(message, status_class) {
|
||||
const status_element = document.getElementById('connection-status');
|
||||
status_element.textContent = message;
|
||||
status_element.className = 'connection-status ' + status_class;
|
||||
}
|
||||
|
||||
function update_event_count() {
|
||||
document.getElementById('event-count').textContent = `(${event_count} events)`;
|
||||
}
|
||||
|
||||
function clear_log() {
|
||||
const event_log = document.getElementById('event-log');
|
||||
event_log.innerHTML = '<div id="placeholder">No events received yet. Connect to WebSocket to start logging.</div>';
|
||||
event_count = 0;
|
||||
update_event_count();
|
||||
}
|
||||
|
||||
function toggle_collapse() {
|
||||
const container = document.getElementById('websocket-container');
|
||||
const content = document.getElementById('websocket-content');
|
||||
const icon = document.querySelector('.toggle-icon');
|
||||
|
||||
is_collapsed = !is_collapsed;
|
||||
|
||||
if (is_collapsed) {
|
||||
container.classList.add('collapsed');
|
||||
content.style.display = 'none';
|
||||
icon.textContent = '▶';
|
||||
} else {
|
||||
container.classList.remove('collapsed');
|
||||
content.style.display = 'block';
|
||||
icon.textContent = '▼';
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-connect on page load (optional)
|
||||
// window.addEventListener('load', connect_websocket);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
BIN
app/active_conferences/resources/images/hear.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
app/active_conferences/resources/images/moderator.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
app/active_conferences/resources/images/not_recording.png
Normal file
|
After Width: | Height: | Size: 813 B |
BIN
app/active_conferences/resources/images/participant.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
app/active_conferences/resources/images/recording.png
Normal file
|
After Width: | Height: | Size: 830 B |
BIN
app/active_conferences/resources/images/speak.png
Normal file
|
After Width: | Height: | Size: 413 B |
BIN
app/active_conferences/resources/images/video.png
Normal file
|
After Width: | Height: | Size: 257 B |
1347
app/active_conferences/resources/javascript/active_conferences.js
Normal file
199
app/active_conferences/resources/javascript/websocket_client.js
Normal file
@@ -0,0 +1,199 @@
|
||||
class ws_client {
|
||||
constructor(url, token) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.addEventListener('message', this._on_message.bind(this));
|
||||
this._next_id = 1;
|
||||
this._pending = new Map();
|
||||
this._event_handlers = new Map();
|
||||
// The token is submitted on every request
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
authenticate() {
|
||||
//
|
||||
// Authentication is with websockets not the service, so we need to send a special
|
||||
// request for authentication and specify the service that will be handling our
|
||||
// future messages. This means the service is authentication and the topic is the
|
||||
// service that will handle our future messages. This is a special case because we
|
||||
// must authenticate with websockets, not the service. The service is only used to
|
||||
// handle future messages.
|
||||
//
|
||||
// service = 'authentication'
|
||||
// topic = active_conferences_service::get_service_name()
|
||||
// payload = token
|
||||
//
|
||||
this.request('authentication', 'active.conferences', { token: this.token });
|
||||
}
|
||||
|
||||
// internal message handler called when event occurs on the socket
|
||||
_on_message(ev) {
|
||||
let message;
|
||||
let switch_event;
|
||||
try {
|
||||
console.log('Raw message received:', ev.data);
|
||||
message = JSON.parse(ev.data);
|
||||
// check for authentication request
|
||||
if (message.status_code === 407) {
|
||||
console.log('Authentication Required');
|
||||
this.authenticate();
|
||||
return;
|
||||
}
|
||||
switch_event = message.payload;
|
||||
if (message.topic === 'authenticated') {
|
||||
console.log('Authenticated');
|
||||
this._dispatch_event('active.conferences', {event_name: 'authenticated'});
|
||||
return; // Don't process further after authenticated
|
||||
}
|
||||
//console.log('envelope received: ',env);
|
||||
} catch (err) {
|
||||
console.error('Error parsing JSON data:', err);
|
||||
//console.error('Invalid JSON:', ev.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pull out the request_id first
|
||||
const rid = message.request_id ?? null;
|
||||
|
||||
// If this is the response to a pending request
|
||||
if (rid && this._pending.has(rid)) {
|
||||
// Destructure with defaults in case they're missing
|
||||
const {
|
||||
service_name = '',
|
||||
topic = '',
|
||||
status = 'ok',
|
||||
code = 200,
|
||||
payload = {}
|
||||
} = message;
|
||||
|
||||
const {resolve, reject} = this._pending.get(rid);
|
||||
this._pending.delete(rid);
|
||||
|
||||
if (status === 'ok' && code >= 200 && code < 300) {
|
||||
console.log('Response received:', {service_name, topic, payload, code});
|
||||
resolve({service_name, topic, payload, code, message});
|
||||
// Also dispatch as an event so handlers get notified
|
||||
// Use topic from message as event_name if payload doesn't have one
|
||||
const event_data = (typeof switch_event === 'object' && switch_event !== null)
|
||||
? { ...switch_event, event_name: switch_event.event_name || topic }
|
||||
: { event_name: topic, data: switch_event };
|
||||
this._dispatch_event(service_name, event_data);
|
||||
} else {
|
||||
const err = new Error(message || `Error ${code}`);
|
||||
err.code = code;
|
||||
reject(err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise it's a 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'][] = "admin";
|
||||
$y++;
|
||||
$apps[$x]['menu'][$y]['title']['en-us'] = "Conference Centers";
|
||||
$apps[$x]['menu'][$y]['title']['ar-eg'] = "مراكز المؤتمرات";
|
||||
$apps[$x]['menu'][$y]['title']['de-at'] = "Konferenz Zentrale";
|
||||
$apps[$x]['menu'][$y]['title']['de-ch'] = "Konferenzzentren";
|
||||
$apps[$x]['menu'][$y]['title']['de-de'] = "Konferenz Zentrale";
|
||||
$apps[$x]['menu'][$y]['title']['es-cl'] = "Cent. de Conferencias";
|
||||
$apps[$x]['menu'][$y]['title']['es-mx'] = "Conference Centers";
|
||||
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Centres de conférences";
|
||||
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Centre de Conférences";
|
||||
$apps[$x]['menu'][$y]['title']['he-il'] = "מרכזי כנסים";
|
||||
$apps[$x]['menu'][$y]['title']['it-it'] = "Centro Conferenze";
|
||||
$apps[$x]['menu'][$y]['title']['ka-ge'] = "კონფერენც-ცენტრები";
|
||||
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Conferentie centra";
|
||||
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Centrum Konferencyjne";
|
||||
$apps[$x]['menu'][$y]['title']['pt-br'] = "Centro de Conferência";
|
||||
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Conferencias";
|
||||
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Centre de conferințe";
|
||||
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференц-центр";
|
||||
$apps[$x]['menu'][$y]['title']['sv-se'] = "Konferenscenter";
|
||||
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Конференц-центр";
|
||||
$apps[$x]['menu'][$y]['title']['zh-cn'] = "会议中心";
|
||||
$apps[$x]['menu'][$y]['title']['ja-jp'] = "カンファレンスセンター";
|
||||
$apps[$x]['menu'][$y]['title']['ko-kr'] = "컨퍼런스 센터";
|
||||
$apps[$x]['menu'][$y]['title']['en-us'] = "Conference Rooms";
|
||||
$apps[$x]['menu'][$y]['title']['en-gb'] = "Conference Rooms";
|
||||
$apps[$x]['menu'][$y]['title']['ar-eg'] = "غرف المؤتمرات";
|
||||
$apps[$x]['menu'][$y]['title']['de-at'] = "Konferenzräume";
|
||||
$apps[$x]['menu'][$y]['title']['de-ch'] = "Konferenzräume";
|
||||
$apps[$x]['menu'][$y]['title']['de-de'] = "Konferenzräume";
|
||||
$apps[$x]['menu'][$y]['title']['es-cl'] = "Salas de Conferencias";
|
||||
$apps[$x]['menu'][$y]['title']['es-mx'] = "Salas de Conferencias";
|
||||
$apps[$x]['menu'][$y]['title']['fr-ca'] = "Salles de conférences";
|
||||
$apps[$x]['menu'][$y]['title']['fr-fr'] = "Salles de Conférences";
|
||||
$apps[$x]['menu'][$y]['title']['he-il'] = "חדרי כנסים";
|
||||
$apps[$x]['menu'][$y]['title']['it-it'] = "Sale Conferenze";
|
||||
$apps[$x]['menu'][$y]['title']['ka-ge'] = "კონფერენციის ოთახები";
|
||||
$apps[$x]['menu'][$y]['title']['nl-nl'] = "Conferentie kamers";
|
||||
$apps[$x]['menu'][$y]['title']['pl-pl'] = "Sale Konferencyjne";
|
||||
$apps[$x]['menu'][$y]['title']['pt-br'] = "Salas de Conferência";
|
||||
$apps[$x]['menu'][$y]['title']['pt-pt'] = "Salas de Conferências";
|
||||
$apps[$x]['menu'][$y]['title']['ro-ro'] = "Săli de conferințe";
|
||||
$apps[$x]['menu'][$y]['title']['ru-ru'] = "Конференц-залы";
|
||||
$apps[$x]['menu'][$y]['title']['sv-se'] = "Konferensrum";
|
||||
$apps[$x]['menu'][$y]['title']['uk-ua'] = "Конференц-зали";
|
||||
$apps[$x]['menu'][$y]['title']['zh-cn'] = "会议室";
|
||||
$apps[$x]['menu'][$y]['title']['ja-jp'] = "会議室";
|
||||
$apps[$x]['menu'][$y]['title']['ko-kr'] = "컨퍼런스 룸";
|
||||
$apps[$x]['menu'][$y]['uuid'] = "b99cb768-ca19-4374-a954-02e344313d84";
|
||||
$apps[$x]['menu'][$y]['parent_uuid'] = "fd29e39c-c936-f5fc-8e2b-611681b266b5";
|
||||
$apps[$x]['menu'][$y]['category'] = "internal";
|
||||
|
||||
@@ -40,16 +40,24 @@
|
||||
$text = $language->get();
|
||||
|
||||
//set additional variables
|
||||
$search = $_GET["search"] ?? null;
|
||||
$show = $_REQUEST["show"] ?? '';
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$search = $_REQUEST['search'] ?? '';
|
||||
$toggle_field = $_REQUEST['toggle_field'] ?? '';
|
||||
|
||||
//check if we are using websockets for the conference view
|
||||
if ($settings->get('active_conferences', 'websockets_enabled', true)) {
|
||||
$conference_view_page = '/app/active_conferences/active_conference_room.php';
|
||||
}
|
||||
else {
|
||||
$conference_view_page = '/app/conferences_active/conferences_active.php';
|
||||
}
|
||||
|
||||
//set from session variables
|
||||
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
|
||||
|
||||
//get the http post data
|
||||
if (!empty($_POST['conference_rooms'])) {
|
||||
$action = $_POST['action'];
|
||||
$toggle_field = $_POST['toggle_field'];
|
||||
$search = $_POST['search'] ?? '';
|
||||
$conference_rooms = $_POST['conference_rooms'];
|
||||
}
|
||||
|
||||
@@ -448,10 +456,10 @@
|
||||
}
|
||||
echo " <td class='no-link no-wrap center'>\n";
|
||||
if (permission_exists('conference_interactive_view')) {
|
||||
echo " <a href='".PROJECT_PATH."/app/conferences_active/conference_interactive.php?c=".urlencode($row['conference_room_uuid'])."'>".$text['label-view']."</a>\n";
|
||||
echo " <a href='".PROJECT_PATH.$conference_view_page."?c=".urlencode($row['conference_room_uuid'])."'>".$text['label-view']."</a>\n";
|
||||
}
|
||||
else if (permission_exists('conference_active_view')) {
|
||||
echo " <a href='".PROJECT_PATH."/app/conferences_active/conferences_active.php'>".$text['label-view']."</a>\n";
|
||||
echo " <a href='".PROJECT_PATH.$conference_view_page."'>".$text['label-view']."</a>\n";
|
||||
}
|
||||
if (permission_exists('conference_cdr_view')) {
|
||||
echo " <a href='/app/conference_cdr/conference_cdr.php?id=".urlencode($row['conference_room_uuid'])."'>".$text['button-cdr']."</a>\n";
|
||||
|
||||
@@ -40,15 +40,23 @@
|
||||
$text = $language->get();
|
||||
|
||||
//set additional variables
|
||||
$show = $_GET["show"] ?? '';
|
||||
$show = $_REQUEST["show"] ?? '';
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$search = $_REQUEST['search'] ?? '';
|
||||
|
||||
//check if we are using websockets for the conference view
|
||||
if ($settings->get('active_conferences', 'websocket_enabled', true)) {
|
||||
$conference_view_page = '/app/active_conferences/active_conferences.php';
|
||||
}
|
||||
else {
|
||||
$conference_view_page = '/app/conferences_active/conferences_active.php';
|
||||
}
|
||||
|
||||
//set from session variables
|
||||
$list_row_edit_button = $settings->get('theme', 'list_row_edit_button', false);
|
||||
|
||||
//get posted data
|
||||
if (!empty($_POST['conferences'])) {
|
||||
$action = $_POST['action'];
|
||||
$search = $_POST['search'] ?? '';
|
||||
$conferences = $_POST['conferences'];
|
||||
}
|
||||
|
||||
@@ -180,7 +188,7 @@
|
||||
echo " <div class='heading'><b>".$text['title-conferences']."</b><div class='count'>".number_format($num_rows)."</div></div>\n";
|
||||
echo " <div class='actions'>\n";
|
||||
if (permission_exists('conference_active_view')) {
|
||||
echo button::create(['type'=>'button','label'=>$text['button-view_active'],'icon'=>'comments','style'=>'margin-right: 15px;','link'=>PROJECT_PATH.'/app/conferences_active/conferences_active.php']);
|
||||
echo button::create(['type'=>'button','label'=>$text['button-view_active'],'icon'=>'comments','style'=>'margin-right: 15px;','link'=>PROJECT_PATH.$conference_view_page]);
|
||||
}
|
||||
if (permission_exists('conference_add')) {
|
||||
echo button::create(['type'=>'button','label'=>$text['button-add'],'icon'=>$settings->get('theme', 'button_icon_add'),'id'=>'btn_add','link'=>'conference_edit.php']);
|
||||
@@ -286,10 +294,10 @@
|
||||
echo " <td class='center'>".escape($row['conference_order'])." </td>\n";
|
||||
echo " <td class='no-link center'>\n";
|
||||
if (permission_exists('conference_interactive_view')) {
|
||||
echo " <a href='".PROJECT_PATH."/app/conferences_active/conference_interactive.php?c=".urlencode($row['conference_extension'])."'>".$text['label-view']."</a>\n";
|
||||
echo " <a href='".PROJECT_PATH."$conference_view_page?c=".urlencode($row['conference_extension'])."'>".$text['label-view']."</a>\n";
|
||||
}
|
||||
else if (permission_exists('conference_active_view')) {
|
||||
echo " <a href='".PROJECT_PATH."/app/conferences_active/conferences_active.php'>".$text['label-view']."</a>\n";
|
||||
echo " <a href='".PROJECT_PATH."$conference_view_page'>".$text['label-view']."</a>\n";
|
||||
}
|
||||
else {
|
||||
echo " &nsbp;\n";
|
||||
|
||||
@@ -144,11 +144,15 @@ abstract class base_websocket_system_service extends service implements websocke
|
||||
$ws_client->disconnect();
|
||||
}, $this->ws_client);
|
||||
|
||||
// Call the register topics in the child classes
|
||||
$this->register_topics();
|
||||
|
||||
// Register the authenticate request
|
||||
$this->on_topic('authenticate', [$this, 'on_authenticate']);
|
||||
|
||||
// Register the authenticated response handler
|
||||
$this->on_topic('authenticated', [$this, 'handle_ws_authenticated']);
|
||||
|
||||
// Track the WebSocket Server Error Message so it doesn't flood the system logs
|
||||
$suppress_ws_message = false;
|
||||
|
||||
@@ -184,7 +188,7 @@ abstract class base_websocket_system_service extends service implements websocke
|
||||
}
|
||||
// stream_select will update $read so re-check it
|
||||
if (!empty($read)) {
|
||||
$this->debug("Received event");
|
||||
//$this->debug("Received event");
|
||||
// Iterate over each socket event
|
||||
foreach ($read as $resource) {
|
||||
// Web socket event
|
||||
@@ -250,7 +254,8 @@ abstract class base_websocket_system_service extends service implements websocke
|
||||
// Disable the stream blocking
|
||||
$this->ws_client->set_blocking(false);
|
||||
|
||||
$this->debug(self::class . " RESOURCE ID: " . $this->ws_client->socket());
|
||||
// Call the on connected event function
|
||||
$this->handle_ws_connected();
|
||||
} catch (\RuntimeException $re) {
|
||||
//unable to connect
|
||||
return false;
|
||||
@@ -258,6 +263,34 @@ abstract class base_websocket_system_service extends service implements websocke
|
||||
return true;
|
||||
}
|
||||
|
||||
private function handle_ws_connected(): void {
|
||||
$this->info("Websocket connection established to server");
|
||||
$this->debug(static::class . " RESOURCE ID: " . $this->ws_client->socket());
|
||||
$this->on_ws_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the web socket is first connected
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function on_ws_connected(): void {
|
||||
// Override in child class if needed
|
||||
}
|
||||
|
||||
private function handle_ws_authenticated(websocket_message $websocket_message): void {
|
||||
$this->info("Successfully authenticated with websocket server");
|
||||
$this->on_ws_authenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the service has successfully authenticated with the websocket server.
|
||||
* Override in child class to perform actions after authentication.
|
||||
*/
|
||||
protected function on_ws_authenticated(): void {
|
||||
// Override in child class if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the message from the web socket client and triggers the appropriate requested topic event
|
||||
*
|
||||
@@ -269,11 +302,11 @@ abstract class base_websocket_system_service extends service implements websocke
|
||||
|
||||
// Nothing to do
|
||||
if ($json_string === null) {
|
||||
$this->warn('Message received from Websocket is empty');
|
||||
$this->warning('Message received from Websocket is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->debug("Received message on websocket: $json_string (" . strlen($json_string) . " bytes)");
|
||||
//$this->debug("Received message on websocket: $json_string (" . strlen($json_string) . " bytes)");
|
||||
|
||||
// Get the web socket message as an object
|
||||
$message = websocket_message::create_from_json_message($json_string);
|
||||
@@ -314,7 +347,9 @@ abstract class base_websocket_system_service extends service implements websocke
|
||||
protected function on_authenticate(websocket_message $websocket_message) {
|
||||
$this->info("Authenticating with websocket server");
|
||||
// Create a service token
|
||||
[$token_name, $token_hash] = websocket_client::create_service_token(active_calls_service::get_service_name(), static::class);
|
||||
$service_name = static::get_service_name();
|
||||
$class_name = static::class;
|
||||
[$token_name, $token_hash] = websocket_client::create_service_token($service_name, $class_name);
|
||||
|
||||
// Request authentication as a service
|
||||
$this->ws_client->authenticate($token_name, $token_hash);
|
||||
|
||||
@@ -184,6 +184,13 @@ class subscriber {
|
||||
*/
|
||||
private $authenticated;
|
||||
|
||||
/**
|
||||
* Whether this subscriber has requested debug subscribe all mode
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $debug_subscribe_all = false;
|
||||
|
||||
/**
|
||||
* User information
|
||||
*
|
||||
@@ -603,6 +610,10 @@ class subscriber {
|
||||
$this->service = is_a($this->service_class, 'websocket_service_interface', true);
|
||||
}
|
||||
|
||||
// Load options (e.g., debug_subscribe_all)
|
||||
$options = $array['options'] ?? [];
|
||||
$this->debug_subscribe_all = !empty($options['debug_subscribe_all']);
|
||||
|
||||
//self::$logger->debug("Permission count(".count($this->permissions) . ")");
|
||||
}
|
||||
|
||||
@@ -637,6 +648,15 @@ class subscriber {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this subscriber has requested debug subscribe all mode.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_debug_subscribe_all(): bool {
|
||||
return $this->debug_subscribe_all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the domain UUID and name
|
||||
*
|
||||
@@ -650,7 +670,7 @@ class subscriber {
|
||||
*/
|
||||
public function set_domain(string $uuid, string $name): self {
|
||||
if (is_uuid($uuid)) {
|
||||
$this->uuid = $uuid;
|
||||
$this->domain_uuid = $uuid;
|
||||
} else {
|
||||
throw new invalid_uuid_exception("UUID is not valid");
|
||||
}
|
||||
@@ -877,16 +897,22 @@ class subscriber {
|
||||
* @param array $token Standard token issued from the token object
|
||||
* @param array $services A simple array list of service names to subscribe to
|
||||
* @param int $time_limit_in_minutes Set a token time limit. Setting to zero will disable the time limit
|
||||
* @param array $options Optional additional options (e.g., debug_subscribe_all)
|
||||
*
|
||||
* @see token::create()
|
||||
*/
|
||||
public static function save_token(array $token, array $services, int $time_limit_in_minutes = 0) {
|
||||
public static function save_token(array $token, array $services, int $time_limit_in_minutes = 0, array $options = []) {
|
||||
|
||||
//
|
||||
// Store the currently logged in user when available
|
||||
//
|
||||
$array['user'] = $_SESSION['user'] ?? [];
|
||||
|
||||
//
|
||||
// Store additional options
|
||||
//
|
||||
$array['options'] = $options;
|
||||
|
||||
//
|
||||
// Store the token service and events
|
||||
//
|
||||
|
||||
@@ -110,7 +110,7 @@ class websocket_message extends base_message {
|
||||
*
|
||||
* @param string $service_name
|
||||
*
|
||||
* @return $this
|
||||
* @return $this|string
|
||||
*/
|
||||
public function service_name($service_name = null) {
|
||||
if (func_num_args() > 0) {
|
||||
@@ -125,7 +125,7 @@ class websocket_message extends base_message {
|
||||
*
|
||||
* @param array $permissions
|
||||
*
|
||||
* @return $this
|
||||
* @return array|$this
|
||||
*/
|
||||
public function permissions($permissions = []) {
|
||||
if (func_num_args() > 0) {
|
||||
@@ -135,6 +135,15 @@ class websocket_message extends base_message {
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of permissions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_permissions(): array {
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a filter to the payload of this message.
|
||||
* When a filter returns null then the payload is set to null
|
||||
|
||||
@@ -93,7 +93,7 @@ class websocket_service extends service {
|
||||
/**
|
||||
* Subscriber Objects
|
||||
*
|
||||
* @var subscriber
|
||||
* @var array<subscriber>
|
||||
*/
|
||||
protected $subscribers;
|
||||
|
||||
@@ -225,8 +225,9 @@ class websocket_service extends service {
|
||||
$subscriber->send(websocket_message::request_authenticated($message->request_id, $message->service));
|
||||
// Check for service authenticated
|
||||
if ($subscriber->is_service()) {
|
||||
$this->info("Service $subscriber->id authenticated");
|
||||
$this->services[$subscriber->service_name()] = $subscriber;
|
||||
$service_name = $subscriber->service_name();
|
||||
$this->info("Service $service_name authenticated using id $subscriber->id");
|
||||
$this->services[$service_name] = $subscriber;
|
||||
} else {
|
||||
// Subscriber authenticated
|
||||
$this->info("Client $subscriber->id authenticated");
|
||||
@@ -237,12 +238,21 @@ class websocket_service extends service {
|
||||
$class_name = $subscriber_service->service_class();
|
||||
// Make sure we can call the 'create_filter_chain_for' method
|
||||
if (is_a($class_name, 'websocket_service_interface', true)) {
|
||||
// Call the service class method to validate the subscriber
|
||||
$filter = $class_name::create_filter_chain_for($subscriber);
|
||||
if ($filter !== null) {
|
||||
// Log the filter has been set for the subscriber
|
||||
$this->info("Set filter for " . $subscriber->id());
|
||||
$subscriber->set_filter($filter);
|
||||
try {
|
||||
// Call the service class method to validate the subscriber
|
||||
$filter = $class_name::create_filter_chain_for($subscriber);
|
||||
if ($filter !== null) {
|
||||
// Log the filter has been set for the subscriber
|
||||
$this->info("Set filter for " . $subscriber->id());
|
||||
$subscriber->set_filter($filter);
|
||||
}
|
||||
} catch (subscriber_missing_permission_exception $smpe) {
|
||||
// Subscriber requested debug mode but service is not in debug mode
|
||||
// Disconnect them for security
|
||||
$this->warning("Client $subscriber->id denied: " . $smpe->getMessage());
|
||||
$subscriber->send(websocket_message::request_unauthorized($message->request_id, $message->service));
|
||||
$this->handle_disconnect($subscriber->socket_id());
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->info("Set permissions for $subscriber->id for service " . $subscriber_service->service_name());
|
||||
@@ -271,7 +281,7 @@ class websocket_service extends service {
|
||||
|
||||
// Ensure we have something to do
|
||||
if ($message === null) {
|
||||
$this->warn("Unable to broadcast empty message");
|
||||
$this->warning("Unable to broadcast empty message");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -301,7 +311,7 @@ class websocket_service extends service {
|
||||
$this->debug("Broadcasting message '" . $message->topic() . "' for service '" . $message->service_name . "' to subscriber $subscriber->id");
|
||||
$subscriber->send_message($message);
|
||||
} catch (subscriber_token_expired_exception $ste) {
|
||||
$this->info("Subscriber $ste->id token expired");
|
||||
$this->info("Subscriber $ste->subscriber_id token expired");
|
||||
// Subscriber token has expired so disconnect them
|
||||
$this->handle_disconnect($subscriber->socket_id());
|
||||
}
|
||||
@@ -315,7 +325,7 @@ class websocket_service extends service {
|
||||
// Remove the resource_id from the message
|
||||
$message->resource_id('');
|
||||
// TODO: Fix removal of request_id
|
||||
$message->request_id('');
|
||||
//$message->request_id('');
|
||||
// Return the requested results back to the subscriber
|
||||
$subscriber->send_message($message);
|
||||
}
|
||||
@@ -448,7 +458,7 @@ class websocket_service extends service {
|
||||
$message = websocket_message::create_from_json_message($json_array);
|
||||
|
||||
if ($message === null) {
|
||||
$this->warn("Message is empty");
|
||||
$this->warning("Message is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -954,6 +954,18 @@ abstract class service {
|
||||
self::log($message, LOG_DEBUG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the service is running in debug mode (LOG_DEBUG level).
|
||||
*
|
||||
* This is useful for security checks where certain features should only
|
||||
* be available when the service is explicitly started with debug logging.
|
||||
*
|
||||
* @return bool True if the service is running at LOG_DEBUG level
|
||||
*/
|
||||
public static function is_debug_mode(): bool {
|
||||
return self::$log_level === LOG_DEBUG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the INFO level.
|
||||
*
|
||||
|
||||