From 1d336e4cac398c418b348e287038dc09f58d3b71 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Thu, 22 Jan 2026 16:57:55 +0000 Subject: [PATCH] Add Webex Interact webhook parser - Added wxinteractParse() function supporting all SMS API events: * Outbound SMS (submitted, delivered, failed) * Shortlink clicked * Inbound SMS received * SMS opt out * Contacts callback - Helper functions for each event type with formatted HTML - Test samples file for all event types - Updated README with Webex Interact documentation - Follows existing parser style and security guidelines --- README.md | 70 ++++ index.php | 604 +++++++++++++++++++++++++++++++++++ wxinteract_test_samples.json | 86 +++++ 3 files changed, 760 insertions(+) create mode 100644 wxinteract_test_samples.json diff --git a/README.md b/README.md index 0bdb246..5aaec0a 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,33 @@ https://your-domain.com/test%40example.com/github - Clean, scannable layout - Relevant emojis for quick identification +#### Webex Interact (`/wxinteract`) +Formats Webex Interact SMS API webhook events into clean, readable emails. + +**Supported events:** +- 📤 Outbound SMS - Submitted +- ✅ Outbound SMS - Delivered +- ❌ Outbound SMS - Failed +- 🔗 Shortlink Clicked +- 📥 Inbound SMS Received +- 🚫 SMS Opt Out Received +- 👤 Contacts Callback + +**Example URL:** +``` +https://your-domain.com/test%40example.com/wxinteract +``` + +**What you get:** +- Event-specific formatting with relevant emojis +- Message content displayed in a clean message box +- Contact and campaign information +- Error details for failed messages +- Click tracking details for shortlinks +- Custom fields support for contact callbacks + +**Test samples available in:** `wxinteract_test_samples.json` + ### Creating Your Own Parser Want to add support for Stripe, GitLab, or other services? Check out the [**Parser Development Guide**](../PARSER_PROMPT.md) for detailed instructions on creating custom parsers. @@ -160,6 +187,49 @@ State: open Branch: feature-auth → main ``` +### With Webex Interact Parser + +Configure your Webex Interact API callback URL with: +``` +https://your-domain.com/yourname%40company.com/wxinteract +``` + +When SMS events occur, you'll receive formatted emails like: + +**Outbound SMS - Submitted:** +``` +📤 Webex Interact: Outbound SMS - Submitted + +Message ID: msg_123456789 +To: +61412345678 +From: +61387654321 +Campaign ID: camp_987654321 +Submitted At: 2026-01-22T16:45:00Z + +Message Content: +Hello! Your appointment is confirmed for tomorrow at 2 PM. +``` + +**SMS Opt Out:** +``` +🚫 Webex Interact: SMS Opt Out Received + +Phone Number: +61412345678 +Keyword: STOP +Opt Out At: 2026-01-22T18:15:00Z +``` + +**Shortlink Clicked:** +``` +🔗 Webex Interact: Shortlink Clicked + +Shortlink: https://wxint.co/abc123 +Original URL: https://example.com/special-offer +Phone Number: +61412345678 +Clicked At: 2026-01-22T17:30:45Z +IP Address: 203.0.113.45 +``` + ## 📦 Installation 1. Clone the repository: diff --git a/index.php b/index.php index 805795b..c2f9f76 100755 --- a/index.php +++ b/index.php @@ -787,6 +787,610 @@ HTML; ]; } +/** + * Webex Interact Webhook Parser + * Parses Webex Interact SMS webhook events into a formatted email + * + * @param string $rawBody The raw request body + * @param array $headers The request headers + * @param array $metadata Request metadata (date, ip, method, contentType) + * @return array|false Array with 'html' and 'subject' keys, or false on failure + */ +function wxinteractParse($rawBody, $headers, $metadata) { + $payload = json_decode($rawBody, true); + if (!$payload || !isset($payload['data'])) { + return false; + } + + $data = $payload['data']; + $status = $data['status'] ?? 'Unknown'; + + // Sanitize common fields + $statusSafe = htmlspecialchars($status, ENT_QUOTES, 'UTF-8'); + $dateSafe = htmlspecialchars($metadata['date'], ENT_QUOTES, 'UTF-8'); + $ipSafe = htmlspecialchars($metadata['ip'], ENT_QUOTES, 'UTF-8'); + + // Determine event type and icon + $eventIcon = '📱'; + $eventTitle = 'SMS Event'; + $eventContent = ''; + + switch ($status) { + case 'submitted': + $eventIcon = '📤'; + $eventTitle = 'Outbound SMS - Submitted'; + $eventContent = buildOutboundSmsSubmittedContent($data); + break; + + case 'delivered': + $eventIcon = '✅'; + $eventTitle = 'Outbound SMS - Delivered'; + $eventContent = buildOutboundSmsDeliveredContent($data); + break; + + case 'failed': + $eventIcon = '❌'; + $eventTitle = 'Outbound SMS - Failed'; + $eventContent = buildOutboundSmsFailedContent($data); + break; + + case 'shortlink_clicked': + $eventIcon = '🔗'; + $eventTitle = 'Shortlink Clicked'; + $eventContent = buildShortlinkClickedContent($data); + break; + + case 'received': + $eventIcon = '📥'; + $eventTitle = 'Inbound SMS Received'; + $eventContent = buildInboundSmsContent($data); + break; + + case 'opt_out': + $eventIcon = '🚫'; + $eventTitle = 'SMS Opt Out Received'; + $eventContent = buildOptOutContent($data); + break; + + case 'contact_created': + case 'contact_updated': + $eventIcon = '👤'; + $eventTitle = 'Contacts Callback'; + $eventContent = buildContactsCallbackContent($data); + break; + + default: + // Unknown event type - display all data + $eventIcon = '📱'; + $eventTitle = "SMS Event: $statusSafe"; + $eventContent = '
Event Details
'; + $eventContent .= renderDataAsTable($data); + $eventContent .= '
'; + } + + $eventTitleSafe = htmlspecialchars($eventTitle, ENT_QUOTES, 'UTF-8'); + + $html = << + + + + + Webex Interact - $eventTitleSafe + + + +
+
+

$eventIcon Webex Interact: $eventTitleSafe

+

$dateSafe

+
+ +
+
📋 Event Information
+ +
+ + $eventContent + + +
+ + +HTML; + + $subject = "$eventIcon Webex Interact: $eventTitle"; + + return [ + 'html' => $html, + 'subject' => $subject + ]; +} + +/** + * Build content for Outbound SMS Submitted event + */ +function buildOutboundSmsSubmittedContent($data) { + $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $submittedAt = htmlspecialchars($data['submitted_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + return << +
📤 SMS Details
+
+ + + + + +
+ + +
+
💬 Message Content
+
$message
+
+HTML; +} + +/** + * Build content for Outbound SMS Delivered event + */ +function buildOutboundSmsDeliveredContent($data) { + $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $deliveredAt = htmlspecialchars($data['delivered_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + return << +
✅ Delivery Details
+
+ + + + + +
+ +HTML; +} + +/** + * Build content for Outbound SMS Failed event + */ +function buildOutboundSmsFailedContent($data) { + $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $failedAt = htmlspecialchars($data['failed_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $errorCode = htmlspecialchars($data['error_code'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $errorMessage = htmlspecialchars($data['error_message'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + return << +
❌ Failure Details
+
+ + + + + +
+ + +
+
⚠️ Error Information
+
+ Error Code: $errorCode
+ Error Message: $errorMessage +
+
+HTML; +} + +/** + * Build content for Shortlink Clicked event + */ +function buildShortlinkClickedContent($data) { + $shortlink = htmlspecialchars($data['shortlink'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $originalUrl = htmlspecialchars($data['original_url'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $clickedAt = htmlspecialchars($data['clicked_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $userAgent = htmlspecialchars($data['user_agent'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $ipAddress = htmlspecialchars($data['ip_address'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + return << +
🔗 Click Details
+
+ + + + + + + +
+ + +
+
🌐 Browser Information
+
$userAgent
+
+HTML; +} + +/** + * Build content for Inbound SMS Received event + */ +function buildInboundSmsContent($data) { + $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $receivedAt = htmlspecialchars($data['received_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + return << +
📥 Inbound SMS Details
+
+ + + + + +
+ + +
+
💬 Message Content
+
$message
+
+HTML; +} + +/** + * Build content for SMS Opt Out event + */ +function buildOptOutContent($data) { + $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $optOutAt = htmlspecialchars($data['opt_out_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + return << +
🚫 Opt Out Details
+
+ + + + + +
+ +HTML; +} + +/** + * Build content for Contacts Callback event + */ +function buildContactsCallbackContent($data) { + $contactId = htmlspecialchars($data['contact_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $firstName = htmlspecialchars($data['first_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $lastName = htmlspecialchars($data['last_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $email = htmlspecialchars($data['email'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $status = htmlspecialchars($data['status'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $createdAt = htmlspecialchars($data['created_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + $updatedAt = htmlspecialchars($data['updated_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8'); + + $customFieldsHtml = ''; + if (isset($data['custom_fields']) && is_array($data['custom_fields']) && !empty($data['custom_fields'])) { + $customFieldsHtml = '
🏷️ Custom Fields
+HTML; + } + $customFieldsHtml .= '
'; + } + + return << +
👤 Contact Details
+
+ + + + + + + + +
+ + + $customFieldsHtml +HTML; +} + // Build email headers to ensure proper HTML rendering and sender information // MIME-Version declares email format capabilities $emailHeaders = "MIME-Version: 1.0\r\n"; diff --git a/wxinteract_test_samples.json b/wxinteract_test_samples.json new file mode 100644 index 0000000..ed0b823 --- /dev/null +++ b/wxinteract_test_samples.json @@ -0,0 +1,86 @@ +{ + "outbound_sms_submitted": { + "data": { + "status": "submitted", + "message_id": "msg_123456789", + "to": "+61412345678", + "from": "+61387654321", + "message": "Hello! Your appointment is confirmed for tomorrow at 2 PM.", + "submitted_at": "2026-01-22T16:45:00Z", + "campaign_id": "camp_987654321" + } + }, + "outbound_sms_delivered": { + "data": { + "status": "delivered", + "message_id": "msg_123456789", + "to": "+61412345678", + "from": "+61387654321", + "delivered_at": "2026-01-22T16:46:15Z", + "campaign_id": "camp_987654321" + } + }, + "outbound_sms_failed": { + "data": { + "status": "failed", + "message_id": "msg_987654321", + "to": "+61499999999", + "from": "+61387654321", + "failed_at": "2026-01-22T16:47:00Z", + "error_code": "ERR_INVALID_NUMBER", + "error_message": "The destination number is invalid or not reachable", + "campaign_id": "camp_987654321" + } + }, + "shortlink_clicked": { + "data": { + "status": "shortlink_clicked", + "shortlink": "https://wxint.co/abc123", + "original_url": "https://example.com/special-offer", + "clicked_at": "2026-01-22T17:30:45Z", + "phone_number": "+61412345678", + "message_id": "msg_123456789", + "campaign_id": "camp_987654321", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15", + "ip_address": "203.0.113.45" + } + }, + "inbound_sms_received": { + "data": { + "status": "received", + "message_id": "msg_inbound_001", + "from": "+61412345678", + "to": "+61387654321", + "message": "YES I would like to receive updates", + "received_at": "2026-01-22T18:00:00Z", + "keyword": "YES" + } + }, + "sms_opt_out": { + "data": { + "status": "opt_out", + "phone_number": "+61412345678", + "opt_out_at": "2026-01-22T18:15:00Z", + "keyword": "STOP", + "message_id": "msg_optout_001", + "from": "+61387654321" + } + }, + "contact_created": { + "data": { + "status": "contact_created", + "contact_id": "contact_abc123", + "phone_number": "+61412345678", + "first_name": "John", + "last_name": "Smith", + "email": "john.smith@example.com", + "created_at": "2026-01-22T19:00:00Z", + "updated_at": "2026-01-22T19:00:00Z", + "custom_fields": { + "company": "Acme Corporation", + "department": "Sales", + "customer_id": "CUST_12345" + } + } + } +}