- Fix token expiration: Extend refresh buffer to 10 minutes for reliability - Add real-time queue updates: Reduce polling to 5 seconds for instant feedback - Implement audible alert system: 30-second repeating notifications with user toggle - Optimize Discord/Slack notifications: Non-blocking requests for immediate delivery - Add persistent alert preferences: Toggle button with localStorage integration - Clean up debug file: Remove unused debug-phone-numbers.php 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
410 lines
13 KiB
PHP
410 lines
13 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Handle Discord and Slack notifications for call events
|
|
*/
|
|
class TWP_Notifications {
|
|
|
|
/**
|
|
* Send notification to Discord and/or Slack
|
|
*/
|
|
public static function send_call_notification($type, $data) {
|
|
// Check if notifications are enabled for this type
|
|
if (!self::is_notification_enabled($type)) {
|
|
return;
|
|
}
|
|
|
|
$message = self::format_message($type, $data);
|
|
|
|
// Send to Discord if configured
|
|
$discord_url = get_option('twp_discord_webhook_url');
|
|
if (!empty($discord_url)) {
|
|
self::send_discord_notification($discord_url, $message, $data);
|
|
}
|
|
|
|
// Send to Slack if configured
|
|
$slack_url = get_option('twp_slack_webhook_url');
|
|
if (!empty($slack_url)) {
|
|
self::send_slack_notification($slack_url, $message, $data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if notifications are enabled for a specific type
|
|
*/
|
|
private static function is_notification_enabled($type) {
|
|
switch ($type) {
|
|
case 'incoming_call':
|
|
return get_option('twp_notify_on_incoming_calls', 1);
|
|
case 'queue_timeout':
|
|
return get_option('twp_notify_on_queue_timeout', 1);
|
|
case 'missed_call':
|
|
return get_option('twp_notify_on_missed_calls', 1);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format message based on notification type
|
|
*/
|
|
private static function format_message($type, $data) {
|
|
$caller = isset($data['caller']) ? $data['caller'] : 'Unknown';
|
|
$queue = isset($data['queue']) ? $data['queue'] : 'Unknown';
|
|
$time = current_time('Y-m-d H:i:s');
|
|
|
|
switch ($type) {
|
|
case 'incoming_call':
|
|
return "📞 **Incoming Call**\n" .
|
|
"**From:** {$caller}\n" .
|
|
"**Queue:** {$queue}\n" .
|
|
"**Time:** {$time}";
|
|
|
|
case 'queue_timeout':
|
|
$duration = isset($data['duration']) ? $data['duration'] : 'Unknown';
|
|
return "⏰ **Queue Timeout Alert**\n" .
|
|
"**Caller:** {$caller}\n" .
|
|
"**Queue:** {$queue}\n" .
|
|
"**Wait Time:** {$duration} seconds\n" .
|
|
"**Time:** {$time}";
|
|
|
|
case 'missed_call':
|
|
return "❌ **Missed Call**\n" .
|
|
"**From:** {$caller}\n" .
|
|
"**Queue:** {$queue}\n" .
|
|
"**Time:** {$time}";
|
|
|
|
default:
|
|
return "📋 **Call Event:** {$type}\n" .
|
|
"**Details:** " . json_encode($data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send notification to Discord
|
|
*/
|
|
public static function send_discord_notification($webhook_url, $data, $message = null) {
|
|
$payload = array(
|
|
'content' => $message,
|
|
'embeds' => array(
|
|
array(
|
|
'title' => self::get_notification_title($data),
|
|
'color' => self::get_notification_color($data),
|
|
'fields' => self::get_discord_fields($data),
|
|
'timestamp' => date('c'),
|
|
'footer' => array(
|
|
'text' => 'Twilio WP Plugin'
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
self::send_webhook_request($webhook_url, $payload, 'Discord');
|
|
}
|
|
|
|
/**
|
|
* Send notification to Slack
|
|
*/
|
|
public static function send_slack_notification($webhook_url, $data, $message = null) {
|
|
$payload = array(
|
|
'text' => self::get_notification_title($data),
|
|
'attachments' => array(
|
|
array(
|
|
'color' => self::get_slack_color($data),
|
|
'fields' => self::get_slack_fields($data),
|
|
'footer' => 'Twilio WP Plugin',
|
|
'ts' => time()
|
|
)
|
|
)
|
|
);
|
|
|
|
self::send_webhook_request($webhook_url, $payload, 'Slack');
|
|
}
|
|
|
|
/**
|
|
* Get notification title
|
|
*/
|
|
private static function get_notification_title($data) {
|
|
$type = isset($data['type']) ? $data['type'] : 'call_event';
|
|
|
|
switch ($type) {
|
|
case 'incoming_call':
|
|
return '📞 Incoming Call';
|
|
case 'queue_timeout':
|
|
return '⏰ Queue Timeout Alert';
|
|
case 'missed_call':
|
|
return '❌ Missed Call';
|
|
case 'urgent_voicemail':
|
|
return '🚨 URGENT Voicemail Alert';
|
|
default:
|
|
return '📋 Call Event';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get notification color for Discord (decimal)
|
|
*/
|
|
private static function get_notification_color($data) {
|
|
$type = isset($data['type']) ? $data['type'] : 'call_event';
|
|
|
|
switch ($type) {
|
|
case 'incoming_call':
|
|
return 3447003; // Blue
|
|
case 'queue_timeout':
|
|
return 16776960; // Yellow
|
|
case 'missed_call':
|
|
return 15158332; // Red
|
|
case 'urgent_voicemail':
|
|
return 16711680; // Bright Red
|
|
default:
|
|
return 9807270; // Gray
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get notification color for Slack (hex)
|
|
*/
|
|
private static function get_slack_color($data) {
|
|
$type = isset($data['type']) ? $data['type'] : 'call_event';
|
|
|
|
switch ($type) {
|
|
case 'incoming_call':
|
|
return '#36a64f'; // Green
|
|
case 'queue_timeout':
|
|
return '#ffcc00'; // Yellow
|
|
case 'missed_call':
|
|
return '#ff0000'; // Red
|
|
case 'urgent_voicemail':
|
|
return '#ff0000'; // Bright Red
|
|
default:
|
|
return '#666666'; // Gray
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Discord fields
|
|
*/
|
|
private static function get_discord_fields($data) {
|
|
$fields = array();
|
|
|
|
if (isset($data['caller'])) {
|
|
$fields[] = array(
|
|
'name' => 'Caller',
|
|
'value' => $data['caller'],
|
|
'inline' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['queue'])) {
|
|
$fields[] = array(
|
|
'name' => 'Queue',
|
|
'value' => $data['queue'],
|
|
'inline' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['duration'])) {
|
|
$fields[] = array(
|
|
'name' => 'Duration',
|
|
'value' => $data['duration'] . ' seconds',
|
|
'inline' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['workflow_number'])) {
|
|
$fields[] = array(
|
|
'name' => 'Number Called',
|
|
'value' => $data['workflow_number'],
|
|
'inline' => true
|
|
);
|
|
}
|
|
|
|
// Urgent voicemail specific fields
|
|
if (isset($data['type']) && $data['type'] === 'urgent_voicemail') {
|
|
if (isset($data['from_number'])) {
|
|
$fields[] = array(
|
|
'name' => '📞 From',
|
|
'value' => $data['from_number'],
|
|
'inline' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['keyword'])) {
|
|
$fields[] = array(
|
|
'name' => '🔴 Keyword Detected',
|
|
'value' => strtoupper($data['keyword']),
|
|
'inline' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['transcription'])) {
|
|
// Truncate transcription if too long
|
|
$transcription = $data['transcription'];
|
|
if (strlen($transcription) > 500) {
|
|
$transcription = substr($transcription, 0, 497) . '...';
|
|
}
|
|
$fields[] = array(
|
|
'name' => '📝 Transcription',
|
|
'value' => $transcription,
|
|
'inline' => false
|
|
);
|
|
}
|
|
|
|
if (isset($data['admin_url'])) {
|
|
$fields[] = array(
|
|
'name' => '🔗 Action',
|
|
'value' => '[Listen to Voicemail](' . $data['admin_url'] . ')',
|
|
'inline' => false
|
|
);
|
|
}
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Get Slack fields
|
|
*/
|
|
private static function get_slack_fields($data) {
|
|
$fields = array();
|
|
|
|
if (isset($data['caller'])) {
|
|
$fields[] = array(
|
|
'title' => 'Caller',
|
|
'value' => $data['caller'],
|
|
'short' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['queue'])) {
|
|
$fields[] = array(
|
|
'title' => 'Queue',
|
|
'value' => $data['queue'],
|
|
'short' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['duration'])) {
|
|
$fields[] = array(
|
|
'title' => 'Duration',
|
|
'value' => $data['duration'] . ' seconds',
|
|
'short' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['workflow_number'])) {
|
|
$fields[] = array(
|
|
'title' => 'Number Called',
|
|
'value' => $data['workflow_number'],
|
|
'short' => true
|
|
);
|
|
}
|
|
|
|
// Urgent voicemail specific fields
|
|
if (isset($data['type']) && $data['type'] === 'urgent_voicemail') {
|
|
if (isset($data['from_number'])) {
|
|
$fields[] = array(
|
|
'title' => 'From',
|
|
'value' => $data['from_number'],
|
|
'short' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['keyword'])) {
|
|
$fields[] = array(
|
|
'title' => '🔴 Keyword Detected',
|
|
'value' => strtoupper($data['keyword']),
|
|
'short' => true
|
|
);
|
|
}
|
|
|
|
if (isset($data['transcription'])) {
|
|
// Truncate transcription if too long
|
|
$transcription = $data['transcription'];
|
|
if (strlen($transcription) > 500) {
|
|
$transcription = substr($transcription, 0, 497) . '...';
|
|
}
|
|
$fields[] = array(
|
|
'title' => 'Transcription',
|
|
'value' => $transcription,
|
|
'short' => false
|
|
);
|
|
}
|
|
|
|
if (isset($data['admin_url'])) {
|
|
$fields[] = array(
|
|
'title' => 'Action',
|
|
'value' => '<' . $data['admin_url'] . '|Listen to Voicemail>',
|
|
'short' => false
|
|
);
|
|
}
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Send webhook request
|
|
*/
|
|
private static function send_webhook_request($webhook_url, $payload, $service) {
|
|
// Send notification immediately without blocking
|
|
$response = wp_remote_post($webhook_url, array(
|
|
'headers' => array(
|
|
'Content-Type' => 'application/json',
|
|
),
|
|
'body' => json_encode($payload),
|
|
'timeout' => 10, // Reduce timeout for faster processing
|
|
'blocking' => false, // Non-blocking request for immediate processing
|
|
));
|
|
|
|
if (is_wp_error($response)) {
|
|
error_log("TWP {$service} Notification Error: " . $response->get_error_message());
|
|
return false;
|
|
}
|
|
|
|
// For non-blocking requests, we can't check response code immediately
|
|
error_log("TWP {$service} notification sent (non-blocking)");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Monitor queue timeouts
|
|
*/
|
|
public static function check_queue_timeouts() {
|
|
global $wpdb;
|
|
|
|
$threshold = get_option('twp_queue_timeout_threshold', 300); // Default 5 minutes
|
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
// Find calls that have been waiting too long
|
|
$timeout_calls = $wpdb->get_results($wpdb->prepare("
|
|
SELECT qc.*, q.queue_name
|
|
FROM {$calls_table} qc
|
|
LEFT JOIN {$queues_table} q ON qc.queue_id = q.id
|
|
WHERE qc.status = 'waiting'
|
|
AND TIMESTAMPDIFF(SECOND, qc.joined_at, NOW()) > %d
|
|
AND qc.notified_timeout IS NULL
|
|
", $threshold));
|
|
|
|
foreach ($timeout_calls as $call) {
|
|
// Send timeout notification
|
|
self::send_call_notification('queue_timeout', array(
|
|
'type' => 'queue_timeout',
|
|
'caller' => $call->from_number,
|
|
'queue' => $call->queue_name,
|
|
'duration' => time() - strtotime($call->joined_at),
|
|
'workflow_number' => $call->to_number
|
|
));
|
|
|
|
// Mark as notified to avoid duplicate notifications
|
|
$wpdb->update(
|
|
$calls_table,
|
|
array('notified_timeout' => current_time('mysql')),
|
|
array('id' => $call->id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
}
|
|
}
|
|
} |