mirror of
https://github.com/fusionpbx/fusionpbx.git
synced 2026-03-31 05:29:58 +00:00
Fix live updating (#7816)
This commit is contained in:
@@ -237,6 +237,94 @@
|
||||
$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'] = "ec8f0281-5d2b-4a3b-9481-f2c9880db101";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_mute";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-microphone";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference mute action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "3d4dbe79-f1af-4bea-804e-27ee0ac31302";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_unmute";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-microphone-slash";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference unmute action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "b15d1ae4-6087-48cf-a46f-148fb5236103";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_deaf";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-headphones";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference deaf action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "2b560cff-58ef-42c2-b4e5-08d77c339a04";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_undeaf";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-deaf";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference undeaf action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "21f8af30-d08f-4342-bddb-bb68804e1005";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_energy_up";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-plus";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference energy up action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "4fdc30b2-7db8-4c9f-9b7e-4c78fa757706";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_energy_down";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-minus";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference energy down action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "2b706528-8a6b-409c-84bb-95f905344707";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_volume_down";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-volume-down";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference volume down action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "9220ec76-6760-4b75-a29e-503b94ca7508";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_volume_up";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-volume-up";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference volume up action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "866dc078-68cb-43aa-b70b-b73d213dbd09";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_gain_down";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-sort-amount-down";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference gain down action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "d4bde1f1-b0d8-4836-a78b-f8c4bb510a10";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_gain_up";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-sort-amount-up";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference gain up action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "f55d2796-46b5-4ca2-998f-256184489b11";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_kick";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_name'] = "text";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-ban";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference kick action.";
|
||||
$y++;
|
||||
$apps[$x]['default_settings'][$y]['default_setting_uuid'] = "b367ce0d-beb0-424a-9fc9-0c83690001ee";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_category'] = "theme";
|
||||
$apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_status_show_icon";
|
||||
|
||||
@@ -81,6 +81,19 @@
|
||||
'disconnected' => $settings->get('theme', 'operator_panel_status_icon_disconnected', 'fa-solid fa-plug-circle-xmark'),
|
||||
'connecting' => $settings->get('theme', 'operator_panel_status_icon_connecting', 'fa-solid fa-plug fa-fade'),
|
||||
];
|
||||
$conference_action_icons = [
|
||||
'mute' => $settings->get('theme', 'operator_panel_conference_icon_mute', 'fas fa-microphone'),
|
||||
'unmute' => $settings->get('theme', 'operator_panel_conference_icon_unmute', 'fas fa-microphone-slash'),
|
||||
'deaf' => $settings->get('theme', 'operator_panel_conference_icon_deaf', 'fas fa-headphones'),
|
||||
'undeaf' => $settings->get('theme', 'operator_panel_conference_icon_undeaf', 'fas fa-deaf'),
|
||||
'energy_up' => $settings->get('theme', 'operator_panel_conference_icon_energy_up', 'fas fa-plus'),
|
||||
'energy_down' => $settings->get('theme', 'operator_panel_conference_icon_energy_down', 'fas fa-minus'),
|
||||
'volume_down' => $settings->get('theme', 'operator_panel_conference_icon_volume_down', 'fas fa-volume-down'),
|
||||
'volume_up' => $settings->get('theme', 'operator_panel_conference_icon_volume_up', 'fas fa-volume-up'),
|
||||
'gain_down' => $settings->get('theme', 'operator_panel_conference_icon_gain_down', 'fas fa-sort-amount-down'),
|
||||
'gain_up' => $settings->get('theme', 'operator_panel_conference_icon_gain_up', 'fas fa-sort-amount-up'),
|
||||
'kick' => $settings->get('theme', 'operator_panel_conference_icon_kick', 'fas fa-ban'),
|
||||
];
|
||||
$status_show_icon = $settings->get('theme', 'operator_panel_status_show_icon', 'true') === 'true';
|
||||
|
||||
// Optional user status list for the presence dropdown
|
||||
@@ -127,6 +140,7 @@
|
||||
// Theme colors and icons for connection status indicator
|
||||
const status_colors = <?= json_encode($status_colors, JSON_UNESCAPED_SLASHES) ?>;
|
||||
const status_icons = <?= json_encode($status_icons, JSON_UNESCAPED_SLASHES) ?>;
|
||||
const conference_action_icons = <?= json_encode($conference_action_icons, JSON_UNESCAPED_SLASHES) ?>;
|
||||
const status_tooltips = {
|
||||
connected: <?= json_encode($text['status-connected'] ?? 'Connected') ?>,
|
||||
warning: <?= json_encode($text['status-warning'] ?? 'Warning') ?>,
|
||||
|
||||
@@ -27,33 +27,46 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Filters conference events to only include keys relevant to the operator panel,
|
||||
* and drops messages from domains other than the subscriber's.
|
||||
* Filters conference events using conference-aware domain detection.
|
||||
*
|
||||
* Passes only the keys listed in {@see operator_panel_service::conf_event_keys}
|
||||
* and enforces domain isolation using caller_context.
|
||||
*
|
||||
* @author Tim Fry <tim@fusionpbx.com>
|
||||
* Conference maintenance events are not consistent about which field carries
|
||||
* the domain. Accept a message when the domain can be proven from either the
|
||||
* conference identifier or the caller context, and otherwise keep the message
|
||||
* so operator panel conference updates are not dropped.
|
||||
*/
|
||||
class operator_panel_conf_filter implements filter {
|
||||
|
||||
/**
|
||||
* Allowed domain names keyed for fast lookup
|
||||
* Allowed domain names keyed for fast lookup.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $domains;
|
||||
private $domains = [];
|
||||
|
||||
/**
|
||||
* Keys that are permitted through the filter
|
||||
* Keys that are permitted through the filter.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $allowed_keys;
|
||||
private $allowed_keys = [];
|
||||
|
||||
/**
|
||||
* @param array $domain_names Domain names this subscriber is allowed to see.
|
||||
* @param array $allowed_keys Event keys to include in the forwarded payload.
|
||||
* Whether the current event should be dropped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $drop_message = false;
|
||||
|
||||
/**
|
||||
* Whether a matching domain has been positively identified.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $matched_domain = false;
|
||||
|
||||
/**
|
||||
* @param array $domain_names Domain names this subscriber is allowed to see.
|
||||
* @param array $allowed_keys Event keys to include in the forwarded payload.
|
||||
*/
|
||||
public function __construct(array $domain_names, array $allowed_keys) {
|
||||
foreach ($domain_names as $name) {
|
||||
@@ -68,15 +81,69 @@ class operator_panel_conf_filter implements filter {
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool|null true to keep, false to skip this key, null to drop the entire message.
|
||||
* @return bool|null true to keep, false to skip this key, null to drop the entire message.
|
||||
*/
|
||||
public function __invoke($key, $value): ?bool {
|
||||
// Domain guard — drop whole message if context is wrong
|
||||
if ($key === 'caller_context') {
|
||||
return isset($this->domains[$value]) ? true : null;
|
||||
if ($this->drop_message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Key allow-list
|
||||
return isset($this->allowed_keys[$key]) ? true : false;
|
||||
if ($this->is_domain_key($key)) {
|
||||
$matched = $this->match_domain((string)$value, $key === 'caller_context');
|
||||
if ($matched === false) {
|
||||
$this->drop_message = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->matched_domain || !$this->is_domain_key($key)) {
|
||||
return isset($this->allowed_keys[$key]) ? true : false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the key can identify the conference domain.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_domain_key(string $key): bool {
|
||||
return in_array($key, ['caller_context', 'conference_name', 'channel_presence_id'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the event's domain against the allowed set.
|
||||
*
|
||||
* @param string $value
|
||||
* @param bool $is_context
|
||||
*
|
||||
* @return bool|null false when the event belongs to another domain, true when it matches,
|
||||
* or null when the key does not conclusively identify a domain.
|
||||
*/
|
||||
private function match_domain(string $value, bool $is_context): ?bool {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domain = $value;
|
||||
if (!$is_context && strpos($value, '@') !== false) {
|
||||
$parts = explode('@', $value);
|
||||
$domain = end($parts) ?: '';
|
||||
}
|
||||
|
||||
if ($domain === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($this->domains[$domain])) {
|
||||
$this->matched_domain = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,15 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
'recording_state' => 'operator_panel_record',
|
||||
'registrations_state' => 'operator_panel_view',
|
||||
'originate' => 'operator_panel_originate',
|
||||
// Conference member actions
|
||||
'mute' => 'operator_panel_manage',
|
||||
'unmute' => 'operator_panel_manage',
|
||||
'deaf' => 'operator_panel_manage',
|
||||
'undeaf' => 'operator_panel_manage',
|
||||
'energy' => 'operator_panel_manage',
|
||||
'volume_in' => 'operator_panel_manage',
|
||||
'volume_out' => 'operator_panel_manage',
|
||||
'kick' => 'operator_panel_hangup',
|
||||
// User presence status (own status only, enforced inside handler)
|
||||
'user_status' => 'operator_panel_view',
|
||||
// Call-center agent status (supervisor action)
|
||||
@@ -854,6 +863,9 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
$destination = $payload['destination'] ?? '';
|
||||
$domain_name = $payload['domain_name'] ?? '';
|
||||
$context = $payload['context'] ?? ($domain_name !== '' ? $domain_name : 'default');
|
||||
$conference_name = html_entity_decode(urldecode($payload['conference_name'] ?? ''));
|
||||
$member_id = $payload['member_id'] ?? '';
|
||||
$direction = $payload['direction'] ?? '';
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
@@ -993,6 +1005,69 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
}
|
||||
return ['success' => true, 'message' => 'Registrations state updated', 'states' => $states];
|
||||
|
||||
case 'mute':
|
||||
case 'unmute':
|
||||
if (empty($conference_name) || strpos($conference_name, '@') === false) {
|
||||
return ['success' => false, 'message' => 'Invalid conference name'];
|
||||
}
|
||||
if (empty($member_id)) {
|
||||
return ['success' => false, 'message' => 'Member ID required'];
|
||||
}
|
||||
event_socket::api("conference '$conference_name' $action $member_id");
|
||||
if (!empty($uuid)) {
|
||||
event_socket::api("uuid_setvar $uuid hand_raised false");
|
||||
}
|
||||
return ['success' => true, 'message' => 'Conference member updated'];
|
||||
|
||||
case 'deaf':
|
||||
case 'undeaf':
|
||||
if (empty($conference_name) || strpos($conference_name, '@') === false) {
|
||||
return ['success' => false, 'message' => 'Invalid conference name'];
|
||||
}
|
||||
if (empty($member_id)) {
|
||||
return ['success' => false, 'message' => 'Member ID required'];
|
||||
}
|
||||
event_socket::api("conference '$conference_name' $action $member_id");
|
||||
return ['success' => true, 'message' => 'Conference member updated'];
|
||||
|
||||
case 'kick':
|
||||
if (empty($uuid)) {
|
||||
return ['success' => false, 'message' => 'UUID required'];
|
||||
}
|
||||
event_socket::api("uuid_kill $uuid");
|
||||
return ['success' => true, 'message' => 'Conference member removed'];
|
||||
|
||||
case 'energy':
|
||||
if (empty($conference_name) || strpos($conference_name, '@') === false) {
|
||||
return ['success' => false, 'message' => 'Invalid conference name'];
|
||||
}
|
||||
if (empty($member_id) || empty($direction)) {
|
||||
return ['success' => false, 'message' => 'Member ID and direction required'];
|
||||
}
|
||||
$current = trim((string) event_socket::api("conference '$conference_name' energy $member_id"));
|
||||
if (preg_match('/=(\d+)/', $current, $matches)) {
|
||||
$value = (int) $matches[1];
|
||||
$value = ($direction === 'up') ? $value + 100 : $value - 100;
|
||||
event_socket::api("conference '$conference_name' energy $member_id $value");
|
||||
}
|
||||
return ['success' => true, 'message' => 'Energy updated'];
|
||||
|
||||
case 'volume_in':
|
||||
case 'volume_out':
|
||||
if (empty($conference_name) || strpos($conference_name, '@') === false) {
|
||||
return ['success' => false, 'message' => 'Invalid conference name'];
|
||||
}
|
||||
if (empty($member_id) || empty($direction)) {
|
||||
return ['success' => false, 'message' => 'Member ID and direction required'];
|
||||
}
|
||||
$current = trim((string) event_socket::api("conference '$conference_name' $action $member_id"));
|
||||
if (preg_match('/=(-?\d+)/', $current, $matches)) {
|
||||
$value = (int) $matches[1];
|
||||
$value = ($direction === 'up') ? $value + 1 : $value - 1;
|
||||
event_socket::api("conference '$conference_name' $action $member_id $value");
|
||||
}
|
||||
return ['success' => true, 'message' => 'Volume updated'];
|
||||
|
||||
case 'originate':
|
||||
$source = $payload['source'] ?? '';
|
||||
$dest = $payload['destination'] ?? '';
|
||||
@@ -1282,7 +1357,7 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
case 'energy_level':
|
||||
case 'gain_level':
|
||||
case 'volume_level':
|
||||
$this->broadcast_call_event($event_message, $action);
|
||||
$this->broadcast_conference_event($event_message, $action, $conference_name);
|
||||
break;
|
||||
|
||||
case 'add_member':
|
||||
@@ -1445,6 +1520,53 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
websocket_client::send($this->ws_client->socket(), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a conference event with a normalized conference identity.
|
||||
*
|
||||
* @param event_message $event_message
|
||||
* @param string $action
|
||||
* @param string $conference_name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function broadcast_conference_event(event_message $event_message, string $action, string $conference_name): void {
|
||||
$event_data = $event_message->to_array();
|
||||
$event_data = $this->normalize_conference_payload($event_data, $conference_name);
|
||||
|
||||
|
||||
$message = new websocket_message();
|
||||
$message
|
||||
->service_name(self::get_service_name())
|
||||
->topic($action)
|
||||
->payload($event_data)
|
||||
;
|
||||
websocket_client::send($this->ws_client->socket(), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a normalized conference identity and context to an event payload.
|
||||
*
|
||||
* @param array $event_data
|
||||
* @param string $conference_name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function normalize_conference_payload(array $event_data, string $conference_name): array {
|
||||
$event_data['conference_name'] = $conference_name;
|
||||
$event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name);
|
||||
|
||||
if (strpos($conference_name, '@') !== false) {
|
||||
$parts = explode('@', $conference_name, 2);
|
||||
$domain_name = $parts[1] ?? '';
|
||||
if ($domain_name !== '') {
|
||||
$event_data['domain_name'] = $domain_name;
|
||||
$event_data['caller_context'] = $domain_name;
|
||||
}
|
||||
}
|
||||
|
||||
return $event_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an action response back to the requesting client.
|
||||
*
|
||||
@@ -1521,9 +1643,7 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
];
|
||||
}
|
||||
|
||||
$event_data['conference_name'] = $conference_name;
|
||||
$event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name);
|
||||
return $event_data;
|
||||
return $this->normalize_conference_payload($event_data, $conference_name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1546,10 +1666,8 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
: 0;
|
||||
}
|
||||
|
||||
$event_data['member_count'] = $member_count ?? 0;
|
||||
$event_data['conference_name'] = $conference_name;
|
||||
$event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name);
|
||||
return $event_data;
|
||||
$event_data['member_count'] = $member_count ?? 0;
|
||||
return $this->normalize_conference_payload($event_data, $conference_name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1562,10 +1680,8 @@ class operator_panel_service extends base_websocket_system_service implements we
|
||||
*/
|
||||
private function enrich_conference_create_event(event_message $event_message, string $conference_name): array {
|
||||
$event_data = $event_message->to_array();
|
||||
$event_data['conference_name'] = $conference_name;
|
||||
$event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name);
|
||||
$event_data['member_count'] = 0;
|
||||
return $event_data;
|
||||
return $this->normalize_conference_payload($event_data, $conference_name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -496,6 +496,17 @@ function esc(text) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function jsq(value) {
|
||||
return JSON.stringify(value === null || value === undefined ? '' : String(value));
|
||||
}
|
||||
|
||||
function get_conference_action_icon(name, fallback) {
|
||||
if (conference_action_icons && conference_action_icons[name]) {
|
||||
return conference_action_icons[name];
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** Format a Unix microsecond timestamp as elapsed time hh:mm:ss */
|
||||
function format_elapsed(us_timestamp) {
|
||||
if (!us_timestamp || us_timestamp === '0') return '--:--:--';
|
||||
@@ -571,7 +582,7 @@ function on_calls_snapshot_event(event) {
|
||||
* @param {object} event
|
||||
*/
|
||||
function on_call_event(event) {
|
||||
const name = (event.event_name || event.topic || '').toLowerCase();
|
||||
const name = (event.topic || event.event_name || '').toLowerCase();
|
||||
const uuid = get_call_uuid(event);
|
||||
|
||||
if (!uuid) return;
|
||||
@@ -881,7 +892,7 @@ function load_conferences_snapshot() {
|
||||
* @param {object} event
|
||||
*/
|
||||
function on_conference_event(event) {
|
||||
const action = (event.event_name || event.topic || '').toLowerCase();
|
||||
const action = (event.topic || event.event_name || '').toLowerCase();
|
||||
const conference_name = event.conference_name || event.channel_presence_id || '';
|
||||
|
||||
if (!conference_name) return;
|
||||
@@ -1023,32 +1034,57 @@ function render_conferences_tab() {
|
||||
html += ` </tr>\n`;
|
||||
|
||||
members.forEach(m => {
|
||||
const conf_name_js = jsq(name);
|
||||
const member_id_js = jsq(String(m.id || ''));
|
||||
const uuid_js = jsq(m.uuid || '');
|
||||
const mid = esc(String(m.id || ''));
|
||||
const muuid = esc(m.uuid || '');
|
||||
const cid = esc(m.caller_id_name || '');
|
||||
const cid_num = esc(m.caller_id_number || '');
|
||||
const flags = m.flags || {};
|
||||
let flags_html = '';
|
||||
|
||||
if (flags.talking) {
|
||||
flags_html += ` <span class="badge bg-info" title="${esc(text['label-talking'] || 'Talking')}" style="margin:0 3px;">${esc(text['label-talking'] || 'Talking')}</span>`;
|
||||
}
|
||||
if (flags.can_speak === false) {
|
||||
flags_html += ` <span class="badge bg-warning text-dark" title="${esc(text['label-muted'] || 'Muted')}" style="margin:0 3px;">${esc(text['label-muted'] || 'Muted')}</span>`;
|
||||
}
|
||||
if (flags.can_hear === false) {
|
||||
flags_html += ` <span class="badge bg-warning text-dark" title="${esc(text['label-deaf'] || 'Deaf')}" style="margin:0 3px;">${esc(text['label-deaf'] || 'Deaf')}</span>`;
|
||||
}
|
||||
if (flags.has_floor) {
|
||||
flags_html += ` <span class="badge bg-warning text-dark" title="${esc(text['label-floor'] || 'Floor')}" style="margin:0 3px;">${esc(text['label-floor'] || 'Floor')}</span>`;
|
||||
}
|
||||
if (flags.is_moderator) {
|
||||
flags_html += ` <span class="badge bg-primary" title="${esc(text['label-moderator'] || 'Moderator')}" style="margin:0 3px;">${esc(text['label-moderator'] || 'Moderator')}</span>`;
|
||||
}
|
||||
|
||||
html += ` <tr class="list-row" id="conf_member_${muuid}">\n`;
|
||||
html += ` <td>${cid ? `${cid}<br><small>${cid_num}</small>` : cid_num}</td>\n`;
|
||||
html += ` <td>${mid}</td>\n`;
|
||||
html += ` <td style="white-space:nowrap;">`;
|
||||
html += ` <span class="badge bg-${flags.talking ? 'info' : 'secondary'}" title="${esc(text['label-talking'] || 'Talking')}" style="margin:0 3px;">${esc(text['label-talking'] || 'Talking')}</span>`;
|
||||
html += ` <span class="badge bg-${flags.can_speak === false ? 'warning text-dark' : 'success'}" title="${esc(text['label-muted'] || 'Muted')}" style="margin:0 3px;">${esc(text['label-muted'] || 'Muted')}</span>`;
|
||||
html += ` <span class="badge bg-${flags.can_hear === false ? 'warning text-dark' : 'success'}" title="${esc(text['label-deaf'] || 'Deaf')}" style="margin:0 3px;">${esc(text['label-deaf'] || 'Deaf')}</span>`;
|
||||
if (flags.has_floor) html += ` <span class="badge bg-warning text-dark" title="${esc(text['label-floor'] || 'Floor')}" style="margin:0 3px;">${esc(text['label-floor'] || 'Floor')}</span>`;
|
||||
if (flags.is_moderator) html += ` <span class="badge bg-primary" title="${esc(text['label-moderator'] || 'Moderator')}" style="margin:0 3px;">${esc(text['label-moderator'] || 'Moderator')}</span>`;
|
||||
html += flags_html;
|
||||
html += ` </td>\n`;
|
||||
|
||||
if (permissions.operator_panel_hangup || permissions.operator_panel_manage) {
|
||||
html += ` <td class="right">\n`;
|
||||
if (permissions.operator_panel_hangup) {
|
||||
html += ` <a class="btn-action" href="javascript:void(0)" title="${esc(text['button-hangup'] || 'Hangup')}" onclick="action_hangup('${muuid}')">`
|
||||
+ `<img class="op-ext-action-icon" src="../operator_panel/resources/images/kill.png" alt="${esc(text['button-hangup'] || 'Hangup')}"></a> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['button-hangup'] || 'Hangup')}" onclick='action_conference_member("kick", ${conf_name_js}, ${member_id_js}, ${uuid_js})'><span class="${esc(get_conference_action_icon('kick', 'fas fa-ban'))}" aria-hidden="true"></span></button> `;
|
||||
}
|
||||
if (permissions.operator_panel_manage) {
|
||||
html += ` <a class="btn-action" href="javascript:void(0)" title="${esc(text['button-transfer'] || 'Transfer')}" onclick="open_transfer_modal('${muuid}')">`
|
||||
+ `<img class="op-ext-action-icon" src="../operator_panel/resources/images/keypad_transfer.png" alt="${esc(text['button-transfer'] || 'Transfer')}"></a> `;
|
||||
const mute_action = flags.can_speak === false ? 'unmute' : 'mute';
|
||||
const mute_label = flags.can_speak === false ? (text['button-unmute'] || 'Unmute') : (text['button-mute'] || 'Mute');
|
||||
const deaf_action = flags.can_hear === false ? 'undeaf' : 'deaf';
|
||||
const deaf_label = flags.can_hear === false ? (text['button-undeaf'] || 'Undeaf') : (text['button-deaf'] || 'Deaf');
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(mute_label)}" onclick='action_conference_member(${jsq(mute_action)}, ${conf_name_js}, ${member_id_js}, ${uuid_js})'><span class="${esc(get_conference_action_icon(mute_action, mute_action === 'mute' ? 'fas fa-microphone-slash' : 'fas fa-microphone'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(deaf_label)}" onclick='action_conference_member(${jsq(deaf_action)}, ${conf_name_js}, ${member_id_js}, ${uuid_js})'><span class="${esc(get_conference_action_icon(deaf_action, deaf_action === 'deaf' ? 'fas fa-deaf' : 'fas fa-headphones'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['label-energy'] || 'Energy')}` + ` +" onclick='action_conference_member("energy", ${conf_name_js}, ${member_id_js}, ${uuid_js}, "up")'><span class="${esc(get_conference_action_icon('energy_up', 'fas fa-plus'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['label-energy'] || 'Energy')}` + ` -" onclick='action_conference_member("energy", ${conf_name_js}, ${member_id_js}, ${uuid_js}, "down")'><span class="${esc(get_conference_action_icon('energy_down', 'fas fa-minus'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['label-volume'] || 'Volume')}` + ` -" onclick='action_conference_member("volume_in", ${conf_name_js}, ${member_id_js}, ${uuid_js}, "down")'><span class="${esc(get_conference_action_icon('volume_down', 'fas fa-volume-down'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['label-volume'] || 'Volume')}` + ` +" onclick='action_conference_member("volume_in", ${conf_name_js}, ${member_id_js}, ${uuid_js}, "up")'><span class="${esc(get_conference_action_icon('volume_up', 'fas fa-volume-up'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['label-gain'] || 'Gain')}` + ` -" onclick='action_conference_member("volume_out", ${conf_name_js}, ${member_id_js}, ${uuid_js}, "down")'><span class="${esc(get_conference_action_icon('gain_down', 'fas fa-sort-amount-down'))}" aria-hidden="true"></span></button> `;
|
||||
html += ` <button type="button" class="btn btn-default btn-xs" title="${esc(text['label-gain'] || 'Gain')}` + ` +" onclick='action_conference_member("volume_out", ${conf_name_js}, ${member_id_js}, ${uuid_js}, "up")'><span class="${esc(get_conference_action_icon('gain_up', 'fas fa-sort-amount-up'))}" aria-hidden="true"></span></button> `;
|
||||
}
|
||||
html += ` </td>\n`;
|
||||
}
|
||||
@@ -1351,6 +1387,22 @@ function action_agent_status(agent_name, status) {
|
||||
send_action('agent_status', { agent_name, status }).catch(console.error);
|
||||
}
|
||||
|
||||
function action_conference_member(action, conference_name, member_id, uuid, direction) {
|
||||
const payload = { conference_name, member_id, uuid };
|
||||
if (direction) {
|
||||
payload.direction = direction;
|
||||
}
|
||||
send_action(action, payload)
|
||||
.then(() => {
|
||||
if (action === 'kick') {
|
||||
show_toast(text['button-hangup'] || 'Member removed', 'success');
|
||||
return;
|
||||
}
|
||||
show_toast(text['label-actions'] || 'Action executed', 'success');
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a brief Bootstrap toast notification.
|
||||
* @param {string} message
|
||||
|
||||
Reference in New Issue
Block a user