code revision
This commit is contained in:
63
CLAUDE.md
Normal file
63
CLAUDE.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a WordPress plugin for integrating Twilio functionality. The plugin is in early development stage.
|
||||
|
||||
## WordPress Plugin Development Structure
|
||||
|
||||
When developing this plugin, follow WordPress plugin conventions:
|
||||
- Main plugin file should be in the root directory (e.g., `twilio-wp-plugin.php`)
|
||||
- Use `includes/` directory for PHP class files and core functionality
|
||||
- Use `admin/` directory for admin-specific functionality
|
||||
- Use `public/` directory for frontend functionality
|
||||
- Use `assets/` directory for CSS, JS, and image files
|
||||
|
||||
## Development Commands
|
||||
|
||||
Since this is a WordPress plugin, typical commands include:
|
||||
|
||||
```bash
|
||||
# Install WordPress development dependencies (if using Composer)
|
||||
composer install
|
||||
|
||||
# Install JavaScript dependencies (if using npm)
|
||||
npm install
|
||||
|
||||
# Build assets (if using build tools)
|
||||
npm run build
|
||||
|
||||
# Run PHP CodeSniffer for WordPress coding standards
|
||||
vendor/bin/phpcs
|
||||
|
||||
# Fix PHP coding standards automatically
|
||||
vendor/bin/phpcbf
|
||||
|
||||
# Run PHPUnit tests
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## Key WordPress Plugin Conventions
|
||||
|
||||
- Use WordPress hooks and filters system for extending functionality
|
||||
- Follow WordPress coding standards for PHP, JavaScript, and CSS
|
||||
- Prefix all functions, classes, and global variables with plugin-specific prefix to avoid conflicts
|
||||
- Use WordPress's built-in functions for database operations, HTTP requests, and sanitization
|
||||
- Store Twilio API credentials using WordPress options API with encryption
|
||||
|
||||
## Twilio Integration Points
|
||||
|
||||
When working with Twilio API:
|
||||
- Store API credentials securely in WordPress options
|
||||
- Use WordPress's `wp_remote_post()` and `wp_remote_get()` for API calls
|
||||
- Implement proper error handling and logging using WordPress error logging
|
||||
- Consider rate limiting and webhook verification for security
|
||||
|
||||
## Testing Approach
|
||||
|
||||
- Use PHPUnit for unit testing PHP code
|
||||
- Mock WordPress functions using Brain Monkey or WP_Mock
|
||||
- Test Twilio API interactions using mock responses
|
||||
- Use WordPress testing framework for integration tests
|
2983
admin/class-twp-admin.php
Normal file
2983
admin/class-twp-admin.php
Normal file
File diff suppressed because it is too large
Load Diff
543
assets/css/admin.css
Normal file
543
assets/css/admin.css
Normal file
@@ -0,0 +1,543 @@
|
||||
/* Twilio WP Plugin Admin Styles */
|
||||
|
||||
.twp-dashboard {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.twp-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.twp-stat-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.twp-stat-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.twp-stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.twp-recent-activity {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.twp-recent-activity h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.twp-status {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.twp-status.active {
|
||||
background: #d4f4dd;
|
||||
color: #00a32a;
|
||||
}
|
||||
|
||||
.twp-status.inactive {
|
||||
background: #fef8ee;
|
||||
color: #996800;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.twp-modal {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.twp-modal-content {
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.twp-modal-content.large {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.twp-modal-content h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.twp-modal-content label {
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.twp-modal-content input[type="text"],
|
||||
.twp-modal-content input[type="number"],
|
||||
.twp-modal-content input[type="time"],
|
||||
.twp-modal-content input[type="url"],
|
||||
.twp-modal-content select,
|
||||
.twp-modal-content textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.twp-modal-content select[multiple] {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Queue Grid */
|
||||
.twp-queue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.twp-queue-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.twp-queue-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.queue-stats .stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.queue-stats .label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.queue-stats .value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.queue-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Workflow Builder */
|
||||
.workflow-builder-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.workflow-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 200px;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.workflow-steps,
|
||||
.workflow-preview {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.workflow-steps h3,
|
||||
.workflow-preview h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.step-types-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.step-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 5px;
|
||||
border: 2px solid #ddd;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.step-btn:hover {
|
||||
border-color: #2271b1;
|
||||
background: #f0f6fc;
|
||||
}
|
||||
|
||||
.step-btn .dashicons {
|
||||
font-size: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.workflow-steps-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.workflow-step {
|
||||
background: #f7f7f7;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.workflow-step.dragging {
|
||||
opacity: 0.5;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.workflow-step .step-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workflow-step .step-type {
|
||||
font-weight: 600;
|
||||
color: #2271b1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workflow-step .step-number {
|
||||
background: #2271b1;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.workflow-step .step-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.workflow-step .step-actions button {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workflow-step-content {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Workflow Preview */
|
||||
.workflow-flow-chart {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-start {
|
||||
background: #2271b1;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
background: #f0f6fc;
|
||||
border: 2px solid #2271b1;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
margin: 10px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-step::before {
|
||||
content: '↓';
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: white;
|
||||
padding: 0 5px;
|
||||
color: #2271b1;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.flow-step:first-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Step Configuration Forms */
|
||||
#step-config-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.step-config-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.step-config-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.step-config-section h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.ivr-options {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ivr-option {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 1fr 120px 100px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.ivr-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ivr-option input[type="text"],
|
||||
.ivr-option select {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.add-ivr-option {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
background: #f0f6fc;
|
||||
border-top: 2px dashed #2271b1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-ivr-option:hover {
|
||||
background: #e6f3ff;
|
||||
}
|
||||
|
||||
/* Phone Numbers Page */
|
||||
.twp-numbers-actions {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.twp-numbers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.twp-number-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.twp-number-card .number {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2271b1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.twp-number-card .number-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.twp-number-card .number-info .label {
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.twp-number-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.twp-search-form {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.twp-search-form label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#search-results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.available-number {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.available-number .number {
|
||||
font-weight: 600;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.available-number .capabilities {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.available-number .price {
|
||||
font-weight: 600;
|
||||
color: #d63638;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.twp-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.twp-queue-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workflow-builder-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.twp-spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #2271b1;
|
||||
border-radius: 50%;
|
||||
animation: twp-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes twp-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Notification Styles */
|
||||
.twp-notice {
|
||||
padding: 12px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.twp-notice.success {
|
||||
border-color: #00a32a;
|
||||
background: #d4f4dd;
|
||||
}
|
||||
|
||||
.twp-notice.error {
|
||||
border-color: #d63638;
|
||||
background: #f4e2e2;
|
||||
}
|
||||
|
||||
.twp-notice.warning {
|
||||
border-color: #dba617;
|
||||
background: #fef8ee;
|
||||
}
|
1591
assets/js/admin.js
Normal file
1591
assets/js/admin.js
Normal file
File diff suppressed because it is too large
Load Diff
281
includes/class-twp-activator.php
Normal file
281
includes/class-twp-activator.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/**
|
||||
* Fired during plugin activation
|
||||
*/
|
||||
class TWP_Activator {
|
||||
|
||||
/**
|
||||
* Run activation tasks
|
||||
*/
|
||||
public static function activate() {
|
||||
// Create database tables
|
||||
self::create_tables();
|
||||
|
||||
// Set default options
|
||||
self::set_default_options();
|
||||
|
||||
// Create webhook endpoints
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tables exist and create them if needed
|
||||
*/
|
||||
public static function ensure_tables_exist() {
|
||||
global $wpdb;
|
||||
|
||||
$required_tables = array(
|
||||
'twp_phone_schedules',
|
||||
'twp_call_queues',
|
||||
'twp_queued_calls',
|
||||
'twp_workflows',
|
||||
'twp_call_log',
|
||||
'twp_sms_log',
|
||||
'twp_voicemails',
|
||||
'twp_agent_groups',
|
||||
'twp_group_members',
|
||||
'twp_agent_status',
|
||||
'twp_callbacks'
|
||||
);
|
||||
|
||||
$missing_tables = array();
|
||||
|
||||
foreach ($required_tables as $table) {
|
||||
$table_name = $wpdb->prefix . $table;
|
||||
$table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
|
||||
|
||||
if (!$table_exists) {
|
||||
$missing_tables[] = $table;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missing_tables)) {
|
||||
error_log('TWP Plugin: Missing database tables: ' . implode(', ', $missing_tables) . '. Creating them now.');
|
||||
self::create_tables();
|
||||
return false; // Tables were missing
|
||||
}
|
||||
|
||||
return true; // All tables exist
|
||||
}
|
||||
|
||||
/**
|
||||
* Create plugin database tables
|
||||
*/
|
||||
private static function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Phone schedules table
|
||||
$table_schedules = $wpdb->prefix . 'twp_phone_schedules';
|
||||
$sql_schedules = "CREATE TABLE $table_schedules (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
phone_number varchar(20),
|
||||
schedule_name varchar(100) NOT NULL,
|
||||
days_of_week varchar(20) NOT NULL,
|
||||
start_time time NOT NULL,
|
||||
end_time time NOT NULL,
|
||||
workflow_id varchar(100),
|
||||
forward_number varchar(20),
|
||||
after_hours_action varchar(20) DEFAULT 'workflow',
|
||||
after_hours_workflow_id varchar(100),
|
||||
after_hours_forward_number varchar(20),
|
||||
is_active tinyint(1) DEFAULT 1,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY phone_number (phone_number)
|
||||
) $charset_collate;";
|
||||
|
||||
// Call queues table
|
||||
$table_queues = $wpdb->prefix . 'twp_call_queues';
|
||||
$sql_queues = "CREATE TABLE $table_queues (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
queue_name varchar(100) NOT NULL,
|
||||
max_size int(11) DEFAULT 10,
|
||||
wait_music_url varchar(255),
|
||||
tts_message text,
|
||||
timeout_seconds int(11) DEFAULT 300,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) $charset_collate;";
|
||||
|
||||
// Queued calls table
|
||||
$table_queued_calls = $wpdb->prefix . 'twp_queued_calls';
|
||||
$sql_queued_calls = "CREATE TABLE $table_queued_calls (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
queue_id int(11) NOT NULL,
|
||||
call_sid varchar(100) NOT NULL,
|
||||
from_number varchar(20) NOT NULL,
|
||||
to_number varchar(20) NOT NULL,
|
||||
position int(11) NOT NULL,
|
||||
status varchar(20) DEFAULT 'waiting',
|
||||
joined_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
answered_at datetime,
|
||||
ended_at datetime,
|
||||
PRIMARY KEY (id),
|
||||
KEY queue_id (queue_id),
|
||||
KEY call_sid (call_sid)
|
||||
) $charset_collate;";
|
||||
|
||||
// Workflows table
|
||||
$table_workflows = $wpdb->prefix . 'twp_workflows';
|
||||
$sql_workflows = "CREATE TABLE $table_workflows (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
workflow_name varchar(100) NOT NULL,
|
||||
phone_number varchar(20) NOT NULL,
|
||||
workflow_data longtext,
|
||||
is_active tinyint(1) DEFAULT 1,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY phone_number (phone_number)
|
||||
) $charset_collate;";
|
||||
|
||||
// Call log table
|
||||
$table_call_log = $wpdb->prefix . 'twp_call_log';
|
||||
$sql_call_log = "CREATE TABLE $table_call_log (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
call_sid varchar(100) NOT NULL,
|
||||
from_number varchar(20),
|
||||
to_number varchar(20),
|
||||
status varchar(20) NOT NULL,
|
||||
duration int(11) DEFAULT 0,
|
||||
workflow_id int(11),
|
||||
workflow_name varchar(100),
|
||||
queue_time int(11) DEFAULT 0,
|
||||
actions_taken text,
|
||||
call_data longtext,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY call_sid (call_sid),
|
||||
KEY from_number (from_number),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
|
||||
// SMS log table
|
||||
$table_sms_log = $wpdb->prefix . 'twp_sms_log';
|
||||
$sql_sms_log = "CREATE TABLE $table_sms_log (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
message_sid varchar(100) NOT NULL,
|
||||
from_number varchar(20) NOT NULL,
|
||||
to_number varchar(20) NOT NULL,
|
||||
body text,
|
||||
received_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY message_sid (message_sid)
|
||||
) $charset_collate;";
|
||||
|
||||
// Voicemails table
|
||||
$table_voicemails = $wpdb->prefix . 'twp_voicemails';
|
||||
$sql_voicemails = "CREATE TABLE $table_voicemails (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
workflow_id int(11),
|
||||
from_number varchar(20) NOT NULL,
|
||||
recording_url varchar(255),
|
||||
duration int(11) DEFAULT 0,
|
||||
transcription text,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY workflow_id (workflow_id)
|
||||
) $charset_collate;";
|
||||
|
||||
// Agent groups table
|
||||
$table_agent_groups = $wpdb->prefix . 'twp_agent_groups';
|
||||
$sql_agent_groups = "CREATE TABLE $table_agent_groups (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
group_name varchar(100) NOT NULL,
|
||||
description text,
|
||||
ring_strategy varchar(20) DEFAULT 'simultaneous',
|
||||
timeout_seconds int(11) DEFAULT 30,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY group_name (group_name)
|
||||
) $charset_collate;";
|
||||
|
||||
// Group members table
|
||||
$table_group_members = $wpdb->prefix . 'twp_group_members';
|
||||
$sql_group_members = "CREATE TABLE $table_group_members (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
group_id int(11) NOT NULL,
|
||||
user_id bigint(20) NOT NULL,
|
||||
priority int(11) DEFAULT 0,
|
||||
is_active tinyint(1) DEFAULT 1,
|
||||
added_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY group_id (group_id),
|
||||
KEY user_id (user_id),
|
||||
UNIQUE KEY group_user (group_id, user_id)
|
||||
) $charset_collate;";
|
||||
|
||||
// Agent status table
|
||||
$table_agent_status = $wpdb->prefix . 'twp_agent_status';
|
||||
$sql_agent_status = "CREATE TABLE $table_agent_status (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) NOT NULL,
|
||||
status varchar(20) DEFAULT 'offline',
|
||||
current_call_sid varchar(100),
|
||||
last_activity datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
available_for_queues tinyint(1) DEFAULT 1,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_id (user_id)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql_schedules);
|
||||
dbDelta($sql_queues);
|
||||
dbDelta($sql_queued_calls);
|
||||
dbDelta($sql_workflows);
|
||||
dbDelta($sql_call_log);
|
||||
dbDelta($sql_sms_log);
|
||||
dbDelta($sql_voicemails);
|
||||
// Callbacks table
|
||||
$table_callbacks = $wpdb->prefix . 'twp_callbacks';
|
||||
$sql_callbacks = "CREATE TABLE $table_callbacks (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
phone_number varchar(20) NOT NULL,
|
||||
requested_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
status varchar(20) DEFAULT 'pending',
|
||||
attempts int(11) DEFAULT 0,
|
||||
last_attempt datetime,
|
||||
completed_at datetime,
|
||||
queue_id int(11),
|
||||
original_call_sid varchar(100),
|
||||
callback_call_sid varchar(100),
|
||||
notes text,
|
||||
PRIMARY KEY (id),
|
||||
KEY phone_number (phone_number),
|
||||
KEY status (status),
|
||||
KEY queue_id (queue_id)
|
||||
) $charset_collate;";
|
||||
|
||||
dbDelta($sql_schedules);
|
||||
dbDelta($sql_queues);
|
||||
dbDelta($sql_queued_calls);
|
||||
dbDelta($sql_workflows);
|
||||
dbDelta($sql_call_log);
|
||||
dbDelta($sql_sms_log);
|
||||
dbDelta($sql_voicemails);
|
||||
dbDelta($sql_agent_groups);
|
||||
dbDelta($sql_group_members);
|
||||
dbDelta($sql_agent_status);
|
||||
dbDelta($sql_callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default plugin options
|
||||
*/
|
||||
private static function set_default_options() {
|
||||
add_option('twp_twilio_account_sid', '');
|
||||
add_option('twp_twilio_auth_token', '');
|
||||
add_option('twp_elevenlabs_api_key', '');
|
||||
add_option('twp_elevenlabs_voice_id', '');
|
||||
add_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2');
|
||||
add_option('twp_default_queue_timeout', 300);
|
||||
add_option('twp_default_queue_size', 10);
|
||||
add_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help');
|
||||
add_option('twp_sms_notification_number', '');
|
||||
}
|
||||
}
|
241
includes/class-twp-agent-groups.php
Normal file
241
includes/class-twp-agent-groups.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Groups management class
|
||||
*/
|
||||
class TWP_Agent_Groups {
|
||||
|
||||
/**
|
||||
* Create a new agent group
|
||||
*/
|
||||
public static function create_group($data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_agent_groups';
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'group_name' => sanitize_text_field($data['group_name']),
|
||||
'description' => sanitize_textarea_field($data['description']),
|
||||
'ring_strategy' => sanitize_text_field($data['ring_strategy'] ?? 'simultaneous'),
|
||||
'timeout_seconds' => intval($data['timeout_seconds'] ?? 30)
|
||||
),
|
||||
array('%s', '%s', '%s', '%d')
|
||||
);
|
||||
|
||||
return $result !== false ? $wpdb->insert_id : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an agent group
|
||||
*/
|
||||
public static function update_group($group_id, $data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_agent_groups';
|
||||
|
||||
$update_data = array();
|
||||
$update_format = array();
|
||||
|
||||
if (isset($data['group_name'])) {
|
||||
$update_data['group_name'] = sanitize_text_field($data['group_name']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$update_data['description'] = sanitize_textarea_field($data['description']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['ring_strategy'])) {
|
||||
$update_data['ring_strategy'] = sanitize_text_field($data['ring_strategy']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['timeout_seconds'])) {
|
||||
$update_data['timeout_seconds'] = intval($data['timeout_seconds']);
|
||||
$update_format[] = '%d';
|
||||
}
|
||||
|
||||
return $wpdb->update(
|
||||
$table_name,
|
||||
$update_data,
|
||||
array('id' => $group_id),
|
||||
$update_format,
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an agent group
|
||||
*/
|
||||
public static function delete_group($group_id) {
|
||||
global $wpdb;
|
||||
$groups_table = $wpdb->prefix . 'twp_agent_groups';
|
||||
$members_table = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
// Delete all members first
|
||||
$wpdb->delete($members_table, array('group_id' => $group_id), array('%d'));
|
||||
|
||||
// Delete the group
|
||||
return $wpdb->delete($groups_table, array('id' => $group_id), array('%d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups
|
||||
*/
|
||||
public static function get_all_groups() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_agent_groups';
|
||||
|
||||
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY group_name ASC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific group
|
||||
*/
|
||||
public static function get_group($group_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_agent_groups';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE id = %d",
|
||||
$group_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
*/
|
||||
public static function add_member($group_id, $user_id, $priority = 0) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
return $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'group_id' => intval($group_id),
|
||||
'user_id' => intval($user_id),
|
||||
'priority' => intval($priority),
|
||||
'is_active' => 1
|
||||
),
|
||||
array('%d', '%d', '%d', '%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user from group
|
||||
*/
|
||||
public static function remove_member($group_id, $user_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
return $wpdb->delete(
|
||||
$table_name,
|
||||
array(
|
||||
'group_id' => $group_id,
|
||||
'user_id' => $user_id
|
||||
),
|
||||
array('%d', '%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group members
|
||||
*/
|
||||
public static function get_group_members($group_id) {
|
||||
global $wpdb;
|
||||
$members_table = $wpdb->prefix . 'twp_group_members';
|
||||
$users_table = $wpdb->prefix . 'users';
|
||||
$usermeta_table = $wpdb->prefix . 'usermeta';
|
||||
|
||||
$query = $wpdb->prepare("
|
||||
SELECT
|
||||
gm.*,
|
||||
u.user_login,
|
||||
u.display_name,
|
||||
u.user_email,
|
||||
um.meta_value as phone_number
|
||||
FROM $members_table gm
|
||||
JOIN $users_table u ON gm.user_id = u.ID
|
||||
LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
|
||||
WHERE gm.group_id = %d
|
||||
ORDER BY gm.priority ASC, u.display_name ASC
|
||||
", $group_id);
|
||||
|
||||
return $wpdb->get_results($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members' phone numbers for a group
|
||||
*/
|
||||
public static function get_group_phone_numbers($group_id) {
|
||||
$members = self::get_group_members($group_id);
|
||||
$numbers = array();
|
||||
|
||||
foreach ($members as $member) {
|
||||
if ($member->phone_number && $member->is_active) {
|
||||
$numbers[] = array(
|
||||
'user_id' => $member->user_id,
|
||||
'phone_number' => $member->phone_number,
|
||||
'display_name' => $member->display_name,
|
||||
'priority' => $member->priority
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups for a user
|
||||
*/
|
||||
public static function get_user_groups($user_id) {
|
||||
global $wpdb;
|
||||
$groups_table = $wpdb->prefix . 'twp_agent_groups';
|
||||
$members_table = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
$query = $wpdb->prepare("
|
||||
SELECT g.*
|
||||
FROM $groups_table g
|
||||
JOIN $members_table gm ON g.id = gm.group_id
|
||||
WHERE gm.user_id = %d AND gm.is_active = 1
|
||||
ORDER BY g.group_name ASC
|
||||
", $user_id);
|
||||
|
||||
return $wpdb->get_results($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is in group
|
||||
*/
|
||||
public static function is_user_in_group($user_id, $group_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
$count = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $table_name WHERE group_id = %d AND user_id = %d",
|
||||
$group_id,
|
||||
$user_id
|
||||
));
|
||||
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update member status
|
||||
*/
|
||||
public static function update_member_status($group_id, $user_id, $is_active) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
return $wpdb->update(
|
||||
$table_name,
|
||||
array('is_active' => $is_active ? 1 : 0),
|
||||
array(
|
||||
'group_id' => $group_id,
|
||||
'user_id' => $user_id
|
||||
),
|
||||
array('%d'),
|
||||
array('%d', '%d')
|
||||
);
|
||||
}
|
||||
}
|
525
includes/class-twp-agent-manager.php
Normal file
525
includes/class-twp-agent-manager.php
Normal file
@@ -0,0 +1,525 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent management class for handling agent status and call distribution
|
||||
*/
|
||||
class TWP_Agent_Manager {
|
||||
|
||||
/**
|
||||
* Initialize agent manager
|
||||
*/
|
||||
public static function init() {
|
||||
// Add hooks for user profile fields
|
||||
add_action('show_user_profile', array(__CLASS__, 'add_user_profile_fields'));
|
||||
add_action('edit_user_profile', array(__CLASS__, 'add_user_profile_fields'));
|
||||
add_action('personal_options_update', array(__CLASS__, 'save_user_profile_fields'));
|
||||
add_action('edit_user_profile_update', array(__CLASS__, 'save_user_profile_fields'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add phone number field to user profile
|
||||
*/
|
||||
public static function add_user_profile_fields($user) {
|
||||
?>
|
||||
<h3>Twilio Phone Settings</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="twp_phone_number">Phone Number</label></th>
|
||||
<td>
|
||||
<input type="text" name="twp_phone_number" id="twp_phone_number"
|
||||
value="<?php echo esc_attr(get_user_meta($user->ID, 'twp_phone_number', true)); ?>"
|
||||
class="regular-text" placeholder="+1234567890" />
|
||||
<p class="description">Your phone number for receiving forwarded calls (include country code)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="twp_agent_status">Agent Status</label></th>
|
||||
<td>
|
||||
<?php
|
||||
$status = self::get_agent_status($user->ID);
|
||||
?>
|
||||
<select name="twp_agent_status" id="twp_agent_status">
|
||||
<option value="available" <?php selected($status->status ?? '', 'available'); ?>>Available</option>
|
||||
<option value="busy" <?php selected($status->status ?? '', 'busy'); ?>>Busy</option>
|
||||
<option value="offline" <?php selected($status->status ?? 'offline', 'offline'); ?>>Offline</option>
|
||||
</select>
|
||||
<p class="description">Your availability for receiving calls</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user profile fields
|
||||
*/
|
||||
public static function save_user_profile_fields($user_id) {
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save phone number with validation
|
||||
if (isset($_POST['twp_phone_number'])) {
|
||||
$phone_number = sanitize_text_field($_POST['twp_phone_number']);
|
||||
|
||||
// Validate phone number format
|
||||
if (!empty($phone_number)) {
|
||||
$validation_result = self::validate_phone_number($phone_number);
|
||||
|
||||
if ($validation_result['valid']) {
|
||||
// Check for duplicates
|
||||
$duplicate_user = self::is_phone_number_duplicate($validation_result['formatted'], $user_id);
|
||||
|
||||
if ($duplicate_user) {
|
||||
add_action('admin_notices', function() use ($duplicate_user) {
|
||||
echo '<div class="notice notice-error"><p>Phone number already in use by ' . esc_html($duplicate_user->display_name) . '</p></div>';
|
||||
});
|
||||
} else {
|
||||
update_user_meta($user_id, 'twp_phone_number', $validation_result['formatted']);
|
||||
}
|
||||
} else {
|
||||
add_action('admin_notices', function() use ($validation_result) {
|
||||
echo '<div class="notice notice-error"><p>Phone number error: ' . esc_html($validation_result['error']) . '</p></div>';
|
||||
});
|
||||
}
|
||||
} else {
|
||||
update_user_meta($user_id, 'twp_phone_number', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Save agent status
|
||||
if (isset($_POST['twp_agent_status'])) {
|
||||
self::set_agent_status_with_notification($user_id, sanitize_text_field($_POST['twp_agent_status']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set agent status
|
||||
*/
|
||||
public static function set_agent_status($user_id, $status, $call_sid = null) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_agent_status';
|
||||
|
||||
$existing = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE user_id = %d",
|
||||
$user_id
|
||||
));
|
||||
|
||||
if ($existing) {
|
||||
return $wpdb->update(
|
||||
$table_name,
|
||||
array(
|
||||
'status' => $status,
|
||||
'current_call_sid' => $call_sid,
|
||||
'last_activity' => current_time('mysql')
|
||||
),
|
||||
array('user_id' => $user_id),
|
||||
array('%s', '%s', '%s'),
|
||||
array('%d')
|
||||
);
|
||||
} else {
|
||||
return $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'status' => $status,
|
||||
'current_call_sid' => $call_sid,
|
||||
'last_activity' => current_time('mysql')
|
||||
),
|
||||
array('%d', '%s', '%s', '%s')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent status
|
||||
*/
|
||||
public static function get_agent_status($user_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_agent_status';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE user_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available agents
|
||||
*/
|
||||
public static function get_available_agents($group_id = null) {
|
||||
global $wpdb;
|
||||
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||
$users_table = $wpdb->prefix . 'users';
|
||||
$usermeta_table = $wpdb->prefix . 'usermeta';
|
||||
|
||||
if ($group_id) {
|
||||
// Get available agents from a specific group
|
||||
$members_table = $wpdb->prefix . 'twp_group_members';
|
||||
|
||||
$query = $wpdb->prepare("
|
||||
SELECT
|
||||
u.ID as user_id,
|
||||
u.display_name,
|
||||
um.meta_value as phone_number,
|
||||
s.status,
|
||||
gm.priority
|
||||
FROM $members_table gm
|
||||
JOIN $users_table u ON gm.user_id = u.ID
|
||||
LEFT JOIN $status_table s ON u.ID = s.user_id
|
||||
LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
|
||||
WHERE gm.group_id = %d
|
||||
AND gm.is_active = 1
|
||||
AND (s.status = 'available' OR s.status IS NULL)
|
||||
AND um.meta_value IS NOT NULL
|
||||
AND um.meta_value != ''
|
||||
ORDER BY gm.priority ASC, u.display_name ASC
|
||||
", $group_id);
|
||||
} else {
|
||||
// Get all available agents
|
||||
$query = "
|
||||
SELECT
|
||||
u.ID as user_id,
|
||||
u.display_name,
|
||||
um.meta_value as phone_number,
|
||||
s.status
|
||||
FROM $users_table u
|
||||
LEFT JOIN $status_table s ON u.ID = s.user_id
|
||||
LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
|
||||
WHERE (s.status = 'available' OR s.status IS NULL)
|
||||
AND um.meta_value IS NOT NULL
|
||||
AND um.meta_value != ''
|
||||
ORDER BY u.display_name ASC
|
||||
";
|
||||
}
|
||||
|
||||
return $wpdb->get_results($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a queued call
|
||||
*/
|
||||
public static function accept_queued_call($call_id, $user_id) {
|
||||
global $wpdb;
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// Get the call details
|
||||
$call = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $calls_table WHERE id = %d AND status = 'waiting'",
|
||||
$call_id
|
||||
));
|
||||
|
||||
if (!$call) {
|
||||
return array('success' => false, 'error' => 'Call not found or already answered');
|
||||
}
|
||||
|
||||
// Get user's phone number
|
||||
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
|
||||
|
||||
if (!$phone_number) {
|
||||
return array('success' => false, 'error' => 'No phone number configured for user');
|
||||
}
|
||||
|
||||
// Update call status
|
||||
$wpdb->update(
|
||||
$calls_table,
|
||||
array(
|
||||
'status' => 'answered',
|
||||
'answered_at' => current_time('mysql')
|
||||
),
|
||||
array('id' => $call_id),
|
||||
array('%s', '%s'),
|
||||
array('%d')
|
||||
);
|
||||
|
||||
// Set agent status to busy
|
||||
self::set_agent_status($user_id, 'busy', $call->call_sid);
|
||||
|
||||
// Forward the call to the agent
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
// Create TwiML to redirect the call
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$dial = $twiml->dial();
|
||||
$dial->number($phone_number, [
|
||||
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/call-status'),
|
||||
'statusCallbackEvent' => array('completed')
|
||||
]);
|
||||
|
||||
// Update the call with new TwiML
|
||||
$result = $twilio->update_call($call->call_sid, array(
|
||||
'Twiml' => $twiml->asXML()
|
||||
));
|
||||
|
||||
if ($result['success']) {
|
||||
// Log the call acceptance
|
||||
TWP_Call_Logger::log_call(array(
|
||||
'call_sid' => $call->call_sid,
|
||||
'from_number' => $call->from_number,
|
||||
'to_number' => $phone_number,
|
||||
'status' => 'agent_answered',
|
||||
'workflow_name' => 'Queue: Agent Accept',
|
||||
'actions_taken' => json_encode(array(
|
||||
'agent_id' => $user_id,
|
||||
'agent_name' => get_userdata($user_id)->display_name,
|
||||
'queue_id' => $call->queue_id
|
||||
))
|
||||
));
|
||||
|
||||
return array('success' => true);
|
||||
} else {
|
||||
return array('success' => false, 'error' => 'Failed to forward call');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle call status callback
|
||||
*/
|
||||
public static function handle_call_status($call_sid, $status) {
|
||||
global $wpdb;
|
||||
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||
|
||||
// If call completed, set agent back to available
|
||||
if ($status === 'completed') {
|
||||
$agent = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $status_table WHERE current_call_sid = %s",
|
||||
$call_sid
|
||||
));
|
||||
|
||||
if ($agent) {
|
||||
self::set_agent_status($agent->user_id, 'available', null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate simultaneous ring to group members
|
||||
*/
|
||||
public static function ring_group($group_id, $call_data) {
|
||||
$members = TWP_Agent_Groups::get_group_phone_numbers($group_id);
|
||||
|
||||
if (empty($members)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
|
||||
// Play a message while dialing
|
||||
$twiml->say('Please wait while we connect your call...', ['voice' => 'alice']);
|
||||
|
||||
// Create a dial with simultaneous ring
|
||||
$dial = $twiml->dial([
|
||||
'timeout' => 30,
|
||||
'action' => home_url('/wp-json/twilio-webhook/v1/dial-status'),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
|
||||
// Add each member's number to the dial
|
||||
foreach ($members as $member) {
|
||||
if ($member['phone_number']) {
|
||||
$dial->number($member['phone_number'], [
|
||||
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/member-status'),
|
||||
'statusCallbackEvent' => array('answered', 'completed')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// If no one answers, go to voicemail
|
||||
$twiml->say('All agents are currently unavailable. Please leave a message after the beep.', ['voice' => 'alice']);
|
||||
$twiml->record([
|
||||
'maxLength' => 120,
|
||||
'transcribe' => true,
|
||||
'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription')
|
||||
]);
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent dashboard stats
|
||||
*/
|
||||
public static function get_agent_stats($user_id) {
|
||||
global $wpdb;
|
||||
$log_table = $wpdb->prefix . 'twp_call_log';
|
||||
|
||||
// Get today's stats
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$stats = array(
|
||||
'calls_today' => $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $log_table
|
||||
WHERE actions_taken LIKE %s
|
||||
AND DATE(created_at) = %s",
|
||||
'%"agent_id":' . $user_id . '%',
|
||||
$today
|
||||
)),
|
||||
'total_calls' => $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $log_table
|
||||
WHERE actions_taken LIKE %s",
|
||||
'%"agent_id":' . $user_id . '%'
|
||||
)),
|
||||
'avg_duration' => $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT AVG(duration) FROM $log_table
|
||||
WHERE actions_taken LIKE %s
|
||||
AND duration > 0",
|
||||
'%"agent_id":' . $user_id . '%'
|
||||
))
|
||||
);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS notification to agent when they become available
|
||||
*/
|
||||
public static function notify_agent_availability($user_id, $status) {
|
||||
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
|
||||
$sms_number = get_option('twp_sms_notification_number');
|
||||
|
||||
if (empty($phone_number) || empty($sms_number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($status === 'available') {
|
||||
// Check for waiting calls immediately
|
||||
global $wpdb;
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
$waiting_count = $wpdb->get_var("SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'");
|
||||
|
||||
if ($waiting_count > 0) {
|
||||
$message = "You are now available. There are {$waiting_count} calls waiting. Text '1' to receive the next call.";
|
||||
} else {
|
||||
$message = "You are now available for calls. You'll receive notifications when calls are waiting.";
|
||||
}
|
||||
|
||||
$twilio = new TWP_Twilio_API();
|
||||
return $twilio->send_sms($phone_number, $message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if agent can receive calls (has phone number and is available)
|
||||
*/
|
||||
public static function can_agent_receive_calls($user_id) {
|
||||
$phone_number = get_user_meta($user_id, 'twp_phone_number', true);
|
||||
$status = self::get_agent_status($user_id);
|
||||
|
||||
return !empty($phone_number) && $status && $status->status === 'available';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agents by group who can receive calls
|
||||
*/
|
||||
public static function get_available_group_agents($group_id) {
|
||||
$group_members = TWP_Agent_Groups::get_group_members($group_id);
|
||||
$available_agents = array();
|
||||
|
||||
foreach ($group_members as $member) {
|
||||
if (self::can_agent_receive_calls($member->user_id)) {
|
||||
$phone_number = get_user_meta($member->user_id, 'twp_phone_number', true);
|
||||
if ($phone_number) {
|
||||
$available_agents[] = array(
|
||||
'user_id' => $member->user_id,
|
||||
'phone_number' => $phone_number,
|
||||
'priority' => $member->priority
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority (lower numbers = higher priority)
|
||||
usort($available_agents, function($a, $b) {
|
||||
return $a['priority'] - $b['priority'];
|
||||
});
|
||||
|
||||
return $available_agents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced set agent status with SMS notifications
|
||||
*/
|
||||
public static function set_agent_status_with_notification($user_id, $status, $call_sid = null) {
|
||||
$old_status = self::get_agent_status($user_id);
|
||||
$result = self::set_agent_status($user_id, $status, $call_sid);
|
||||
|
||||
// Send SMS notification if status changed to available
|
||||
if ($result && $status === 'available' && (!$old_status || $old_status->status !== 'available')) {
|
||||
self::notify_agent_availability($user_id, $status);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate phone number format
|
||||
*/
|
||||
public static function validate_phone_number($phone_number) {
|
||||
$phone = trim($phone_number);
|
||||
|
||||
// Remove any non-numeric characters except + and spaces
|
||||
$cleaned = preg_replace('/[^0-9+\s\-\(\)]/', '', $phone);
|
||||
|
||||
// Check if it starts with + (international format)
|
||||
if (strpos($cleaned, '+') === 0) {
|
||||
$formatted = preg_replace('/[^0-9+]/', '', $cleaned);
|
||||
|
||||
// Must be at least 10 digits after the +
|
||||
if (strlen($formatted) >= 11 && strlen($formatted) <= 16) {
|
||||
return array(
|
||||
'valid' => true,
|
||||
'formatted' => $formatted
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'valid' => false,
|
||||
'error' => 'Phone number must be 10-15 digits with country code (e.g., +1234567890)'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Check if it's a US number without country code
|
||||
$digits = preg_replace('/[^0-9]/', '', $cleaned);
|
||||
|
||||
if (strlen($digits) === 10) {
|
||||
// Assume US number, add +1
|
||||
return array(
|
||||
'valid' => true,
|
||||
'formatted' => '+1' . $digits
|
||||
);
|
||||
} else if (strlen($digits) === 11 && substr($digits, 0, 1) === '1') {
|
||||
// US number with 1 prefix
|
||||
return array(
|
||||
'valid' => true,
|
||||
'formatted' => '+' . $digits
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'valid' => false,
|
||||
'error' => 'Invalid phone number format. Use +1234567890 or 1234567890 format'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if phone number is already in use by another agent
|
||||
*/
|
||||
public static function is_phone_number_duplicate($phone_number, $exclude_user_id = null) {
|
||||
$users = get_users(array(
|
||||
'meta_key' => 'twp_phone_number',
|
||||
'meta_value' => $phone_number,
|
||||
'meta_compare' => '='
|
||||
));
|
||||
|
||||
foreach ($users as $user) {
|
||||
if ($exclude_user_id && $user->ID == $exclude_user_id) {
|
||||
continue;
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
258
includes/class-twp-call-logger.php
Normal file
258
includes/class-twp-call-logger.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
/**
|
||||
* Call logging class
|
||||
*/
|
||||
class TWP_Call_Logger {
|
||||
|
||||
/**
|
||||
* Log a new call
|
||||
*/
|
||||
public static function log_call($call_data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_log';
|
||||
|
||||
$data = array(
|
||||
'call_sid' => sanitize_text_field($call_data['call_sid']),
|
||||
'from_number' => sanitize_text_field($call_data['from_number'] ?? ''),
|
||||
'to_number' => sanitize_text_field($call_data['to_number'] ?? ''),
|
||||
'status' => sanitize_text_field($call_data['status'] ?? 'initiated'),
|
||||
'duration' => intval($call_data['duration'] ?? 0),
|
||||
'workflow_id' => intval($call_data['workflow_id'] ?? 0),
|
||||
'workflow_name' => sanitize_text_field($call_data['workflow_name'] ?? ''),
|
||||
'queue_time' => intval($call_data['queue_time'] ?? 0),
|
||||
'actions_taken' => sanitize_textarea_field($call_data['actions_taken'] ?? ''),
|
||||
'call_data' => json_encode($call_data)
|
||||
);
|
||||
|
||||
$format = array('%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%s', '%s');
|
||||
|
||||
return $wpdb->insert($table_name, $data, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update call log
|
||||
*/
|
||||
public static function update_call($call_sid, $updates) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_log';
|
||||
|
||||
$update_data = array();
|
||||
$format = array();
|
||||
|
||||
if (isset($updates['status'])) {
|
||||
$update_data['status'] = sanitize_text_field($updates['status']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($updates['duration'])) {
|
||||
$update_data['duration'] = intval($updates['duration']);
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (isset($updates['queue_time'])) {
|
||||
$update_data['queue_time'] = intval($updates['queue_time']);
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (isset($updates['actions_taken'])) {
|
||||
// Append to existing actions
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT actions_taken FROM $table_name WHERE call_sid = %s",
|
||||
$call_sid
|
||||
));
|
||||
|
||||
$new_action = sanitize_textarea_field($updates['actions_taken']);
|
||||
$update_data['actions_taken'] = $existing ? $existing . '; ' . $new_action : $new_action;
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($updates['call_data'])) {
|
||||
// Merge with existing call data
|
||||
$existing_data = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT call_data FROM $table_name WHERE call_sid = %s",
|
||||
$call_sid
|
||||
));
|
||||
|
||||
$existing_array = $existing_data ? json_decode($existing_data, true) : array();
|
||||
$new_data = array_merge($existing_array, $updates['call_data']);
|
||||
$update_data['call_data'] = json_encode($new_data);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (empty($update_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $wpdb->update(
|
||||
$table_name,
|
||||
$update_data,
|
||||
array('call_sid' => $call_sid),
|
||||
$format,
|
||||
array('%s')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get call log by SID
|
||||
*/
|
||||
public static function get_call_log($call_sid) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_log';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE call_sid = %s",
|
||||
$call_sid
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get call logs with filtering
|
||||
*/
|
||||
public static function get_call_logs($filters = array(), $limit = 100, $offset = 0) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_log';
|
||||
|
||||
$where_clauses = array();
|
||||
$where_values = array();
|
||||
|
||||
if (!empty($filters['from_number'])) {
|
||||
$where_clauses[] = "from_number = %s";
|
||||
$where_values[] = $filters['from_number'];
|
||||
}
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$where_clauses[] = "status = %s";
|
||||
$where_values[] = $filters['status'];
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$where_clauses[] = "DATE(created_at) >= %s";
|
||||
$where_values[] = $filters['date_from'];
|
||||
}
|
||||
|
||||
if (!empty($filters['date_to'])) {
|
||||
$where_clauses[] = "DATE(created_at) <= %s";
|
||||
$where_values[] = $filters['date_to'];
|
||||
}
|
||||
|
||||
$where_sql = '';
|
||||
if (!empty($where_clauses)) {
|
||||
$where_sql = 'WHERE ' . implode(' AND ', $where_clauses);
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM $table_name $where_sql ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$where_values[] = $limit;
|
||||
$where_values[] = $offset;
|
||||
|
||||
return $wpdb->get_results($wpdb->prepare($sql, $where_values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get call statistics
|
||||
*/
|
||||
public static function get_call_statistics($period = 'today') {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_log';
|
||||
|
||||
$where_clause = '';
|
||||
switch ($period) {
|
||||
case 'today':
|
||||
$where_clause = "WHERE DATE(created_at) = CURDATE()";
|
||||
break;
|
||||
case 'week':
|
||||
$where_clause = "WHERE YEARWEEK(created_at) = YEARWEEK(NOW())";
|
||||
break;
|
||||
case 'month':
|
||||
$where_clause = "WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())";
|
||||
break;
|
||||
}
|
||||
|
||||
$stats = array();
|
||||
|
||||
// Total calls
|
||||
$stats['total_calls'] = $wpdb->get_var("SELECT COUNT(*) FROM $table_name $where_clause");
|
||||
|
||||
// Answered calls
|
||||
$stats['answered_calls'] = $wpdb->get_var("SELECT COUNT(*) FROM $table_name $where_clause AND status = 'completed' AND duration > 0");
|
||||
|
||||
// Average duration
|
||||
$avg_duration = $wpdb->get_var("SELECT AVG(duration) FROM $table_name $where_clause AND duration > 0");
|
||||
$stats['avg_duration'] = $avg_duration ? round($avg_duration) : 0;
|
||||
|
||||
// Average queue time
|
||||
$avg_queue_time = $wpdb->get_var("SELECT AVG(queue_time) FROM $table_name $where_clause AND queue_time > 0");
|
||||
$stats['avg_queue_time'] = $avg_queue_time ? round($avg_queue_time) : 0;
|
||||
|
||||
// Call status breakdown
|
||||
$status_breakdown = $wpdb->get_results("
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM $table_name $where_clause
|
||||
GROUP BY status
|
||||
", ARRAY_A);
|
||||
|
||||
$stats['status_breakdown'] = array();
|
||||
foreach ($status_breakdown as $status) {
|
||||
$stats['status_breakdown'][$status['status']] = $status['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log action taken during call
|
||||
*/
|
||||
public static function log_action($call_sid, $action) {
|
||||
$timestamp = current_time('mysql');
|
||||
$action_with_time = "[{$timestamp}] {$action}";
|
||||
|
||||
return self::update_call($call_sid, array(
|
||||
'actions_taken' => $action_with_time
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get call timeline
|
||||
*/
|
||||
public static function get_call_timeline($call_sid) {
|
||||
$log = self::get_call_log($call_sid);
|
||||
if (!$log) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$timeline = array();
|
||||
$call_data = json_decode($log->call_data, true) ?: array();
|
||||
|
||||
// Parse actions taken
|
||||
if ($log->actions_taken) {
|
||||
$actions = explode(';', $log->actions_taken);
|
||||
foreach ($actions as $action) {
|
||||
$action = trim($action);
|
||||
if (preg_match('/\[(.*?)\] (.*)/', $action, $matches)) {
|
||||
$timeline[] = array(
|
||||
'time' => $matches[1],
|
||||
'event' => $matches[2],
|
||||
'type' => 'action'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add status changes from call data
|
||||
if (isset($call_data['status_changes'])) {
|
||||
foreach ($call_data['status_changes'] as $change) {
|
||||
$timeline[] = array(
|
||||
'time' => $change['timestamp'],
|
||||
'event' => 'Call status changed to: ' . $change['status'],
|
||||
'type' => 'status'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by time
|
||||
usort($timeline, function($a, $b) {
|
||||
return strtotime($a['time']) - strtotime($b['time']);
|
||||
});
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
}
|
361
includes/class-twp-call-queue.php
Normal file
361
includes/class-twp-call-queue.php
Normal file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
/**
|
||||
* Call queue management class
|
||||
*/
|
||||
class TWP_Call_Queue {
|
||||
|
||||
/**
|
||||
* Add call to queue
|
||||
*/
|
||||
public static function add_to_queue($queue_id, $call_data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// Get current position in queue
|
||||
$max_position = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT MAX(position) FROM $table_name WHERE queue_id = %d AND status = 'waiting'",
|
||||
$queue_id
|
||||
));
|
||||
|
||||
$position = $max_position ? $max_position + 1 : 1;
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'queue_id' => $queue_id,
|
||||
'call_sid' => sanitize_text_field($call_data['call_sid']),
|
||||
'from_number' => sanitize_text_field($call_data['from_number']),
|
||||
'to_number' => sanitize_text_field($call_data['to_number']),
|
||||
'position' => $position,
|
||||
'status' => 'waiting'
|
||||
),
|
||||
array('%d', '%s', '%s', '%s', '%d', '%s')
|
||||
);
|
||||
|
||||
return $result !== false ? $position : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove call from queue
|
||||
*/
|
||||
public static function remove_from_queue($call_sid) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// Get call info before removing
|
||||
$call = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE call_sid = %s",
|
||||
$call_sid
|
||||
));
|
||||
|
||||
if ($call) {
|
||||
// Update status
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array(
|
||||
'status' => 'completed',
|
||||
'ended_at' => current_time('mysql')
|
||||
),
|
||||
array('call_sid' => $call_sid),
|
||||
array('%s', '%s'),
|
||||
array('%s')
|
||||
);
|
||||
|
||||
// Reorder queue positions
|
||||
self::reorder_queue($call->queue_id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next call in queue
|
||||
*/
|
||||
public static function get_next_call($queue_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name
|
||||
WHERE queue_id = %d AND status = 'waiting'
|
||||
ORDER BY position ASC
|
||||
LIMIT 1",
|
||||
$queue_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer queued call
|
||||
*/
|
||||
public static function answer_call($call_sid, $agent_number) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// Update call status
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array(
|
||||
'status' => 'answered',
|
||||
'answered_at' => current_time('mysql')
|
||||
),
|
||||
array('call_sid' => $call_sid),
|
||||
array('%s', '%s'),
|
||||
array('%s')
|
||||
);
|
||||
|
||||
// Connect call to agent
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$twilio->forward_call($call_sid, $agent_number);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process waiting calls
|
||||
*/
|
||||
public function process_waiting_calls() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
||||
|
||||
// Get all active queues
|
||||
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
|
||||
|
||||
foreach ($queues as $queue) {
|
||||
// Check for timed out calls
|
||||
$timeout_time = date('Y-m-d H:i:s', strtotime('-' . $queue->timeout_seconds . ' seconds'));
|
||||
|
||||
$timed_out_calls = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table_name
|
||||
WHERE queue_id = %d
|
||||
AND status = 'waiting'
|
||||
AND joined_at <= %s",
|
||||
$queue->id,
|
||||
$timeout_time
|
||||
));
|
||||
|
||||
foreach ($timed_out_calls as $call) {
|
||||
// Handle timeout
|
||||
$this->handle_timeout($call, $queue);
|
||||
}
|
||||
|
||||
// Update caller positions and play position messages
|
||||
$this->update_queue_positions($queue->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle call timeout
|
||||
*/
|
||||
private function handle_timeout($call, $queue) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// Update status
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array(
|
||||
'status' => 'timeout',
|
||||
'ended_at' => current_time('mysql')
|
||||
),
|
||||
array('id' => $call->id),
|
||||
array('%s', '%s'),
|
||||
array('%d')
|
||||
);
|
||||
|
||||
// Offer callback instead of hanging up
|
||||
$callback_twiml = TWP_Callback_Manager::create_callback_twiml($queue->id, $call->from_number);
|
||||
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$twilio->update_call($call->call_sid, array(
|
||||
'Twiml' => $callback_twiml
|
||||
));
|
||||
|
||||
// Reorder queue
|
||||
self::reorder_queue($queue->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update queue positions
|
||||
*/
|
||||
private function update_queue_positions($queue_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table_name
|
||||
WHERE queue_id = %d AND status = 'waiting'
|
||||
ORDER BY position ASC",
|
||||
$queue_id
|
||||
));
|
||||
|
||||
foreach ($waiting_calls as $index => $call) {
|
||||
$position = $index + 1;
|
||||
|
||||
// Update position if changed
|
||||
if ($call->position != $position) {
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array('position' => $position),
|
||||
array('id' => $call->id),
|
||||
array('%d'),
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
|
||||
// Announce position every 30 seconds
|
||||
$last_announcement = get_transient('twp_queue_announce_' . $call->call_sid);
|
||||
|
||||
if (!$last_announcement) {
|
||||
$this->announce_position($call, $position);
|
||||
set_transient('twp_queue_announce_' . $call->call_sid, true, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce queue position
|
||||
*/
|
||||
private function announce_position($call, $position) {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$elevenlabs = new TWP_ElevenLabs_API();
|
||||
|
||||
$message = "You are currently number $position in the queue. Please hold and an agent will be with you shortly.";
|
||||
|
||||
// Generate TTS audio
|
||||
$audio_result = $elevenlabs->text_to_speech($message);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
// Create TwiML with audio
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||
$play->addAttribute('loop', '0');
|
||||
|
||||
// Add wait music
|
||||
$queue = self::get_queue($call->queue_id);
|
||||
if ($queue && $queue->wait_music_url) {
|
||||
$play_music = $twiml->addChild('Play', $queue->wait_music_url);
|
||||
$play_music->addAttribute('loop', '0');
|
||||
}
|
||||
|
||||
$twilio->update_call($call->call_sid, array(
|
||||
'Twiml' => $twiml->asXML()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder queue positions
|
||||
*/
|
||||
private static function reorder_queue($queue_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
$waiting_calls = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id FROM $table_name
|
||||
WHERE queue_id = %d AND status = 'waiting'
|
||||
ORDER BY position ASC",
|
||||
$queue_id
|
||||
));
|
||||
|
||||
foreach ($waiting_calls as $index => $call) {
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array('position' => $index + 1),
|
||||
array('id' => $call->id),
|
||||
array('%d'),
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create queue
|
||||
*/
|
||||
public static function create_queue($data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_queues';
|
||||
|
||||
return $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'queue_name' => sanitize_text_field($data['queue_name']),
|
||||
'max_size' => intval($data['max_size']),
|
||||
'wait_music_url' => esc_url_raw($data['wait_music_url']),
|
||||
'tts_message' => sanitize_textarea_field($data['tts_message']),
|
||||
'timeout_seconds' => intval($data['timeout_seconds'])
|
||||
),
|
||||
array('%s', '%d', '%s', '%s', '%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue
|
||||
*/
|
||||
public static function get_queue($queue_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_queues';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE id = %d",
|
||||
$queue_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all queues
|
||||
*/
|
||||
public static function get_all_queues() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_call_queues';
|
||||
|
||||
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY queue_name ASC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete queue
|
||||
*/
|
||||
public static function delete_queue($queue_id) {
|
||||
global $wpdb;
|
||||
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
// First delete all queued calls for this queue
|
||||
$wpdb->delete($calls_table, array('queue_id' => $queue_id), array('%d'));
|
||||
|
||||
// Then delete the queue itself
|
||||
return $wpdb->delete($queue_table, array('id' => $queue_id), array('%d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue status
|
||||
*/
|
||||
public static function get_queue_status() {
|
||||
global $wpdb;
|
||||
$queue_table = $wpdb->prefix . 'twp_call_queues';
|
||||
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||
|
||||
$queues = $wpdb->get_results("SELECT * FROM $queue_table");
|
||||
|
||||
$status = array();
|
||||
|
||||
foreach ($queues as $queue) {
|
||||
$waiting_count = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $calls_table WHERE queue_id = %d AND status = 'waiting'",
|
||||
$queue->id
|
||||
));
|
||||
|
||||
$status[] = array(
|
||||
'queue_id' => $queue->id,
|
||||
'queue_name' => $queue->queue_name,
|
||||
'waiting_calls' => $waiting_count,
|
||||
'max_size' => $queue->max_size,
|
||||
'available_slots' => $queue->max_size - $waiting_count
|
||||
);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
341
includes/class-twp-callback-manager.php
Normal file
341
includes/class-twp-callback-manager.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
/**
|
||||
* Callback management class for queue callbacks and outbound calling
|
||||
*/
|
||||
class TWP_Callback_Manager {
|
||||
|
||||
/**
|
||||
* Request a callback from queue
|
||||
*/
|
||||
public static function request_callback($phone_number, $queue_id = null, $call_sid = null) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_callbacks';
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'phone_number' => sanitize_text_field($phone_number),
|
||||
'queue_id' => $queue_id ? intval($queue_id) : null,
|
||||
'original_call_sid' => $call_sid,
|
||||
'status' => 'pending'
|
||||
),
|
||||
array('%s', '%d', '%s', '%s')
|
||||
);
|
||||
|
||||
if ($result !== false) {
|
||||
// Send confirmation SMS if configured
|
||||
$sms_number = get_option('twp_sms_notification_number');
|
||||
if ($sms_number) {
|
||||
$message = "Callback requested for " . $phone_number . ". We'll call you back shortly.";
|
||||
self::send_sms($phone_number, $message);
|
||||
}
|
||||
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pending callbacks
|
||||
*/
|
||||
public static function process_callbacks() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_callbacks';
|
||||
|
||||
// Get pending callbacks older than 2 minutes (to avoid immediate callback)
|
||||
$callbacks = $wpdb->get_results("
|
||||
SELECT * FROM $table_name
|
||||
WHERE status = 'pending'
|
||||
AND requested_at <= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
|
||||
AND attempts < 3
|
||||
ORDER BY requested_at ASC
|
||||
LIMIT 10
|
||||
");
|
||||
|
||||
foreach ($callbacks as $callback) {
|
||||
self::initiate_callback($callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a callback
|
||||
*/
|
||||
private static function initiate_callback($callback) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_callbacks';
|
||||
|
||||
// Find an available agent
|
||||
$available_agent = TWP_Agent_Manager::get_available_agents();
|
||||
|
||||
if (empty($available_agent)) {
|
||||
// No agents available, try again later
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array('last_attempt' => current_time('mysql')),
|
||||
array('id' => $callback->id),
|
||||
array('%s'),
|
||||
array('%d')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$agent = $available_agent[0]; // Get first available agent
|
||||
|
||||
// Create a conference call
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
// First call the agent
|
||||
$agent_call_result = $twilio->make_call(
|
||||
$agent->phone_number,
|
||||
home_url('/wp-json/twilio-webhook/v1/callback-agent'),
|
||||
array(
|
||||
'callback_id' => $callback->id,
|
||||
'customer_number' => $callback->phone_number
|
||||
)
|
||||
);
|
||||
|
||||
if ($agent_call_result['success']) {
|
||||
// Update callback status
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array(
|
||||
'status' => 'calling',
|
||||
'attempts' => $callback->attempts + 1,
|
||||
'last_attempt' => current_time('mysql'),
|
||||
'callback_call_sid' => $agent_call_result['call_sid']
|
||||
),
|
||||
array('id' => $callback->id),
|
||||
array('%s', '%d', '%s', '%s'),
|
||||
array('%d')
|
||||
);
|
||||
|
||||
// Set agent to busy
|
||||
TWP_Agent_Manager::set_agent_status($agent->user_id, 'busy', $agent_call_result['call_sid']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle callback agent answered
|
||||
*/
|
||||
public static function handle_agent_answered($callback_id, $agent_call_sid) {
|
||||
global $wpdb;
|
||||
$callbacks_table = $wpdb->prefix . 'twp_callbacks';
|
||||
|
||||
$callback = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $callbacks_table WHERE id = %d",
|
||||
$callback_id
|
||||
));
|
||||
|
||||
if (!$callback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now call the customer and conference them in
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
$customer_call_result = $twilio->make_call(
|
||||
$callback->phone_number,
|
||||
home_url('/wp-json/twilio-webhook/v1/callback-customer'),
|
||||
array(
|
||||
'agent_call_sid' => $agent_call_sid,
|
||||
'callback_id' => $callback_id
|
||||
)
|
||||
);
|
||||
|
||||
if ($customer_call_result['success']) {
|
||||
// Update callback status
|
||||
$wpdb->update(
|
||||
$callbacks_table,
|
||||
array('status' => 'connecting'),
|
||||
array('id' => $callback_id),
|
||||
array('%s'),
|
||||
array('%d')
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete callback
|
||||
*/
|
||||
public static function complete_callback($callback_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_callbacks';
|
||||
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
array(
|
||||
'status' => 'completed',
|
||||
'completed_at' => current_time('mysql')
|
||||
),
|
||||
array('id' => $callback_id),
|
||||
array('%s', '%s'),
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate outbound call (click-to-call)
|
||||
*/
|
||||
public static function initiate_outbound_call($to_number, $agent_user_id) {
|
||||
$agent_phone = get_user_meta($agent_user_id, 'twp_phone_number', true);
|
||||
|
||||
if (!$agent_phone) {
|
||||
return array('success' => false, 'error' => 'No phone number configured');
|
||||
}
|
||||
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
// First call the agent
|
||||
$agent_call_result = $twilio->make_call(
|
||||
$agent_phone,
|
||||
home_url('/wp-json/twilio-webhook/v1/outbound-agent'),
|
||||
array(
|
||||
'target_number' => $to_number,
|
||||
'agent_user_id' => $agent_user_id
|
||||
)
|
||||
);
|
||||
|
||||
if ($agent_call_result['success']) {
|
||||
// Set agent to busy
|
||||
TWP_Agent_Manager::set_agent_status($agent_user_id, 'busy', $agent_call_result['call_sid']);
|
||||
|
||||
// Log the outbound call
|
||||
TWP_Call_Logger::log_call(array(
|
||||
'call_sid' => $agent_call_result['call_sid'],
|
||||
'from_number' => $agent_phone,
|
||||
'to_number' => $to_number,
|
||||
'status' => 'outbound_initiated',
|
||||
'workflow_name' => 'Outbound Call',
|
||||
'actions_taken' => json_encode(array(
|
||||
'agent_id' => $agent_user_id,
|
||||
'agent_name' => get_userdata($agent_user_id)->display_name,
|
||||
'type' => 'click_to_call'
|
||||
))
|
||||
));
|
||||
|
||||
return array('success' => true, 'call_sid' => $agent_call_result['call_sid']);
|
||||
}
|
||||
|
||||
return array('success' => false, 'error' => $agent_call_result['error']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outbound agent answered
|
||||
*/
|
||||
public static function handle_outbound_agent_answered($target_number, $agent_call_sid) {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
// Create TwiML to call the target number
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$twiml->say('Connecting your call...', ['voice' => 'alice']);
|
||||
|
||||
$dial = $twiml->dial([
|
||||
'callerId' => get_option('twp_caller_id_number', ''), // Use configured caller ID
|
||||
'timeout' => 30
|
||||
]);
|
||||
$dial->number($target_number);
|
||||
|
||||
// If no answer, leave a message
|
||||
$twiml->say('The number you called is not available. Please try again later.', ['voice' => 'alice']);
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create callback option TwiML for queue
|
||||
*/
|
||||
public static function create_callback_twiml($queue_id, $caller_number) {
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
|
||||
$gather = $twiml->gather([
|
||||
'numDigits' => 1,
|
||||
'timeout' => 10,
|
||||
'action' => home_url('/wp-json/twilio-webhook/v1/callback-choice'),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
|
||||
$gather->say(
|
||||
'You are currently in the queue. Press 1 to wait on the line, or press 2 to request a callback.',
|
||||
['voice' => 'alice']
|
||||
);
|
||||
|
||||
// Default to callback if no input
|
||||
$twiml->say('No input received. Requesting callback for you.', ['voice' => 'alice']);
|
||||
$twiml->redirect(home_url('/wp-json/twilio-webhook/v1/request-callback?' . http_build_query([
|
||||
'queue_id' => $queue_id,
|
||||
'phone_number' => $caller_number
|
||||
])));
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS notification
|
||||
*/
|
||||
private static function send_sms($to_number, $message) {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
return $twilio->send_sms($to_number, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get callback statistics
|
||||
*/
|
||||
public static function get_callback_stats($days = 7) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_callbacks';
|
||||
|
||||
$since_date = date('Y-m-d H:i:s', strtotime("-$days days"));
|
||||
|
||||
$stats = array(
|
||||
'total_requests' => $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $table_name WHERE requested_at >= %s",
|
||||
$since_date
|
||||
)),
|
||||
'completed' => $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $table_name WHERE requested_at >= %s AND status = 'completed'",
|
||||
$since_date
|
||||
)),
|
||||
'pending' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'pending'"),
|
||||
'avg_completion_time' => $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT AVG(TIMESTAMPDIFF(MINUTE, requested_at, completed_at))
|
||||
FROM $table_name
|
||||
WHERE requested_at >= %s AND status = 'completed'",
|
||||
$since_date
|
||||
))
|
||||
);
|
||||
|
||||
$stats['success_rate'] = $stats['total_requests'] > 0 ?
|
||||
round(($stats['completed'] / $stats['total_requests']) * 100, 1) : 0;
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending callbacks for admin
|
||||
*/
|
||||
public static function get_pending_callbacks() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_callbacks';
|
||||
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||
|
||||
return $wpdb->get_results("
|
||||
SELECT
|
||||
c.*,
|
||||
q.queue_name,
|
||||
TIMESTAMPDIFF(MINUTE, c.requested_at, NOW()) as wait_minutes
|
||||
FROM $table_name c
|
||||
LEFT JOIN $queues_table q ON c.queue_id = q.id
|
||||
WHERE c.status IN ('pending', 'calling', 'connecting')
|
||||
ORDER BY c.requested_at ASC
|
||||
");
|
||||
}
|
||||
}
|
231
includes/class-twp-core.php
Normal file
231
includes/class-twp-core.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* Core plugin class
|
||||
*/
|
||||
class TWP_Core {
|
||||
|
||||
protected $loader;
|
||||
protected $plugin_name;
|
||||
protected $version;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->version = TWP_VERSION;
|
||||
$this->plugin_name = 'twilio-wp-plugin';
|
||||
|
||||
$this->load_dependencies();
|
||||
$this->set_locale();
|
||||
$this->define_admin_hooks();
|
||||
$this->define_public_hooks();
|
||||
$this->define_api_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load required dependencies
|
||||
*/
|
||||
private function load_dependencies() {
|
||||
// Loader class
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-loader.php';
|
||||
|
||||
// API classes
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
|
||||
|
||||
// Feature classes
|
||||
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-workflow.php';
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-webhooks.php';
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-logger.php';
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-agent-groups.php';
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-agent-manager.php';
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-callback-manager.php';
|
||||
|
||||
// Admin classes
|
||||
require_once TWP_PLUGIN_DIR . 'admin/class-twp-admin.php';
|
||||
|
||||
$this->loader = new TWP_Loader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define locale for internationalization
|
||||
*/
|
||||
private function set_locale() {
|
||||
add_action('plugins_loaded', function() {
|
||||
load_plugin_textdomain(
|
||||
'twilio-wp-plugin',
|
||||
false,
|
||||
dirname(TWP_PLUGIN_BASENAME) . '/languages/'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin hooks
|
||||
*/
|
||||
private function define_admin_hooks() {
|
||||
$plugin_admin = new TWP_Admin($this->get_plugin_name(), $this->get_version());
|
||||
|
||||
$this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_styles');
|
||||
$this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts');
|
||||
$this->loader->add_action('admin_menu', $plugin_admin, 'add_plugin_admin_menu');
|
||||
$this->loader->add_action('admin_init', $plugin_admin, 'register_settings');
|
||||
$this->loader->add_action('admin_notices', $plugin_admin, 'show_admin_notices');
|
||||
|
||||
// AJAX handlers
|
||||
$this->loader->add_action('wp_ajax_twp_save_schedule', $plugin_admin, 'ajax_save_schedule');
|
||||
$this->loader->add_action('wp_ajax_twp_delete_schedule', $plugin_admin, 'ajax_delete_schedule');
|
||||
$this->loader->add_action('wp_ajax_twp_save_workflow', $plugin_admin, 'ajax_save_workflow');
|
||||
$this->loader->add_action('wp_ajax_twp_get_workflow', $plugin_admin, 'ajax_get_workflow');
|
||||
$this->loader->add_action('wp_ajax_twp_delete_workflow', $plugin_admin, 'ajax_delete_workflow');
|
||||
$this->loader->add_action('wp_ajax_twp_test_call', $plugin_admin, 'ajax_test_call');
|
||||
|
||||
// Phone number management AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_get_phone_numbers', $plugin_admin, 'ajax_get_phone_numbers');
|
||||
$this->loader->add_action('wp_ajax_twp_search_available_numbers', $plugin_admin, 'ajax_search_available_numbers');
|
||||
$this->loader->add_action('wp_ajax_twp_purchase_number', $plugin_admin, 'ajax_purchase_number');
|
||||
$this->loader->add_action('wp_ajax_twp_configure_number', $plugin_admin, 'ajax_configure_number');
|
||||
$this->loader->add_action('wp_ajax_twp_release_number', $plugin_admin, 'ajax_release_number');
|
||||
|
||||
// Queue management AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_get_queue', $plugin_admin, 'ajax_get_queue');
|
||||
$this->loader->add_action('wp_ajax_twp_save_queue', $plugin_admin, 'ajax_save_queue');
|
||||
$this->loader->add_action('wp_ajax_twp_get_queue_details', $plugin_admin, 'ajax_get_queue_details');
|
||||
$this->loader->add_action('wp_ajax_twp_get_all_queues', $plugin_admin, 'ajax_get_all_queues');
|
||||
$this->loader->add_action('wp_ajax_twp_delete_queue', $plugin_admin, 'ajax_delete_queue');
|
||||
$this->loader->add_action('wp_ajax_twp_get_dashboard_stats', $plugin_admin, 'ajax_get_dashboard_stats');
|
||||
|
||||
// Eleven Labs AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
|
||||
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
|
||||
$this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');
|
||||
|
||||
// Voicemail management AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_get_voicemail', $plugin_admin, 'ajax_get_voicemail');
|
||||
$this->loader->add_action('wp_ajax_twp_delete_voicemail', $plugin_admin, 'ajax_delete_voicemail');
|
||||
$this->loader->add_action('wp_ajax_twp_transcribe_voicemail', $plugin_admin, 'ajax_transcribe_voicemail');
|
||||
|
||||
// Agent group management AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_get_all_groups', $plugin_admin, 'ajax_get_all_groups');
|
||||
$this->loader->add_action('wp_ajax_twp_get_group', $plugin_admin, 'ajax_get_group');
|
||||
$this->loader->add_action('wp_ajax_twp_save_group', $plugin_admin, 'ajax_save_group');
|
||||
$this->loader->add_action('wp_ajax_twp_delete_group', $plugin_admin, 'ajax_delete_group');
|
||||
$this->loader->add_action('wp_ajax_twp_get_group_members', $plugin_admin, 'ajax_get_group_members');
|
||||
$this->loader->add_action('wp_ajax_twp_add_group_member', $plugin_admin, 'ajax_add_group_member');
|
||||
$this->loader->add_action('wp_ajax_twp_remove_group_member', $plugin_admin, 'ajax_remove_group_member');
|
||||
|
||||
// Agent queue management AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_accept_call', $plugin_admin, 'ajax_accept_call');
|
||||
$this->loader->add_action('wp_ajax_twp_get_waiting_calls', $plugin_admin, 'ajax_get_waiting_calls');
|
||||
$this->loader->add_action('wp_ajax_twp_set_agent_status', $plugin_admin, 'ajax_set_agent_status');
|
||||
|
||||
// Callback and outbound call AJAX
|
||||
$this->loader->add_action('wp_ajax_twp_request_callback', $plugin_admin, 'ajax_request_callback');
|
||||
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call', $plugin_admin, 'ajax_initiate_outbound_call');
|
||||
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call_with_from', $plugin_admin, 'ajax_initiate_outbound_call_with_from');
|
||||
$this->loader->add_action('wp_ajax_twp_get_callbacks', $plugin_admin, 'ajax_get_callbacks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register public hooks
|
||||
*/
|
||||
private function define_public_hooks() {
|
||||
// Webhook endpoints
|
||||
$webhooks = new TWP_Webhooks();
|
||||
$this->loader->add_action('init', $webhooks, 'register_endpoints');
|
||||
|
||||
// Initialize Agent Manager
|
||||
TWP_Agent_Manager::init();
|
||||
|
||||
// Scheduled events
|
||||
$scheduler = new TWP_Scheduler();
|
||||
$this->loader->add_action('twp_check_schedules', $scheduler, 'check_active_schedules');
|
||||
|
||||
$queue = new TWP_Call_Queue();
|
||||
$this->loader->add_action('twp_process_queue', $queue, 'process_waiting_calls');
|
||||
|
||||
// Callback processing
|
||||
$this->loader->add_action('twp_process_callbacks', 'TWP_Callback_Manager', 'process_callbacks');
|
||||
|
||||
// Schedule cron events
|
||||
if (!wp_next_scheduled('twp_check_schedules')) {
|
||||
wp_schedule_event(time(), 'twp_every_minute', 'twp_check_schedules');
|
||||
}
|
||||
|
||||
if (!wp_next_scheduled('twp_process_queue')) {
|
||||
wp_schedule_event(time(), 'twp_every_30_seconds', 'twp_process_queue');
|
||||
}
|
||||
|
||||
if (!wp_next_scheduled('twp_process_callbacks')) {
|
||||
wp_schedule_event(time(), 'twp_every_minute', 'twp_process_callbacks');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register API hooks
|
||||
*/
|
||||
private function define_api_hooks() {
|
||||
// REST API endpoints
|
||||
add_action('rest_api_init', function() {
|
||||
register_rest_route('twilio-wp/v1', '/schedules', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array('TWP_Scheduler', 'get_schedules'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
));
|
||||
|
||||
register_rest_route('twilio-wp/v1', '/workflows', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array('TWP_Workflow', 'get_workflows'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
));
|
||||
|
||||
register_rest_route('twilio-wp/v1', '/queue-status', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array('TWP_Call_Queue', 'get_queue_status'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the loader
|
||||
*/
|
||||
public function run() {
|
||||
// Add custom cron schedules
|
||||
add_filter('cron_schedules', function($schedules) {
|
||||
$schedules['twp_every_minute'] = array(
|
||||
'interval' => 60,
|
||||
'display' => __('Every Minute', 'twilio-wp-plugin')
|
||||
);
|
||||
$schedules['twp_every_30_seconds'] = array(
|
||||
'interval' => 30,
|
||||
'display' => __('Every 30 Seconds', 'twilio-wp-plugin')
|
||||
);
|
||||
return $schedules;
|
||||
});
|
||||
|
||||
$this->loader->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin name
|
||||
*/
|
||||
public function get_plugin_name() {
|
||||
return $this->plugin_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version
|
||||
*/
|
||||
public function get_version() {
|
||||
return $this->version;
|
||||
}
|
||||
}
|
18
includes/class-twp-deactivator.php
Normal file
18
includes/class-twp-deactivator.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Fired during plugin deactivation
|
||||
*/
|
||||
class TWP_Deactivator {
|
||||
|
||||
/**
|
||||
* Run deactivation tasks
|
||||
*/
|
||||
public static function deactivate() {
|
||||
// Clear scheduled events
|
||||
wp_clear_scheduled_hook('twp_check_schedules');
|
||||
wp_clear_scheduled_hook('twp_process_queue');
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
296
includes/class-twp-elevenlabs-api.php
Normal file
296
includes/class-twp-elevenlabs-api.php
Normal file
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
/**
|
||||
* Eleven Labs API integration class
|
||||
*/
|
||||
class TWP_ElevenLabs_API {
|
||||
|
||||
private $api_key;
|
||||
private $voice_id;
|
||||
private $model_id;
|
||||
private $api_base = 'https://api.elevenlabs.io/v1';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->api_key = get_option('twp_elevenlabs_api_key');
|
||||
$this->voice_id = get_option('twp_elevenlabs_voice_id');
|
||||
$this->model_id = get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert text to speech
|
||||
*/
|
||||
public function text_to_speech($text, $voice_id = null) {
|
||||
if (!$voice_id) {
|
||||
$voice_id = $this->voice_id;
|
||||
}
|
||||
|
||||
$url = $this->api_base . '/text-to-speech/' . $voice_id;
|
||||
|
||||
$data = array(
|
||||
'text' => $text,
|
||||
'model_id' => $this->model_id,
|
||||
'voice_settings' => array(
|
||||
'stability' => 0.5,
|
||||
'similarity_boost' => 0.5
|
||||
)
|
||||
);
|
||||
|
||||
$response = $this->make_request('POST', $url, $data);
|
||||
|
||||
if ($response['success']) {
|
||||
// Save audio file
|
||||
$upload_dir = wp_upload_dir();
|
||||
$filename = 'tts_' . uniqid() . '.mp3';
|
||||
$file_path = $upload_dir['path'] . '/' . $filename;
|
||||
$file_url = $upload_dir['url'] . '/' . $filename;
|
||||
|
||||
file_put_contents($file_path, $response['data']);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'file_path' => $file_path,
|
||||
'file_url' => $file_url
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available voices
|
||||
*/
|
||||
public function get_voices() {
|
||||
$url = $this->api_base . '/voices';
|
||||
$response = $this->make_request('GET', $url);
|
||||
|
||||
if ($response['success'] && isset($response['data']['voices'])) {
|
||||
// Cache voices for 1 hour to reduce API calls
|
||||
set_transient('twp_elevenlabs_voices', $response['data']['voices'], HOUR_IN_SECONDS);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached voices or fetch from API
|
||||
*/
|
||||
public function get_cached_voices() {
|
||||
$cached_voices = get_transient('twp_elevenlabs_voices');
|
||||
|
||||
if ($cached_voices !== false) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => array('voices' => $cached_voices)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->get_voices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get voice details
|
||||
*/
|
||||
public function get_voice($voice_id) {
|
||||
$url = $this->api_base . '/voices/' . $voice_id;
|
||||
return $this->make_request('GET', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user subscription info
|
||||
*/
|
||||
public function get_subscription_info() {
|
||||
$url = $this->api_base . '/user/subscription';
|
||||
return $this->make_request('GET', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models
|
||||
*/
|
||||
public function get_models() {
|
||||
$url = $this->api_base . '/models';
|
||||
$response = $this->make_request('GET', $url);
|
||||
|
||||
if ($response['success'] && isset($response['data'])) {
|
||||
// Filter models that support text-to-speech
|
||||
$tts_models = array();
|
||||
foreach ($response['data'] as $model) {
|
||||
if (isset($model['can_do_text_to_speech']) && $model['can_do_text_to_speech']) {
|
||||
$tts_models[] = $model;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache models for 1 hour
|
||||
set_transient('twp_elevenlabs_models', $tts_models, HOUR_IN_SECONDS);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => $tts_models
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached models or fetch from API
|
||||
*/
|
||||
public function get_cached_models() {
|
||||
$cached_models = get_transient('twp_elevenlabs_models');
|
||||
|
||||
if ($cached_models !== false) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => $cached_models
|
||||
);
|
||||
}
|
||||
|
||||
return $this->get_models();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate speech for IVR menu
|
||||
*/
|
||||
public function generate_ivr_prompt($text, $options = array()) {
|
||||
$default_options = array(
|
||||
'voice_id' => $this->voice_id,
|
||||
'stability' => 0.75,
|
||||
'similarity_boost' => 0.75,
|
||||
'style' => 0,
|
||||
'use_speaker_boost' => true
|
||||
);
|
||||
|
||||
$options = wp_parse_args($options, $default_options);
|
||||
|
||||
$url = $this->api_base . '/text-to-speech/' . $options['voice_id'];
|
||||
|
||||
$data = array(
|
||||
'text' => $text,
|
||||
'model_id' => $this->model_id,
|
||||
'voice_settings' => array(
|
||||
'stability' => $options['stability'],
|
||||
'similarity_boost' => $options['similarity_boost'],
|
||||
'style' => $options['style'],
|
||||
'use_speaker_boost' => $options['use_speaker_boost']
|
||||
)
|
||||
);
|
||||
|
||||
return $this->make_request('POST', $url, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate speech for queue messages
|
||||
*/
|
||||
public function generate_queue_messages($messages) {
|
||||
$generated_files = array();
|
||||
|
||||
foreach ($messages as $key => $message) {
|
||||
$result = $this->text_to_speech($message);
|
||||
|
||||
if ($result['success']) {
|
||||
$generated_files[$key] = $result['file_url'];
|
||||
}
|
||||
}
|
||||
|
||||
return $generated_files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream text to speech (for real-time applications)
|
||||
*/
|
||||
public function stream_text_to_speech($text, $voice_id = null) {
|
||||
if (!$voice_id) {
|
||||
$voice_id = $this->voice_id;
|
||||
}
|
||||
|
||||
$url = $this->api_base . '/text-to-speech/' . $voice_id . '/stream';
|
||||
|
||||
$data = array(
|
||||
'text' => $text,
|
||||
'model_id' => $this->model_id,
|
||||
'voice_settings' => array(
|
||||
'stability' => 0.5,
|
||||
'similarity_boost' => 0.5
|
||||
),
|
||||
'optimize_streaming_latency' => 3
|
||||
);
|
||||
|
||||
return $this->make_request('POST', $url, $data, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
*/
|
||||
private function make_request($method, $url, $data = array(), $stream = false) {
|
||||
$args = array(
|
||||
'method' => $method,
|
||||
'headers' => array(
|
||||
'xi-api-key' => $this->api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => $stream ? 'audio/mpeg' : 'application/json'
|
||||
),
|
||||
'timeout' => 60
|
||||
);
|
||||
|
||||
if ($method === 'POST' && !empty($data)) {
|
||||
$args['body'] = json_encode($data);
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$response = wp_remote_get($url, $args);
|
||||
} else {
|
||||
$response = wp_remote_post($url, $args);
|
||||
}
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message()
|
||||
);
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
if ($status_code >= 200 && $status_code < 300) {
|
||||
if ($stream || strpos(wp_remote_retrieve_header($response, 'content-type'), 'audio') !== false) {
|
||||
// Return raw audio data
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => $body
|
||||
);
|
||||
} else {
|
||||
$decoded = json_decode($body, true);
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => $decoded
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$decoded = json_decode($body, true);
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => isset($decoded['detail']) ? $decoded['detail'] : 'API request failed',
|
||||
'code' => $status_code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache generated audio
|
||||
*/
|
||||
public function cache_audio($text, $audio_data) {
|
||||
$cache_key = 'twp_tts_' . md5($text . $this->voice_id);
|
||||
set_transient($cache_key, $audio_data, DAY_IN_SECONDS * 7);
|
||||
return $cache_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached audio
|
||||
*/
|
||||
public function get_cached_audio($text) {
|
||||
$cache_key = 'twp_tts_' . md5($text . $this->voice_id);
|
||||
return get_transient($cache_key);
|
||||
}
|
||||
}
|
59
includes/class-twp-loader.php
Normal file
59
includes/class-twp-loader.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/**
|
||||
* Register all actions and filters for the plugin
|
||||
*/
|
||||
class TWP_Loader {
|
||||
|
||||
protected $actions;
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* Initialize collections
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->actions = array();
|
||||
$this->filters = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action
|
||||
*/
|
||||
public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
|
||||
$this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add filter
|
||||
*/
|
||||
public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
|
||||
$this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hook to collection
|
||||
*/
|
||||
private function add($hooks, $hook, $component, $callback, $priority, $accepted_args) {
|
||||
$hooks[] = array(
|
||||
'hook' => $hook,
|
||||
'component' => $component,
|
||||
'callback' => $callback,
|
||||
'priority' => $priority,
|
||||
'accepted_args' => $accepted_args
|
||||
);
|
||||
|
||||
return $hooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks with WordPress
|
||||
*/
|
||||
public function run() {
|
||||
foreach ($this->filters as $hook) {
|
||||
add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
|
||||
}
|
||||
|
||||
foreach ($this->actions as $hook) {
|
||||
add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
|
||||
}
|
||||
}
|
||||
}
|
301
includes/class-twp-scheduler.php
Normal file
301
includes/class-twp-scheduler.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
/**
|
||||
* Phone schedule management class
|
||||
*/
|
||||
class TWP_Scheduler {
|
||||
|
||||
/**
|
||||
* Check active schedules
|
||||
*/
|
||||
public function check_active_schedules() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||
|
||||
$current_time = current_time('H:i:s');
|
||||
$current_day = strtolower(date('l'));
|
||||
|
||||
$schedules = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table_name
|
||||
WHERE is_active = 1
|
||||
AND days_of_week LIKE %s
|
||||
AND start_time <= %s
|
||||
AND end_time >= %s",
|
||||
'%' . $current_day . '%',
|
||||
$current_time,
|
||||
$current_time
|
||||
));
|
||||
|
||||
foreach ($schedules as $schedule) {
|
||||
$this->apply_schedule($schedule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply schedule to phone number
|
||||
*/
|
||||
private function apply_schedule($schedule) {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
// Get phone numbers
|
||||
$numbers = $twilio->get_phone_numbers();
|
||||
|
||||
if ($numbers['success']) {
|
||||
foreach ($numbers['data']['incoming_phone_numbers'] as $number) {
|
||||
if ($number['phone_number'] == $schedule->phone_number) {
|
||||
// Configure webhook based on schedule
|
||||
$webhook_url = home_url('/twilio-webhook/voice');
|
||||
$webhook_url = add_query_arg('schedule_id', $schedule->id, $webhook_url);
|
||||
|
||||
$twilio->configure_phone_number(
|
||||
$number['sid'],
|
||||
$webhook_url
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schedule
|
||||
*/
|
||||
public static function create_schedule($data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||
|
||||
$insert_data = array(
|
||||
'schedule_name' => sanitize_text_field($data['schedule_name']),
|
||||
'days_of_week' => sanitize_text_field($data['days_of_week']),
|
||||
'start_time' => sanitize_text_field($data['start_time']),
|
||||
'end_time' => sanitize_text_field($data['end_time']),
|
||||
'workflow_id' => sanitize_text_field($data['workflow_id']),
|
||||
'is_active' => isset($data['is_active']) ? 1 : 0
|
||||
);
|
||||
|
||||
$format = array('%s', '%s', '%s', '%s', '%s', '%d');
|
||||
|
||||
// Add optional fields if provided
|
||||
if (!empty($data['phone_number'])) {
|
||||
$insert_data['phone_number'] = sanitize_text_field($data['phone_number']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($data['forward_number'])) {
|
||||
$insert_data['forward_number'] = sanitize_text_field($data['forward_number']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($data['after_hours_action'])) {
|
||||
$insert_data['after_hours_action'] = sanitize_text_field($data['after_hours_action']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($data['after_hours_workflow_id'])) {
|
||||
$insert_data['after_hours_workflow_id'] = sanitize_text_field($data['after_hours_workflow_id']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($data['after_hours_forward_number'])) {
|
||||
$insert_data['after_hours_forward_number'] = sanitize_text_field($data['after_hours_forward_number']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
$result = $wpdb->insert($table_name, $insert_data, $format);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update schedule
|
||||
*/
|
||||
public static function update_schedule($id, $data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||
|
||||
$update_data = array();
|
||||
$update_format = array();
|
||||
|
||||
if (isset($data['phone_number'])) {
|
||||
$update_data['phone_number'] = sanitize_text_field($data['phone_number']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['schedule_name'])) {
|
||||
$update_data['schedule_name'] = sanitize_text_field($data['schedule_name']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['days_of_week'])) {
|
||||
$update_data['days_of_week'] = sanitize_text_field($data['days_of_week']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['start_time'])) {
|
||||
$update_data['start_time'] = sanitize_text_field($data['start_time']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['end_time'])) {
|
||||
$update_data['end_time'] = sanitize_text_field($data['end_time']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['workflow_id'])) {
|
||||
$update_data['workflow_id'] = sanitize_text_field($data['workflow_id']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['forward_number'])) {
|
||||
$update_data['forward_number'] = sanitize_text_field($data['forward_number']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['after_hours_action'])) {
|
||||
$update_data['after_hours_action'] = sanitize_text_field($data['after_hours_action']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['after_hours_workflow_id'])) {
|
||||
$update_data['after_hours_workflow_id'] = sanitize_text_field($data['after_hours_workflow_id']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['after_hours_forward_number'])) {
|
||||
$update_data['after_hours_forward_number'] = sanitize_text_field($data['after_hours_forward_number']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['is_active'])) {
|
||||
$update_data['is_active'] = $data['is_active'] ? 1 : 0;
|
||||
$update_format[] = '%d';
|
||||
}
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table_name,
|
||||
$update_data,
|
||||
array('id' => $id),
|
||||
$update_format,
|
||||
array('%d')
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete schedule
|
||||
*/
|
||||
public static function delete_schedule($id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||
|
||||
return $wpdb->delete(
|
||||
$table_name,
|
||||
array('id' => $id),
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schedules
|
||||
*/
|
||||
public static function get_schedules($phone_number = null) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||
|
||||
if ($phone_number) {
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE phone_number = %s ORDER BY created_at DESC",
|
||||
$phone_number
|
||||
));
|
||||
} else {
|
||||
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schedule by ID
|
||||
*/
|
||||
public static function get_schedule($id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_phone_schedules';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE id = %d",
|
||||
$id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if schedule is active now
|
||||
*/
|
||||
public static function is_schedule_active($schedule_id) {
|
||||
$schedule = self::get_schedule($schedule_id);
|
||||
|
||||
if (!$schedule || !$schedule->is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current_time = current_time('H:i:s');
|
||||
$current_day = strtolower(date('l'));
|
||||
|
||||
// Check if current day is in schedule
|
||||
if (strpos($schedule->days_of_week, $current_day) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if current time is within schedule
|
||||
return $current_time >= $schedule->start_time && $current_time <= $schedule->end_time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate routing for schedule
|
||||
*/
|
||||
public static function get_schedule_routing($schedule_id) {
|
||||
$schedule = self::get_schedule($schedule_id);
|
||||
|
||||
if (!$schedule || !$schedule->is_active) {
|
||||
return array(
|
||||
'action' => 'default',
|
||||
'data' => null
|
||||
);
|
||||
}
|
||||
|
||||
$is_within_hours = self::is_schedule_active($schedule_id);
|
||||
|
||||
if ($is_within_hours) {
|
||||
// Within business hours - use main workflow
|
||||
return array(
|
||||
'action' => 'workflow',
|
||||
'data' => array(
|
||||
'workflow_id' => $schedule->workflow_id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// After hours - use after hours routing
|
||||
if ($schedule->after_hours_action === 'forward' && $schedule->after_hours_forward_number) {
|
||||
return array(
|
||||
'action' => 'forward',
|
||||
'data' => array(
|
||||
'forward_number' => $schedule->after_hours_forward_number
|
||||
)
|
||||
);
|
||||
} else if ($schedule->after_hours_action === 'workflow' && $schedule->after_hours_workflow_id) {
|
||||
return array(
|
||||
'action' => 'workflow',
|
||||
'data' => array(
|
||||
'workflow_id' => $schedule->after_hours_workflow_id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Default to main workflow if no after hours action specified
|
||||
return array(
|
||||
'action' => 'workflow',
|
||||
'data' => array(
|
||||
'workflow_id' => $schedule->workflow_id
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
276
includes/class-twp-twilio-api.php
Normal file
276
includes/class-twp-twilio-api.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* Twilio API integration class
|
||||
*/
|
||||
class TWP_Twilio_API {
|
||||
|
||||
private $account_sid;
|
||||
private $auth_token;
|
||||
private $phone_number;
|
||||
private $api_base = 'https://api.twilio.com/2010-04-01';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->account_sid = get_option('twp_twilio_account_sid');
|
||||
$this->auth_token = get_option('twp_twilio_auth_token');
|
||||
$this->phone_number = get_option('twp_twilio_phone_number');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a phone call
|
||||
*/
|
||||
public function make_call($to_number, $twiml_url, $status_callback = null, $from_number = null) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls.json';
|
||||
|
||||
$data = array(
|
||||
'To' => $to_number,
|
||||
'From' => $from_number ?: $this->phone_number,
|
||||
'Url' => $twiml_url
|
||||
);
|
||||
|
||||
if ($status_callback) {
|
||||
$data['StatusCallback'] = $status_callback;
|
||||
$data['StatusCallbackEvent'] = array('initiated', 'ringing', 'answered', 'completed');
|
||||
}
|
||||
|
||||
return $this->make_request('POST', $url, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward a call
|
||||
*/
|
||||
public function forward_call($call_sid, $to_number) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
$dial = $twiml->addChild('Dial');
|
||||
$dial->addChild('Number', $to_number);
|
||||
|
||||
return $this->update_call($call_sid, array(
|
||||
'Twiml' => $twiml->asXML()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an active call
|
||||
*/
|
||||
public function update_call($call_sid, $params) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls/' . $call_sid . '.json';
|
||||
return $this->make_request('POST', $url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get call details
|
||||
*/
|
||||
public function get_call($call_sid) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls/' . $call_sid . '.json';
|
||||
return $this->make_request('GET', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TwiML for queue
|
||||
*/
|
||||
public function create_queue_twiml($queue_name, $wait_url = null, $wait_message = null) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if ($wait_message) {
|
||||
$say = $twiml->addChild('Say', $wait_message);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
|
||||
$enqueue = $twiml->addChild('Enqueue', $queue_name);
|
||||
|
||||
if ($wait_url) {
|
||||
$enqueue->addAttribute('waitUrl', $wait_url);
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TwiML for IVR menu
|
||||
*/
|
||||
public function create_ivr_twiml($message, $options = array()) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
$gather = $twiml->addChild('Gather');
|
||||
$gather->addAttribute('numDigits', '1');
|
||||
$gather->addAttribute('timeout', '10');
|
||||
|
||||
if (!empty($options['action_url'])) {
|
||||
$gather->addAttribute('action', $options['action_url']);
|
||||
}
|
||||
|
||||
$say = $gather->addChild('Say', $message);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
|
||||
// Fallback if no input
|
||||
if (!empty($options['no_input_message'])) {
|
||||
$say_fallback = $twiml->addChild('Say', $options['no_input_message']);
|
||||
$say_fallback->addAttribute('voice', 'alice');
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS
|
||||
*/
|
||||
public function send_sms($to_number, $message) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/Messages.json';
|
||||
|
||||
$data = array(
|
||||
'To' => $to_number,
|
||||
'From' => $this->phone_number,
|
||||
'Body' => $message
|
||||
);
|
||||
|
||||
return $this->make_request('POST', $url, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available phone numbers
|
||||
*/
|
||||
public function get_phone_numbers() {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers.json';
|
||||
return $this->make_request('GET', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for available phone numbers
|
||||
*/
|
||||
public function search_available_numbers($country_code = 'US', $area_code = null, $contains = null, $limit = 20) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/AvailablePhoneNumbers/' . $country_code . '/Local.json';
|
||||
|
||||
$params = array('Limit' => $limit);
|
||||
|
||||
if ($area_code) {
|
||||
$params['AreaCode'] = $area_code;
|
||||
}
|
||||
|
||||
if ($contains) {
|
||||
$params['Contains'] = $contains;
|
||||
}
|
||||
|
||||
$url .= '?' . http_build_query($params);
|
||||
|
||||
return $this->make_request('GET', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purchase a phone number
|
||||
*/
|
||||
public function purchase_phone_number($phone_number, $voice_url = null, $sms_url = null) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers.json';
|
||||
|
||||
$data = array(
|
||||
'PhoneNumber' => $phone_number
|
||||
);
|
||||
|
||||
if ($voice_url) {
|
||||
$data['VoiceUrl'] = $voice_url;
|
||||
$data['VoiceMethod'] = 'POST';
|
||||
}
|
||||
|
||||
if ($sms_url) {
|
||||
$data['SmsUrl'] = $sms_url;
|
||||
$data['SmsMethod'] = 'POST';
|
||||
}
|
||||
|
||||
return $this->make_request('POST', $url, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a phone number
|
||||
*/
|
||||
public function release_phone_number($phone_number_sid) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers/' . $phone_number_sid . '.json';
|
||||
return $this->make_request('DELETE', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure phone number webhook
|
||||
*/
|
||||
public function configure_phone_number($phone_sid, $voice_url, $sms_url = null) {
|
||||
$url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers/' . $phone_sid . '.json';
|
||||
|
||||
$data = array(
|
||||
'VoiceUrl' => $voice_url,
|
||||
'VoiceMethod' => 'POST'
|
||||
);
|
||||
|
||||
if ($sms_url) {
|
||||
$data['SmsUrl'] = $sms_url;
|
||||
$data['SmsMethod'] = 'POST';
|
||||
}
|
||||
|
||||
return $this->make_request('POST', $url, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
*/
|
||||
private function make_request($method, $url, $data = array()) {
|
||||
$args = array(
|
||||
'method' => $method,
|
||||
'headers' => array(
|
||||
'Authorization' => 'Basic ' . base64_encode($this->account_sid . ':' . $this->auth_token),
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
),
|
||||
'timeout' => 30
|
||||
);
|
||||
|
||||
if ($method === 'POST' && !empty($data)) {
|
||||
$args['body'] = $data;
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$response = wp_remote_get($url, $args);
|
||||
} else {
|
||||
$response = wp_remote_post($url, $args);
|
||||
}
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message()
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$decoded = json_decode($body, true);
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status_code >= 200 && $status_code < 300) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => $decoded
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => isset($decoded['message']) ? $decoded['message'] : 'API request failed',
|
||||
'code' => $status_code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate webhook signature
|
||||
*/
|
||||
public function validate_webhook_signature($url, $params, $signature) {
|
||||
$data = $url;
|
||||
|
||||
if (is_array($params) && !empty($params)) {
|
||||
ksort($params);
|
||||
foreach ($params as $key => $value) {
|
||||
$data .= $key . $value;
|
||||
}
|
||||
}
|
||||
|
||||
$computed_signature = base64_encode(hash_hmac('sha1', $data, $this->auth_token, true));
|
||||
|
||||
return hash_equals($signature, $computed_signature);
|
||||
}
|
||||
}
|
1089
includes/class-twp-webhooks.php
Normal file
1089
includes/class-twp-webhooks.php
Normal file
File diff suppressed because it is too large
Load Diff
527
includes/class-twp-workflow.php
Normal file
527
includes/class-twp-workflow.php
Normal file
@@ -0,0 +1,527 @@
|
||||
<?php
|
||||
/**
|
||||
* Workflow management class
|
||||
*/
|
||||
class TWP_Workflow {
|
||||
|
||||
/**
|
||||
* Create workflow
|
||||
*/
|
||||
public static function create_workflow($data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_workflows';
|
||||
|
||||
$workflow_data = array(
|
||||
'steps' => $data['steps'],
|
||||
'conditions' => $data['conditions'],
|
||||
'actions' => $data['actions']
|
||||
);
|
||||
|
||||
return $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'workflow_name' => sanitize_text_field($data['workflow_name']),
|
||||
'phone_number' => sanitize_text_field($data['phone_number']),
|
||||
'workflow_data' => json_encode($workflow_data),
|
||||
'is_active' => isset($data['is_active']) ? 1 : 0
|
||||
),
|
||||
array('%s', '%s', '%s', '%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute workflow
|
||||
*/
|
||||
public static function execute_workflow($workflow_id, $call_data) {
|
||||
$workflow = self::get_workflow($workflow_id);
|
||||
|
||||
if (!$workflow || !$workflow->is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$workflow_data = json_decode($workflow->workflow_data, true);
|
||||
$twilio = new TWP_Twilio_API();
|
||||
$elevenlabs = new TWP_ElevenLabs_API();
|
||||
|
||||
// Process workflow steps
|
||||
foreach ($workflow_data['steps'] as $step) {
|
||||
switch ($step['type']) {
|
||||
case 'greeting':
|
||||
$twiml = self::create_greeting_twiml($step, $elevenlabs);
|
||||
break;
|
||||
|
||||
case 'ivr_menu':
|
||||
$twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
|
||||
break;
|
||||
|
||||
case 'forward':
|
||||
$twiml = self::create_forward_twiml($step);
|
||||
break;
|
||||
|
||||
case 'queue':
|
||||
$twiml = self::create_queue_twiml($step);
|
||||
break;
|
||||
|
||||
case 'ring_group':
|
||||
$twiml = self::create_ring_group_twiml($step);
|
||||
break;
|
||||
|
||||
case 'voicemail':
|
||||
$twiml = self::create_voicemail_twiml($step, $elevenlabs);
|
||||
break;
|
||||
|
||||
case 'schedule_check':
|
||||
$twiml = self::handle_schedule_check($step, $call_data);
|
||||
break;
|
||||
|
||||
case 'sms':
|
||||
self::send_sms_notification($step, $call_data);
|
||||
continue 2;
|
||||
|
||||
default:
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// Check conditions
|
||||
if (isset($step['conditions'])) {
|
||||
if (!self::check_conditions($step['conditions'], $call_data)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute step
|
||||
if ($twiml) {
|
||||
return $twiml;
|
||||
}
|
||||
}
|
||||
|
||||
// Default response
|
||||
return self::create_default_response();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create greeting TwiML
|
||||
*/
|
||||
private static function create_greeting_twiml($step, $elevenlabs) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
||||
// Generate TTS audio
|
||||
$audio_result = $elevenlabs->text_to_speech($step['message']);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create IVR menu TwiML
|
||||
*/
|
||||
private static function create_ivr_menu_twiml($step, $elevenlabs) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
$gather = $twiml->addChild('Gather');
|
||||
$gather->addAttribute('numDigits', isset($step['num_digits']) ? $step['num_digits'] : '1');
|
||||
$gather->addAttribute('timeout', isset($step['timeout']) ? $step['timeout'] : '10');
|
||||
|
||||
if (isset($step['action_url'])) {
|
||||
$gather->addAttribute('action', $step['action_url']);
|
||||
} else {
|
||||
$webhook_url = home_url('/twilio-webhook/ivr-response');
|
||||
$webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
|
||||
$webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
|
||||
$gather->addAttribute('action', $webhook_url);
|
||||
}
|
||||
|
||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
||||
// Generate TTS for menu options
|
||||
$audio_result = $elevenlabs->text_to_speech($step['message']);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
$play = $gather->addChild('Play', $audio_result['file_url']);
|
||||
} else {
|
||||
$say = $gather->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
} else {
|
||||
$say = $gather->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
|
||||
// Fallback if no input
|
||||
if (isset($step['no_input_action'])) {
|
||||
switch ($step['no_input_action']) {
|
||||
case 'repeat':
|
||||
$redirect = $twiml->addChild('Redirect');
|
||||
break;
|
||||
|
||||
case 'hangup':
|
||||
$say = $twiml->addChild('Say', 'Goodbye');
|
||||
$say->addAttribute('voice', 'alice');
|
||||
$twiml->addChild('Hangup');
|
||||
break;
|
||||
|
||||
case 'forward':
|
||||
if (isset($step['forward_number'])) {
|
||||
$dial = $twiml->addChild('Dial');
|
||||
$dial->addChild('Number', $step['forward_number']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create forward TwiML
|
||||
*/
|
||||
private static function create_forward_twiml($step) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
$dial = $twiml->addChild('Dial');
|
||||
|
||||
if (isset($step['timeout'])) {
|
||||
$dial->addAttribute('timeout', $step['timeout']);
|
||||
}
|
||||
|
||||
if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
|
||||
// Sequential forwarding
|
||||
foreach ($step['forward_numbers'] as $number) {
|
||||
$dial->addChild('Number', $number);
|
||||
}
|
||||
} elseif (isset($step['forward_number'])) {
|
||||
$dial->addChild('Number', $step['forward_number']);
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create queue TwiML
|
||||
*/
|
||||
private static function create_queue_twiml($step) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['announce_message'])) {
|
||||
$say = $twiml->addChild('Say', $step['announce_message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
|
||||
$enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
|
||||
|
||||
if (isset($step['wait_url'])) {
|
||||
$enqueue->addAttribute('waitUrl', $step['wait_url']);
|
||||
} else {
|
||||
$wait_url = home_url('/twilio-webhook/queue-wait');
|
||||
$wait_url = add_query_arg('queue_id', $step['queue_id'], $wait_url);
|
||||
$enqueue->addAttribute('waitUrl', $wait_url);
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ring group TwiML
|
||||
*/
|
||||
private static function create_ring_group_twiml($step) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['announce_message'])) {
|
||||
$say = $twiml->addChild('Say', $step['announce_message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
|
||||
// Get group phone numbers
|
||||
$group_id = intval($step['group_id']);
|
||||
$phone_numbers = TWP_Agent_Groups::get_group_phone_numbers($group_id);
|
||||
|
||||
if (empty($phone_numbers)) {
|
||||
$say = $twiml->addChild('Say', 'No agents are available in this group. Please try again later.');
|
||||
$say->addAttribute('voice', 'alice');
|
||||
$twiml->addChild('Hangup');
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
$dial = $twiml->addChild('Dial');
|
||||
|
||||
if (isset($step['timeout'])) {
|
||||
$dial->addAttribute('timeout', $step['timeout']);
|
||||
} else {
|
||||
$dial->addAttribute('timeout', '30');
|
||||
}
|
||||
|
||||
if (isset($step['caller_id'])) {
|
||||
$dial->addAttribute('callerId', $step['caller_id']);
|
||||
}
|
||||
|
||||
// Set action URL to handle no-answer scenarios
|
||||
$action_url = home_url('/wp-json/twilio-webhook/v1/ring-group-result?' . http_build_query([
|
||||
'group_id' => $group_id,
|
||||
'queue_name' => isset($step['queue_name']) ? $step['queue_name'] : null,
|
||||
'fallback_action' => isset($step['fallback_action']) ? $step['fallback_action'] : 'queue'
|
||||
]));
|
||||
$dial->addAttribute('action', $action_url);
|
||||
|
||||
// Add all group numbers for simultaneous ring
|
||||
foreach ($phone_numbers as $number) {
|
||||
if (!empty($number)) {
|
||||
$dial->addChild('Number', $number);
|
||||
}
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create voicemail TwiML
|
||||
*/
|
||||
private static function create_voicemail_twiml($step, $elevenlabs) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['greeting_message'])) {
|
||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
||||
$audio_result = $elevenlabs->text_to_speech($step['greeting_message']);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['greeting_message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['greeting_message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
}
|
||||
|
||||
$record = $twiml->addChild('Record');
|
||||
$record->addAttribute('maxLength', isset($step['max_length']) ? $step['max_length'] : '120');
|
||||
$record->addAttribute('playBeep', 'true');
|
||||
$record->addAttribute('transcribe', 'true');
|
||||
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
||||
|
||||
$callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
|
||||
$callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
|
||||
$record->addAttribute('recordingStatusCallback', $callback_url);
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle schedule check
|
||||
*/
|
||||
private static function handle_schedule_check($step, $call_data) {
|
||||
$schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null;
|
||||
|
||||
if (!$schedule_id) {
|
||||
// No schedule specified, return false to continue to next step
|
||||
return false;
|
||||
}
|
||||
|
||||
$routing = TWP_Scheduler::get_schedule_routing($schedule_id);
|
||||
|
||||
if ($routing['action'] === 'workflow' && $routing['data']['workflow_id']) {
|
||||
// Route to different workflow
|
||||
$workflow_id = $routing['data']['workflow_id'];
|
||||
$workflow = self::get_workflow($workflow_id);
|
||||
|
||||
if ($workflow && $workflow->is_active) {
|
||||
return self::execute_workflow($workflow_id, $call_data);
|
||||
}
|
||||
} else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) {
|
||||
// Forward call
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$dial = $twiml->dial();
|
||||
$dial->number($routing['data']['forward_number']);
|
||||
return $twiml;
|
||||
}
|
||||
|
||||
// Fallback to legacy behavior if new routing doesn't work
|
||||
if (TWP_Scheduler::is_schedule_active($schedule_id)) {
|
||||
// Execute in-hours action
|
||||
if (isset($step['in_hours_action'])) {
|
||||
return self::execute_action($step['in_hours_action'], $call_data);
|
||||
}
|
||||
} else {
|
||||
// Execute after-hours action
|
||||
if (isset($step['after_hours_action'])) {
|
||||
return self::execute_action($step['after_hours_action'], $call_data);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute action
|
||||
*/
|
||||
private static function execute_action($action, $call_data) {
|
||||
switch ($action['type']) {
|
||||
case 'forward':
|
||||
return self::create_forward_twiml($action);
|
||||
|
||||
case 'voicemail':
|
||||
$elevenlabs = new TWP_ElevenLabs_API();
|
||||
return self::create_voicemail_twiml($action, $elevenlabs);
|
||||
|
||||
case 'queue':
|
||||
return self::create_queue_twiml($action);
|
||||
|
||||
case 'ring_group':
|
||||
return self::create_ring_group_twiml($action);
|
||||
|
||||
case 'message':
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
$say = $twiml->addChild('Say', $action['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
return $twiml->asXML();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check conditions
|
||||
*/
|
||||
private static function check_conditions($conditions, $call_data) {
|
||||
foreach ($conditions as $condition) {
|
||||
switch ($condition['type']) {
|
||||
case 'time':
|
||||
$current_time = current_time('H:i');
|
||||
if ($current_time < $condition['start_time'] || $current_time > $condition['end_time']) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'day_of_week':
|
||||
$current_day = strtolower(date('l'));
|
||||
if (!in_array($current_day, $condition['days'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'caller_id':
|
||||
if (!in_array($call_data['From'], $condition['numbers'])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS notification
|
||||
*/
|
||||
private static function send_sms_notification($step, $call_data) {
|
||||
$twilio = new TWP_Twilio_API();
|
||||
|
||||
$message = str_replace(
|
||||
array('{from}', '{to}', '{time}'),
|
||||
array($call_data['From'], $call_data['To'], current_time('g:i A')),
|
||||
$step['message']
|
||||
);
|
||||
|
||||
$twilio->send_sms($step['to_number'], $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default response
|
||||
*/
|
||||
private static function create_default_response() {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
$say = $twiml->addChild('Say', 'Thank you for calling. Goodbye.');
|
||||
$say->addAttribute('voice', 'alice');
|
||||
$twiml->addChild('Hangup');
|
||||
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow
|
||||
*/
|
||||
public static function get_workflow($workflow_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_workflows';
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE id = %d",
|
||||
$workflow_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflows
|
||||
*/
|
||||
public static function get_workflows() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_workflows';
|
||||
|
||||
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update workflow
|
||||
*/
|
||||
public static function update_workflow($workflow_id, $data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_workflows';
|
||||
|
||||
$update_data = array();
|
||||
$update_format = array();
|
||||
|
||||
if (isset($data['workflow_name'])) {
|
||||
$update_data['workflow_name'] = sanitize_text_field($data['workflow_name']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['phone_number'])) {
|
||||
$update_data['phone_number'] = sanitize_text_field($data['phone_number']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['workflow_data'])) {
|
||||
$update_data['workflow_data'] = json_encode($data['workflow_data']);
|
||||
$update_format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['is_active'])) {
|
||||
$update_data['is_active'] = $data['is_active'] ? 1 : 0;
|
||||
$update_format[] = '%d';
|
||||
}
|
||||
|
||||
return $wpdb->update(
|
||||
$table_name,
|
||||
$update_data,
|
||||
array('id' => $workflow_id),
|
||||
$update_format,
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workflow
|
||||
*/
|
||||
public static function delete_workflow($workflow_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'twp_workflows';
|
||||
|
||||
return $wpdb->delete(
|
||||
$table_name,
|
||||
array('id' => $workflow_id),
|
||||
array('%d')
|
||||
);
|
||||
}
|
||||
}
|
55
twilio-wp-plugin.php
Normal file
55
twilio-wp-plugin.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Twilio WP Plugin
|
||||
* Plugin URI: https://example.com/twilio-wp-plugin
|
||||
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
|
||||
* Version: 1.0.0
|
||||
* Author: Your Name
|
||||
* License: GPL v2 or later
|
||||
* Text Domain: twilio-wp-plugin
|
||||
*/
|
||||
|
||||
// If this file is called directly, abort.
|
||||
if (!defined('WPINC')) {
|
||||
die;
|
||||
}
|
||||
|
||||
// Plugin constants
|
||||
define('TWP_VERSION', '1.0.0');
|
||||
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('TWP_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||
|
||||
/**
|
||||
* Plugin activation hook
|
||||
*/
|
||||
function twp_activate() {
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
|
||||
TWP_Activator::activate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation hook
|
||||
*/
|
||||
function twp_deactivate() {
|
||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-deactivator.php';
|
||||
TWP_Deactivator::deactivate();
|
||||
}
|
||||
|
||||
register_activation_hook(__FILE__, 'twp_activate');
|
||||
register_deactivation_hook(__FILE__, 'twp_deactivate');
|
||||
|
||||
/**
|
||||
* Core plugin class
|
||||
*/
|
||||
require plugin_dir_path(__FILE__) . 'includes/class-twp-core.php';
|
||||
|
||||
/**
|
||||
* Begin execution of the plugin
|
||||
*/
|
||||
function run_twilio_wp_plugin() {
|
||||
$plugin = new TWP_Core();
|
||||
$plugin->run();
|
||||
}
|
||||
|
||||
run_twilio_wp_plugin();
|
Reference in New Issue
Block a user