Add queue timeout voicemail and Amazon SNS SMS provider support

This update adds two major features:

1. Queue Timeout Voicemail
   - Callers can now leave voicemail when queue timeout is reached
   - Configurable per-queue voicemail prompts with TTS support
   - Automatic transcription and urgent keyword detection
   - Admin setting to choose between voicemail or callback on timeout

2. Amazon SNS SMS Provider
   - Alternative SMS provider to Twilio for sending text messages
   - Useful when Twilio SMS approval is difficult to obtain
   - Provider abstraction layer allows switching between Twilio/SNS
   - Full AWS SNS configuration in admin settings
   - Supports custom sender IDs in compatible countries
   - Lower cost per SMS compared to Twilio

New Files:
- includes/class-twp-voicemail-handler.php - Voicemail recording handler
- includes/interface-twp-sms-provider.php - SMS provider interface
- includes/class-twp-sms-provider-twilio.php - Twilio SMS implementation
- includes/class-twp-sms-provider-sns.php - Amazon SNS implementation
- includes/class-twp-sms-manager.php - SMS provider abstraction manager
- QUEUE_VOICEMAIL_SMS_FEATURES.md - Complete feature documentation

Modified Files:
- includes/class-twp-call-queue.php - Added voicemail option to timeout handler
- includes/class-twp-twilio-api.php - Updated send_sms() to use provider abstraction
- admin/class-twp-admin.php - Added SMS provider and timeout action settings
- composer.json - Added AWS SDK dependency

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-21 11:13:54 -07:00
parent 82b735f5df
commit 4baa8f539a
10 changed files with 1189 additions and 53 deletions

View File

@@ -0,0 +1,293 @@
# Queue Timeout Voicemail & Amazon SNS SMS Features
## Overview
This update adds two major features to the Twilio WordPress Plugin:
1. **Queue Timeout Voicemail**: Automatically prompt callers to leave a voicemail when they reach the queue timeout limit
2. **Amazon SNS SMS Provider**: Use Amazon SNS as an alternative SMS provider to Twilio
## Feature 1: Queue Timeout Voicemail
### What it does
When a caller waits in a queue beyond the configured timeout period, instead of just disconnecting or offering a callback, the system can now automatically prompt them to leave a voicemail message.
### Benefits
- **Better Caller Experience**: Callers can leave a message instead of being disconnected
- **No Missed Opportunities**: Capture important messages even when agents are unavailable
- **Automatic Transcription**: Voicemails are automatically transcribed
- **Urgent Keyword Detection**: System detects urgent keywords in transcriptions and sends priority notifications
### Configuration
1. Go to **WordPress Admin → Twilio WP → Settings**
2. Find the **SMS Provider Settings** section
3. Under **Queue Timeout Action**, select:
- **Take Voicemail** (recommended) - Prompts caller to leave a message
- **Offer Callback** (original behavior) - Offers to call them back
### How it works
1. Caller waits in queue beyond timeout limit
2. System plays customizable voicemail prompt (can be set per-queue)
3. Caller records voicemail (max 5 minutes)
4. Recording is automatically transcribed
5. If urgent keywords detected, priority notifications sent
6. Voicemail appears in admin panel under **Voicemails & Recordings**
### Customization
Each queue can have a custom voicemail prompt. The default prompt is:
> "We're sorry, but all our agents are currently unavailable. Please leave a message after the tone, and we'll get back to you as soon as possible."
### Files Added
- `includes/class-twp-voicemail-handler.php` - Handles voicemail recording and processing
### Files Modified
- `includes/class-twp-call-queue.php` - Updated `handle_timeout()` method
---
## Feature 2: Amazon SNS SMS Provider
### What it does
Provides an alternative to Twilio for sending SMS messages using Amazon SNS (Simple Notification Service). This is particularly useful if you're having difficulty getting Twilio SMS messaging approved.
### Benefits
- **Alternative to Twilio SMS**: No need for Twilio SMS approval
- **AWS Integration**: Use your existing AWS infrastructure
- **Cost Effective**: Pay-as-you-go pricing with AWS
- **Global Reach**: Support for international SMS
- **Sender ID Support**: Use custom sender names in supported countries
### Prerequisites
1. **AWS Account**: You need an AWS account
2. **IAM User**: Create an IAM user with SNS permissions
3. **AWS SDK**: Installed automatically via Composer
#### Required IAM Permissions
Your AWS IAM user needs the following permissions:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sns:Publish",
"sns:SetSMSAttributes",
"sns:GetSMSAttributes"
],
"Resource": "*"
}
]
}
```
### Installation
1. **Install AWS SDK** (if not already installed):
```bash
cd /home/jknapp/code/twilio-wp-plugin
composer require aws/aws-sdk-php
```
2. **Configure AWS Credentials**:
- Go to **WordPress Admin → Twilio WP → Settings**
- Find **SMS Provider Settings** section
- Select **Amazon SNS** from the SMS Provider dropdown
- Enter your AWS credentials:
- AWS Access Key ID
- AWS Secret Access Key
- AWS Region (e.g., us-east-1)
- SMS Sender ID (optional, 3-11 alphanumeric characters)
3. **Test the Configuration**:
```php
// Test via WordPress admin or run this code
require_once 'includes/class-twp-sms-manager.php';
$result = TWP_SMS_Manager::send_test_sms('+1234567890');
```
### Configuration Options
#### SMS Provider Selection
Navigate to **Settings → SMS Provider Settings**:
- **Twilio** (default): Uses your existing Twilio setup
- **Amazon SNS**: Uses AWS Simple Notification Service
#### AWS SNS Settings
When Amazon SNS is selected, configure:
1. **AWS Access Key ID**: Your IAM access key
2. **AWS Secret Access Key**: Your IAM secret key
3. **AWS Region**: Choose your preferred AWS region
4. **SMS Sender ID** (Optional): Alphanumeric sender name (3-11 chars)
- Supported in: UK, EU, India, and other countries
- Not supported in: USA, Canada
- Leave blank to use AWS's default number
### Usage
The SMS provider is transparent to the rest of the plugin. All existing SMS functionality will automatically use the selected provider:
- Agent notifications
- Queue alerts
- Urgent voicemail notifications
- Workflow SMS steps
### Switching Providers
You can switch between Twilio and Amazon SNS at any time:
1. Go to **Settings → SMS Provider Settings**
2. Change the **SMS Provider** dropdown
3. Click **Save Changes**
All SMS messages will immediately use the new provider.
### Cost Comparison
#### Twilio SMS Pricing (approximate)
- US/Canada: $0.0079 per SMS
- Requires SMS verification/approval
- Monthly fees may apply
#### Amazon SNS SMS Pricing (approximate)
- US: $0.00645 per SMS
- No approval required for transactional messages
- Pay only for what you use
- [AWS SNS Pricing Details](https://aws.amazon.com/sns/sms-pricing/)
### Files Added
- `includes/interface-twp-sms-provider.php` - SMS provider interface
- `includes/class-twp-sms-provider-twilio.php` - Twilio SMS provider implementation
- `includes/class-twp-sms-provider-sns.php` - Amazon SNS SMS provider implementation
- `includes/class-twp-sms-manager.php` - SMS provider manager
### Files Modified
- `includes/class-twp-twilio-api.php` - Updated `send_sms()` to use SMS manager
- `admin/class-twp-admin.php` - Added SMS provider settings UI
- `composer.json` - Added AWS SDK dependency
---
## Troubleshooting
### Queue Timeout Voicemail
**Issue**: Voicemail not recording
- Check that queue has a timeout value set (not 0)
- Verify Twilio webhooks are accessible
- Check WordPress error logs for details
**Issue**: No transcription
- Transcription is automatic from Twilio
- Can take a few minutes to process
- Check voicemail record in database
### Amazon SNS SMS
**Issue**: SMS not sending
- Verify AWS credentials are correct
- Check IAM permissions include `sns:Publish`
- Ensure phone number is in E.164 format (+1XXXXXXXXXX)
- Check AWS region is correct
**Issue**: "AWS SDK not found"
- Run: `composer require aws/aws-sdk-php`
- Ensure composer autoload is working
- Check that `vendor/` directory exists
**Issue**: Sender ID not showing
- Sender ID only works in certain countries
- US/Canada don't support alphanumeric sender IDs
- Use default AWS number instead
### Testing
#### Test Voicemail
1. Call your Twilio number
2. Wait for queue timeout (or set timeout to 10 seconds for testing)
3. Leave a voicemail when prompted
4. Check admin panel → Voicemails & Recordings
#### Test SMS Provider
```php
// Add to a test script or run via admin
require_once 'includes/class-twp-sms-manager.php';
// Test current provider
$validation = TWP_SMS_Manager::validate_current_provider();
print_r($validation);
// Send test message
$result = TWP_SMS_Manager::send_test_sms('+1XXXXXXXXXX');
print_r($result);
```
---
## Migration Notes
### Upgrading
Both features are backward compatible:
- **Queue Timeout**: Defaults to voicemail, can be changed to callback
- **SMS Provider**: Defaults to Twilio, no action required
### Database Changes
No database schema changes required. Uses existing:
- `twp_voicemails` table
- WordPress options table for settings
### Rollback
To revert to original behavior:
1. **Queue Timeout**: Change setting to "Offer Callback"
2. **SMS Provider**: Change setting to "Twilio"
---
## Support
For issues or questions:
1. Check error logs: `/wp-content/debug.log`
2. Review Twilio webhook logs
3. Check AWS CloudWatch logs (for SNS)
4. Contact plugin support
---
## Version History
**v2.4.0** - October 2025
- Added queue timeout voicemail feature
- Added Amazon SNS SMS provider support
- Added SMS provider abstraction layer
- Updated admin settings UI
---
## Credits
Developed for Twilio WordPress Plugin

View File

@@ -553,7 +553,103 @@ class TWP_Admin {
<p class="description">Default Twilio phone number to use as sender for SMS messages when not in a workflow context.</p> <p class="description">Default Twilio phone number to use as sender for SMS messages when not in a workflow context.</p>
</td> </td>
</tr> </tr>
</table>
<h2>SMS Provider Settings</h2>
<table class="form-table">
<tr>
<th scope="row">SMS Provider</th>
<td>
<?php $sms_provider = get_option('twp_sms_provider', 'twilio'); ?>
<select name="twp_sms_provider" id="twp_sms_provider" class="regular-text">
<option value="twilio" <?php selected($sms_provider, 'twilio'); ?>>Twilio (Default)</option>
<option value="aws_sns" <?php selected($sms_provider, 'aws_sns'); ?>>Amazon SNS</option>
</select>
<p class="description">Choose which service to use for sending SMS messages. If you're having trouble getting Twilio SMS approved, Amazon SNS is an alternative.</p>
</td>
</tr>
<!-- Amazon SNS Settings -->
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">AWS Access Key ID</th>
<td>
<input type="text" name="twp_aws_access_key"
value="<?php echo esc_attr(get_option('twp_aws_access_key')); ?>"
class="regular-text"
placeholder="AKIAIOSFODNN7EXAMPLE" />
<p class="description">Your AWS IAM access key with SNS permissions. <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html" target="_blank">How to create AWS access keys</a></p>
</td>
</tr>
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">AWS Secret Access Key</th>
<td>
<input type="password" name="twp_aws_secret_key"
value="<?php echo esc_attr(get_option('twp_aws_secret_key')); ?>"
class="regular-text"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
<p class="description">Your AWS IAM secret key. Keep this secure.</p>
</td>
</tr>
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">AWS Region</th>
<td>
<?php $aws_region = get_option('twp_aws_region', 'us-east-1'); ?>
<select name="twp_aws_region" class="regular-text">
<option value="us-east-1" <?php selected($aws_region, 'us-east-1'); ?>>US East (N. Virginia) - us-east-1</option>
<option value="us-east-2" <?php selected($aws_region, 'us-east-2'); ?>>US East (Ohio) - us-east-2</option>
<option value="us-west-1" <?php selected($aws_region, 'us-west-1'); ?>>US West (N. California) - us-west-1</option>
<option value="us-west-2" <?php selected($aws_region, 'us-west-2'); ?>>US West (Oregon) - us-west-2</option>
<option value="eu-west-1" <?php selected($aws_region, 'eu-west-1'); ?>>EU (Ireland) - eu-west-1</option>
<option value="eu-central-1" <?php selected($aws_region, 'eu-central-1'); ?>>EU (Frankfurt) - eu-central-1</option>
<option value="ap-southeast-1" <?php selected($aws_region, 'ap-southeast-1'); ?>>Asia Pacific (Singapore) - ap-southeast-1</option>
<option value="ap-northeast-1" <?php selected($aws_region, 'ap-northeast-1'); ?>>Asia Pacific (Tokyo) - ap-northeast-1</option>
</select>
<p class="description">AWS region where your SNS service is configured.</p>
</td>
</tr>
<tr class="aws-sns-setting" style="<?php echo ($sms_provider !== 'aws_sns') ? 'display:none;' : ''; ?>">
<th scope="row">SMS Sender ID (Optional)</th>
<td>
<input type="text" name="twp_aws_sns_sender_id"
value="<?php echo esc_attr(get_option('twp_aws_sns_sender_id')); ?>"
class="regular-text"
placeholder="MyCompany"
maxlength="11" />
<p class="description">Alphanumeric sender ID (3-11 characters). Supported in some countries. Leave blank to use default AWS number.</p>
</td>
</tr>
<tr>
<th scope="row">Queue Timeout Action</th>
<td>
<?php $timeout_action = get_option('twp_queue_timeout_action', 'voicemail'); ?>
<select name="twp_queue_timeout_action" class="regular-text">
<option value="voicemail" <?php selected($timeout_action, 'voicemail'); ?>>Take Voicemail</option>
<option value="callback" <?php selected($timeout_action, 'callback'); ?>>Offer Callback</option>
</select>
<p class="description">What to do when a caller reaches the queue timeout limit. Voicemail is recommended for better caller experience.</p>
</td>
</tr>
</table>
<script>
jQuery(document).ready(function($) {
// Show/hide AWS SNS settings based on provider selection
$('#twp_sms_provider').on('change', function() {
if ($(this).val() === 'aws_sns') {
$('.aws-sns-setting').show();
} else {
$('.aws-sns-setting').hide();
}
});
});
</script>
<h2>Voicemail & Notification Settings</h2>
<table class="form-table">
<!-- Discord/Slack Notifications Section --> <!-- Discord/Slack Notifications Section -->
<tr valign="top"> <tr valign="top">
<td colspan="2"> <td colspan="2">
@@ -3723,7 +3819,15 @@ class TWP_Admin {
register_setting('twilio-wp-settings-group', 'twp_urgent_keywords'); register_setting('twilio-wp-settings-group', 'twp_urgent_keywords');
register_setting('twilio-wp-settings-group', 'twp_sms_notification_number'); register_setting('twilio-wp-settings-group', 'twp_sms_notification_number');
register_setting('twilio-wp-settings-group', 'twp_default_sms_number'); register_setting('twilio-wp-settings-group', 'twp_default_sms_number');
// SMS Provider settings
register_setting('twilio-wp-settings-group', 'twp_sms_provider');
register_setting('twilio-wp-settings-group', 'twp_aws_access_key');
register_setting('twilio-wp-settings-group', 'twp_aws_secret_key');
register_setting('twilio-wp-settings-group', 'twp_aws_region');
register_setting('twilio-wp-settings-group', 'twp_aws_sns_sender_id');
register_setting('twilio-wp-settings-group', 'twp_queue_timeout_action');
// Discord/Slack notification settings // Discord/Slack notification settings
register_setting('twilio-wp-settings-group', 'twp_discord_webhook_url'); register_setting('twilio-wp-settings-group', 'twp_discord_webhook_url');
register_setting('twilio-wp-settings-group', 'twp_slack_webhook_url'); register_setting('twilio-wp-settings-group', 'twp_slack_webhook_url');

View File

@@ -4,7 +4,11 @@
"type": "wordpress-plugin", "type": "wordpress-plugin",
"require": { "require": {
"php": ">=8.0", "php": ">=8.0",
"twilio/sdk": "^8.7" "twilio/sdk": "^8.7",
"aws/aws-sdk-php": "^3.0"
},
"suggest": {
"aws/aws-sdk-php": "Required for Amazon SNS SMS provider support"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [

View File

@@ -168,7 +168,7 @@ class TWP_Call_Queue {
private function handle_timeout($call, $queue) { private function handle_timeout($call, $queue) {
global $wpdb; global $wpdb;
$table_name = $wpdb->prefix . 'twp_queued_calls'; $table_name = $wpdb->prefix . 'twp_queued_calls';
// Update status // Update status
$wpdb->update( $wpdb->update(
$table_name, $table_name,
@@ -180,15 +180,36 @@ class TWP_Call_Queue {
array('%s', '%s'), array('%s', '%s'),
array('%d') array('%d')
); );
// Offer callback instead of hanging up // Get timeout action preference (default to voicemail)
$callback_twiml = TWP_Callback_Manager::create_callback_twiml($queue->id, $call->from_number); $timeout_action = get_option('twp_queue_timeout_action', 'voicemail');
$twilio = new TWP_Twilio_API(); $twilio = new TWP_Twilio_API();
$twilio->update_call($call->call_sid, array(
'twiml' => $callback_twiml if ($timeout_action === 'voicemail') {
)); // Offer voicemail recording
require_once dirname(__FILE__) . '/class-twp-voicemail-handler.php';
$voicemail_twiml = TWP_Voicemail_Handler::create_voicemail_twiml(
$call->from_number,
$queue->id
);
$twilio->update_call($call->call_sid, array(
'twiml' => $voicemail_twiml
));
error_log("TWP Queue Timeout: Directing call {$call->call_sid} to voicemail");
} else {
// Offer callback (original behavior)
$callback_twiml = TWP_Callback_Manager::create_callback_twiml($queue->id, $call->from_number);
$twilio->update_call($call->call_sid, array(
'twiml' => $callback_twiml
));
error_log("TWP Queue Timeout: Offering callback to {$call->from_number}");
}
// Reorder queue // Reorder queue
self::reorder_queue($queue->id); self::reorder_queue($queue->id);
} }

View File

@@ -0,0 +1,150 @@
<?php
/**
* SMS Manager
*
* Manages SMS provider selection and message sending
*/
class TWP_SMS_Manager {
private static $provider = null;
/**
* Get the active SMS provider instance
*
* @return TWP_SMS_Provider|null
*/
public static function get_provider() {
if (self::$provider !== null) {
return self::$provider;
}
// Load interface and providers
require_once dirname(__FILE__) . '/interface-twp-sms-provider.php';
require_once dirname(__FILE__) . '/class-twp-sms-provider-twilio.php';
require_once dirname(__FILE__) . '/class-twp-sms-provider-sns.php';
// Get selected provider from settings
$selected_provider = get_option('twp_sms_provider', 'twilio');
switch ($selected_provider) {
case 'aws_sns':
self::$provider = new TWP_SMS_Provider_SNS();
break;
case 'twilio':
default:
self::$provider = new TWP_SMS_Provider_Twilio();
break;
}
return self::$provider;
}
/**
* Send an SMS message using the configured provider
*
* @param string $to_number Recipient phone number (E.164 format)
* @param string $message Message body
* @param string $from_number Sender phone number (E.164 format)
* @return array Response array with 'success' and 'data' or 'error'
*/
public static function send_sms($to_number, $message, $from_number = null) {
$provider = self::get_provider();
if (!$provider) {
return [
'success' => false,
'error' => 'No SMS provider configured'
];
}
// Validate provider configuration before sending
$validation = $provider->validate_configuration();
if (!$validation['success']) {
error_log('TWP SMS Error: Provider validation failed - ' . $validation['error']);
return $validation;
}
// Send SMS
$result = $provider->send_sms($to_number, $message, $from_number);
// Log the result
if ($result['success']) {
error_log(sprintf(
'TWP SMS: Message sent via %s to %s',
$provider->get_provider_name(),
$to_number
));
} else {
error_log(sprintf(
'TWP SMS Error: Failed to send via %s to %s - %s',
$provider->get_provider_name(),
$to_number,
$result['error'] ?? 'Unknown error'
));
}
return $result;
}
/**
* Get list of available SMS providers
*
* @return array Array of provider IDs and names
*/
public static function get_available_providers() {
return [
'twilio' => 'Twilio',
'aws_sns' => 'Amazon SNS'
];
}
/**
* Validate current provider configuration
*
* @return array Response array with 'success' and 'message' or 'error'
*/
public static function validate_current_provider() {
$provider = self::get_provider();
if (!$provider) {
return [
'success' => false,
'error' => 'No SMS provider configured'
];
}
return $provider->validate_configuration();
}
/**
* Get current provider name
*
* @return string Provider name
*/
public static function get_current_provider_name() {
$provider = self::get_provider();
if (!$provider) {
return 'None';
}
return $provider->get_provider_name();
}
/**
* Test SMS send
*
* @param string $to_number Test recipient number
* @return array Response array
*/
public static function send_test_sms($to_number) {
$message = sprintf(
'This is a test message from Twilio WordPress Plugin using %s provider at %s',
self::get_current_provider_name(),
current_time('Y-m-d H:i:s')
);
return self::send_sms($to_number, $message);
}
}

View File

@@ -0,0 +1,206 @@
<?php
/**
* Amazon SNS SMS Provider
*
* SMS provider implementation for Amazon SNS/SMS
*/
class TWP_SMS_Provider_SNS implements TWP_SMS_Provider {
private $sns_client;
private $default_sender_id;
/**
* Constructor
*/
public function __construct() {
$aws_access_key = get_option('twp_aws_access_key');
$aws_secret_key = get_option('twp_aws_secret_key');
$aws_region = get_option('twp_aws_region', 'us-east-1');
$this->default_sender_id = get_option('twp_aws_sns_sender_id', '');
// Initialize AWS SNS client if credentials are available
if (!empty($aws_access_key) && !empty($aws_secret_key)) {
try {
// Check if AWS SDK is available
if (!class_exists('Aws\Sns\SnsClient')) {
error_log('TWP SNS Error: AWS SDK not found. Please install via Composer: composer require aws/aws-sdk-php');
return;
}
$this->sns_client = new Aws\Sns\SnsClient([
'version' => 'latest',
'region' => $aws_region,
'credentials' => [
'key' => $aws_access_key,
'secret' => $aws_secret_key
]
]);
} catch (Exception $e) {
error_log('TWP SNS Error: Failed to initialize SNS client: ' . $e->getMessage());
}
}
}
/**
* Send an SMS message
*
* @param string $to_number Recipient phone number (E.164 format)
* @param string $message Message body
* @param string $from_number Sender phone number or Sender ID (not used by SNS in the same way)
* @return array Response array with 'success' and 'data' or 'error'
*/
public function send_sms($to_number, $message, $from_number = null) {
try {
if (!$this->sns_client) {
return [
'success' => false,
'error' => 'AWS SNS client not initialized. Check AWS credentials and ensure AWS SDK is installed.'
];
}
// Prepare message attributes
$message_attributes = [
'AWS.SNS.SMS.SMSType' => [
'DataType' => 'String',
'StringValue' => 'Transactional' // Transactional for higher reliability
]
];
// Use sender ID if provided or use default
$sender_id = $from_number ?: $this->default_sender_id;
if (!empty($sender_id)) {
// Remove '+' and non-alphanumeric characters for Sender ID
// Note: Sender ID is alphanumeric (3-11 chars) in many countries
$sender_id_clean = preg_replace('/[^a-zA-Z0-9]/', '', $sender_id);
if (strlen($sender_id_clean) >= 3 && strlen($sender_id_clean) <= 11) {
$message_attributes['AWS.SNS.SMS.SenderID'] = [
'DataType' => 'String',
'StringValue' => $sender_id_clean
];
}
}
// Send SMS via SNS
$result = $this->sns_client->publish([
'Message' => $message,
'PhoneNumber' => $to_number,
'MessageAttributes' => $message_attributes
]);
return [
'success' => true,
'provider' => 'aws_sns',
'data' => [
'message_id' => $result['MessageId'],
'to' => $to_number,
'body' => $message,
'sender_id' => !empty($sender_id_clean) ? $sender_id_clean : null
]
];
} catch (Aws\Exception\AwsException $e) {
return [
'success' => false,
'provider' => 'aws_sns',
'error' => $e->getAwsErrorMessage(),
'code' => $e->getAwsErrorCode()
];
} catch (Exception $e) {
return [
'success' => false,
'provider' => 'aws_sns',
'error' => $e->getMessage()
];
}
}
/**
* Get provider name
*
* @return string Provider name
*/
public function get_provider_name() {
return 'Amazon SNS';
}
/**
* Validate provider configuration
*
* @return array Response array with 'success' and 'message' or 'error'
*/
public function validate_configuration() {
// Check if AWS SDK is available
if (!class_exists('Aws\Sns\SnsClient')) {
return [
'success' => false,
'error' => 'AWS SDK not installed. Please run: composer require aws/aws-sdk-php'
];
}
$aws_access_key = get_option('twp_aws_access_key');
$aws_secret_key = get_option('twp_aws_secret_key');
$aws_region = get_option('twp_aws_region');
if (empty($aws_access_key)) {
return [
'success' => false,
'error' => 'AWS Access Key is not configured'
];
}
if (empty($aws_secret_key)) {
return [
'success' => false,
'error' => 'AWS Secret Key is not configured'
];
}
if (empty($aws_region)) {
return [
'success' => false,
'error' => 'AWS Region is not configured'
];
}
if (!$this->sns_client) {
return [
'success' => false,
'error' => 'Failed to initialize AWS SNS client'
];
}
return [
'success' => true,
'message' => 'Amazon SNS SMS provider is properly configured'
];
}
/**
* Set SMS spending limit (optional administrative function)
*
* @param float $monthly_limit Monthly spending limit in USD
* @return array Response array
*/
public function set_spending_limit($monthly_limit) {
try {
if (!$this->sns_client) {
return ['success' => false, 'error' => 'SNS client not initialized'];
}
$this->sns_client->setSMSAttributes([
'attributes' => [
'MonthlySpendLimit' => (string)$monthly_limit
]
]);
return [
'success' => true,
'message' => "SMS spending limit set to \$$monthly_limit per month"
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Twilio SMS Provider
*
* SMS provider implementation for Twilio
*/
class TWP_SMS_Provider_Twilio implements TWP_SMS_Provider {
private $client;
private $default_from_number;
/**
* Constructor
*/
public function __construct() {
$account_sid = get_option('twp_account_sid');
$auth_token = get_option('twp_auth_token');
$this->default_from_number = get_option('twp_sms_from_number');
if (!empty($account_sid) && !empty($auth_token)) {
$this->client = new Twilio\Rest\Client($account_sid, $auth_token);
}
}
/**
* Send an SMS message
*
* @param string $to_number Recipient phone number (E.164 format)
* @param string $message Message body
* @param string $from_number Sender phone number (E.164 format)
* @return array Response array with 'success' and 'data' or 'error'
*/
public function send_sms($to_number, $message, $from_number = null) {
try {
if (!$this->client) {
return [
'success' => false,
'error' => 'Twilio client not initialized. Check API credentials.'
];
}
// Determine the from number
$from = $from_number ?: $this->default_from_number;
// Validate we have a from number
if (empty($from)) {
error_log('TWP SMS Error: No from number available. Please configure SMS notification number in settings.');
return [
'success' => false,
'error' => 'No SMS from number configured. Please set SMS notification number in plugin settings.'
];
}
$sms = $this->client->messages->create(
$to_number,
[
'from' => $from,
'body' => $message
]
);
return [
'success' => true,
'provider' => 'twilio',
'data' => [
'sid' => $sms->sid,
'status' => $sms->status,
'from' => $sms->from,
'to' => $sms->to,
'body' => $sms->body,
'price' => $sms->price,
'priceUnit' => $sms->priceUnit
]
];
} catch (\Twilio\Exceptions\TwilioException $e) {
return [
'success' => false,
'provider' => 'twilio',
'error' => $e->getMessage(),
'code' => $e->getCode()
];
}
}
/**
* Get provider name
*
* @return string Provider name
*/
public function get_provider_name() {
return 'Twilio';
}
/**
* Validate provider configuration
*
* @return array Response array with 'success' and 'message' or 'error'
*/
public function validate_configuration() {
$account_sid = get_option('twp_account_sid');
$auth_token = get_option('twp_auth_token');
$from_number = get_option('twp_sms_from_number');
if (empty($account_sid)) {
return [
'success' => false,
'error' => 'Twilio Account SID is not configured'
];
}
if (empty($auth_token)) {
return [
'success' => false,
'error' => 'Twilio Auth Token is not configured'
];
}
if (empty($from_number)) {
return [
'success' => false,
'error' => 'SMS from number is not configured'
];
}
if (!$this->client) {
return [
'success' => false,
'error' => 'Failed to initialize Twilio client'
];
}
return [
'success' => true,
'message' => 'Twilio SMS provider is properly configured'
];
}
}

View File

@@ -275,49 +275,12 @@ class TWP_Twilio_API {
} }
/** /**
* Send SMS * Send SMS (uses configured SMS provider - Twilio or Amazon SNS)
*/ */
public function send_sms($to_number, $message, $from_number = null) { public function send_sms($to_number, $message, $from_number = null) {
try { // Use SMS Manager to handle provider abstraction
// Determine the from number require_once dirname(__FILE__) . '/class-twp-sms-manager.php';
$from = $from_number ?: $this->phone_number; return TWP_SMS_Manager::send_sms($to_number, $message, $from_number);
// Validate we have a from number
if (empty($from)) {
error_log('TWP SMS Error: No from number available. Please configure SMS notification number in settings.');
return [
'success' => false,
'error' => 'No SMS from number configured. Please set SMS notification number in plugin settings.'
];
}
$sms = $this->client->messages->create(
$to_number,
[
'from' => $from,
'body' => $message
]
);
return [
'success' => true,
'data' => [
'sid' => $sms->sid,
'status' => $sms->status,
'from' => $sms->from,
'to' => $sms->to,
'body' => $sms->body,
'price' => $sms->price,
'priceUnit' => $sms->priceUnit
]
];
} catch (\Twilio\Exceptions\TwilioException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'code' => $e->getCode()
];
}
} }
/** /**

View File

@@ -0,0 +1,226 @@
<?php
/**
* Voicemail Handler
*
* Handles voicemail recording prompts and processing
*/
class TWP_Voicemail_Handler {
/**
* Create TwiML for voicemail prompt and recording
*
* @param string $caller_number Caller's phone number
* @param int $queue_id Queue ID that timed out
* @param string $custom_prompt Optional custom voicemail prompt
* @return string TwiML XML
*/
public static function create_voicemail_twiml($caller_number, $queue_id = null, $custom_prompt = null) {
global $wpdb;
// Get queue information if provided
$queue = null;
if ($queue_id) {
$queue_table = $wpdb->prefix . 'twp_call_queues';
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $queue_table WHERE id = %d",
$queue_id
));
}
// Determine the prompt message
$prompt_message = $custom_prompt;
if (!$prompt_message && $queue && !empty($queue->voicemail_prompt)) {
$prompt_message = $queue->voicemail_prompt;
}
if (!$prompt_message) {
$prompt_message = "We're sorry, but all our agents are currently unavailable. Please leave a message after the tone, and we'll get back to you as soon as possible.";
}
// Generate TTS for the prompt
require_once dirname(__FILE__) . '/class-twp-tts-helper.php';
$tts_result = TWP_TTS_Helper::text_to_speech($prompt_message);
// Build TwiML response
$response = new \Twilio\TwiML\VoiceResponse();
if ($tts_result['success'] && !empty($tts_result['file_url'])) {
// Use generated TTS audio
$response->play($tts_result['file_url']);
} else {
// Fallback to Twilio's Say
$response->say($prompt_message, ['voice' => 'alice']);
}
// Record the voicemail
$record_params = [
'action' => home_url('/wp-json/twilio-webhook/v1/voicemail-complete'),
'recordingStatusCallback' => home_url('/wp-json/twilio-webhook/v1/voicemail-callback?' . http_build_query([
'from' => $caller_number,
'queue_id' => $queue_id,
'source' => 'queue_timeout'
])),
'recordingStatusCallbackMethod' => 'POST',
'maxLength' => 300, // 5 minutes max
'playBeep' => true,
'finishOnKey' => '#',
'transcribe' => true,
'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/voicemail-transcription')
];
$response->record($record_params);
// Thank you message after recording
$response->say('Thank you for your message. Goodbye.', ['voice' => 'alice']);
return $response;
}
/**
* Save voicemail to database
*
* @param array $voicemail_data Voicemail data
* @return int|false Voicemail ID or false on failure
*/
public static function save_voicemail($voicemail_data) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$insert_data = array(
'call_sid' => sanitize_text_field($voicemail_data['call_sid']),
'from_number' => sanitize_text_field($voicemail_data['from_number']),
'to_number' => !empty($voicemail_data['to_number']) ? sanitize_text_field($voicemail_data['to_number']) : '',
'recording_url' => esc_url_raw($voicemail_data['recording_url']),
'recording_duration' => intval($voicemail_data['recording_duration']),
'workflow_id' => !empty($voicemail_data['workflow_id']) ? intval($voicemail_data['workflow_id']) : null,
'queue_id' => !empty($voicemail_data['queue_id']) ? intval($voicemail_data['queue_id']) : null,
'source' => !empty($voicemail_data['source']) ? sanitize_text_field($voicemail_data['source']) : 'workflow',
'status' => 'new',
'received_at' => current_time('mysql')
);
$result = $wpdb->insert($table_name, $insert_data);
if ($result) {
$voicemail_id = $wpdb->insert_id;
// Log the voicemail
if (class_exists('TWP_Call_Logger')) {
TWP_Call_Logger::log_action(
$voicemail_data['call_sid'],
'Voicemail recorded from queue timeout (' . $voicemail_data['recording_duration'] . 's)'
);
}
return $voicemail_id;
}
return false;
}
/**
* Update voicemail transcription
*
* @param int $voicemail_id Voicemail ID
* @param string $transcription Transcription text
* @param string $transcription_status Transcription status
* @return bool Success
*/
public static function update_transcription($voicemail_id, $transcription, $transcription_status = 'completed') {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$result = $wpdb->update(
$table_name,
array(
'transcription' => sanitize_textarea_field($transcription),
'transcription_status' => $transcription_status
),
array('id' => $voicemail_id),
array('%s', '%s'),
array('%d')
);
// Check for urgent keywords in transcription
if ($result && !empty($transcription)) {
self::check_urgent_keywords($voicemail_id, $transcription);
}
return $result !== false;
}
/**
* Check transcription for urgent keywords
*
* @param int $voicemail_id Voicemail ID
* @param string $transcription Transcription text
*/
private static function check_urgent_keywords($voicemail_id, $transcription) {
global $wpdb;
// Get urgent keywords from settings
$urgent_keywords = get_option('twp_urgent_voicemail_keywords', array('urgent', 'emergency', 'asap', 'critical'));
if (is_string($urgent_keywords)) {
$urgent_keywords = array_map('trim', explode(',', $urgent_keywords));
}
// Check if transcription contains any urgent keywords
$transcription_lower = strtolower($transcription);
$found_keyword = null;
foreach ($urgent_keywords as $keyword) {
if (stripos($transcription_lower, strtolower(trim($keyword))) !== false) {
$found_keyword = $keyword;
break;
}
}
if ($found_keyword) {
// Mark voicemail as urgent
$table_name = $wpdb->prefix . 'twp_voicemails';
$wpdb->update(
$table_name,
array('is_urgent' => 1),
array('id' => $voicemail_id),
array('%d'),
array('%d')
);
// Send urgent notification
$voicemail = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$voicemail_id
));
if ($voicemail && class_exists('TWP_Notifications')) {
TWP_Notifications::send_call_notification('urgent_voicemail', array(
'type' => 'urgent_voicemail',
'from_number' => $voicemail->from_number,
'keyword' => $found_keyword,
'transcription' => $transcription,
'voicemail_id' => $voicemail_id,
'admin_url' => admin_url('admin.php?page=twilio-wp-voicemails&voicemail_id=' . $voicemail_id)
));
}
error_log("TWP Voicemail: Urgent keyword '$found_keyword' detected in voicemail $voicemail_id");
}
}
/**
* Get voicemail by ID
*
* @param int $voicemail_id Voicemail ID
* @return object|null Voicemail object
*/
public static function get_voicemail($voicemail_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$voicemail_id
));
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* SMS Provider Interface
*
* Interface for SMS providers (Twilio, Amazon SNS, etc.)
*/
interface TWP_SMS_Provider {
/**
* Send an SMS message
*
* @param string $to_number Recipient phone number (E.164 format)
* @param string $message Message body
* @param string $from_number Sender phone number (E.164 format)
* @return array Response array with 'success' and 'data' or 'error'
*/
public function send_sms($to_number, $message, $from_number = null);
/**
* Get provider name
*
* @return string Provider name
*/
public function get_provider_name();
/**
* Validate provider configuration
*
* @return array Response array with 'success' and 'message' or 'error'
*/
public function validate_configuration();
}