diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php index f2b32f0..ef6f093 100644 --- a/admin/class-twp-admin.php +++ b/admin/class-twp-admin.php @@ -481,6 +481,60 @@ class TWP_Admin {

Default Twilio phone number to use as sender for SMS messages when not in a workflow context.

+ + + + +

Discord & Slack Notifications

+

Configure webhook URLs to receive call notifications in Discord and/or Slack channels.

+ + + + + Discord Webhook URL + + +

Discord webhook URL for call notifications. How to create a Discord webhook

+ + + + + Slack Webhook URL + + +

Slack webhook URL for call notifications. How to create a Slack webhook

+ + + + + Notification Settings + +
+
+
+ +
+

Choose which events trigger Discord/Slack notifications.

+ + + + + Queue Timeout Threshold + + + seconds +

Send notification if call stays in queue longer than this time (30-1800 seconds).

+ + @@ -2725,6 +2779,14 @@ class TWP_Admin { 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_default_sms_number'); + + // Discord/Slack notification settings + 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_notify_on_incoming_calls'); + register_setting('twilio-wp-settings-group', 'twp_notify_on_queue_timeout'); + register_setting('twilio-wp-settings-group', 'twp_notify_on_missed_calls'); + register_setting('twilio-wp-settings-group', 'twp_queue_timeout_threshold'); } /** diff --git a/includes/class-twp-activator.php b/includes/class-twp-activator.php index 7a533d6..e44e3b0 100644 --- a/includes/class-twp-activator.php +++ b/includes/class-twp-activator.php @@ -366,6 +366,12 @@ class TWP_Activator { if (empty($status_index_exists)) { $wpdb->query("ALTER TABLE $table_queued_calls ADD INDEX status (status)"); } + + // Add notified_timeout column for Discord/Slack notifications + $notified_timeout_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_queued_calls LIKE 'notified_timeout'"); + if (empty($notified_timeout_exists)) { + $wpdb->query("ALTER TABLE $table_queued_calls ADD COLUMN notified_timeout datetime AFTER agent_call_sid"); + } } /** diff --git a/includes/class-twp-call-queue.php b/includes/class-twp-call-queue.php index 9e020e2..0ea9e44 100644 --- a/includes/class-twp-call-queue.php +++ b/includes/class-twp-call-queue.php @@ -574,6 +574,15 @@ class TWP_Call_Queue { return; } + // Send Discord/Slack notification for incoming call + require_once dirname(__FILE__) . '/class-twp-notifications.php'; + TWP_Notifications::send_call_notification('incoming_call', array( + 'type' => 'incoming_call', + 'caller' => $caller_number, + 'queue' => $queue->queue_name, + 'queue_id' => $queue_id + )); + // Get members of the assigned agent group require_once dirname(__FILE__) . '/class-twp-agent-groups.php'; $members = TWP_Agent_Groups::get_group_members($queue->agent_group_id); diff --git a/includes/class-twp-core.php b/includes/class-twp-core.php index a7479cf..9f6101f 100644 --- a/includes/class-twp-core.php +++ b/includes/class-twp-core.php @@ -167,6 +167,10 @@ class TWP_Core { // Agent queue management AJAX $this->loader->add_action('wp_ajax_twp_accept_call', $plugin_admin, 'ajax_accept_call'); + + // Discord/Slack notification system + $this->loader->add_action('init', $this, 'setup_notification_cron'); + $this->loader->add_action('twp_check_queue_timeouts', $this, 'check_queue_timeouts'); $this->loader->add_action('wp_ajax_twp_accept_next_queue_call', $plugin_admin, 'ajax_accept_next_queue_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'); @@ -385,4 +389,21 @@ class TWP_Core { error_log("TWP Cleanup: Updated {$updated_waiting} old waiting calls to 'timeout' status"); } } + + /** + * Setup notification cron job + */ + public function setup_notification_cron() { + if (!wp_next_scheduled('twp_check_queue_timeouts')) { + wp_schedule_event(time(), 'twp_every_minute', 'twp_check_queue_timeouts'); + } + } + + /** + * Check for queue timeouts and send notifications + */ + public function check_queue_timeouts() { + require_once dirname(__FILE__) . '/class-twp-notifications.php'; + TWP_Notifications::check_queue_timeouts(); + } } \ No newline at end of file diff --git a/includes/class-twp-notifications.php b/includes/class-twp-notifications.php new file mode 100644 index 0000000..2b6e9fe --- /dev/null +++ b/includes/class-twp-notifications.php @@ -0,0 +1,328 @@ + $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') + ); + } + } +} \ No newline at end of file diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php index 1a56897..1448b1f 100644 --- a/includes/class-twp-webhooks.php +++ b/includes/class-twp-webhooks.php @@ -338,6 +338,15 @@ class TWP_Webhooks { * Send SMS notification to agents about missed browser call */ private function send_agent_notification_sms($customer_number, $twilio_number) { + // Send Discord/Slack notification for missed browser call + require_once dirname(__FILE__) . '/class-twp-notifications.php'; + TWP_Notifications::send_call_notification('missed_call', array( + 'type' => 'missed_call', + 'caller' => $customer_number, + 'queue' => 'Browser Phone', + 'workflow_number' => $twilio_number + )); + // Get agents with phone numbers $agents = get_users(array( 'meta_key' => 'twp_phone_number', @@ -556,6 +565,15 @@ class TWP_Webhooks { * Send SMS notification to agents about missed call */ private function send_missed_call_notification($customer_number, $twilio_number) { + // Send Discord/Slack notification for missed call + require_once dirname(__FILE__) . '/class-twp-notifications.php'; + TWP_Notifications::send_call_notification('missed_call', array( + 'type' => 'missed_call', + 'caller' => $customer_number, + 'queue' => 'General', + 'workflow_number' => $twilio_number + )); + // Get agents with phone numbers $agents = get_users(array( 'meta_key' => 'twp_phone_number',