Implement Discord and Slack notifications for call events

Settings & Configuration:
- Added Discord webhook URL, Slack webhook URL settings in admin
- Added notification type toggles (incoming calls, queue timeouts, missed calls)
- Added queue timeout threshold setting (30-1800 seconds)
- Registered all new settings with WordPress options system

Notification System:
- Created TWP_Notifications class for Discord/Slack webhook handling
- Rich message formatting with embeds/attachments for both platforms
- Color-coded notifications (blue=incoming, yellow=timeout, red=missed)
- Comprehensive error handling and logging

Integration Points:
- Incoming calls: Notifications sent when calls enter queues
- Queue timeouts: Automated monitoring via cron job (every minute)
- Missed calls: Notifications for browser phone and general missed calls
- Added notified_timeout column to prevent duplicate timeout notifications

Features:
- Professional Discord embeds with fields and timestamps
- Slack attachments with proper formatting and colors
- Automatic cron job setup for queue timeout monitoring
- Fallback to SMS notifications while Discord/Slack also work
- Configurable notification types and timeout thresholds

This provides real-time call notifications to Discord channels and Slack channels,
helping teams stay informed about incoming calls and queue issues even when
SMS notifications aren't working due to validation delays.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-13 10:47:59 -07:00
parent 97a064bf83
commit 534d343526
6 changed files with 444 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
<?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
*/
private static function send_discord_notification($webhook_url, $message, $data) {
$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
*/
private static function send_slack_notification($webhook_url, $message, $data) {
$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';
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
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
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
);
}
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
);
}
return $fields;
}
/**
* Send webhook request
*/
private static function send_webhook_request($webhook_url, $payload, $service) {
$response = wp_remote_post($webhook_url, array(
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => json_encode($payload),
'timeout' => 30,
));
if (is_wp_error($response)) {
error_log("TWP {$service} Notification Error: " . $response->get_error_message());
return false;
}
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code >= 200 && $response_code < 300) {
error_log("TWP {$service} notification sent successfully");
return true;
} else {
error_log("TWP {$service} notification failed with response code: " . $response_code);
error_log("Response body: " . wp_remote_retrieve_body($response));
return false;
}
}
/**
* 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')
);
}
}
}