3 Commits

Author SHA1 Message Date
a3345ed854 Use version placeholder for auto-deployment
All checks were successful
Create Release / build (push) Successful in 3s
Replace hardcoded version with {auto_update_value_on_deploy} placeholder that gets replaced during the Gitea workflow build process.

Changes:
- Updated Version comment to use placeholder
- Updated TWP_VERSION constant to use placeholder
- Modified release workflow to replace both instances of the placeholder

This matches the pattern used in the fourthwall plugin and ensures the version is automatically set during the release build process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 16:32:42 -08:00
384ad5e265 Add Gitea CI/CD workflows for automated releases
All checks were successful
Create Release / build (push) Successful in 2s
Add automated release creation and versioning workflows similar to the fourthwall plugin.

Features:
- Automatically creates a release on every push to main branch
- Generates release notes from commit messages since last tag
- Updates plugin version using timestamp-based versioning (YYYY.MM.DD-HHMM)
- Creates and uploads plugin zip file to release
- Supports manual release creation/editing to update version

Workflows:
- .gitea/workflows/release.yml - Triggers on push to main
- .gitea/workflows/update-version.yml - Triggers on release creation/edit

The release zip will be automatically picked up by the Gitea auto-updater for WordPress.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 16:13:42 -08:00
86dd477d4f Add mobile app infrastructure and Gitea auto-update support
This commit adds comprehensive mobile app support to enable a native Android app that won't timeout or sleep when the screen goes dark.

New Features:
- JWT-based authentication system (no WordPress session dependency)
- REST API endpoints for mobile app (agent status, queue management, call control)
- Server-Sent Events (SSE) for real-time updates to mobile app
- Firebase Cloud Messaging (FCM) integration for push notifications
- Gitea-based automatic plugin updates
- Mobile app admin settings page

New Files:
- includes/class-twp-mobile-auth.php - JWT authentication with login/refresh/logout
- includes/class-twp-mobile-api.php - REST API endpoints under /twilio-mobile/v1
- includes/class-twp-mobile-sse.php - Real-time event streaming
- includes/class-twp-fcm.php - Push notification handling
- includes/class-twp-auto-updater.php - Gitea-based auto-updates
- admin/mobile-app-settings.php - Admin configuration page

Modified Files:
- includes/class-twp-activator.php - Added twp_mobile_sessions table
- includes/class-twp-core.php - Load and initialize mobile classes
- admin/class-twp-admin.php - Added Mobile App menu item and settings page

Database Changes:
- New table: twp_mobile_sessions (stores JWT refresh tokens and FCM tokens)

API Endpoints:
- POST /twilio-mobile/v1/auth/login
- POST /twilio-mobile/v1/auth/refresh
- POST /twilio-mobile/v1/auth/logout
- GET/POST /twilio-mobile/v1/agent/status
- GET /twilio-mobile/v1/queues/state
- POST /twilio-mobile/v1/calls/{call_sid}/accept
- GET /twilio-mobile/v1/stream/events (SSE)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:43:14 -08:00
12 changed files with 2505 additions and 8 deletions

View File

@@ -0,0 +1,82 @@
name: Create Release
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
fetch-depth: 0 # Important: Fetch all history for commit messages
- name: Get version
id: get_version
run: |
if [ "${{ github.ref_type }}" = "tag" ]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
echo "version=$(date +'%Y.%m.%d-%H%M')" >> $GITHUB_OUTPUT
fi
- name: Generate release notes
id: release_notes
run: |
# Find the most recent tag
LATEST_TAG=$(git describe --tags --abbrev=0 --always 2>/dev/null || echo "none")
if [ "$LATEST_TAG" = "none" ]; then
# If no previous tag exists, get all commits
COMMITS=$(git log --pretty=format:"* %s (%h)" --no-merges)
else
# Get commits since the last tag
COMMITS=$(git log --pretty=format:"* %s (%h)" ${LATEST_TAG}..HEAD --no-merges)
fi
# Create release notes with header (without encoding newlines)
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "## What's New in ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Update plugin version
run: |
# Replace version placeholder with actual version
sed -i "s/Version: {auto_update_value_on_deploy}/Version: ${{ steps.get_version.outputs.version }}/" twilio-wp-plugin.php
sed -i "s/TWP_VERSION', '{auto_update_value_on_deploy}/TWP_VERSION', '${{ steps.get_version.outputs.version }}/" twilio-wp-plugin.php
# Verify the changes were made
grep "Version:" twilio-wp-plugin.php
grep "TWP_VERSION" twilio-wp-plugin.php
- name: Create ZIP archive
run: |
# Create a temp directory with the correct plugin folder name
mkdir -p /tmp/twilio-wp-plugin
# Copy files to the temp directory (excluding git and other unnecessary files)
cp -r * /tmp/twilio-wp-plugin/ 2>/dev/null || true
# Exclude .git and .gitea directories
rm -rf /tmp/twilio-wp-plugin/.git /tmp/twilio-wp-plugin/.gitea 2>/dev/null || true
# Create the ZIP file with the proper structure
cd /tmp
zip -r $GITHUB_WORKSPACE/twilio-wp-plugin.zip twilio-wp-plugin
- name: Create Release
uses: softprops/action-gh-release@v2
with:
token: "${{ secrets.REPO_TOKEN }}"
title: "Twilio WP Plugin Release ${{ steps.get_version.outputs.version }}"
tag_name: ${{ steps.get_version.outputs.version }}
body: "${{ steps.release_notes.outputs.notes }}"
files: |
twilio-wp-plugin.zip

View File

@@ -0,0 +1,51 @@
name: Update Plugin Version
on:
release:
types: [created, edited]
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get release tag
id: get_tag
run: echo "TAG=${GITEA_REF#refs/tags/}" >> $GITEA_ENV
- name: Update version in plugin file
run: |
# Replace version in main plugin file
sed -i "s/Version: .*/Version: ${{ env.TAG }}/" twilio-wp-plugin.php
# Verify change
grep "Version:" twilio-wp-plugin.php
- name: Commit changes
run: |
git config --local user.email "action@gitea.com"
git config --local user.name "Gitea Action"
git add twilio-wp-plugin.php
git commit -m "Update version to ${{ env.TAG }}" || echo "No changes to commit"
git push || echo "Nothing to push"
- name: Create plugin zip
run: |
mkdir -p /tmp/twilio-wp-plugin
rsync -av --exclude=".git" --exclude=".gitea" --exclude="build" . /tmp/twilio-wp-plugin/
cd /tmp
zip -r $GITEA_WORK_DIR/twilio-wp-plugin.zip twilio-wp-plugin
- name: Upload zip to release
uses: actions/upload-release-asset@v1
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
with:
upload_url: ${{ gitea.event.release.upload_url }}
asset_path: twilio-wp-plugin.zip
asset_name: twilio-wp-plugin.zip
asset_content_type: application/zip

View File

@@ -119,7 +119,16 @@ class TWP_Admin {
'twilio-wp-settings', 'twilio-wp-settings',
array($this, 'display_plugin_settings') array($this, 'display_plugin_settings')
); );
add_submenu_page(
'twilio-wp-plugin',
'Mobile App',
'Mobile App',
'manage_options',
'twilio-wp-mobile-app',
array($this, 'display_mobile_app_settings')
);
add_submenu_page( add_submenu_page(
'twilio-wp-plugin', 'twilio-wp-plugin',
'Phone Schedules', 'Phone Schedules',
@@ -10049,5 +10058,12 @@ class TWP_Admin {
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage()); wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
} }
} }
/**
* Display mobile app settings page
*/
public function display_mobile_app_settings() {
require_once TWP_PLUGIN_DIR . 'admin/mobile-app-settings.php';
}
} }

View File

@@ -0,0 +1,353 @@
<?php
/**
* Mobile App Settings Page
*/
// Prevent direct access
if (!defined('WPINC')) {
die;
}
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.'));
}
// Handle manual update check
if (isset($_POST['twp_check_updates']) && check_admin_referer('twp_mobile_settings')) {
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
$updater = new TWP_Auto_Updater();
$update_result = $updater->manual_check_for_updates();
}
// Handle test notification
if (isset($_POST['twp_test_notification']) && check_admin_referer('twp_mobile_settings')) {
require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
$fcm = new TWP_FCM();
$test_user_id = get_current_user_id();
$notification_sent = $fcm->send_test_notification($test_user_id);
if ($notification_sent) {
$notification_result = array('success' => true, 'message' => 'Test notification sent successfully!');
} else {
$notification_result = array('success' => false, 'message' => 'Failed to send test notification. Check FCM configuration.');
}
}
// Save settings
if (isset($_POST['twp_save_mobile_settings']) && check_admin_referer('twp_mobile_settings')) {
update_option('twp_fcm_server_key', sanitize_text_field($_POST['twp_fcm_server_key']));
update_option('twp_auto_update_enabled', isset($_POST['twp_auto_update_enabled']) ? '1' : '0');
update_option('twp_gitea_repo', sanitize_text_field($_POST['twp_gitea_repo']));
update_option('twp_gitea_token', sanitize_text_field($_POST['twp_gitea_token']));
$settings_saved = true;
}
// Get current settings
$fcm_server_key = get_option('twp_fcm_server_key', '');
$auto_update_enabled = get_option('twp_auto_update_enabled', '1') === '1';
$gitea_repo = get_option('twp_gitea_repo', 'wp-plugins/twilio-wp-plugin');
$gitea_token = get_option('twp_gitea_token', '');
// Get update status
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
$updater = new TWP_Auto_Updater();
$update_status = $updater->get_update_status();
// Get mobile app statistics
global $wpdb;
$sessions_table = $wpdb->prefix . 'twp_mobile_sessions';
$active_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table WHERE is_active = 1 AND expires_at > NOW()");
$total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<?php if (isset($settings_saved)): ?>
<div class="notice notice-success is-dismissible">
<p><strong>Settings saved successfully!</strong></p>
</div>
<?php endif; ?>
<?php if (isset($update_result)): ?>
<div class="notice notice-<?php echo $update_result['update_available'] ? 'warning' : 'success'; ?> is-dismissible">
<p><strong><?php echo esc_html($update_result['message']); ?></strong></p>
</div>
<?php endif; ?>
<?php if (isset($notification_result)): ?>
<div class="notice notice-<?php echo $notification_result['success'] ? 'success' : 'error'; ?> is-dismissible">
<p><strong><?php echo esc_html($notification_result['message']); ?></strong></p>
</div>
<?php endif; ?>
<div class="twp-mobile-settings">
<!-- Mobile App Overview -->
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
<h2>Mobile App Overview</h2>
<table class="widefat">
<tbody>
<tr>
<td><strong>API Endpoint:</strong></td>
<td><code><?php echo esc_html(site_url('/wp-json/twilio-mobile/v1')); ?></code></td>
</tr>
<tr>
<td><strong>Active Sessions:</strong></td>
<td><?php echo esc_html($active_sessions); ?> active / <?php echo esc_html($total_sessions); ?> total</td>
</tr>
<tr>
<td><strong>Plugin Version:</strong></td>
<td><?php echo esc_html(TWP_VERSION); ?></td>
</tr>
</tbody>
</table>
</div>
<!-- Mobile App Settings Form -->
<form method="post" action="">
<?php wp_nonce_field('twp_mobile_settings'); ?>
<!-- FCM Configuration -->
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
<h2>Firebase Cloud Messaging (FCM)</h2>
<p>Configure FCM to enable push notifications for the mobile app.</p>
<table class="form-table">
<tr>
<th scope="row">
<label for="twp_fcm_server_key">FCM Server Key</label>
</th>
<td>
<input type="text"
id="twp_fcm_server_key"
name="twp_fcm_server_key"
value="<?php echo esc_attr($fcm_server_key); ?>"
class="regular-text"
placeholder="AAAA...">
<p class="description">
Get your server key from Firebase Console > Project Settings > Cloud Messaging > Server Key
</p>
</td>
</tr>
</table>
<?php if (!empty($fcm_server_key)): ?>
<p>
<button type="submit" name="twp_test_notification" class="button">
Send Test Notification
</button>
<span class="description">Send a test notification to your devices</span>
</p>
<?php endif; ?>
</div>
<!-- Auto-Update Settings -->
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
<h2>Automatic Updates</h2>
<table class="form-table">
<tr>
<th scope="row">Current Version</th>
<td>
<strong><?php echo esc_html($update_status['current_version']); ?></strong>
<?php if ($update_status['update_available']): ?>
<span style="color: #d63638; margin-left: 10px;">
⚠ Update available: <?php echo esc_html($update_status['latest_version']); ?>
</span>
<?php else: ?>
<span style="color: #00a32a; margin-left: 10px;">✓ Up to date</span>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row">
<label for="twp_auto_update_enabled">Enable Auto-Updates</label>
</th>
<td>
<label>
<input type="checkbox"
id="twp_auto_update_enabled"
name="twp_auto_update_enabled"
value="1"
<?php checked($auto_update_enabled); ?>>
Automatically check for updates every 12 hours
</label>
</td>
</tr>
<tr>
<th scope="row">
<label for="twp_gitea_repo">Gitea Repository</label>
</th>
<td>
<input type="text"
id="twp_gitea_repo"
name="twp_gitea_repo"
value="<?php echo esc_attr($gitea_repo); ?>"
class="regular-text"
placeholder="org/repo-name">
<p class="description">
Format: organization/repository (e.g., wp-plugins/twilio-wp-plugin)
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="twp_gitea_token">Gitea Access Token</label>
</th>
<td>
<input type="password"
id="twp_gitea_token"
name="twp_gitea_token"
value="<?php echo esc_attr($gitea_token); ?>"
class="regular-text"
placeholder="">
<p class="description">
Optional. Required only for private repositories. Create token at:
<a href="https://repo.anhonesthost.net/user/settings/applications" target="_blank">Gitea Settings > Applications</a>
</p>
</td>
</tr>
<tr>
<th scope="row">Last Update Check</th>
<td>
<?php
$last_check = $update_status['last_check'];
if ($last_check > 0) {
echo esc_html(human_time_diff($last_check, current_time('timestamp')) . ' ago');
} else {
echo 'Never';
}
?>
<button type="submit" name="twp_check_updates" class="button" style="margin-left: 15px;">
Check Now
</button>
</td>
</tr>
</table>
</div>
<!-- API Documentation -->
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
<h2>API Endpoints</h2>
<p>Available REST API endpoints for mobile app development:</p>
<table class="widefat striped">
<thead>
<tr>
<th>Endpoint</th>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/twilio-mobile/v1/auth/login</code></td>
<td>POST</td>
<td>Authenticate and get JWT tokens</td>
</tr>
<tr>
<td><code>/twilio-mobile/v1/auth/refresh</code></td>
<td>POST</td>
<td>Refresh access token</td>
</tr>
<tr>
<td><code>/twilio-mobile/v1/agent/status</code></td>
<td>GET/POST</td>
<td>Get or update agent status</td>
</tr>
<tr>
<td><code>/twilio-mobile/v1/queues/state</code></td>
<td>GET</td>
<td>Get all queue states</td>
</tr>
<tr>
<td><code>/twilio-mobile/v1/calls/{call_sid}/accept</code></td>
<td>POST</td>
<td>Accept a queued call</td>
</tr>
<tr>
<td><code>/twilio-mobile/v1/stream/events</code></td>
<td>GET</td>
<td>Server-Sent Events stream for real-time updates</td>
</tr>
</tbody>
</table>
<p style="margin-top: 15px;">
<strong>Authentication:</strong> All endpoints (except login/refresh) require
<code>Authorization: Bearer &lt;access_token&gt;</code> header.
</p>
</div>
<p class="submit">
<button type="submit" name="twp_save_mobile_settings" class="button button-primary">
Save Settings
</button>
</p>
</form>
<!-- Active Sessions -->
<?php if ($active_sessions > 0): ?>
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
<h2>Active Mobile Sessions</h2>
<?php
$sessions = $wpdb->get_results("
SELECT s.user_id, s.device_info, s.logged_in_at, s.last_used, u.user_login, u.display_name
FROM $sessions_table s
JOIN {$wpdb->users} u ON s.user_id = u.ID
WHERE s.is_active = 1 AND s.expires_at > NOW()
ORDER BY s.last_used DESC
LIMIT 20
");
?>
<table class="widefat striped">
<thead>
<tr>
<th>User</th>
<th>Device</th>
<th>Last Activity</th>
</tr>
</thead>
<tbody>
<?php foreach ($sessions as $session): ?>
<tr>
<td><?php echo esc_html($session->display_name ?: $session->user_login); ?></td>
<td><?php echo esc_html($session->device_info ?: 'Unknown device'); ?></td>
<td><?php echo esc_html(human_time_diff(strtotime($session->last_used), current_time('timestamp')) . ' ago'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<style>
.twp-mobile-settings .card {
padding: 20px;
background: #fff;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.twp-mobile-settings .card h2 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f1;
}
.twp-mobile-settings code {
background: #f0f0f1;
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}
.twp-mobile-settings table.widefat td code {
background: #f6f7f7;
}
</style>

View File

@@ -52,7 +52,8 @@ class TWP_Activator {
'twp_callbacks', 'twp_callbacks',
'twp_call_recordings', 'twp_call_recordings',
'twp_user_extensions', 'twp_user_extensions',
'twp_queue_assignments' 'twp_queue_assignments',
'twp_mobile_sessions'
); );
$missing_tables = array(); $missing_tables = array();
@@ -361,7 +362,25 @@ class TWP_Activator {
KEY agent_id (agent_id), KEY agent_id (agent_id),
KEY started_at (started_at) KEY started_at (started_at)
) $charset_collate;"; ) $charset_collate;";
// Mobile sessions table
$table_mobile_sessions = $wpdb->prefix . 'twp_mobile_sessions';
$sql_mobile_sessions = "CREATE TABLE $table_mobile_sessions (
id int(11) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
refresh_token varchar(500) NOT NULL,
fcm_token text,
device_info text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
expires_at datetime NOT NULL,
last_used datetime DEFAULT CURRENT_TIMESTAMP,
is_active tinyint(1) DEFAULT 1,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY is_active (is_active),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_schedules); dbDelta($sql_schedules);
dbDelta($sql_queues); dbDelta($sql_queues);
dbDelta($sql_queued_calls); dbDelta($sql_queued_calls);
@@ -377,7 +396,8 @@ class TWP_Activator {
dbDelta($sql_recordings); dbDelta($sql_recordings);
dbDelta($sql_user_extensions); dbDelta($sql_user_extensions);
dbDelta($sql_queue_assignments); dbDelta($sql_queue_assignments);
dbDelta($sql_mobile_sessions);
// Add missing columns for existing installations // Add missing columns for existing installations
self::add_missing_columns(); self::add_missing_columns();
} }

View File

@@ -0,0 +1,291 @@
<?php
/**
* Automatic Plugin Updater
*
* Checks for updates from Gitea repository and installs them automatically
*/
class TWP_Auto_Updater {
private $plugin_slug = 'twilio-wp-plugin';
private $plugin_basename;
private $gitea_repo = 'wp-plugins/twilio-wp-plugin';
private $gitea_api_url;
private $current_version;
private $gitea_base_url = 'https://repo.anhonesthost.net';
/**
* Constructor
*/
public function __construct() {
$this->plugin_basename = plugin_basename(dirname(dirname(__FILE__)) . '/twilio-wp-plugin.php');
$this->current_version = defined('TWP_VERSION') ? TWP_VERSION : '0.0.0';
$this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
}
/**
* Initialize updater hooks
*/
public function init() {
// Hook into WordPress update checks
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_update'));
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
// Add settings page for manual check
add_action('admin_init', array($this, 'register_settings'));
// Add update check to admin notices
if (get_option('twp_auto_update_enabled', '1') === '1') {
add_action('admin_init', array($this, 'maybe_auto_check_updates'));
}
}
/**
* Register auto-update settings
*/
public function register_settings() {
register_setting('twp_settings', 'twp_auto_update_enabled');
register_setting('twp_settings', 'twp_gitea_repo');
register_setting('twp_settings', 'twp_gitea_token'); // Optional for private repos
}
/**
* Check for updates periodically
*/
public function maybe_auto_check_updates() {
$last_check = get_option('twp_last_update_check', 0);
$check_interval = 12 * HOUR_IN_SECONDS; // Check every 12 hours
if (time() - $last_check > $check_interval) {
update_option('twp_last_update_check', time());
// Force WordPress to check for updates
wp_clean_plugins_cache();
}
}
/**
* Check for plugin updates
*/
public function check_for_update($transient) {
if (empty($transient->checked)) {
return $transient;
}
// Get Gitea repo from settings if available
$custom_repo = get_option('twp_gitea_repo', '');
if (!empty($custom_repo)) {
$this->gitea_repo = $custom_repo;
$this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
}
$update_info = $this->get_latest_release();
if ($update_info && version_compare($this->current_version, $update_info->version, '<')) {
$plugin_data = array(
'id' => 'twilio-wp-plugin',
'slug' => $this->plugin_slug,
'plugin' => $this->plugin_basename,
'new_version' => $update_info->version,
'url' => $update_info->homepage,
'package' => $update_info->download_url,
'tested' => '6.8',
'requires' => '5.8',
'requires_php' => '7.4',
'icons' => array(),
'banners' => array(),
'compatibility' => new stdClass(),
);
$transient->response[$this->plugin_basename] = (object) $plugin_data;
error_log("TWP Auto-Updater: New version {$update_info->version} available (current: {$this->current_version})");
} else {
error_log("TWP Auto-Updater: No updates available (current: {$this->current_version})");
}
return $transient;
}
/**
* Get plugin information for update screen
*/
public function plugin_info($false, $action, $args) {
if ($action !== 'plugin_information') {
return $false;
}
if (!isset($args->slug) || $args->slug !== $this->plugin_slug) {
return $false;
}
$update_info = $this->get_latest_release();
if (!$update_info) {
return $false;
}
$plugin_info = new stdClass();
$plugin_info->name = 'Twilio WP Plugin';
$plugin_info->slug = $this->plugin_slug;
$plugin_info->version = $update_info->version;
$plugin_info->author = '<a href="https://cybercove.io/">Joshua Knapp</a>';
$plugin_info->homepage = $update_info->homepage;
$plugin_info->download_link = $update_info->download_url;
$plugin_info->requires = '5.8';
$plugin_info->tested = '6.8';
$plugin_info->requires_php = '7.4';
$plugin_info->last_updated = $update_info->release_date;
$plugin_info->downloaded = 10;
$plugin_info->sections = array(
'description' => '<p>Twilio WordPress Plugin for call management and mobile app support.</p>',
'changelog' => '<pre>' . esc_html($update_info->changelog) . '</pre>',
'installation' => '<p>Upload the plugin to your WordPress site and activate it.</p>'
);
return $plugin_info;
}
/**
* Get latest release information from Gitea
*/
private function get_latest_release() {
// Check cache first (1 hour)
$cache_key = 'twp_latest_release';
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
// Use cURL for Gitea API
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->gitea_api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress/Twilio-WP-Plugin-Updater');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
// Add Gitea token if configured (for private repos)
$gitea_token = get_option('twp_gitea_token', '');
if (!empty($gitea_token)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Accept: application/json',
'Authorization: token ' . $gitea_token
));
}
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$response || $http_code !== 200) {
error_log("TWP Auto-Updater: Gitea API returned status $http_code");
return false;
}
$release = json_decode($response);
if (!$release || !isset($release->tag_name)) {
error_log('TWP Auto-Updater: Invalid release data from Gitea');
return false;
}
// Parse release information
$version = ltrim($release->tag_name, 'v'); // Remove 'v' prefix if present
$download_url = null;
// Find the zip asset
if (isset($release->assets) && is_array($release->assets)) {
foreach ($release->assets as $asset) {
if (strpos($asset->name, '.zip') !== false) {
$download_url = $asset->browser_download_url;
break;
}
}
}
// Fallback to zipball if no asset found
if (!$download_url) {
$download_url = $release->zipball_url;
}
// Format changelog
$changelog = !empty($release->body) ? $release->body : 'No changelog provided for this release.';
// Handle empty changelog
if (empty(trim($changelog))) {
$changelog = "Version " . $version . "\n\n" .
"Released on " . date('F j, Y', strtotime($release->published_at)) . "\n\n" .
"* Updated plugin files";
}
$update_info = (object) array(
'version' => $version,
'download_url' => $download_url,
'homepage' => $this->gitea_base_url . '/' . $this->gitea_repo,
'release_date' => $release->published_at,
'description' => $changelog,
'changelog' => $changelog
);
// Cache for 1 hour
set_transient($cache_key, $update_info, HOUR_IN_SECONDS);
return $update_info;
}
/**
* Manual update check (for admin page)
*/
public function manual_check_for_updates() {
// Clear cache
delete_transient('twp_latest_release');
update_option('twp_last_update_check', 0);
// Force WordPress to check
wp_clean_plugins_cache();
delete_site_transient('update_plugins');
$update_info = $this->get_latest_release();
if (!$update_info) {
return array(
'success' => false,
'message' => 'Failed to check for updates. Please check your internet connection and Gitea repository settings.'
);
}
if (version_compare($this->current_version, $update_info->version, '<')) {
return array(
'success' => true,
'update_available' => true,
'current_version' => $this->current_version,
'latest_version' => $update_info->version,
'message' => "Update available: Version {$update_info->version}. Go to Plugins page to update."
);
} else {
return array(
'success' => true,
'update_available' => false,
'current_version' => $this->current_version,
'message' => 'You are running the latest version.'
);
}
}
/**
* Get current update status
*/
public function get_update_status() {
$update_info = $this->get_latest_release();
return array(
'current_version' => $this->current_version,
'latest_version' => $update_info ? $update_info->version : 'Unknown',
'update_available' => $update_info && version_compare($this->current_version, $update_info->version, '<'),
'last_check' => get_option('twp_last_update_check', 0),
'auto_update_enabled' => get_option('twp_auto_update_enabled', '1') === '1'
);
}
}

View File

@@ -33,7 +33,14 @@ class TWP_Core {
// API classes // API classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php'; require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php'; require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
// Mobile app classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-auth.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-api.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-sse.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
// Feature classes // Feature classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php'; require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php'; require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
@@ -318,6 +325,20 @@ class TWP_Core {
// Initialize webhooks // Initialize webhooks
$webhooks = new TWP_Webhooks(); $webhooks = new TWP_Webhooks();
$webhooks->register_endpoints(); $webhooks->register_endpoints();
// Initialize mobile app endpoints
$mobile_auth = new TWP_Mobile_Auth();
$mobile_auth->register_endpoints();
$mobile_api = new TWP_Mobile_API();
$mobile_api->register_endpoints();
$mobile_sse = new TWP_Mobile_SSE();
$mobile_sse->register_endpoints();
// Initialize auto-updater
$updater = new TWP_Auto_Updater();
$updater->init();
// Add custom cron schedules // Add custom cron schedules
add_filter('cron_schedules', function($schedules) { add_filter('cron_schedules', function($schedules) {

214
includes/class-twp-fcm.php Normal file
View File

@@ -0,0 +1,214 @@
<?php
/**
* Firebase Cloud Messaging (FCM) Integration
*
* Handles push notifications to mobile devices via FCM
*/
class TWP_FCM {
private $server_key;
private $fcm_url = 'https://fcm.googleapis.com/fcm/send';
/**
* Constructor
*/
public function __construct() {
$this->server_key = get_option('twp_fcm_server_key', '');
}
/**
* Send push notification to user's devices
*/
public function send_notification($user_id, $title, $body, $data = array()) {
if (empty($this->server_key)) {
error_log('TWP FCM: Server key not configured');
return false;
}
// Get user's FCM tokens
$tokens = $this->get_user_tokens($user_id);
if (empty($tokens)) {
error_log("TWP FCM: No tokens found for user $user_id");
return false;
}
$success_count = 0;
$failed_tokens = array();
foreach ($tokens as $token) {
$result = $this->send_to_token($token, $title, $body, $data);
if ($result['success']) {
$success_count++;
} else {
$failed_tokens[] = $token;
// If token is invalid, remove it from database
if ($result['error'] === 'invalid_token') {
$this->remove_invalid_token($token);
}
}
}
error_log("TWP FCM: Sent notification to $success_count/" . count($tokens) . " devices for user $user_id");
return $success_count > 0;
}
/**
* Send notification to specific token
*/
private function send_to_token($token, $title, $body, $data = array()) {
$notification = array(
'title' => $title,
'body' => $body,
'sound' => 'default',
'priority' => 'high',
'click_action' => 'FLUTTER_NOTIFICATION_CLICK'
);
$payload = array(
'to' => $token,
'notification' => $notification,
'data' => array_merge($data, array(
'title' => $title,
'body' => $body,
'timestamp' => time()
)),
'priority' => 'high'
);
$headers = array(
'Authorization: key=' . $this->server_key,
'Content-Type: application/json'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->fcm_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
error_log("TWP FCM: Failed to send notification. HTTP $http_code: $response");
// Check if token is invalid
$response_data = json_decode($response, true);
if (isset($response_data['results'][0]['error']) &&
in_array($response_data['results'][0]['error'], array('InvalidRegistration', 'NotRegistered'))) {
return array('success' => false, 'error' => 'invalid_token');
}
return array('success' => false, 'error' => 'http_error');
}
return array('success' => true);
}
/**
* Get all active FCM tokens for a user
*/
private function get_user_tokens($user_id) {
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
return $wpdb->get_col($wpdb->prepare(
"SELECT fcm_token FROM $table
WHERE user_id = %d
AND is_active = 1
AND fcm_token IS NOT NULL
AND fcm_token != ''
AND expires_at > NOW()",
$user_id
));
}
/**
* Remove invalid FCM token from database
*/
private function remove_invalid_token($token) {
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
$wpdb->update(
$table,
array('fcm_token' => null),
array('fcm_token' => $token),
array('%s'),
array('%s')
);
error_log("TWP FCM: Removed invalid token from database");
}
/**
* Send incoming call notification
*/
public function notify_incoming_call($user_id, $from_number, $queue_name, $call_sid) {
$title = 'Incoming Call';
$body = "Call from $from_number in $queue_name queue";
$data = array(
'type' => 'incoming_call',
'call_sid' => $call_sid,
'from_number' => $from_number,
'queue_name' => $queue_name
);
return $this->send_notification($user_id, $title, $body, $data);
}
/**
* Send queue timeout notification
*/
public function notify_queue_timeout($user_id, $queue_name, $waiting_count) {
$title = 'Queue Alert';
$body = "$queue_name has $waiting_count waiting call" . ($waiting_count > 1 ? 's' : '');
$data = array(
'type' => 'queue_timeout',
'queue_name' => $queue_name,
'waiting_count' => $waiting_count
);
return $this->send_notification($user_id, $title, $body, $data);
}
/**
* Send agent status change notification
*/
public function notify_status_change($user_id, $old_status, $new_status) {
$title = 'Status Changed';
$body = "Your status changed from $old_status to $new_status";
$data = array(
'type' => 'status_change',
'old_status' => $old_status,
'new_status' => $new_status
);
return $this->send_notification($user_id, $title, $body, $data);
}
/**
* Test notification (for settings page)
*/
public function send_test_notification($user_id) {
$title = 'Test Notification';
$body = 'This is a test notification from Twilio WordPress Plugin';
$data = array(
'type' => 'test',
'test' => true
);
return $this->send_notification($user_id, $title, $body, $data);
}
}

View File

@@ -0,0 +1,684 @@
<?php
/**
* Mobile App REST API Endpoints
*
* Provides REST API endpoints for mobile app functionality
*/
class TWP_Mobile_API {
private $auth;
/**
* Constructor
*/
public function __construct() {
// Initialize auth handler
require_once plugin_dir_path(__FILE__) . 'class-twp-mobile-auth.php';
$this->auth = new TWP_Mobile_Auth();
}
/**
* Register REST API endpoints
*/
public function register_endpoints() {
add_action('rest_api_init', function() {
// Agent status endpoints
register_rest_route('twilio-mobile/v1', '/agent/status', array(
'methods' => 'GET',
'callback' => array($this, 'get_agent_status'),
'permission_callback' => array($this->auth, 'verify_token')
));
register_rest_route('twilio-mobile/v1', '/agent/status', array(
'methods' => 'POST',
'callback' => array($this, 'update_agent_status'),
'permission_callback' => array($this->auth, 'verify_token')
));
// Queue state endpoint
register_rest_route('twilio-mobile/v1', '/queues/state', array(
'methods' => 'GET',
'callback' => array($this, 'get_queue_state'),
'permission_callback' => array($this->auth, 'verify_token')
));
// Queue calls (specific queue)
register_rest_route('twilio-mobile/v1', '/queues/(?P<id>\d+)/calls', array(
'methods' => 'GET',
'callback' => array($this, 'get_queue_calls'),
'permission_callback' => array($this->auth, 'verify_token')
));
// Call control endpoints
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/accept', array(
'methods' => 'POST',
'callback' => array($this, 'accept_call'),
'permission_callback' => array($this->auth, 'verify_token')
));
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/reject', array(
'methods' => 'POST',
'callback' => array($this, 'reject_call'),
'permission_callback' => array($this->auth, 'verify_token')
));
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/hold', array(
'methods' => 'POST',
'callback' => array($this, 'hold_call'),
'permission_callback' => array($this->auth, 'verify_token')
));
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/unhold', array(
'methods' => 'POST',
'callback' => array($this, 'unhold_call'),
'permission_callback' => array($this->auth, 'verify_token')
));
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/transfer', array(
'methods' => 'POST',
'callback' => array($this, 'transfer_call'),
'permission_callback' => array($this->auth, 'verify_token')
));
// FCM token registration
register_rest_route('twilio-mobile/v1', '/fcm/register', array(
'methods' => 'POST',
'callback' => array($this, 'register_fcm_token'),
'permission_callback' => array($this->auth, 'verify_token')
));
// Agent phone number
register_rest_route('twilio-mobile/v1', '/agent/phone', array(
'methods' => 'GET',
'callback' => array($this, 'get_agent_phone'),
'permission_callback' => array($this->auth, 'verify_token')
));
register_rest_route('twilio-mobile/v1', '/agent/phone', array(
'methods' => 'POST',
'callback' => array($this, 'update_agent_phone'),
'permission_callback' => array($this->auth, 'verify_token')
));
});
}
/**
* Get agent status
*/
public function get_agent_status($request) {
$user_id = $this->auth->get_current_user_id();
global $wpdb;
$table = $wpdb->prefix . 'twp_agent_status';
$status = $wpdb->get_row($wpdb->prepare(
"SELECT status, is_logged_in, current_call_sid, last_activity, available_for_queues FROM $table WHERE user_id = %d",
$user_id
));
if (!$status) {
// Create default status
$wpdb->insert(
$table,
array('user_id' => $user_id, 'status' => 'offline', 'is_logged_in' => 0),
array('%d', '%s', '%d')
);
$status = (object) array(
'status' => 'offline',
'is_logged_in' => 0,
'current_call_sid' => null,
'last_activity' => current_time('mysql'),
'available_for_queues' => 1
);
}
return new WP_REST_Response(array(
'success' => true,
'status' => $status->status,
'is_logged_in' => (bool)$status->is_logged_in,
'current_call_sid' => $status->current_call_sid,
'last_activity' => $status->last_activity,
'available_for_queues' => (bool)$status->available_for_queues
), 200);
}
/**
* Update agent status
*/
public function update_agent_status($request) {
$user_id = $this->auth->get_current_user_id();
$new_status = $request->get_param('status');
$is_logged_in = $request->get_param('is_logged_in');
if (!in_array($new_status, array('available', 'busy', 'offline'))) {
return new WP_Error('invalid_status', 'Status must be available, busy, or offline', array('status' => 400));
}
global $wpdb;
$table = $wpdb->prefix . 'twp_agent_status';
// Check if status exists
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $table WHERE user_id = %d",
$user_id
));
$data = array(
'status' => $new_status,
'last_activity' => current_time('mysql')
);
if ($is_logged_in !== null) {
$data['is_logged_in'] = $is_logged_in ? 1 : 0;
if ($is_logged_in) {
$data['logged_in_at'] = current_time('mysql');
}
}
if ($exists) {
$wpdb->update(
$table,
$data,
array('user_id' => $user_id),
array('%s', '%s'),
array('%d')
);
} else {
$data['user_id'] = $user_id;
$wpdb->insert($table, $data);
}
return new WP_REST_Response(array(
'success' => true,
'message' => 'Status updated successfully'
), 200);
}
/**
* Get queue state (all queues user has access to)
*/
public function get_queue_state($request) {
$user_id = $this->auth->get_current_user_id();
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
// Get queues assigned to this user
$queue_ids = $wpdb->get_col($wpdb->prepare(
"SELECT queue_id FROM $assignments_table WHERE user_id = %d",
$user_id
));
// Also include personal queues
$personal_queue_ids = $wpdb->get_col($wpdb->prepare(
"SELECT id FROM $queues_table WHERE user_id = %d",
$user_id
));
$all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
if (empty($all_queue_ids)) {
return new WP_REST_Response(array(
'success' => true,
'queues' => array()
), 200);
}
$queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
// Get queue information with call counts
$queues = $wpdb->get_results("
SELECT
q.id,
q.queue_name,
q.queue_type,
q.extension,
COUNT(c.id) as waiting_count
FROM $queues_table q
LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
WHERE q.id IN ($queue_ids_str)
GROUP BY q.id
");
$result = array();
foreach ($queues as $queue) {
$result[] = array(
'id' => (int)$queue->id,
'name' => $queue->queue_name,
'type' => $queue->queue_type,
'extension' => $queue->extension,
'waiting_count' => (int)$queue->waiting_count
);
}
return new WP_REST_Response(array(
'success' => true,
'queues' => $result
), 200);
}
/**
* Get calls in a specific queue
*/
public function get_queue_calls($request) {
$user_id = $this->auth->get_current_user_id();
$queue_id = (int)$request['id'];
// Verify user has access to this queue
if (!$this->user_has_queue_access($user_id, $queue_id)) {
return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
}
global $wpdb;
$table = $wpdb->prefix . 'twp_queued_calls';
$calls = $wpdb->get_results($wpdb->prepare(
"SELECT call_sid, from_number, to_number, position, status, joined_at, enqueued_at
FROM $table
WHERE queue_id = %d AND status = 'waiting'
ORDER BY position ASC",
$queue_id
));
$result = array();
foreach ($calls as $call) {
$result[] = array(
'call_sid' => $call->call_sid,
'from_number' => $call->from_number,
'to_number' => $call->to_number,
'position' => (int)$call->position,
'status' => $call->status,
'wait_time' => $this->calculate_wait_time($call->enqueued_at ?: $call->joined_at)
);
}
return new WP_REST_Response(array(
'success' => true,
'calls' => $result
), 200);
}
/**
* Accept a call (dequeue and connect to agent)
*/
public function accept_call($request) {
$user_id = $this->auth->get_current_user_id();
$call_sid = $request['call_sid'];
// Get agent phone number
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
if (empty($agent_number)) {
return new WP_Error('no_phone', 'No phone number configured for agent', array('status' => 400));
}
// Initialize Twilio API
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
$twilio = new TWP_Twilio_API();
// Get call info from queue
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'",
$call_sid
));
if (!$call) {
return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404));
}
// Verify user has access to this queue
if (!$this->user_has_queue_access($user_id, $call->queue_id)) {
return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
}
try {
// Connect agent to call
$agent_call = $twilio->create_call(
$agent_number,
$call->to_number,
array(
'url' => site_url('/wp-json/twilio-webhook/v1/connect-agent'),
'statusCallback' => site_url('/wp-json/twilio-webhook/v1/agent-call-status'),
'statusCallbackEvent' => array('completed', 'no-answer', 'busy', 'failed'),
'timeout' => 30
)
);
// Update call record
$wpdb->update(
$calls_table,
array(
'status' => 'connecting',
'agent_phone' => $agent_number,
'agent_call_sid' => $agent_call->sid
),
array('call_sid' => $call_sid),
array('%s', '%s', '%s'),
array('%s')
);
// Update agent status
$status_table = $wpdb->prefix . 'twp_agent_status';
$wpdb->update(
$status_table,
array('status' => 'busy', 'current_call_sid' => $call_sid),
array('user_id' => $user_id),
array('%s', '%s'),
array('%d')
);
return new WP_REST_Response(array(
'success' => true,
'message' => 'Call accepted, connecting to agent',
'agent_call_sid' => $agent_call->sid
), 200);
} catch (Exception $e) {
return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
}
}
/**
* Reject a call (send to voicemail)
*/
public function reject_call($request) {
$user_id = $this->auth->get_current_user_id();
$call_sid = $request['call_sid'];
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'",
$call_sid
));
if (!$call) {
return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404));
}
// Verify user has access to this queue
if (!$this->user_has_queue_access($user_id, $call->queue_id)) {
return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
}
try {
// Initialize Twilio API
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
$twilio = new TWP_Twilio_API();
// Redirect call to voicemail
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('The agent is unavailable. Please leave a message after the tone.');
$twiml->record(array(
'action' => site_url('/wp-json/twilio-webhook/v1/voicemail-complete'),
'maxLength' => 120,
'transcribe' => true
));
$twiml->say('We did not receive a recording. Goodbye.');
$twilio->update_call($call_sid, array('twiml' => $twiml->asXML()));
// Update call status
$wpdb->update(
$calls_table,
array('status' => 'voicemail', 'ended_at' => current_time('mysql')),
array('call_sid' => $call_sid),
array('%s', '%s'),
array('%s')
);
return new WP_REST_Response(array(
'success' => true,
'message' => 'Call sent to voicemail'
), 200);
} catch (Exception $e) {
return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
}
}
/**
* Hold a call
*/
public function hold_call($request) {
$user_id = $this->auth->get_current_user_id();
$call_sid = $request['call_sid'];
try {
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php';
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
$admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
$twilio = new TWP_Twilio_API();
// Find customer call leg
$customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio);
if (!$customer_call_sid) {
return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404));
}
// Get user's hold queue
global $wpdb;
$ext_table = $wpdb->prefix . 'twp_user_extensions';
$queues_table = $wpdb->prefix . 'twp_call_queues';
$extension = $wpdb->get_row($wpdb->prepare(
"SELECT hold_queue_id FROM $ext_table WHERE user_id = %d",
$user_id
));
if (!$extension || !$extension->hold_queue_id) {
return new WP_Error('no_hold_queue', 'No hold queue configured', array('status' => 400));
}
$hold_queue = $wpdb->get_row($wpdb->prepare(
"SELECT queue_name, wait_music_url FROM $queues_table WHERE id = %d",
$extension->hold_queue_id
));
// Put call on hold
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Please hold while we transfer your call.');
$enqueue = $twiml->enqueue($hold_queue->queue_name, array(
'waitUrl' => $hold_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait')
));
$twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML()));
return new WP_REST_Response(array(
'success' => true,
'message' => 'Call placed on hold'
), 200);
} catch (Exception $e) {
return new WP_Error('hold_error', $e->getMessage(), array('status' => 500));
}
}
/**
* Unhold a call (resume from hold queue)
*/
public function unhold_call($request) {
// Implementation would retrieve from hold queue and reconnect
return new WP_REST_Response(array(
'success' => true,
'message' => 'Unhold functionality - to be implemented with queue retrieval'
), 501);
}
/**
* Transfer a call to another extension/queue
*/
public function transfer_call($request) {
$user_id = $this->auth->get_current_user_id();
$call_sid = $request['call_sid'];
$target = $request->get_param('target'); // Extension number or queue ID
if (empty($target)) {
return new WP_Error('missing_target', 'Transfer target is required', array('status' => 400));
}
try {
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php';
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
$admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
$twilio = new TWP_Twilio_API();
// Find customer call leg
$customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio);
if (!$customer_call_sid) {
return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404));
}
// Look up target (extension or queue)
global $wpdb;
$ext_table = $wpdb->prefix . 'twp_user_extensions';
$queues_table = $wpdb->prefix . 'twp_call_queues';
// Try as extension first
$target_queue = $wpdb->get_row($wpdb->prepare(
"SELECT q.* FROM $queues_table q
JOIN $ext_table e ON q.id = e.personal_queue_id
WHERE e.extension = %s",
$target
));
// If not extension, try as queue ID
if (!$target_queue && is_numeric($target)) {
$target_queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $queues_table WHERE id = %d",
$target
));
}
if (!$target_queue) {
return new WP_Error('invalid_target', 'Transfer target not found', array('status' => 404));
}
// Transfer to queue
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Transferring your call.');
$twiml->enqueue($target_queue->queue_name, array(
'waitUrl' => $target_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait')
));
$twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML()));
return new WP_REST_Response(array(
'success' => true,
'message' => 'Call transferred successfully'
), 200);
} catch (Exception $e) {
return new WP_Error('transfer_error', $e->getMessage(), array('status' => 500));
}
}
/**
* Register FCM token for push notifications
*/
public function register_fcm_token($request) {
$user_id = $this->auth->get_current_user_id();
$fcm_token = $request->get_param('fcm_token');
$refresh_token = $request->get_param('refresh_token');
if (empty($fcm_token)) {
return new WP_Error('missing_token', 'FCM token is required', array('status' => 400));
}
$this->auth->update_fcm_token($user_id, $refresh_token, $fcm_token);
return new WP_REST_Response(array(
'success' => true,
'message' => 'FCM token registered successfully'
), 200);
}
/**
* Get agent phone number
*/
public function get_agent_phone($request) {
$user_id = $this->auth->get_current_user_id();
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
return new WP_REST_Response(array(
'success' => true,
'phone_number' => $agent_number ?: null
), 200);
}
/**
* Update agent phone number
*/
public function update_agent_phone($request) {
$user_id = $this->auth->get_current_user_id();
$phone_number = $request->get_param('phone_number');
if (empty($phone_number)) {
return new WP_Error('missing_phone', 'Phone number is required', array('status' => 400));
}
// Validate E.164 format
if (!preg_match('/^\+[1-9]\d{1,14}$/', $phone_number)) {
return new WP_Error('invalid_phone', 'Phone number must be in E.164 format (+1XXXXXXXXXX)', array('status' => 400));
}
update_user_meta($user_id, 'twp_agent_phone', $phone_number);
return new WP_REST_Response(array(
'success' => true,
'message' => 'Phone number updated successfully'
), 200);
}
/**
* Check if user has access to a queue
*/
private function user_has_queue_access($user_id, $queue_id) {
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
// Check if it's user's personal queue
$is_personal = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $queues_table WHERE id = %d AND user_id = %d",
$queue_id, $user_id
));
if ($is_personal) {
return true;
}
// Check if user is assigned to this queue
$is_assigned = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $assignments_table WHERE queue_id = %d AND user_id = %d",
$queue_id, $user_id
));
return (bool)$is_assigned;
}
/**
* Calculate wait time in seconds
*/
private function calculate_wait_time($start_time) {
if (!$start_time) {
return 0;
}
$start = strtotime($start_time);
$now = current_time('timestamp');
return max(0, $now - $start);
}
}

View File

@@ -0,0 +1,457 @@
<?php
/**
* Mobile App JWT Authentication Handler
*
* Handles JWT token generation, validation, and refresh for Android/iOS apps
*/
class TWP_Mobile_Auth {
private $secret_key;
private $token_expiry = 86400; // 24 hours in seconds
private $refresh_expiry = 2592000; // 30 days in seconds
/**
* Constructor
*/
public function __construct() {
$this->secret_key = $this->get_secret_key();
}
/**
* Get or generate JWT secret key
*/
private function get_secret_key() {
$key = get_option('twp_mobile_jwt_secret');
if (empty($key)) {
// Generate a secure random key
$key = bin2hex(random_bytes(32));
update_option('twp_mobile_jwt_secret', $key);
}
return $key;
}
/**
* Register REST API endpoints
*/
public function register_endpoints() {
add_action('rest_api_init', function() {
// Login endpoint
register_rest_route('twilio-mobile/v1', '/auth/login', array(
'methods' => 'POST',
'callback' => array($this, 'handle_login'),
'permission_callback' => '__return_true'
));
// Refresh token endpoint
register_rest_route('twilio-mobile/v1', '/auth/refresh', array(
'methods' => 'POST',
'callback' => array($this, 'handle_refresh'),
'permission_callback' => '__return_true'
));
// Logout endpoint
register_rest_route('twilio-mobile/v1', '/auth/logout', array(
'methods' => 'POST',
'callback' => array($this, 'handle_logout'),
'permission_callback' => array($this, 'verify_token')
));
// Validate token endpoint (for debugging)
register_rest_route('twilio-mobile/v1', '/auth/validate', array(
'methods' => 'GET',
'callback' => array($this, 'handle_validate'),
'permission_callback' => array($this, 'verify_token')
));
});
}
/**
* Handle login request
*/
public function handle_login($request) {
$username = $request->get_param('username');
$password = $request->get_param('password');
$fcm_token = $request->get_param('fcm_token'); // Optional
$device_info = $request->get_param('device_info'); // Optional
if (empty($username) || empty($password)) {
return new WP_Error('missing_credentials', 'Username and password are required', array('status' => 400));
}
// Authenticate user
$user = wp_authenticate($username, $password);
if (is_wp_error($user)) {
return new WP_Error('invalid_credentials', 'Invalid username or password', array('status' => 401));
}
// Check if user has phone agent capabilities
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
return new WP_Error('insufficient_permissions', 'User does not have phone agent access', array('status' => 403));
}
// Generate tokens
$access_token = $this->generate_token($user->ID, 'access');
$refresh_token = $this->generate_token($user->ID, 'refresh');
// Store session in database
$this->store_session($user->ID, $refresh_token, $fcm_token, $device_info);
// Get user data
$user_data = $this->get_user_data($user->ID);
return new WP_REST_Response(array(
'success' => true,
'access_token' => $access_token,
'refresh_token' => $refresh_token,
'expires_in' => $this->token_expiry,
'user' => $user_data
), 200);
}
/**
* Handle token refresh request
*/
public function handle_refresh($request) {
$refresh_token = $request->get_param('refresh_token');
if (empty($refresh_token)) {
return new WP_Error('missing_token', 'Refresh token is required', array('status' => 400));
}
// Verify refresh token
$payload = $this->decode_token($refresh_token);
if (!$payload || $payload->type !== 'refresh') {
return new WP_Error('invalid_token', 'Invalid refresh token', array('status' => 401));
}
// Check if session exists and is valid
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
$session = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table WHERE user_id = %d AND refresh_token = %s AND is_active = 1 AND expires_at > NOW()",
$payload->user_id,
$refresh_token
));
if (!$session) {
return new WP_Error('invalid_session', 'Session expired or invalid', array('status' => 401));
}
// Generate new access token
$access_token = $this->generate_token($payload->user_id, 'access');
// Update last_used timestamp
$wpdb->update(
$table,
array('last_used' => current_time('mysql')),
array('id' => $session->id),
array('%s'),
array('%d')
);
return new WP_REST_Response(array(
'success' => true,
'access_token' => $access_token,
'expires_in' => $this->token_expiry
), 200);
}
/**
* Handle logout request
*/
public function handle_logout($request) {
$user_id = $this->get_current_user_id();
if (!$user_id) {
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
}
// Get refresh token from request
$refresh_token = $request->get_param('refresh_token');
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
if ($refresh_token) {
// Invalidate specific session
$wpdb->update(
$table,
array('is_active' => 0),
array('user_id' => $user_id, 'refresh_token' => $refresh_token),
array('%d'),
array('%d', '%s')
);
} else {
// Invalidate all sessions for this user
$wpdb->update(
$table,
array('is_active' => 0),
array('user_id' => $user_id),
array('%d'),
array('%d')
);
}
return new WP_REST_Response(array(
'success' => true,
'message' => 'Logged out successfully'
), 200);
}
/**
* Handle token validation request
*/
public function handle_validate($request) {
$user_id = $this->get_current_user_id();
if (!$user_id) {
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
}
$user_data = $this->get_user_data($user_id);
return new WP_REST_Response(array(
'success' => true,
'valid' => true,
'user' => $user_data
), 200);
}
/**
* Generate JWT token
*/
private function generate_token($user_id, $type = 'access') {
$issued_at = time();
$expiry = $type === 'refresh' ? $this->refresh_expiry : $this->token_expiry;
$payload = array(
'iat' => $issued_at,
'exp' => $issued_at + $expiry,
'user_id' => $user_id,
'type' => $type
);
return $this->encode_token($payload);
}
/**
* Simple JWT encoding (header.payload.signature)
*/
private function encode_token($payload) {
$header = array('typ' => 'JWT', 'alg' => 'HS256');
$segments = array();
$segments[] = $this->base64url_encode(json_encode($header));
$segments[] = $this->base64url_encode(json_encode($payload));
$signing_input = implode('.', $segments);
$signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
$segments[] = $this->base64url_encode($signature);
return implode('.', $segments);
}
/**
* Simple JWT decoding
*/
private function decode_token($token) {
$segments = explode('.', $token);
if (count($segments) !== 3) {
return false;
}
list($header_b64, $payload_b64, $signature_b64) = $segments;
// Verify signature
$signing_input = $header_b64 . '.' . $payload_b64;
$signature = $this->base64url_decode($signature_b64);
$expected_signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
if (!hash_equals($signature, $expected_signature)) {
return false;
}
// Decode payload
$payload = json_decode($this->base64url_decode($payload_b64));
if (!$payload) {
return false;
}
// Check expiration
if (isset($payload->exp) && $payload->exp < time()) {
return false;
}
return $payload;
}
/**
* Base64 URL encode
*/
private function base64url_encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Base64 URL decode
*/
private function base64url_decode($data) {
return base64_decode(strtr($data, '-_', '+/'));
}
/**
* Verify token (permission callback)
*/
public function verify_token($request) {
$auth_header = $request->get_header('Authorization');
if (empty($auth_header)) {
return false;
}
// Extract token from "Bearer <token>"
if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
$token = $matches[1];
} else {
return false;
}
$payload = $this->decode_token($token);
if (!$payload || $payload->type !== 'access') {
return false;
}
// Store user ID for later use
$request->set_param('_twp_user_id', $payload->user_id);
return true;
}
/**
* Get current user ID from token
*/
public function get_current_user_id() {
$request = rest_get_server()->get_request();
return $request->get_param('_twp_user_id');
}
/**
* Store session in database
*/
private function store_session($user_id, $refresh_token, $fcm_token = null, $device_info = null) {
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
$wpdb->insert(
$table,
array(
'user_id' => $user_id,
'refresh_token' => $refresh_token,
'fcm_token' => $fcm_token,
'device_info' => $device_info,
'created_at' => current_time('mysql'),
'expires_at' => date('Y-m-d H:i:s', time() + $this->refresh_expiry),
'last_used' => current_time('mysql'),
'is_active' => 1
),
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
);
}
/**
* Get user data for response
*/
private function get_user_data($user_id) {
$user = get_userdata($user_id);
if (!$user) {
return null;
}
// Get agent phone number
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
// Get agent status
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
$status = $wpdb->get_row($wpdb->prepare(
"SELECT status, is_logged_in, current_call_sid FROM $status_table WHERE user_id = %d",
$user_id
));
// Get user extension
$ext_table = $wpdb->prefix . 'twp_user_extensions';
$extension = $wpdb->get_row($wpdb->prepare(
"SELECT extension, direct_dial_number FROM $ext_table WHERE user_id = %d",
$user_id
));
return array(
'id' => $user->ID,
'username' => $user->user_login,
'display_name' => $user->display_name,
'email' => $user->user_email,
'phone_number' => $agent_number,
'extension' => $extension ? $extension->extension : null,
'direct_dial' => $extension ? $extension->direct_dial_number : null,
'status' => $status ? $status->status : 'offline',
'is_logged_in' => $status ? (bool)$status->is_logged_in : false,
'current_call_sid' => $status ? $status->current_call_sid : null,
'capabilities' => array(
'can_access_browser_phone' => user_can($user_id, 'twp_access_browser_phone'),
'can_access_voicemails' => user_can($user_id, 'twp_access_voicemails'),
'can_access_call_log' => user_can($user_id, 'twp_access_call_log'),
'can_access_agent_queue' => user_can($user_id, 'twp_access_agent_queue'),
'can_access_sms_inbox' => user_can($user_id, 'twp_access_sms_inbox'),
'is_admin' => user_can($user_id, 'manage_options')
)
);
}
/**
* Update FCM token for existing session
*/
public function update_fcm_token($user_id, $refresh_token, $fcm_token) {
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
$wpdb->update(
$table,
array('fcm_token' => $fcm_token),
array('user_id' => $user_id, 'refresh_token' => $refresh_token, 'is_active' => 1),
array('%s'),
array('%d', '%s', '%d')
);
}
/**
* Get all active FCM tokens for a user
*/
public function get_user_fcm_tokens($user_id) {
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
return $wpdb->get_col($wpdb->prepare(
"SELECT fcm_token FROM $table WHERE user_id = %d AND is_active = 1 AND fcm_token IS NOT NULL AND expires_at > NOW()",
$user_id
));
}
/**
* Clean up expired sessions
*/
public static function cleanup_expired_sessions() {
global $wpdb;
$table = $wpdb->prefix . 'twp_mobile_sessions';
$wpdb->query("UPDATE $table SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1");
}
}

View File

@@ -0,0 +1,308 @@
<?php
/**
* Server-Sent Events (SSE) Handler for Mobile App
*
* Provides real-time updates for queue state, incoming calls, and agent status
*/
class TWP_Mobile_SSE {
private $auth;
/**
* Constructor
*/
public function __construct() {
require_once plugin_dir_path(__FILE__) . 'class-twp-mobile-auth.php';
$this->auth = new TWP_Mobile_Auth();
}
/**
* Register SSE endpoint
*/
public function register_endpoints() {
add_action('rest_api_init', function() {
register_rest_route('twilio-mobile/v1', '/stream/events', array(
'methods' => 'GET',
'callback' => array($this, 'stream_events'),
'permission_callback' => array($this->auth, 'verify_token')
));
});
}
/**
* Stream events to mobile app
*/
public function stream_events($request) {
$user_id = $this->auth->get_current_user_id();
if (!$user_id) {
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
}
// Set headers for SSE
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // Disable nginx buffering
// Disable PHP output buffering
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', '1');
}
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
ob_implicit_flush(1);
while (ob_get_level() > 0) {
ob_end_flush();
}
// Send initial connection event
$this->send_event('connected', array('user_id' => $user_id, 'timestamp' => time()));
// Get initial state
$last_check = time();
$previous_state = $this->get_current_state($user_id);
// Stream loop - check for changes every 2 seconds
$max_duration = 300; // 5 minutes max connection time
$start_time = time();
while (time() - $start_time < $max_duration) {
// Check if connection is still alive
if (connection_aborted()) {
break;
}
// Get current state
$current_state = $this->get_current_state($user_id);
// Compare and send updates
$this->check_and_send_updates($previous_state, $current_state);
// Update previous state
$previous_state = $current_state;
// Send heartbeat every 15 seconds
if (time() - $last_check >= 15) {
$this->send_event('heartbeat', array('timestamp' => time()));
$last_check = time();
}
// Sleep for 2 seconds
sleep(2);
}
// Connection closing
$this->send_event('disconnect', array('reason' => 'timeout', 'timestamp' => time()));
exit;
}
/**
* Get current state for agent
*/
private function get_current_state($user_id) {
global $wpdb;
$state = array(
'agent_status' => $this->get_agent_status($user_id),
'queues' => $this->get_queues_state($user_id),
'current_call' => $this->get_current_call($user_id)
);
return $state;
}
/**
* Get agent status
*/
private function get_agent_status($user_id) {
global $wpdb;
$table = $wpdb->prefix . 'twp_agent_status';
$status = $wpdb->get_row($wpdb->prepare(
"SELECT status, is_logged_in, current_call_sid FROM $table WHERE user_id = %d",
$user_id
));
if (!$status) {
return array('status' => 'offline', 'is_logged_in' => false, 'current_call_sid' => null);
}
return array(
'status' => $status->status,
'is_logged_in' => (bool)$status->is_logged_in,
'current_call_sid' => $status->current_call_sid
);
}
/**
* Get queues state
*/
private function get_queues_state($user_id) {
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
// Get queue IDs
$queue_ids = $wpdb->get_col($wpdb->prepare(
"SELECT queue_id FROM $assignments_table WHERE user_id = %d",
$user_id
));
$personal_queue_ids = $wpdb->get_col($wpdb->prepare(
"SELECT id FROM $queues_table WHERE user_id = %d",
$user_id
));
$all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
if (empty($all_queue_ids)) {
return array();
}
$queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
$queues = $wpdb->get_results("
SELECT
q.id,
q.queue_name,
COUNT(c.id) as waiting_count,
MIN(c.enqueued_at) as oldest_call_time
FROM $queues_table q
LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
WHERE q.id IN ($queue_ids_str)
GROUP BY q.id
");
$result = array();
foreach ($queues as $queue) {
$result[$queue->id] = array(
'id' => (int)$queue->id,
'name' => $queue->queue_name,
'waiting_count' => (int)$queue->waiting_count,
'oldest_call_time' => $queue->oldest_call_time
);
}
return $result;
}
/**
* Get current call for agent
*/
private function get_current_call($user_id) {
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
if (!$agent_number) {
return null;
}
$call = $wpdb->get_row($wpdb->prepare(
"SELECT call_sid, from_number, queue_id, status, joined_at
FROM $calls_table
WHERE agent_phone = %s AND status IN ('connecting', 'in_progress')
ORDER BY joined_at DESC
LIMIT 1",
$agent_number
));
if (!$call) {
return null;
}
return array(
'call_sid' => $call->call_sid,
'from_number' => $call->from_number,
'queue_id' => (int)$call->queue_id,
'status' => $call->status,
'duration' => time() - strtotime($call->joined_at)
);
}
/**
* Check state changes and send updates
*/
private function check_and_send_updates($previous, $current) {
// Check agent status changes
if ($previous['agent_status'] !== $current['agent_status']) {
$this->send_event('agent_status_changed', $current['agent_status']);
}
// Check queue changes
$this->check_queue_changes($previous['queues'], $current['queues']);
// Check current call changes
if ($previous['current_call'] !== $current['current_call']) {
if ($current['current_call'] && !$previous['current_call']) {
// New call started
$this->send_event('call_started', $current['current_call']);
} elseif (!$current['current_call'] && $previous['current_call']) {
// Call ended
$this->send_event('call_ended', $previous['current_call']);
} elseif ($current['current_call'] && $previous['current_call']) {
// Call status changed
if ($current['current_call']['status'] !== $previous['current_call']['status']) {
$this->send_event('call_status_changed', $current['current_call']);
}
}
}
}
/**
* Check for queue changes
*/
private function check_queue_changes($previous_queues, $current_queues) {
foreach ($current_queues as $queue_id => $current_queue) {
$previous_queue = $previous_queues[$queue_id] ?? null;
if (!$previous_queue) {
// New queue added
$this->send_event('queue_added', $current_queue);
continue;
}
// Check for waiting count changes
if ($current_queue['waiting_count'] !== $previous_queue['waiting_count']) {
if ($current_queue['waiting_count'] > $previous_queue['waiting_count']) {
// New call in queue
$this->send_event('call_enqueued', array(
'queue_id' => $queue_id,
'queue_name' => $current_queue['name'],
'waiting_count' => $current_queue['waiting_count']
));
} else {
// Call removed from queue
$this->send_event('call_dequeued', array(
'queue_id' => $queue_id,
'queue_name' => $current_queue['name'],
'waiting_count' => $current_queue['waiting_count']
));
}
}
}
// Check for removed queues
foreach ($previous_queues as $queue_id => $previous_queue) {
if (!isset($current_queues[$queue_id])) {
$this->send_event('queue_removed', array('queue_id' => $queue_id));
}
}
}
/**
* Send SSE event
*/
private function send_event($event_type, $data) {
echo "event: $event_type\n";
echo "data: " . json_encode($data) . "\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
}

View File

@@ -3,7 +3,7 @@
* Plugin Name: Twilio WP Plugin * Plugin Name: Twilio WP Plugin
* Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin * Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS * Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
* Version: 2.8.9 * Version: {auto_update_value_on_deploy}
* Author: Josh Knapp * Author: Josh Knapp
* License: GPL v2 or later * License: GPL v2 or later
* Text Domain: twilio-wp-plugin * Text Domain: twilio-wp-plugin
@@ -15,7 +15,7 @@ if (!defined('WPINC')) {
} }
// Plugin constants // Plugin constants
define('TWP_VERSION', '2.8.9'); define('TWP_VERSION', '{auto_update_value_on_deploy}');
define('TWP_DB_VERSION', '1.6.2'); // Track database version separately define('TWP_DB_VERSION', '1.6.2'); // Track database version separately
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));