Compare commits
9 Commits
v2.2.0
...
2025.12.02
| Author | SHA1 | Date | |
|---|---|---|---|
| a3345ed854 | |||
| 384ad5e265 | |||
| 86dd477d4f | |||
| 4baa8f539a | |||
| 82b735f5df | |||
| 349840840b | |||
| e475e68a5f | |||
| 0ee8210fef | |||
| 90cb03acfd |
82
.gitea/workflows/release.yml
Normal file
82
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Create Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.CI_USER }}
|
||||||
|
password: ${{ secrets.CI_TOKEN }}
|
||||||
|
fetch-depth: 0 # Important: Fetch all history for commit messages
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "version=$(date +'%Y.%m.%d-%H%M')" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
# Find the most recent tag
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 --always 2>/dev/null || echo "none")
|
||||||
|
|
||||||
|
if [ "$LATEST_TAG" = "none" ]; then
|
||||||
|
# If no previous tag exists, get all commits
|
||||||
|
COMMITS=$(git log --pretty=format:"* %s (%h)" --no-merges)
|
||||||
|
else
|
||||||
|
# Get commits since the last tag
|
||||||
|
COMMITS=$(git log --pretty=format:"* %s (%h)" ${LATEST_TAG}..HEAD --no-merges)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release notes with header (without encoding newlines)
|
||||||
|
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "## What's New in ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "" >> $GITHUB_OUTPUT
|
||||||
|
echo "$COMMITS" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Update plugin version
|
||||||
|
run: |
|
||||||
|
# Replace version placeholder with actual version
|
||||||
|
sed -i "s/Version: {auto_update_value_on_deploy}/Version: ${{ steps.get_version.outputs.version }}/" twilio-wp-plugin.php
|
||||||
|
sed -i "s/TWP_VERSION', '{auto_update_value_on_deploy}/TWP_VERSION', '${{ steps.get_version.outputs.version }}/" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
# Verify the changes were made
|
||||||
|
grep "Version:" twilio-wp-plugin.php
|
||||||
|
grep "TWP_VERSION" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
- name: Create ZIP archive
|
||||||
|
run: |
|
||||||
|
# Create a temp directory with the correct plugin folder name
|
||||||
|
mkdir -p /tmp/twilio-wp-plugin
|
||||||
|
|
||||||
|
# Copy files to the temp directory (excluding git and other unnecessary files)
|
||||||
|
cp -r * /tmp/twilio-wp-plugin/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Exclude .git and .gitea directories
|
||||||
|
rm -rf /tmp/twilio-wp-plugin/.git /tmp/twilio-wp-plugin/.gitea 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create the ZIP file with the proper structure
|
||||||
|
cd /tmp
|
||||||
|
zip -r $GITHUB_WORKSPACE/twilio-wp-plugin.zip twilio-wp-plugin
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.REPO_TOKEN }}"
|
||||||
|
title: "Twilio WP Plugin Release ${{ steps.get_version.outputs.version }}"
|
||||||
|
tag_name: ${{ steps.get_version.outputs.version }}
|
||||||
|
body: "${{ steps.release_notes.outputs.notes }}"
|
||||||
|
files: |
|
||||||
|
twilio-wp-plugin.zip
|
||||||
51
.gitea/workflows/update-version.yml
Normal file
51
.gitea/workflows/update-version.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Update Plugin Version
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get release tag
|
||||||
|
id: get_tag
|
||||||
|
run: echo "TAG=${GITEA_REF#refs/tags/}" >> $GITEA_ENV
|
||||||
|
|
||||||
|
- name: Update version in plugin file
|
||||||
|
run: |
|
||||||
|
# Replace version in main plugin file
|
||||||
|
sed -i "s/Version: .*/Version: ${{ env.TAG }}/" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
# Verify change
|
||||||
|
grep "Version:" twilio-wp-plugin.php
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@gitea.com"
|
||||||
|
git config --local user.name "Gitea Action"
|
||||||
|
git add twilio-wp-plugin.php
|
||||||
|
git commit -m "Update version to ${{ env.TAG }}" || echo "No changes to commit"
|
||||||
|
git push || echo "Nothing to push"
|
||||||
|
|
||||||
|
- name: Create plugin zip
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/twilio-wp-plugin
|
||||||
|
rsync -av --exclude=".git" --exclude=".gitea" --exclude="build" . /tmp/twilio-wp-plugin/
|
||||||
|
cd /tmp
|
||||||
|
zip -r $GITEA_WORK_DIR/twilio-wp-plugin.zip twilio-wp-plugin
|
||||||
|
|
||||||
|
- name: Upload zip to release
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ gitea.event.release.upload_url }}
|
||||||
|
asset_path: twilio-wp-plugin.zip
|
||||||
|
asset_name: twilio-wp-plugin.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
886
CLAUDE.md
886
CLAUDE.md
@@ -1,839 +1,65 @@
|
|||||||
# CLAUDE.md - Twilio WordPress Plugin Documentation
|
# Twilio WordPress Plugin - Quick Reference
|
||||||
|
|
||||||
This file provides comprehensive guidance to Claude Code (claude.ai/code) when working with the Twilio WordPress Plugin codebase.
|
## Environment
|
||||||
|
- **Production**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/`
|
||||||
## 🚨 CRITICAL: Testing & Deployment Environment
|
- **Dev**: `/home/jknapp/code/twilio-wp-plugin/`
|
||||||
|
- **URL**: `https://phone.cloud-hosting.io/`
|
||||||
**THIS PLUGIN RUNS ON A REMOTE SERVER IN A DOCKER CONTAINER - NOT LOCALLY**
|
- **Deployment**: rsync to Docker (remote server only, not local)
|
||||||
- **Production Server Path**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/`
|
- **SDK**: Twilio PHP SDK v8.7.0
|
||||||
- **Production URL**: `https://phone.cloud-hosting.io/`
|
|
||||||
- **Development Path**: `/home/jknapp/code/twilio-wp-plugin/`
|
## Phone Variable Names
|
||||||
- **Deployment Method**: Files synced via rsync from development to Docker container
|
**Use**: `incoming_number`, `agent_number`, `customer_number`, `workflow_number`, `queue_number`, `default_number`
|
||||||
|
**Don't use**: `from_number`, `to_number`, `phone_number`, `$agent_phone`
|
||||||
**IMPORTANT NOTES**:
|
**Test numbers**: Twilio `+19516215107`, Agent `+19095737372`
|
||||||
- NEVER assume local testing - all tests must work on remote server
|
|
||||||
- Direct PHP tests work (`php test-twilio-direct.php send`)
|
## Key Classes
|
||||||
- WordPress admin context has issues that need investigation
|
- **TWP_Twilio_API**: Use `new TWP_Twilio_API()` not singleton
|
||||||
- The plugin requires Twilio PHP SDK v8.7.0 to function
|
- **TWP_Admin**: Has `find_customer_call_leg()` - CRITICAL for call control
|
||||||
|
- **TWP_TTS_Helper**: ElevenLabs/Alice fallback, 30-day cache
|
||||||
## 📞 Standardized Phone Number Variable Names
|
- **TWP_User_Queue_Manager**: Auto-creates queues/extensions (100-9999)
|
||||||
|
- **TWP_Webhooks**: 26 endpoints at `twilio-webhook/v1`
|
||||||
**THESE NAMING CONVENTIONS MUST BE STRICTLY FOLLOWED:**
|
- **TWP_Activator**: Creates 15 DB tables, run `ensure_tables_exist()` if missing
|
||||||
|
|
||||||
### Required Variable Names:
|
## Database
|
||||||
- **`incoming_number`** = Phone number that initiated contact (sent SMS/call TO the system)
|
15 tables with `twp_` prefix. Key notes:
|
||||||
- **`agent_number`** = Phone number used to reach a specific agent
|
- `twp_call_queues`: User queues (general/personal/hold)
|
||||||
- **`customer_number`** = Phone number of customer calling into the system
|
- `twp_agent_status`: Has `auto_busy_at` for 1-min auto-revert
|
||||||
- **`workflow_number`** = Twilio number assigned to a specific workflow
|
- `twp_queued_calls`: Uses `enqueued_at` not `joined_at`
|
||||||
- **`queue_number`** = Twilio number assigned to a specific queue
|
|
||||||
- **`default_number`** = Default Twilio number when none specified
|
## Critical Functions
|
||||||
|
|
||||||
### BANNED Variable Names (DO NOT USE):
|
### Call Control (MUST use call leg detection)
|
||||||
- ❌ `from_number` - Ambiguous, could be customer or system
|
|
||||||
- ❌ `to_number` - Ambiguous, could be agent or system
|
|
||||||
- ❌ `phone_number` - Too generic, must specify whose number
|
|
||||||
- ❌ `$agent_phone` - Use `$agent_number` instead
|
|
||||||
|
|
||||||
### Test Numbers:
|
|
||||||
- **Twilio Number**: `+19516215107`
|
|
||||||
- **Test Agent Number**: `+19095737372`
|
|
||||||
- **Fake Test Number**: `+19512345678` (DO NOT SEND SMS TO THIS)
|
|
||||||
|
|
||||||
### Legacy Webhook URLs:
|
|
||||||
- **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms`
|
|
||||||
- **Voice**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/voice`
|
|
||||||
|
|
||||||
## 🏗️ Plugin Architecture Overview
|
|
||||||
|
|
||||||
### Core Plugin Structure
|
|
||||||
```
|
|
||||||
twilio-wp-plugin/
|
|
||||||
├── twilio-wp-plugin.php # Main plugin file (entry point)
|
|
||||||
├── includes/ # Core functionality classes
|
|
||||||
│ ├── class-twp-core.php # Main plugin initialization
|
|
||||||
│ ├── class-twp-activator.php # Database setup
|
|
||||||
│ ├── class-twp-twilio-api.php # Twilio SDK wrapper
|
|
||||||
│ ├── class-twp-webhooks.php # REST API endpoints
|
|
||||||
│ ├── class-twp-tts-helper.php # TTS with ElevenLabs/Twilio
|
|
||||||
│ └── ... # Other core classes
|
|
||||||
├── admin/ # Admin interface
|
|
||||||
│ └── class-twp-admin.php # Admin pages & AJAX handlers
|
|
||||||
├── assets/ # Frontend resources
|
|
||||||
│ ├── css/ # Stylesheets
|
|
||||||
│ ├── js/ # JavaScript files
|
|
||||||
│ └── audio/ # Audio resources
|
|
||||||
└── CLAUDE.md # This documentation file
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 Core Classes Documentation
|
|
||||||
|
|
||||||
### Main Classes (`includes/` directory)
|
|
||||||
|
|
||||||
#### TWP_Core
|
|
||||||
- **Purpose**: Main plugin initialization and hook registration
|
|
||||||
- **Key Methods**:
|
|
||||||
- `define_admin_hooks()`: Registers all admin AJAX actions
|
|
||||||
- `define_public_hooks()`: Registers frontend hooks
|
|
||||||
- `define_webhook_hooks()`: Registers REST API endpoints
|
|
||||||
|
|
||||||
#### TWP_Activator
|
|
||||||
- **Purpose**: Database table creation and plugin activation
|
|
||||||
- **Tables Created**: 15 database tables (see Database Schema section)
|
|
||||||
- **Key Methods**:
|
|
||||||
- `ensure_tables_exist()`: Checks and creates missing tables
|
|
||||||
- `migrate_tables()`: Handles schema migrations
|
|
||||||
- `add_missing_columns()`: Updates table structures
|
|
||||||
|
|
||||||
#### TWP_Twilio_API
|
|
||||||
- **Purpose**: Wrapper for Twilio PHP SDK v8.7.0
|
|
||||||
- **Important**: Uses direct instantiation (`new TWP_Twilio_API()`)
|
|
||||||
- **Key Methods**:
|
|
||||||
- `make_call()`: Initiates outbound calls
|
|
||||||
- `send_sms()`: Sends SMS messages
|
|
||||||
- `update_call()`: Updates call state (hold/resume)
|
|
||||||
- `create_queue_twiml()`: Generates queue TwiML
|
|
||||||
|
|
||||||
#### TWP_Webhooks
|
|
||||||
- **Purpose**: Handles all Twilio webhook endpoints
|
|
||||||
- **Namespace**: `twilio-webhook/v1`
|
|
||||||
- **Total Endpoints**: 26 REST API routes
|
|
||||||
|
|
||||||
#### TWP_TTS_Helper (UNIVERSALLY INTEGRATED)
|
|
||||||
- **Purpose**: Text-to-Speech with ElevenLabs integration, caching, and universal call control integration
|
|
||||||
- **Features**:
|
|
||||||
- Universal integration across all call control functions
|
|
||||||
- Automatic ElevenLabs detection with Alice fallback
|
|
||||||
- 30-day intelligent cache for identical text
|
|
||||||
- Professional voice consistency throughout call lifecycle
|
|
||||||
- **Key Methods**:
|
|
||||||
- `add_tts_to_twiml()`: Universal TTS integration for all voice prompts
|
|
||||||
- `generate_tts_audio()`: Pre-generates cached audio for performance
|
|
||||||
- **Coverage**: Hold, transfer, requeue, workflow steps, and queue announcements
|
|
||||||
|
|
||||||
#### TWP_User_Queue_Manager (NEW)
|
|
||||||
- **Purpose**: Comprehensive user-specific queue management with automatic creation
|
|
||||||
- **Features**:
|
|
||||||
- Automatic personal and hold queue creation for any user
|
|
||||||
- Unique extension generation (100-9999) with collision detection
|
|
||||||
- Database consistency with proper foreign key relationships
|
|
||||||
- Browser phone call support for complex topologies
|
|
||||||
- Schema compatibility (`enqueued_at` and `joined_at` columns)
|
|
||||||
- Comprehensive error handling with rollback mechanisms
|
|
||||||
- **Key Methods**:
|
|
||||||
- `create_user_queues()`: Creates personal and hold queues with unique extensions
|
|
||||||
- `transfer_to_hold_queue()`: Enhanced hold with auto-queue creation fallback
|
|
||||||
- `resume_from_hold()`: Comprehensive resume with target queue support
|
|
||||||
- `generate_unique_extension()`: Intelligent extension generation (3-4 digits)
|
|
||||||
- `get_user_extension_data()`: Retrieves complete user queue information
|
|
||||||
- **Auto-Creation**: Seamlessly integrates with hold, transfer, and queue operations
|
|
||||||
|
|
||||||
#### TWP_ElevenLabs_API
|
|
||||||
- **Purpose**: Integration with ElevenLabs TTS service
|
|
||||||
- **Configuration**: Uses `twp_elevenlabs_*` options
|
|
||||||
- **Features**: Voice selection, model configuration, audio generation
|
|
||||||
|
|
||||||
#### TWP_Workflow
|
|
||||||
- **Purpose**: Call flow processing and TwiML generation
|
|
||||||
- **Features**: IVR menus, queue routing, schedule checking
|
|
||||||
|
|
||||||
#### TWP_Call_Queue
|
|
||||||
- **Purpose**: Queue management and position tracking
|
|
||||||
- **Features**: User-specific queues, hold queues, priority handling
|
|
||||||
|
|
||||||
#### TWP_Agent_Manager
|
|
||||||
- **Purpose**: Agent status and call acceptance
|
|
||||||
- **Features**: Phone number validation, availability tracking
|
|
||||||
|
|
||||||
#### TWP_Callback_Manager
|
|
||||||
- **Purpose**: Callback request handling
|
|
||||||
- **Features**: SMS confirmations, automatic processing
|
|
||||||
|
|
||||||
#### TWP_Call_Logger
|
|
||||||
- **Purpose**: Call logging and statistics
|
|
||||||
- **Features**: Detailed call records, duration tracking
|
|
||||||
|
|
||||||
#### TWP_Shortcodes
|
|
||||||
- **Purpose**: WordPress shortcodes for frontend features
|
|
||||||
- **Shortcode**: `[twp_browser_phone]` - Admin browser phone redirect interface
|
|
||||||
|
|
||||||
## 💾 Database Schema
|
|
||||||
|
|
||||||
### Complete Table List (15 tables)
|
|
||||||
1. `twp_phone_schedules` - Business hours definitions
|
|
||||||
2. `twp_call_queues` - Queue configurations (includes user-specific)
|
|
||||||
3. `twp_queued_calls` - Active calls in queues
|
|
||||||
4. `twp_workflows` - Call flow definitions
|
|
||||||
5. `twp_workflow_phones` - Phone-to-workflow mappings
|
|
||||||
6. `twp_call_log` - Complete call history
|
|
||||||
7. `twp_sms_log` - SMS message tracking
|
|
||||||
8. `twp_voicemails` - Voicemail recordings and transcriptions
|
|
||||||
9. `twp_agent_groups` - Agent group definitions
|
|
||||||
10. `twp_group_members` - User-to-group relationships
|
|
||||||
11. `twp_agent_status` - Real-time agent availability with auto_busy_at column
|
|
||||||
12. `twp_callbacks` - Callback request queue
|
|
||||||
13. `twp_call_recordings` - Call recording metadata
|
|
||||||
14. `twp_user_extensions` - User extension numbers
|
|
||||||
15. `twp_queue_assignments` - User queue assignments
|
|
||||||
|
|
||||||
### Key Table Structures
|
|
||||||
|
|
||||||
#### twp_call_queues (Enhanced)
|
|
||||||
- Supports user-specific queues (`user_id` field)
|
|
||||||
- Queue types: `general`, `personal`, `hold`
|
|
||||||
- Extension support for direct dialing
|
|
||||||
- TTS message configuration
|
|
||||||
|
|
||||||
#### twp_agent_status (Enhanced v1.6.2)
|
|
||||||
- Real-time agent availability tracking
|
|
||||||
- **auto_busy_at** column for automatic status management
|
|
||||||
- 1-minute auto-revert system for busy agents
|
|
||||||
- WordPress cron job integration for status automation
|
|
||||||
- Database version 1.6.2 with automatic migration
|
|
||||||
|
|
||||||
#### twp_queued_calls
|
|
||||||
- Uses `enqueued_at` timestamp (migrated from `joined_at`)
|
|
||||||
- No `customer_number` field (uses `from_number`/`to_number`)
|
|
||||||
- Tracks agent assignment and status
|
|
||||||
|
|
||||||
## 🔌 REST API Endpoints (Webhooks)
|
|
||||||
|
|
||||||
### Voice Endpoints
|
|
||||||
- `/voice` - Main incoming call handler
|
|
||||||
- `/browser-voice` - Browser phone calls
|
|
||||||
- `/smart-routing` - Intelligent call routing
|
|
||||||
- `/agent-screen` - Agent screening before connection
|
|
||||||
- `/agent-confirm` - Agent confirmation handler
|
|
||||||
- `/ring-group-result` - Handle ring group outcomes
|
|
||||||
- `/agent-connect` - Connect accepted agents
|
|
||||||
|
|
||||||
### SMS Endpoints
|
|
||||||
- `/sms` - Main SMS handler (includes "1" responses)
|
|
||||||
- `/status` - Call/SMS status updates
|
|
||||||
|
|
||||||
### Queue Management
|
|
||||||
- `/queue-wait` - Queue hold music and announcements
|
|
||||||
- `/queue-action` - Queue-specific actions
|
|
||||||
- `/ivr-response` - IVR menu selections
|
|
||||||
|
|
||||||
### Voicemail
|
|
||||||
- `/voicemail-callback` - Voicemail recording handler
|
|
||||||
- `/voicemail-complete` - Post-recording processing
|
|
||||||
- `/voicemail-audio/{id}` - Audio playback proxy
|
|
||||||
|
|
||||||
### Recording
|
|
||||||
- `/recording-status` - Recording status callbacks
|
|
||||||
- `/recording-audio/{id}` - Recording playback proxy
|
|
||||||
- `/transcription` - Transcription webhooks
|
|
||||||
|
|
||||||
### Callback System
|
|
||||||
- `/callback-choice` - Customer callback selection
|
|
||||||
- `/request-callback` - Callback request handler
|
|
||||||
- `/callback-agent` - Agent-side callback
|
|
||||||
- `/callback-customer` - Customer-side callback
|
|
||||||
|
|
||||||
### Outbound Calling
|
|
||||||
- `/outbound-agent` - Basic outbound calls
|
|
||||||
- `/outbound-agent-with-from` - Outbound with caller ID selection
|
|
||||||
|
|
||||||
### Utility
|
|
||||||
- `/resume-call` - Resume held calls
|
|
||||||
- `/smart-routing-fallback` - Routing error handler
|
|
||||||
- `/browser-fallback` - Browser phone fallback
|
|
||||||
|
|
||||||
## 🎛️ AJAX Endpoints (Admin Only)
|
|
||||||
|
|
||||||
### Total: 68 AJAX Actions
|
|
||||||
|
|
||||||
### Categories:
|
|
||||||
1. **Schedule Management** (4 actions)
|
|
||||||
2. **Workflow Management** (5 actions)
|
|
||||||
3. **Phone Number Management** (5 actions)
|
|
||||||
4. **Queue Management** (6 actions)
|
|
||||||
5. **Agent Management** (11 actions)
|
|
||||||
6. **Call Control** (15 actions)
|
|
||||||
7. **Voicemail** (5 actions)
|
|
||||||
8. **Recording** (4 actions)
|
|
||||||
9. **SMS Management** (4 actions)
|
|
||||||
10. **ElevenLabs Integration** (3 actions)
|
|
||||||
11. **Browser Phone** (4 actions)
|
|
||||||
12. **Transfer & Hold** (8 actions)
|
|
||||||
|
|
||||||
### Key AJAX Actions:
|
|
||||||
- `twp_toggle_hold` - Put call on hold/resume (uses call leg detection)
|
|
||||||
- `twp_transfer_call` - Transfer to agent/queue (uses call leg detection)
|
|
||||||
- `twp_start_recording` - Start call recording
|
|
||||||
- `twp_stop_recording` - Stop call recording
|
|
||||||
- `twp_requeue_call` - Return call to queue (uses call leg detection)
|
|
||||||
- `twp_initiate_outbound_call_with_from` - Outbound with caller ID
|
|
||||||
|
|
||||||
### Call Control Architecture (CRITICAL):
|
|
||||||
All call control functions (`twp_toggle_hold`, `twp_transfer_call`, `twp_requeue_call`) now use intelligent call leg detection to ensure actions are applied to the customer call leg, not the agent leg. This prevents customer disconnections in complex call topologies.
|
|
||||||
|
|
||||||
## 🎨 Frontend Components
|
|
||||||
|
|
||||||
### Shortcode Implementation
|
|
||||||
The `[twp_browser_phone]` shortcode now provides a **redirect interface** instead of a full browser phone:
|
|
||||||
|
|
||||||
#### Shortcode Attributes
|
|
||||||
- **`title`**: Display title (default: "Browser Phone")
|
|
||||||
- **`button_text`**: Button text (default: "Access Browser Phone")
|
|
||||||
- **`target`**: Link target (default: "_blank" - opens in new tab)
|
|
||||||
|
|
||||||
#### Security Features
|
|
||||||
- **Login Required**: Users must be logged in to see the redirect
|
|
||||||
- **Permission Check**: Requires `twp_access_browser_phone` or `manage_options` capability
|
|
||||||
- **Error Messages**: Clear feedback for unauthorized access
|
|
||||||
|
|
||||||
#### Styling
|
|
||||||
- **Inline CSS Only**: No external CSS files loaded
|
|
||||||
- **Minimal Assets**: Reduces frontend bloat
|
|
||||||
- **Responsive Design**: Works on all device sizes
|
|
||||||
|
|
||||||
### JavaScript Files
|
|
||||||
1. **admin.js** (116KB) - Admin interface functionality
|
|
||||||
2. **twp-service-worker.js** (2.5KB) - Push notifications
|
|
||||||
|
|
||||||
### Browser Phone Features (Admin Only)
|
|
||||||
- **Enhanced Security**: All browser phone functionality restricted to admin area
|
|
||||||
- **Admin URL**: `admin.php?page=twilio-wp-browser-phone`
|
|
||||||
- Twilio Device SDK integration
|
|
||||||
- Real-time call controls (hold, transfer, record)
|
|
||||||
- Queue monitoring dashboard
|
|
||||||
- Agent status management
|
|
||||||
- Call history display
|
|
||||||
- Visual call state indicators
|
|
||||||
|
|
||||||
### CSS Files
|
|
||||||
- **admin.css** - Admin interface styling (includes browser phone UI)
|
|
||||||
|
|
||||||
## 🔧 Recent Fixes & Improvements
|
|
||||||
|
|
||||||
### SECURITY ENHANCEMENT: Frontend Browser Phone Removal (September 2025) - PRODUCTION READY
|
|
||||||
Major security enhancement by removing frontend browser phone interface and implementing admin-only access.
|
|
||||||
|
|
||||||
#### Browser Phone Security Enhancement
|
|
||||||
- **Frontend Interface Removed**: Eliminated full browser phone interface from frontend shortcode
|
|
||||||
- **Admin-Only Access**: All browser phone functionality moved to secure admin area
|
|
||||||
- **Asset Reduction**: Removed 108KB of frontend assets (browser-phone-frontend.js and browser-phone-frontend.css)
|
|
||||||
- **Redirect Interface**: Shortcode now provides secure redirect to admin browser phone page
|
|
||||||
- **Enhanced Permissions**: Strict capability checking with clear error messages for unauthorized users
|
|
||||||
- **Reduced Attack Surface**: Minimized frontend JavaScript exposure and potential security vectors
|
|
||||||
- **Performance Improvement**: Reduced frontend asset loading and improved page load times
|
|
||||||
|
|
||||||
#### Shortcode Transformation
|
|
||||||
- **Security-First Design**: Login and permission validation before any functionality access
|
|
||||||
- **Minimal Asset Loading**: Only essential inline CSS for redirect interface styling
|
|
||||||
- **Responsive Redirect**: Professional redirect interface works on all devices
|
|
||||||
- **Customizable Attributes**: title, button_text, and target attributes for flexibility
|
|
||||||
- **Clear Error Messaging**: Informative error messages for authentication and authorization failures
|
|
||||||
|
|
||||||
#### Technical Implementation
|
|
||||||
- **File Removal**: `assets/js/browser-phone-frontend.js` and `assets/css/browser-phone-frontend.css` eliminated
|
|
||||||
- **Inline Styling**: Minimal CSS injected only when shortcode is present on page
|
|
||||||
- **Permission System**: Leverages WordPress capability system (`twp_access_browser_phone` or `manage_options`)
|
|
||||||
- **Admin URL Generation**: Secure admin_url() function for proper WordPress admin integration
|
|
||||||
- **Target Control**: Configurable link target (_blank by default for better UX)
|
|
||||||
|
|
||||||
### PRODUCTION READY: Extension Transfer System Overhaul (September 2025) - FULLY RESOLVED
|
|
||||||
Comprehensive overhaul of extension-based call transfers with enterprise-grade reliability.
|
|
||||||
|
|
||||||
#### Extension Transfer Complete Solution
|
|
||||||
- **Critical Issue Fixed**: Extension transfers were going directly to voicemail even for available agents
|
|
||||||
- **Root Cause**: Active client dialing bypassed agent availability detection
|
|
||||||
- **Solution**: Implemented queue-based system with intelligent agent detection
|
|
||||||
- **Key Features**:
|
|
||||||
- **Queue-Based Routing**: All extension transfers now use proper queue system
|
|
||||||
- **2-Minute Timeout**: Automatic voicemail fallback after timeout period
|
|
||||||
- **Agent Detection**: Enhanced availability checking for browser phone users
|
|
||||||
- **Professional Audio**: "Please hold while we connect you to extension X" messages
|
|
||||||
- **Browser Phone Integration**: Seamless compatibility with browser phone agents
|
|
||||||
- **Fallback Mechanism**: Graceful voicemail handling when agent unavailable
|
|
||||||
- **Result**: Extension transfers now work reliably for all agent types with professional user experience
|
|
||||||
|
|
||||||
### CRITICAL: Browser Phone Compatibility & Firefox Support (September 2025) - FULLY RESOLVED
|
|
||||||
Complete browser phone reliability overhaul with universal browser support.
|
|
||||||
|
|
||||||
#### Browser Phone Permission & Compatibility Fixes
|
|
||||||
- **Firefox Support Added**: Explicit getUserMedia calls for Firefox microphone/speaker access
|
|
||||||
- **Permission System**: Automatic permission requests prevent silent failures
|
|
||||||
- **Call Stability**: Fixed browser phone call disconnections during transfers
|
|
||||||
- **Error Handling**: Comprehensive error messages for permission denials
|
|
||||||
- **User Experience**: Clear prompts guide users through permission setup
|
|
||||||
- **Cross-Browser**: Tested and working on Chrome, Firefox, Safari, and Edge
|
|
||||||
- **Mobile Support**: Enhanced mobile browser compatibility
|
|
||||||
- **Recovery Mechanisms**: Automatic reconnection on permission restoration
|
|
||||||
|
|
||||||
#### Client Name Generation Consistency Fix
|
|
||||||
- **Critical Issue Fixed**: Inconsistent client naming between capability tokens and call acceptance
|
|
||||||
- **Root Cause**: Mismatch between user_login vs display_name usage across functions
|
|
||||||
- **Solution**: Standardized client naming throughout entire system
|
|
||||||
- **Components Synchronized**:
|
|
||||||
- Capability token generation
|
|
||||||
- Browser phone connection logic
|
|
||||||
- Transfer system client detection
|
|
||||||
- Call acceptance mechanisms
|
|
||||||
- **Result**: Browser phone connections and transfers now work seamlessly without client mismatch errors
|
|
||||||
|
|
||||||
### ENTERPRISE: Automatic Agent Status Management (September 2025) - NEW FEATURE
|
|
||||||
Intelligent agent status management with automatic productivity optimization.
|
|
||||||
|
|
||||||
#### 1-Minute Auto-Revert System
|
|
||||||
- **Feature**: Automatic revert from busy to available status after 1 minute
|
|
||||||
- **Database**: Added `auto_busy_at` column to `twp_agent_status` table
|
|
||||||
- **Automation**: WordPress cron job checks and updates agent status automatically
|
|
||||||
- **Schema Migration**: Database version updated to 1.6.2 with automatic migration
|
|
||||||
- **Agent Productivity**: Prevents agents from remaining in busy status indefinitely
|
|
||||||
- **Flexibility**: Manual status changes still respected, auto-revert only applies to system-set busy status
|
|
||||||
- **Logging**: Comprehensive tracking of status changes for auditing
|
|
||||||
- **Performance**: Efficient cron job processing with minimal server impact
|
|
||||||
|
|
||||||
### MAJOR: Call Statistics & Logging Improvements (September 2025) - PRODUCTION READY
|
|
||||||
Comprehensive call tracking and statistics system with enhanced accuracy.
|
|
||||||
|
|
||||||
#### Browser Phone Call Statistics Fix
|
|
||||||
- **Issue Fixed**: Browser phone calls not appearing in agent statistics dashboards
|
|
||||||
- **Enhancement**: Improved call logging with proper agent_id association
|
|
||||||
- **JSON Format**: Enhanced call data storage in structured JSON format
|
|
||||||
- **Customer Detection**: Advanced customer number identification for complex call topologies
|
|
||||||
- **Statistics Accuracy**: All call types now properly tracked and reported
|
|
||||||
- **Performance Metrics**: Real-time statistics updates for browser phone usage
|
|
||||||
- **Debugging Enhanced**: Comprehensive logging for call leg detection and customer identification
|
|
||||||
|
|
||||||
### PROFESSIONAL: Voicemail & Transcription System Enhancement (September 2025) - PRODUCTION READY
|
|
||||||
Enterprise-grade voicemail system with real Twilio API integration.
|
|
||||||
|
|
||||||
#### Real Transcription API Integration
|
|
||||||
- **Major Enhancement**: Replaced placeholder transcription with actual Twilio API integration
|
|
||||||
- **Manual Transcription**: Added capability to request transcriptions for existing voicemails
|
|
||||||
- **API Integration**: Direct integration with Twilio's transcription service
|
|
||||||
- **Webhook Processing**: Enhanced transcription webhook handling
|
|
||||||
- **User Experience**: Real voicemail transcriptions available in admin interface
|
|
||||||
- **Reliability**: Proper error handling for transcription failures
|
|
||||||
|
|
||||||
#### Extension Voicemail Enhancement
|
|
||||||
- **User ID Support**: Enhanced voicemail callback handling with proper user_id association
|
|
||||||
- **Extension Integration**: Seamless integration with extension-based voicemail systems
|
|
||||||
- **Call Routing**: Proper routing for extension-specific voicemails
|
|
||||||
- **Customer Detection**: Enhanced customer number identification for voicemail callbacks
|
|
||||||
- **Professional Audio**: ElevenLabs TTS integration for voicemail prompts
|
|
||||||
|
|
||||||
### CRITICAL: Advanced Call Control Fixes (September 2025) - FULLY RESOLVED
|
|
||||||
Major overhaul of hold, transfer, and requeue functionality with comprehensive fixes for complex call topologies.
|
|
||||||
|
|
||||||
#### Hold Function Complete Redesign
|
|
||||||
- **Issue Fixed**: "Queue not found" errors when putting calls on hold
|
|
||||||
- **Root Cause**: User-specific queues (personal and hold queues) weren't automatically created
|
|
||||||
- **Solution**: Automatic queue creation system with intelligent fallbacks
|
|
||||||
- **Key Features**:
|
|
||||||
- **Auto-Queue Creation**: Creates personal and hold queues automatically if missing
|
|
||||||
- **Extension Assignment**: Auto-generates unique 3-4 digit extensions (100-9999)
|
|
||||||
- **Database Integration**: Proper queue assignments and extension tracking
|
|
||||||
- **Browser Phone Support**: Handles calls not initially in queue database
|
|
||||||
- **ElevenLabs TTS**: Enhanced hold messages with premium voice synthesis
|
|
||||||
- **Call Leg Detection**: Uses `find_customer_call_leg()` for proper call control
|
|
||||||
- **Functions Enhanced**: `ajax_toggle_hold()`, `TWP_User_Queue_Manager::transfer_to_hold_queue()`
|
|
||||||
- **Result**: Hold functionality now works seamlessly for all call types
|
|
||||||
|
|
||||||
#### Transfer Function Comprehensive Fix
|
|
||||||
- **Issue Fixed**: Customers hearing webhook URLs instead of proper transfer messages
|
|
||||||
- **Root Cause**: Improper TwiML generation causing raw webhook URLs to be spoken
|
|
||||||
- **Solution**: Complete TwiML generation overhaul with proper VoiceResponse usage
|
|
||||||
- **Key Features**:
|
|
||||||
- **Proper TwiML Generation**: Uses `\Twilio\TwiML\VoiceResponse()` for all transfer types
|
|
||||||
- **Multiple Transfer Types**: Extension, queue, client (browser phone), and phone number transfers
|
|
||||||
- **Customer Call Leg Detection**: Identifies correct call leg for outbound calls
|
|
||||||
- **ElevenLabs Integration**: Premium TTS for all transfer announcements
|
|
||||||
- **Enhanced Logging**: Comprehensive debugging for transfer operations
|
|
||||||
- **Browser Phone Transfers**: Fixed `client:` identifier handling
|
|
||||||
- **Transfer Types Supported**:
|
|
||||||
- Extension transfers: Redirects to queue-wait endpoint with TTS announcement
|
|
||||||
- Queue transfers: Proper queue routing with hold music
|
|
||||||
- Client transfers: `$dial->client($agent_name)` for browser phone agents
|
|
||||||
- Phone transfers: Direct `$twiml->dial($target)` for external numbers
|
|
||||||
- **Functions Enhanced**: `ajax_transfer_call()` with intelligent target detection
|
|
||||||
- **Result**: All transfer types now provide proper audio experience without exposing technical URLs
|
|
||||||
|
|
||||||
#### Requeue Function Complete Rebuild
|
|
||||||
- **Issue Fixed**: Customers hearing webhook URLs when requeued to waiting queues
|
|
||||||
- **Root Cause**: Faulty `create_queue_twiml()` method generating improper TwiML
|
|
||||||
- **Solution**: Replaced with proper TwiML generation and redirect methodology
|
|
||||||
- **Key Features**:
|
|
||||||
- **VoiceResponse Integration**: Uses proper Twilio TwiML classes
|
|
||||||
- **Redirect Method**: Uses `$twiml->redirect()` instead of raw webhook calls
|
|
||||||
- **Queue-Wait Integration**: Proper integration with `/queue-wait` endpoint
|
|
||||||
- **Database Consistency**: Maintains call tracking with `enqueued_at` column support
|
|
||||||
- **ElevenLabs TTS**: Premium voice synthesis for requeue messages
|
|
||||||
- **Call Leg Detection**: Ensures customer (not agent) is requeued
|
|
||||||
- **Functions Enhanced**: `ajax_requeue_call()` with proper TwiML flow
|
|
||||||
- **Result**: Customers now hear professional requeue messages instead of technical errors
|
|
||||||
|
|
||||||
### ElevenLabs TTS Integration Enhanced (September 2025)
|
|
||||||
- **Universal Integration**: All voice prompts now use `TWP_TTS_Helper::add_tts_to_twiml()`
|
|
||||||
- **Automatic Fallback**: Seamlessly falls back to Twilio's Alice voice if ElevenLabs unavailable
|
|
||||||
- **Voice Consistency**: Hold, transfer, and requeue messages use consistent premium voices
|
|
||||||
- **Caching System**: 30-day cache reduces API calls and improves performance
|
|
||||||
- **Configuration**: Works with existing ElevenLabs API key settings
|
|
||||||
- **Coverage**: Applies to all new call control functions and existing workflow steps
|
|
||||||
|
|
||||||
### Call Leg Detection System Enhanced (September 2025)
|
|
||||||
- **Function**: `find_customer_call_leg($call_sid, $api)` in `TWP_Admin` class
|
|
||||||
- **Enhanced Logic**: Improved detection for complex outbound call topologies
|
|
||||||
- **Browser Phone Detection**: Identifies `client:` prefixes and finds real customer legs
|
|
||||||
- **Parent Call Analysis**: Uses parent call relationships for proper leg identification
|
|
||||||
- **Active Call Search**: Searches active calls when parent relationship insufficient
|
|
||||||
- **Comprehensive Logging**: Detailed debugging output for troubleshooting
|
|
||||||
- **Fallback Mechanisms**: Multiple detection methods ensure reliability
|
|
||||||
- **Result**: 100% accuracy in identifying customer vs agent call legs
|
|
||||||
|
|
||||||
### Automatic Queue Management System (NEW)
|
|
||||||
- **Auto-Creation**: Personal and hold queues created automatically for users
|
|
||||||
- **Extension System**: Unique 3-4 digit extensions (100-9999) auto-assigned
|
|
||||||
- **Database Integration**: Proper foreign key relationships and constraints
|
|
||||||
- **Queue Assignment**: Auto-assignment to personal and hold queues
|
|
||||||
- **Migration Support**: Handles users without existing queue infrastructure
|
|
||||||
- **Error Handling**: Comprehensive rollback on queue creation failures
|
|
||||||
- **User Experience**: Seamless queue access without manual setup
|
|
||||||
|
|
||||||
### CRITICAL: Outbound Call Issues RESOLVED (September 2025)
|
|
||||||
- **Issue**: Hold, transfer, and requeue functions were applying actions to wrong call leg (agent instead of customer), causing customer disconnections
|
|
||||||
- **Root Cause**: Complex call topologies in outbound calls create agent and customer call legs with different SIDs
|
|
||||||
- **Solution**: New intelligent call leg detection system
|
|
||||||
- **Functions Fixed**: `ajax_toggle_hold()`, `ajax_transfer_call()`, `ajax_requeue_call()`
|
|
||||||
- **Result**: All functions now work correctly for both inbound and outbound calls without disconnecting customers
|
|
||||||
|
|
||||||
### Customer Number Detection Issues RESOLVED (September 2025)
|
|
||||||
- **Issue**: Customer numbers showing as "client:agentname" instead of actual phone numbers in voicemail and call recording admin interfaces
|
|
||||||
- **Root Cause**: Browser phone calls create complex call topologies where customer information is stored in different call legs
|
|
||||||
- **Solution**: Enhanced customer number detection with fallback mechanisms
|
|
||||||
- **Areas Fixed**: Voicemail callback handling and call recording customer identification
|
|
||||||
- **Result**: Both inbound and outbound calls now properly identify real customer phone numbers
|
|
||||||
|
|
||||||
### Call Leg Detection System (NEW)
|
|
||||||
- **Function**: `find_customer_call_leg($call_sid, $api)` in `TWP_Admin` class
|
|
||||||
- **Purpose**: Identifies customer vs agent call legs in complex call topologies
|
|
||||||
- **Detection Logic**:
|
|
||||||
- Detects browser phone calls by checking for `client:` prefixes
|
|
||||||
- Uses parent call relationships to find customer leg
|
|
||||||
- Searches active calls for related customer connections
|
|
||||||
- Comprehensive fallback mechanisms
|
|
||||||
- **Logging**: Extensive debugging output for call relationship tracking
|
|
||||||
|
|
||||||
### Enhanced Customer Number Detection (NEW)
|
|
||||||
- **Voicemail Callback Enhancement** (`TWP_Webhooks::handle_voicemail_callback()`):
|
|
||||||
- Fallback logic retrieves customer numbers from call log when From parameter missing
|
|
||||||
- Browser phone detection identifies `client:` calls and finds real customer numbers
|
|
||||||
- Parent call analysis and related call search functionality
|
|
||||||
- Comprehensive logging for customer number detection process
|
|
||||||
- **Call Recording Enhancement** (`TWP_Admin::ajax_start_recording()`):
|
|
||||||
- Browser phone detection using `client:` prefix identification
|
|
||||||
- Integration with `find_customer_call_leg()` helper for proper customer identification
|
|
||||||
- Smart number extraction from appropriate call legs (from/to field analysis)
|
|
||||||
- Enhanced logging for recording customer number detection
|
|
||||||
|
|
||||||
### Browser Phone Call Support Enhanced
|
|
||||||
- **Client Transfer Support**: Fixed "Invalid phone number format" errors for `client:` transfers
|
|
||||||
- **Detection**: Automatically identifies `client:agentname` format calls
|
|
||||||
- **Transfer Methods**:
|
|
||||||
- Client transfers: `$dial->client($agent_name)`
|
|
||||||
- Phone transfers: `$twiml->dial($target)`
|
|
||||||
- Queue transfers: Uses customer leg for proper queue placement
|
|
||||||
- **Customer Number Resolution**: Properly identifies real phone numbers in complex call topologies
|
|
||||||
- **Admin Interface Fixes**: Voicemail and recording interfaces now show actual customer numbers instead of client identifiers
|
|
||||||
|
|
||||||
### Hold Functionality (COMPLETELY REDESIGNED)
|
|
||||||
- **Previous Issue**: `TWP_Twilio_API::get_instance()` error and queue not found errors
|
|
||||||
- **New Solution**: Automatic user queue creation with comprehensive fallback system
|
|
||||||
- **Key Enhancements**:
|
|
||||||
- Auto-creates personal and hold queues if missing
|
|
||||||
- Generates unique extensions (100-9999) automatically
|
|
||||||
- Handles browser phone calls not initially in queue database
|
|
||||||
- Uses proper call leg detection for outbound calls
|
|
||||||
- Integrates ElevenLabs TTS for professional hold messages
|
|
||||||
- Maintains database consistency with `enqueued_at` column support
|
|
||||||
- **Customer Impact**: Seamless hold experience with premium audio and proper call handling
|
|
||||||
|
|
||||||
### Transfer System (FULLY REBUILT)
|
|
||||||
- **Previous Issue**: Customers hearing webhook URLs and transfer failures in outbound calls
|
|
||||||
- **New Solution**: Complete TwiML generation overhaul with proper VoiceResponse usage
|
|
||||||
- **Key Enhancements**:
|
|
||||||
- Proper TwiML generation prevents webhook URLs from being spoken
|
|
||||||
- All transfer types (extension, queue, client, phone) now work correctly
|
|
||||||
- Customer call leg detection ensures proper call routing
|
|
||||||
- ElevenLabs TTS integration for professional transfer announcements
|
|
||||||
- Enhanced error handling and debugging
|
|
||||||
- **Transfer Types Enhanced**:
|
|
||||||
- **Extension**: Redirects to queue-wait with TTS "Transferring to extension X"
|
|
||||||
- **Queue**: Direct queue routing with proper hold experience
|
|
||||||
- **Client**: `$dial->client()` for browser phone agents with detection
|
|
||||||
- **Phone**: Direct dial with "Transferring your call" announcement
|
|
||||||
- **Customer Impact**: Professional transfer experience without technical errors
|
|
||||||
|
|
||||||
### Requeue Functionality (COMPLETELY REDESIGNED)
|
|
||||||
- **Previous Issue**: Customers hearing webhook URLs instead of proper queue experience
|
|
||||||
- **New Solution**: Complete replacement of faulty `create_queue_twiml()` with proper TwiML
|
|
||||||
- **Key Enhancements**:
|
|
||||||
- Uses VoiceResponse and redirect method instead of raw webhook calls
|
|
||||||
- Proper integration with `/queue-wait` endpoint for seamless experience
|
|
||||||
- Customer call leg detection ensures correct call is requeued
|
|
||||||
- ElevenLabs TTS for "Placing you back in the queue" messages
|
|
||||||
- Database tracking maintains call history and position
|
|
||||||
- Supports both `enqueued_at` and `joined_at` column schemas
|
|
||||||
- **Customer Impact**: Professional requeue experience with proper hold music and announcements
|
|
||||||
|
|
||||||
### Recording Management (Fixed)
|
|
||||||
- **Issue**: "Unknown subresource update" error
|
|
||||||
- **Fix**: Proper SDK v8 syntax using call recordings subresource
|
|
||||||
- **Fallback**: Tries `Twilio.CURRENT` if specific SID fails
|
|
||||||
|
|
||||||
### Queue Management (Fixed)
|
|
||||||
- **Issue**: `Enqueue::waitUrl()` undefined method
|
|
||||||
- **Fix**: Pass `waitUrl` as option: `$response->enqueue($queue_name, ['waitUrl' => $url])`
|
|
||||||
|
|
||||||
### TTS Integration (UNIVERSALLY ENHANCED)
|
|
||||||
- **Universal Coverage**: All call control functions now use `TWP_TTS_Helper::add_tts_to_twiml()`
|
|
||||||
- **ElevenLabs Integration**: Automatic premium voice synthesis when configured
|
|
||||||
- **Intelligent Fallback**: Seamless fallback to Twilio's Alice voice
|
|
||||||
- **Cache Duration**: 30 days for identical text with automatic cleanup
|
|
||||||
- **Performance**: Instant cached audio delivery
|
|
||||||
- **Professional Messages**:
|
|
||||||
- Hold: "Please hold while we prepare your call"
|
|
||||||
- Transfer: "Transferring to extension X" or "Transferring your call"
|
|
||||||
- Requeue: "Placing you back in the queue. Please hold."
|
|
||||||
- **Consistency**: Same voice experience across all call operations
|
|
||||||
- **Configuration**: Works with existing ElevenLabs API key settings
|
|
||||||
- **Integration Points**: Hold operations, all transfer types, requeue operations, workflow steps
|
|
||||||
|
|
||||||
## 🚀 Twilio Integration Details
|
|
||||||
|
|
||||||
### SDK Version
|
|
||||||
- **Required**: Twilio PHP SDK v8.7.0
|
|
||||||
- **Installation**: Via Composer or install script
|
|
||||||
- **PHP Requirement**: PHP 8.0+
|
|
||||||
|
|
||||||
### API Response Structure
|
|
||||||
```php
|
```php
|
||||||
// Success response
|
// ALWAYS do this for hold/transfer/requeue:
|
||||||
[
|
|
||||||
'success' => true,
|
|
||||||
'data' => [...] // Twilio response data
|
|
||||||
]
|
|
||||||
|
|
||||||
// Error response
|
|
||||||
[
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'Error message',
|
|
||||||
'code' => 400
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### TwiML Generation Best Practices
|
|
||||||
- Always use SDK classes for TwiML generation
|
|
||||||
- Include proper XML headers when needed
|
|
||||||
- Use TTS helper for voice synthesis
|
|
||||||
- Implement proper error handling
|
|
||||||
|
|
||||||
### Recording Stop Methods (SDK v8)
|
|
||||||
```php
|
|
||||||
// Method 1: Specific recording
|
|
||||||
$client->calls($call_sid)
|
|
||||||
->recordings($recording_sid)
|
|
||||||
->update(['status' => 'stopped']);
|
|
||||||
|
|
||||||
// Method 2: Single active recording
|
|
||||||
$client->calls($call_sid)
|
|
||||||
->recordings('Twilio.CURRENT')
|
|
||||||
->update(['status' => 'stopped']);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Development Guidelines
|
|
||||||
|
|
||||||
### Call Control Functions (CRITICAL)
|
|
||||||
**ALWAYS use call leg detection for hold, transfer, and requeue operations:**
|
|
||||||
```php
|
|
||||||
// Correct pattern for call control functions
|
|
||||||
private function find_customer_call_leg($call_sid, $api) {
|
|
||||||
// Detects browser phone vs regular calls
|
|
||||||
// Uses parent call relationships
|
|
||||||
// Searches active calls for customer leg
|
|
||||||
// Returns correct SID for operations
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in AJAX handlers
|
|
||||||
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
|
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
|
||||||
$result = $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
|
$api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why This Matters:**
|
### Common Fixes
|
||||||
- Outbound calls create separate agent and customer call legs
|
- Recording: Use `Twilio.CURRENT` for SDK v8
|
||||||
- Applying actions to agent leg disconnects customer
|
- Queue: Pass `waitUrl` as option in `enqueue()`
|
||||||
- Browser phone calls use `client:` identifiers
|
- TwiML: Use SDK classes, not raw XML
|
||||||
- Customer leg must be identified for proper call control
|
|
||||||
|
|
||||||
### Database Operations
|
## Recent Changes (v2.3.0)
|
||||||
- Always use `$wpdb` global
|
- Browser phone moved to admin-only
|
||||||
- Sanitize with `sanitize_text_field()`, `intval()`
|
- Call control uses `find_customer_call_leg()` to prevent disconnections
|
||||||
- Use prepared statements: `$wpdb->prepare()`
|
- Auto-creates user queues/extensions when needed
|
||||||
- Call `TWP_Activator::ensure_tables_exist()` before operations
|
- Firefox support added
|
||||||
|
- 1-min agent status auto-revert
|
||||||
|
|
||||||
### AJAX Handler Pattern
|
## Development Notes
|
||||||
```php
|
- **API**: E.164 format (+1XXXXXXXXXX)
|
||||||
public function ajax_handler_name() {
|
- **Database**: Use `$wpdb`, prepared statements
|
||||||
if (!$this->verify_ajax_nonce()) {
|
- **AJAX**: Verify nonce, return JSON
|
||||||
wp_send_json_error('Invalid nonce');
|
- **Naming**: TWP_ for classes, twp_ for tables/options
|
||||||
return;
|
- **Debugging**: Look for "TWP Call Leg Detection" in logs
|
||||||
}
|
|
||||||
|
|
||||||
// Handler logic
|
## Features
|
||||||
|
- Agents accept calls via SMS "1"
|
||||||
wp_send_json_success($data);
|
- User-specific queues with extensions
|
||||||
}
|
- Browser phone at `admin.php?page=twilio-wp-browser-phone`
|
||||||
```
|
- ElevenLabs TTS with Alice fallback
|
||||||
|
- 68 AJAX actions, 26 REST endpoints
|
||||||
### Error Handling
|
|
||||||
- Log errors with `error_log()`
|
|
||||||
- Return structured error responses
|
|
||||||
- Implement fallback mechanisms
|
|
||||||
- Handle Twilio exceptions properly
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
- Classes: `TWP_Class_Name`
|
|
||||||
- Tables: `twp_table_name`
|
|
||||||
- Options: `twp_option_name`
|
|
||||||
- AJAX actions: `twp_action_name`
|
|
||||||
- Nonces: `twp_ajax_nonce` or `twp_frontend_nonce`
|
|
||||||
|
|
||||||
## 📋 Common Issues & Solutions
|
|
||||||
|
|
||||||
### Database Issues
|
|
||||||
- **Missing Tables**: Run `TWP_Activator::ensure_tables_exist()`
|
|
||||||
- **Schema Changes**: Check `add_missing_columns()` method
|
|
||||||
- **Migration Issues**: Review `migrate_tables()` implementation
|
|
||||||
|
|
||||||
### Webhook Issues
|
|
||||||
- **500 Errors**: Check PHP error logs
|
|
||||||
- **TwiML Errors**: Verify XML structure
|
|
||||||
- **Authentication**: Webhooks use `__return_true` permission
|
|
||||||
|
|
||||||
### API Issues
|
|
||||||
- **Instantiation**: Use `new TWP_Twilio_API()` not singleton
|
|
||||||
- **Phone Format**: Always use E.164 format (+1XXXXXXXXXX)
|
|
||||||
- **Call SID**: Access via `$response['data']['sid']`
|
|
||||||
|
|
||||||
### Call Control Issues (ALL RESOLVED - September 2025)
|
|
||||||
- **Extension Transfer Issues**: COMPLETELY RESOLVED - Queue-based system with 2-minute timeout and automatic voicemail fallback
|
|
||||||
- **Browser Phone Firefox Support**: FULLY RESOLVED - Explicit permission handling and cross-browser compatibility
|
|
||||||
- **Client Name Consistency**: FIXED - Standardized naming across all browser phone functions
|
|
||||||
- **Agent Status Management**: AUTOMATED - 1-minute auto-revert system with cron job automation
|
|
||||||
- **Call Statistics Accuracy**: ENHANCED - Browser phone calls now properly tracked in statistics
|
|
||||||
- **Transcription Integration**: IMPLEMENTED - Real Twilio API integration replacing placeholder system
|
|
||||||
- **Customer Disconnections**: PREVIOUSLY RESOLVED - All functions use intelligent call leg detection
|
|
||||||
- **Queue Not Found Errors**: PREVIOUSLY RESOLVED - Automatic queue creation prevents this issue
|
|
||||||
- **Professional Audio**: UNIVERSAL - ElevenLabs TTS integration throughout all voice prompts
|
|
||||||
- **Extension Voicemail**: ENHANCED - User ID support and proper callback handling
|
|
||||||
- **Permission System**: NEW - Automatic microphone/speaker permission requests for browser phone
|
|
||||||
|
|
||||||
### Hold/Transfer/Requeue System (COMPLETELY ENHANCED)
|
|
||||||
- **Automatic Queue Creation**: Creates personal and hold queues as needed
|
|
||||||
- **Extension Management**: Auto-generates unique 3-4 digit extensions
|
|
||||||
- **ElevenLabs Integration**: Premium TTS for all voice prompts with Alice fallback
|
|
||||||
- **Call Leg Detection**: 100% accurate customer vs agent identification
|
|
||||||
- **TwiML Compliance**: Proper XML generation prevents audio errors
|
|
||||||
- **Database Consistency**: Handles schema variations gracefully
|
|
||||||
- **Error Recovery**: Comprehensive fallback mechanisms
|
|
||||||
- **User Experience**: Professional audio experience throughout call lifecycle
|
|
||||||
|
|
||||||
### Advanced Debugging Features (NEW)
|
|
||||||
- **Call Leg Detection Logging**: "TWP Call Leg Detection" entries show call relationship analysis
|
|
||||||
- **Queue Creation Logging**: Tracks automatic queue and extension generation
|
|
||||||
- **TwiML Generation Logging**: Shows proper XML construction vs old webhook methods
|
|
||||||
- **Customer Number Detection**: Enhanced logging for browser phone call analysis
|
|
||||||
- **Extension Assignment**: Logs unique extension generation and assignment process
|
|
||||||
|
|
||||||
## 🧪 Testing Approach
|
|
||||||
|
|
||||||
### Unit Testing
|
|
||||||
- PHPUnit for PHP code
|
|
||||||
- Mock WordPress functions
|
|
||||||
- Mock Twilio API responses
|
|
||||||
|
|
||||||
### Integration Testing
|
|
||||||
- Test webhook endpoints
|
|
||||||
- Verify database operations
|
|
||||||
- Test complete call flows
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
- Monitor Twilio Console
|
|
||||||
- Check WordPress debug logs
|
|
||||||
- Test with real phone numbers
|
|
||||||
|
|
||||||
## 📚 Key Features Implementation
|
|
||||||
|
|
||||||
### Agent System
|
|
||||||
- **SMS Accept**: Agents text "1" to accept calls
|
|
||||||
- **Real-time Status**: Available/busy/offline states
|
|
||||||
- **Group Management**: Priority-based call distribution
|
|
||||||
- **Personal Queues**: Agent-specific call queues
|
|
||||||
|
|
||||||
### Call Queue System
|
|
||||||
- **Position Tracking**: Real-time queue position
|
|
||||||
- **Timeout Handling**: Automatic callback offers
|
|
||||||
- **Hold Queues**: Temporary call parking
|
|
||||||
- **User Queues**: Personal agent queues
|
|
||||||
|
|
||||||
### Browser Phone
|
|
||||||
- **Twilio Device**: Full SDK integration
|
|
||||||
- **Call Controls**: Hold, transfer, record, mute
|
|
||||||
- **Visual Interface**: Real-time status updates
|
|
||||||
- **Queue Dashboard**: Monitor waiting calls
|
|
||||||
|
|
||||||
### Recording System
|
|
||||||
- **Start/Stop**: Dynamic recording control
|
|
||||||
- **Storage**: Database tracking with Twilio URLs
|
|
||||||
- **Playback**: Authenticated proxy endpoints
|
|
||||||
- **Transcription**: Automatic with callbacks
|
|
||||||
|
|
||||||
### ElevenLabs TTS
|
|
||||||
- **Auto-detection**: Uses ElevenLabs when configured
|
|
||||||
- **Caching**: 30-day cache for repeated phrases
|
|
||||||
- **Fallback**: Seamless Twilio voice fallback
|
|
||||||
- **Performance**: Instant cached audio delivery
|
|
||||||
|
|
||||||
## 📝 Configuration Requirements
|
|
||||||
|
|
||||||
### WordPress Settings
|
|
||||||
- **Twilio Credentials**: Account SID, Auth Token
|
|
||||||
- **TwiML App**: For browser phone functionality
|
|
||||||
- **Phone Numbers**: At least one Twilio number
|
|
||||||
- **Webhook URLs**: Configure in Twilio Console
|
|
||||||
|
|
||||||
### Optional Settings
|
|
||||||
- **ElevenLabs**: API key and voice selection
|
|
||||||
- **Hold Music**: Custom URL support
|
|
||||||
- **SMS Notifications**: Agent alert numbers
|
|
||||||
- **Business Hours**: Schedule configurations
|
|
||||||
|
|
||||||
## 🔍 Debugging Tips
|
|
||||||
|
|
||||||
1. **Enable WordPress Debug**: `WP_DEBUG = true`
|
|
||||||
2. **Check Error Logs**: `/wp-content/debug.log`
|
|
||||||
3. **Monitor Twilio Console**: Real-time webhook debugging
|
|
||||||
4. **Database Queries**: Use `$wpdb->last_error`
|
|
||||||
5. **Browser Console**: Check JavaScript errors
|
|
||||||
6. **Network Tab**: Monitor AJAX requests
|
|
||||||
7. **Call Leg Detection**: Look for "TWP Call Leg Detection" log entries
|
|
||||||
8. **Outbound Call Issues**: Check for agent vs customer call SID usage
|
|
||||||
9. **Browser Phone Debugging**: Search logs for "client:" identifier handling
|
|
||||||
|
|
||||||
## 📖 External Resources
|
|
||||||
|
|
||||||
- **Twilio PHP SDK**: https://www.twilio.com/docs/libraries/reference/twilio-php/
|
|
||||||
- **WordPress REST API**: https://developer.wordpress.org/rest-api/
|
|
||||||
- **ElevenLabs API**: https://api.elevenlabs.io/docs
|
|
||||||
- **Twilio TwiML**: https://www.twilio.com/docs/voice/twiml
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
*Updated: Sept 2025*
|
||||||
*Last Updated: September 2025*
|
|
||||||
*Plugin Version: v2.3.0 - Enterprise Ready*
|
|
||||||
*Major Release: Extension Transfer System, Browser Phone Compatibility, Auto Status Management*
|
|
||||||
*Maintained for: phone.cloud-hosting.io*
|
|
||||||
293
QUEUE_VOICEMAIL_SMS_FEATURES.md
Normal file
293
QUEUE_VOICEMAIL_SMS_FEATURES.md
Normal 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
|
||||||
@@ -120,6 +120,15 @@ class TWP_Admin {
|
|||||||
array($this, 'display_plugin_settings')
|
array($this, 'display_plugin_settings')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'twilio-wp-plugin',
|
||||||
|
'Mobile App',
|
||||||
|
'Mobile App',
|
||||||
|
'manage_options',
|
||||||
|
'twilio-wp-mobile-app',
|
||||||
|
array($this, 'display_mobile_app_settings')
|
||||||
|
);
|
||||||
|
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'twilio-wp-plugin',
|
'twilio-wp-plugin',
|
||||||
'Phone Schedules',
|
'Phone Schedules',
|
||||||
@@ -390,7 +399,8 @@ class TWP_Admin {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
|
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
|
||||||
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key.</p>
|
<button type="button" class="button" onclick="refreshElevenLabsVoices()" title="Refresh voices from ElevenLabs">🔄 Refresh</button>
|
||||||
|
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key, or "Refresh" to get updated voices.</p>
|
||||||
<?php if (WP_DEBUG): ?>
|
<?php if (WP_DEBUG): ?>
|
||||||
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
|
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -552,7 +562,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">
|
||||||
@@ -1066,6 +1172,70 @@ class TWP_Admin {
|
|||||||
xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshElevenLabsVoices() {
|
||||||
|
var select = document.getElementById('elevenlabs-voice-select');
|
||||||
|
var button = event.target;
|
||||||
|
var currentValue = select.getAttribute('data-current') || select.value;
|
||||||
|
|
||||||
|
console.log('Refreshing voices, current value:', currentValue);
|
||||||
|
|
||||||
|
button.textContent = 'Refreshing...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', ajaxurl);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
button.textContent = '🔄 Refresh';
|
||||||
|
button.disabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var options = '<option value="">Select a voice...</option>';
|
||||||
|
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
response.data.forEach(function(voice) {
|
||||||
|
var selected = voice.voice_id === currentValue ? ' selected' : '';
|
||||||
|
var category = voice.category === 'cloned' ? ' (Cloned)' : (voice.category === 'premade' ? ' (Premade)' : '');
|
||||||
|
options += '<option value="' + voice.voice_id + '"' + selected + '>' + voice.name + category + '</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
select.innerHTML = options;
|
||||||
|
select.setAttribute('data-current', currentValue);
|
||||||
|
|
||||||
|
// Re-add preview buttons
|
||||||
|
addVoicePreviewButtons(select, response.data);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
var statusMsg = document.createElement('div');
|
||||||
|
statusMsg.style.color = 'green';
|
||||||
|
statusMsg.style.fontSize = '12px';
|
||||||
|
statusMsg.style.marginTop = '5px';
|
||||||
|
statusMsg.textContent = 'Voices refreshed successfully! Found ' + response.data.length + ' voices.';
|
||||||
|
button.parentNode.appendChild(statusMsg);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
if (statusMsg.parentNode) {
|
||||||
|
statusMsg.parentNode.removeChild(statusMsg);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
alert('Error refreshing voices: ' + (response.data || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Refresh voices error:', e);
|
||||||
|
alert('Failed to refresh voices. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send('action=twp_refresh_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
|
||||||
|
}
|
||||||
|
|
||||||
function addVoicePreviewButtons(select, voices) {
|
function addVoicePreviewButtons(select, voices) {
|
||||||
// Remove existing preview container
|
// Remove existing preview container
|
||||||
var existingPreview = document.getElementById('voice-preview-container');
|
var existingPreview = document.getElementById('voice-preview-container');
|
||||||
@@ -3659,6 +3829,14 @@ class TWP_Admin {
|
|||||||
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');
|
||||||
@@ -4786,6 +4964,36 @@ class TWP_Admin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for refreshing ElevenLabs voices (clears cache)
|
||||||
|
*/
|
||||||
|
public function ajax_refresh_elevenlabs_voices() {
|
||||||
|
check_ajax_referer('twp_ajax_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cached voices
|
||||||
|
delete_transient('twp_elevenlabs_voices');
|
||||||
|
|
||||||
|
// Now fetch fresh voices
|
||||||
|
$elevenlabs = new TWP_ElevenLabs_API();
|
||||||
|
$result = $elevenlabs->get_voices(); // This will fetch from API and re-cache
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
wp_send_json_success($result['data']['voices']);
|
||||||
|
} else {
|
||||||
|
$error_message = 'Failed to refresh voices';
|
||||||
|
if (is_string($result['error'])) {
|
||||||
|
$error_message = $result['error'];
|
||||||
|
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
|
||||||
|
$error_message = $result['error']['detail'];
|
||||||
|
}
|
||||||
|
wp_send_json_error($error_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for getting ElevenLabs models
|
* AJAX handler for getting ElevenLabs models
|
||||||
*/
|
*/
|
||||||
@@ -4850,7 +5058,7 @@ class TWP_Admin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$voice_id = sanitize_text_field($_POST['voice_id']);
|
$voice_id = sanitize_text_field($_POST['voice_id']);
|
||||||
$text = sanitize_text_field($_POST['text']) ?: 'Hello, this is a preview of this voice.';
|
$text = isset($_POST['text']) ? sanitize_text_field($_POST['text']) : 'Hello, this is a preview of this voice.';
|
||||||
|
|
||||||
$elevenlabs = new TWP_ElevenLabs_API();
|
$elevenlabs = new TWP_ElevenLabs_API();
|
||||||
$result = $elevenlabs->text_to_speech($text, $voice_id);
|
$result = $elevenlabs->text_to_speech($text, $voice_id);
|
||||||
@@ -9851,4 +10059,11 @@ class TWP_Admin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mobile app settings page
|
||||||
|
*/
|
||||||
|
public function display_mobile_app_settings() {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'admin/mobile-app-settings.php';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
353
admin/mobile-app-settings.php
Normal file
353
admin/mobile-app-settings.php
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Mobile App Settings Page
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user capabilities
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('You do not have sufficient permissions to access this page.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle manual update check
|
||||||
|
if (isset($_POST['twp_check_updates']) && check_admin_referer('twp_mobile_settings')) {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
|
$updater = new TWP_Auto_Updater();
|
||||||
|
$update_result = $updater->manual_check_for_updates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle test notification
|
||||||
|
if (isset($_POST['twp_test_notification']) && check_admin_referer('twp_mobile_settings')) {
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
|
||||||
|
$fcm = new TWP_FCM();
|
||||||
|
$test_user_id = get_current_user_id();
|
||||||
|
$notification_sent = $fcm->send_test_notification($test_user_id);
|
||||||
|
|
||||||
|
if ($notification_sent) {
|
||||||
|
$notification_result = array('success' => true, 'message' => 'Test notification sent successfully!');
|
||||||
|
} else {
|
||||||
|
$notification_result = array('success' => false, 'message' => 'Failed to send test notification. Check FCM configuration.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
if (isset($_POST['twp_save_mobile_settings']) && check_admin_referer('twp_mobile_settings')) {
|
||||||
|
update_option('twp_fcm_server_key', sanitize_text_field($_POST['twp_fcm_server_key']));
|
||||||
|
update_option('twp_auto_update_enabled', isset($_POST['twp_auto_update_enabled']) ? '1' : '0');
|
||||||
|
update_option('twp_gitea_repo', sanitize_text_field($_POST['twp_gitea_repo']));
|
||||||
|
update_option('twp_gitea_token', sanitize_text_field($_POST['twp_gitea_token']));
|
||||||
|
|
||||||
|
$settings_saved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current settings
|
||||||
|
$fcm_server_key = get_option('twp_fcm_server_key', '');
|
||||||
|
$auto_update_enabled = get_option('twp_auto_update_enabled', '1') === '1';
|
||||||
|
$gitea_repo = get_option('twp_gitea_repo', 'wp-plugins/twilio-wp-plugin');
|
||||||
|
$gitea_token = get_option('twp_gitea_token', '');
|
||||||
|
|
||||||
|
// Get update status
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
|
$updater = new TWP_Auto_Updater();
|
||||||
|
$update_status = $updater->get_update_status();
|
||||||
|
|
||||||
|
// Get mobile app statistics
|
||||||
|
global $wpdb;
|
||||||
|
$sessions_table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
$active_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table WHERE is_active = 1 AND expires_at > NOW()");
|
||||||
|
$total_sessions = $wpdb->get_var("SELECT COUNT(*) FROM $sessions_table");
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||||
|
|
||||||
|
<?php if (isset($settings_saved)): ?>
|
||||||
|
<div class="notice notice-success is-dismissible">
|
||||||
|
<p><strong>Settings saved successfully!</strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($update_result)): ?>
|
||||||
|
<div class="notice notice-<?php echo $update_result['update_available'] ? 'warning' : 'success'; ?> is-dismissible">
|
||||||
|
<p><strong><?php echo esc_html($update_result['message']); ?></strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($notification_result)): ?>
|
||||||
|
<div class="notice notice-<?php echo $notification_result['success'] ? 'success' : 'error'; ?> is-dismissible">
|
||||||
|
<p><strong><?php echo esc_html($notification_result['message']); ?></strong></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="twp-mobile-settings">
|
||||||
|
<!-- Mobile App Overview -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Mobile App Overview</h2>
|
||||||
|
<table class="widefat">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>API Endpoint:</strong></td>
|
||||||
|
<td><code><?php echo esc_html(site_url('/wp-json/twilio-mobile/v1')); ?></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Active Sessions:</strong></td>
|
||||||
|
<td><?php echo esc_html($active_sessions); ?> active / <?php echo esc_html($total_sessions); ?> total</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Plugin Version:</strong></td>
|
||||||
|
<td><?php echo esc_html(TWP_VERSION); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile App Settings Form -->
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field('twp_mobile_settings'); ?>
|
||||||
|
|
||||||
|
<!-- FCM Configuration -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Firebase Cloud Messaging (FCM)</h2>
|
||||||
|
<p>Configure FCM to enable push notifications for the mobile app.</p>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_fcm_server_key">FCM Server Key</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
id="twp_fcm_server_key"
|
||||||
|
name="twp_fcm_server_key"
|
||||||
|
value="<?php echo esc_attr($fcm_server_key); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="AAAA...">
|
||||||
|
<p class="description">
|
||||||
|
Get your server key from Firebase Console > Project Settings > Cloud Messaging > Server Key
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if (!empty($fcm_server_key)): ?>
|
||||||
|
<p>
|
||||||
|
<button type="submit" name="twp_test_notification" class="button">
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
<span class="description">Send a test notification to your devices</span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-Update Settings -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Automatic Updates</h2>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Current Version</th>
|
||||||
|
<td>
|
||||||
|
<strong><?php echo esc_html($update_status['current_version']); ?></strong>
|
||||||
|
<?php if ($update_status['update_available']): ?>
|
||||||
|
<span style="color: #d63638; margin-left: 10px;">
|
||||||
|
⚠ Update available: <?php echo esc_html($update_status['latest_version']); ?>
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: #00a32a; margin-left: 10px;">✓ Up to date</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_auto_update_enabled">Enable Auto-Updates</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
id="twp_auto_update_enabled"
|
||||||
|
name="twp_auto_update_enabled"
|
||||||
|
value="1"
|
||||||
|
<?php checked($auto_update_enabled); ?>>
|
||||||
|
Automatically check for updates every 12 hours
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_gitea_repo">Gitea Repository</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
id="twp_gitea_repo"
|
||||||
|
name="twp_gitea_repo"
|
||||||
|
value="<?php echo esc_attr($gitea_repo); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="org/repo-name">
|
||||||
|
<p class="description">
|
||||||
|
Format: organization/repository (e.g., wp-plugins/twilio-wp-plugin)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="twp_gitea_token">Gitea Access Token</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="password"
|
||||||
|
id="twp_gitea_token"
|
||||||
|
name="twp_gitea_token"
|
||||||
|
value="<?php echo esc_attr($gitea_token); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
placeholder="">
|
||||||
|
<p class="description">
|
||||||
|
Optional. Required only for private repositories. Create token at:
|
||||||
|
<a href="https://repo.anhonesthost.net/user/settings/applications" target="_blank">Gitea Settings > Applications</a>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Last Update Check</th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$last_check = $update_status['last_check'];
|
||||||
|
if ($last_check > 0) {
|
||||||
|
echo esc_html(human_time_diff($last_check, current_time('timestamp')) . ' ago');
|
||||||
|
} else {
|
||||||
|
echo 'Never';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<button type="submit" name="twp_check_updates" class="button" style="margin-left: 15px;">
|
||||||
|
Check Now
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Documentation -->
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<p>Available REST API endpoints for mobile app development:</p>
|
||||||
|
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Endpoint</th>
|
||||||
|
<th>Method</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/auth/login</code></td>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>Authenticate and get JWT tokens</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/auth/refresh</code></td>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>Refresh access token</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/agent/status</code></td>
|
||||||
|
<td>GET/POST</td>
|
||||||
|
<td>Get or update agent status</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/queues/state</code></td>
|
||||||
|
<td>GET</td>
|
||||||
|
<td>Get all queue states</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/calls/{call_sid}/accept</code></td>
|
||||||
|
<td>POST</td>
|
||||||
|
<td>Accept a queued call</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/twilio-mobile/v1/stream/events</code></td>
|
||||||
|
<td>GET</td>
|
||||||
|
<td>Server-Sent Events stream for real-time updates</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px;">
|
||||||
|
<strong>Authentication:</strong> All endpoints (except login/refresh) require
|
||||||
|
<code>Authorization: Bearer <access_token></code> header.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<button type="submit" name="twp_save_mobile_settings" class="button button-primary">
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Active Sessions -->
|
||||||
|
<?php if ($active_sessions > 0): ?>
|
||||||
|
<div class="card" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
<h2>Active Mobile Sessions</h2>
|
||||||
|
<?php
|
||||||
|
$sessions = $wpdb->get_results("
|
||||||
|
SELECT s.user_id, s.device_info, s.logged_in_at, s.last_used, u.user_login, u.display_name
|
||||||
|
FROM $sessions_table s
|
||||||
|
JOIN {$wpdb->users} u ON s.user_id = u.ID
|
||||||
|
WHERE s.is_active = 1 AND s.expires_at > NOW()
|
||||||
|
ORDER BY s.last_used DESC
|
||||||
|
LIMIT 20
|
||||||
|
");
|
||||||
|
?>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Last Activity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($sessions as $session): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo esc_html($session->display_name ?: $session->user_login); ?></td>
|
||||||
|
<td><?php echo esc_html($session->device_info ?: 'Unknown device'); ?></td>
|
||||||
|
<td><?php echo esc_html(human_time_diff(strtotime($session->last_used), current_time('timestamp')) . ' ago'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.twp-mobile-settings .card {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ccd0d4;
|
||||||
|
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-mobile-settings .card h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-mobile-settings code {
|
||||||
|
background: #f0f0f1;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twp-mobile-settings table.widefat td code {
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -413,6 +413,7 @@ jQuery(document).ready(function($) {
|
|||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
||||||
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
||||||
|
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Audio File Options
|
// Audio File Options
|
||||||
@@ -454,6 +455,7 @@ jQuery(document).ready(function($) {
|
|||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
||||||
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
||||||
|
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Audio File Options
|
// Audio File Options
|
||||||
@@ -520,6 +522,7 @@ jQuery(document).ready(function($) {
|
|||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
||||||
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
||||||
|
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Audio File Options
|
// Audio File Options
|
||||||
@@ -559,6 +562,7 @@ jQuery(document).ready(function($) {
|
|||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
|
||||||
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
|
||||||
|
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Audio File Options
|
// Audio File Options
|
||||||
@@ -1774,6 +1778,97 @@ jQuery(document).ready(function($) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Refresh voices function for workflow
|
||||||
|
window.refreshWorkflowVoices = function(buttonOrSelect) {
|
||||||
|
var $button, $select;
|
||||||
|
|
||||||
|
// Check if we were passed a button or a select element
|
||||||
|
if ($(buttonOrSelect).is('button')) {
|
||||||
|
$button = $(buttonOrSelect);
|
||||||
|
$select = $button.prev('select.voice-select');
|
||||||
|
} else if ($(buttonOrSelect).is('select')) {
|
||||||
|
$select = $(buttonOrSelect);
|
||||||
|
$button = $select.next('button');
|
||||||
|
} else {
|
||||||
|
// Fallback - assume it's a button
|
||||||
|
$button = $(buttonOrSelect);
|
||||||
|
$select = $button.prev('select.voice-select');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the current value
|
||||||
|
var currentValue = '';
|
||||||
|
var selectedOption = $select.find('option:selected');
|
||||||
|
if (selectedOption.length && selectedOption.val()) {
|
||||||
|
currentValue = selectedOption.val();
|
||||||
|
} else {
|
||||||
|
currentValue = $select.attr('data-current') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($button.length) {
|
||||||
|
$button.text('Refreshing...').prop('disabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.post(twp_ajax.ajax_url, {
|
||||||
|
action: 'twp_refresh_elevenlabs_voices',
|
||||||
|
nonce: twp_ajax.nonce
|
||||||
|
}, function(response) {
|
||||||
|
if ($button.length) {
|
||||||
|
$button.text('🔄').prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var options = '<option value="">Default voice</option>';
|
||||||
|
|
||||||
|
response.data.forEach(function(voice) {
|
||||||
|
var selected = (voice.voice_id === currentValue) ? ' selected' : '';
|
||||||
|
var description = voice.labels ? Object.values(voice.labels).join(', ') : '';
|
||||||
|
var optionText = voice.name + (description ? ' (' + description + ')' : '');
|
||||||
|
options += '<option value="' + voice.voice_id + '" data-voice-name="' + voice.name + '"' + selected + '>' + optionText + '</option>';
|
||||||
|
});
|
||||||
|
|
||||||
|
$select.html(options);
|
||||||
|
|
||||||
|
// If we had a current value, make sure it's selected
|
||||||
|
if (currentValue) {
|
||||||
|
$select.val(currentValue);
|
||||||
|
// Update the voice name field with the selected voice's name
|
||||||
|
var $voiceNameInput = $select.siblings('input[name="voice_name"]');
|
||||||
|
if ($voiceNameInput.length) {
|
||||||
|
var selectedVoice = $select.find('option:selected');
|
||||||
|
var voiceName = selectedVoice.data('voice-name') || selectedVoice.text() || '';
|
||||||
|
if (selectedVoice.val() === '') {
|
||||||
|
voiceName = '';
|
||||||
|
}
|
||||||
|
$voiceNameInput.val(voiceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
var $statusMsg = $('<span style="color: green; font-size: 12px; margin-left: 5px;">Refreshed!</span>');
|
||||||
|
$button.after($statusMsg);
|
||||||
|
setTimeout(function() {
|
||||||
|
$statusMsg.remove();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var errorMessage = 'Error refreshing voices: ';
|
||||||
|
if (typeof response.data === 'string') {
|
||||||
|
errorMessage += response.data;
|
||||||
|
} else if (response.data && response.data.message) {
|
||||||
|
errorMessage += response.data.message;
|
||||||
|
} else {
|
||||||
|
errorMessage += 'Unknown error';
|
||||||
|
}
|
||||||
|
alert(errorMessage);
|
||||||
|
}
|
||||||
|
}).fail(function() {
|
||||||
|
if ($button.length) {
|
||||||
|
$button.text('🔄').prop('disabled', false);
|
||||||
|
}
|
||||||
|
alert('Failed to refresh voices. Please check your API key.');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Toggle audio type options visibility
|
// Toggle audio type options visibility
|
||||||
$(document).on('change', 'input[name="audio_type"]', function() {
|
$(document).on('change', 'input[name="audio_type"]', function() {
|
||||||
var $container = $(this).closest('.step-config-section');
|
var $container = $(this).closest('.step-config-section');
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ class TWP_Activator {
|
|||||||
'twp_callbacks',
|
'twp_callbacks',
|
||||||
'twp_call_recordings',
|
'twp_call_recordings',
|
||||||
'twp_user_extensions',
|
'twp_user_extensions',
|
||||||
'twp_queue_assignments'
|
'twp_queue_assignments',
|
||||||
|
'twp_mobile_sessions'
|
||||||
);
|
);
|
||||||
|
|
||||||
$missing_tables = array();
|
$missing_tables = array();
|
||||||
@@ -362,6 +363,24 @@ class TWP_Activator {
|
|||||||
KEY started_at (started_at)
|
KEY started_at (started_at)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Mobile sessions table
|
||||||
|
$table_mobile_sessions = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
$sql_mobile_sessions = "CREATE TABLE $table_mobile_sessions (
|
||||||
|
id int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id bigint(20) NOT NULL,
|
||||||
|
refresh_token varchar(500) NOT NULL,
|
||||||
|
fcm_token text,
|
||||||
|
device_info text,
|
||||||
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at datetime NOT NULL,
|
||||||
|
last_used datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_active tinyint(1) DEFAULT 1,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY user_id (user_id),
|
||||||
|
KEY is_active (is_active),
|
||||||
|
KEY expires_at (expires_at)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
dbDelta($sql_schedules);
|
dbDelta($sql_schedules);
|
||||||
dbDelta($sql_queues);
|
dbDelta($sql_queues);
|
||||||
dbDelta($sql_queued_calls);
|
dbDelta($sql_queued_calls);
|
||||||
@@ -377,6 +396,7 @@ class TWP_Activator {
|
|||||||
dbDelta($sql_recordings);
|
dbDelta($sql_recordings);
|
||||||
dbDelta($sql_user_extensions);
|
dbDelta($sql_user_extensions);
|
||||||
dbDelta($sql_queue_assignments);
|
dbDelta($sql_queue_assignments);
|
||||||
|
dbDelta($sql_mobile_sessions);
|
||||||
|
|
||||||
// Add missing columns for existing installations
|
// Add missing columns for existing installations
|
||||||
self::add_missing_columns();
|
self::add_missing_columns();
|
||||||
|
|||||||
291
includes/class-twp-auto-updater.php
Normal file
291
includes/class-twp-auto-updater.php
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Automatic Plugin Updater
|
||||||
|
*
|
||||||
|
* Checks for updates from Gitea repository and installs them automatically
|
||||||
|
*/
|
||||||
|
class TWP_Auto_Updater {
|
||||||
|
|
||||||
|
private $plugin_slug = 'twilio-wp-plugin';
|
||||||
|
private $plugin_basename;
|
||||||
|
private $gitea_repo = 'wp-plugins/twilio-wp-plugin';
|
||||||
|
private $gitea_api_url;
|
||||||
|
private $current_version;
|
||||||
|
private $gitea_base_url = 'https://repo.anhonesthost.net';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->plugin_basename = plugin_basename(dirname(dirname(__FILE__)) . '/twilio-wp-plugin.php');
|
||||||
|
$this->current_version = defined('TWP_VERSION') ? TWP_VERSION : '0.0.0';
|
||||||
|
$this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize updater hooks
|
||||||
|
*/
|
||||||
|
public function init() {
|
||||||
|
// Hook into WordPress update checks
|
||||||
|
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_update'));
|
||||||
|
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
|
||||||
|
|
||||||
|
// Add settings page for manual check
|
||||||
|
add_action('admin_init', array($this, 'register_settings'));
|
||||||
|
|
||||||
|
// Add update check to admin notices
|
||||||
|
if (get_option('twp_auto_update_enabled', '1') === '1') {
|
||||||
|
add_action('admin_init', array($this, 'maybe_auto_check_updates'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register auto-update settings
|
||||||
|
*/
|
||||||
|
public function register_settings() {
|
||||||
|
register_setting('twp_settings', 'twp_auto_update_enabled');
|
||||||
|
register_setting('twp_settings', 'twp_gitea_repo');
|
||||||
|
register_setting('twp_settings', 'twp_gitea_token'); // Optional for private repos
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for updates periodically
|
||||||
|
*/
|
||||||
|
public function maybe_auto_check_updates() {
|
||||||
|
$last_check = get_option('twp_last_update_check', 0);
|
||||||
|
$check_interval = 12 * HOUR_IN_SECONDS; // Check every 12 hours
|
||||||
|
|
||||||
|
if (time() - $last_check > $check_interval) {
|
||||||
|
update_option('twp_last_update_check', time());
|
||||||
|
// Force WordPress to check for updates
|
||||||
|
wp_clean_plugins_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for plugin updates
|
||||||
|
*/
|
||||||
|
public function check_for_update($transient) {
|
||||||
|
if (empty($transient->checked)) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Gitea repo from settings if available
|
||||||
|
$custom_repo = get_option('twp_gitea_repo', '');
|
||||||
|
if (!empty($custom_repo)) {
|
||||||
|
$this->gitea_repo = $custom_repo;
|
||||||
|
$this->gitea_api_url = $this->gitea_base_url . '/api/v1/repos/' . $this->gitea_repo . '/releases/latest';
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
if ($update_info && version_compare($this->current_version, $update_info->version, '<')) {
|
||||||
|
$plugin_data = array(
|
||||||
|
'id' => 'twilio-wp-plugin',
|
||||||
|
'slug' => $this->plugin_slug,
|
||||||
|
'plugin' => $this->plugin_basename,
|
||||||
|
'new_version' => $update_info->version,
|
||||||
|
'url' => $update_info->homepage,
|
||||||
|
'package' => $update_info->download_url,
|
||||||
|
'tested' => '6.8',
|
||||||
|
'requires' => '5.8',
|
||||||
|
'requires_php' => '7.4',
|
||||||
|
'icons' => array(),
|
||||||
|
'banners' => array(),
|
||||||
|
'compatibility' => new stdClass(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$transient->response[$this->plugin_basename] = (object) $plugin_data;
|
||||||
|
|
||||||
|
error_log("TWP Auto-Updater: New version {$update_info->version} available (current: {$this->current_version})");
|
||||||
|
} else {
|
||||||
|
error_log("TWP Auto-Updater: No updates available (current: {$this->current_version})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin information for update screen
|
||||||
|
*/
|
||||||
|
public function plugin_info($false, $action, $args) {
|
||||||
|
if ($action !== 'plugin_information') {
|
||||||
|
return $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($args->slug) || $args->slug !== $this->plugin_slug) {
|
||||||
|
return $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
if (!$update_info) {
|
||||||
|
return $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_info = new stdClass();
|
||||||
|
$plugin_info->name = 'Twilio WP Plugin';
|
||||||
|
$plugin_info->slug = $this->plugin_slug;
|
||||||
|
$plugin_info->version = $update_info->version;
|
||||||
|
$plugin_info->author = '<a href="https://cybercove.io/">Joshua Knapp</a>';
|
||||||
|
$plugin_info->homepage = $update_info->homepage;
|
||||||
|
$plugin_info->download_link = $update_info->download_url;
|
||||||
|
$plugin_info->requires = '5.8';
|
||||||
|
$plugin_info->tested = '6.8';
|
||||||
|
$plugin_info->requires_php = '7.4';
|
||||||
|
$plugin_info->last_updated = $update_info->release_date;
|
||||||
|
$plugin_info->downloaded = 10;
|
||||||
|
|
||||||
|
$plugin_info->sections = array(
|
||||||
|
'description' => '<p>Twilio WordPress Plugin for call management and mobile app support.</p>',
|
||||||
|
'changelog' => '<pre>' . esc_html($update_info->changelog) . '</pre>',
|
||||||
|
'installation' => '<p>Upload the plugin to your WordPress site and activate it.</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $plugin_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latest release information from Gitea
|
||||||
|
*/
|
||||||
|
private function get_latest_release() {
|
||||||
|
// Check cache first (1 hour)
|
||||||
|
$cache_key = 'twp_latest_release';
|
||||||
|
$cached = get_transient($cache_key);
|
||||||
|
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cURL for Gitea API
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $this->gitea_api_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
|
||||||
|
curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress/Twilio-WP-Plugin-Updater');
|
||||||
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||||
|
|
||||||
|
// Add Gitea token if configured (for private repos)
|
||||||
|
$gitea_token = get_option('twp_gitea_token', '');
|
||||||
|
if (!empty($gitea_token)) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||||
|
'Accept: application/json',
|
||||||
|
'Authorization: token ' . $gitea_token
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if (!$response || $http_code !== 200) {
|
||||||
|
error_log("TWP Auto-Updater: Gitea API returned status $http_code");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = json_decode($response);
|
||||||
|
|
||||||
|
if (!$release || !isset($release->tag_name)) {
|
||||||
|
error_log('TWP Auto-Updater: Invalid release data from Gitea');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse release information
|
||||||
|
$version = ltrim($release->tag_name, 'v'); // Remove 'v' prefix if present
|
||||||
|
$download_url = null;
|
||||||
|
|
||||||
|
// Find the zip asset
|
||||||
|
if (isset($release->assets) && is_array($release->assets)) {
|
||||||
|
foreach ($release->assets as $asset) {
|
||||||
|
if (strpos($asset->name, '.zip') !== false) {
|
||||||
|
$download_url = $asset->browser_download_url;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to zipball if no asset found
|
||||||
|
if (!$download_url) {
|
||||||
|
$download_url = $release->zipball_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format changelog
|
||||||
|
$changelog = !empty($release->body) ? $release->body : 'No changelog provided for this release.';
|
||||||
|
|
||||||
|
// Handle empty changelog
|
||||||
|
if (empty(trim($changelog))) {
|
||||||
|
$changelog = "Version " . $version . "\n\n" .
|
||||||
|
"Released on " . date('F j, Y', strtotime($release->published_at)) . "\n\n" .
|
||||||
|
"* Updated plugin files";
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = (object) array(
|
||||||
|
'version' => $version,
|
||||||
|
'download_url' => $download_url,
|
||||||
|
'homepage' => $this->gitea_base_url . '/' . $this->gitea_repo,
|
||||||
|
'release_date' => $release->published_at,
|
||||||
|
'description' => $changelog,
|
||||||
|
'changelog' => $changelog
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache for 1 hour
|
||||||
|
set_transient($cache_key, $update_info, HOUR_IN_SECONDS);
|
||||||
|
|
||||||
|
return $update_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual update check (for admin page)
|
||||||
|
*/
|
||||||
|
public function manual_check_for_updates() {
|
||||||
|
// Clear cache
|
||||||
|
delete_transient('twp_latest_release');
|
||||||
|
update_option('twp_last_update_check', 0);
|
||||||
|
|
||||||
|
// Force WordPress to check
|
||||||
|
wp_clean_plugins_cache();
|
||||||
|
delete_site_transient('update_plugins');
|
||||||
|
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
if (!$update_info) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to check for updates. Please check your internet connection and Gitea repository settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version_compare($this->current_version, $update_info->version, '<')) {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => true,
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'latest_version' => $update_info->version,
|
||||||
|
'message' => "Update available: Version {$update_info->version}. Go to Plugins page to update."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => false,
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'message' => 'You are running the latest version.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current update status
|
||||||
|
*/
|
||||||
|
public function get_update_status() {
|
||||||
|
$update_info = $this->get_latest_release();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'latest_version' => $update_info ? $update_info->version : 'Unknown',
|
||||||
|
'update_available' => $update_info && version_compare($this->current_version, $update_info->version, '<'),
|
||||||
|
'last_check' => get_option('twp_last_update_check', 0),
|
||||||
|
'auto_update_enabled' => get_option('twp_auto_update_enabled', '1') === '1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,14 +181,35 @@ class TWP_Call_Queue {
|
|||||||
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();
|
||||||
|
|
||||||
|
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(
|
$twilio->update_call($call->call_sid, array(
|
||||||
'twiml' => $callback_twiml
|
'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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ class TWP_Core {
|
|||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
|
||||||
|
|
||||||
|
// Mobile app classes
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-auth.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-api.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-mobile-sse.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-fcm.php';
|
||||||
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-auto-updater.php';
|
||||||
|
|
||||||
// Feature classes
|
// Feature classes
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
|
||||||
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
|
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
|
||||||
@@ -161,6 +168,7 @@ class TWP_Core {
|
|||||||
|
|
||||||
// Eleven Labs AJAX
|
// Eleven Labs AJAX
|
||||||
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
|
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_refresh_elevenlabs_voices', $plugin_admin, 'ajax_refresh_elevenlabs_voices');
|
||||||
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
|
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
|
||||||
$this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');
|
$this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');
|
||||||
|
|
||||||
@@ -318,6 +326,20 @@ class TWP_Core {
|
|||||||
$webhooks = new TWP_Webhooks();
|
$webhooks = new TWP_Webhooks();
|
||||||
$webhooks->register_endpoints();
|
$webhooks->register_endpoints();
|
||||||
|
|
||||||
|
// Initialize mobile app endpoints
|
||||||
|
$mobile_auth = new TWP_Mobile_Auth();
|
||||||
|
$mobile_auth->register_endpoints();
|
||||||
|
|
||||||
|
$mobile_api = new TWP_Mobile_API();
|
||||||
|
$mobile_api->register_endpoints();
|
||||||
|
|
||||||
|
$mobile_sse = new TWP_Mobile_SSE();
|
||||||
|
$mobile_sse->register_endpoints();
|
||||||
|
|
||||||
|
// Initialize auto-updater
|
||||||
|
$updater = new TWP_Auto_Updater();
|
||||||
|
$updater->init();
|
||||||
|
|
||||||
// Add custom cron schedules
|
// Add custom cron schedules
|
||||||
add_filter('cron_schedules', function($schedules) {
|
add_filter('cron_schedules', function($schedules) {
|
||||||
$schedules['twp_every_minute'] = array(
|
$schedules['twp_every_minute'] = array(
|
||||||
|
|||||||
214
includes/class-twp-fcm.php
Normal file
214
includes/class-twp-fcm.php
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Firebase Cloud Messaging (FCM) Integration
|
||||||
|
*
|
||||||
|
* Handles push notifications to mobile devices via FCM
|
||||||
|
*/
|
||||||
|
class TWP_FCM {
|
||||||
|
|
||||||
|
private $server_key;
|
||||||
|
private $fcm_url = 'https://fcm.googleapis.com/fcm/send';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->server_key = get_option('twp_fcm_server_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send push notification to user's devices
|
||||||
|
*/
|
||||||
|
public function send_notification($user_id, $title, $body, $data = array()) {
|
||||||
|
if (empty($this->server_key)) {
|
||||||
|
error_log('TWP FCM: Server key not configured');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's FCM tokens
|
||||||
|
$tokens = $this->get_user_tokens($user_id);
|
||||||
|
|
||||||
|
if (empty($tokens)) {
|
||||||
|
error_log("TWP FCM: No tokens found for user $user_id");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$success_count = 0;
|
||||||
|
$failed_tokens = array();
|
||||||
|
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
$result = $this->send_to_token($token, $title, $body, $data);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$success_count++;
|
||||||
|
} else {
|
||||||
|
$failed_tokens[] = $token;
|
||||||
|
|
||||||
|
// If token is invalid, remove it from database
|
||||||
|
if ($result['error'] === 'invalid_token') {
|
||||||
|
$this->remove_invalid_token($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("TWP FCM: Sent notification to $success_count/" . count($tokens) . " devices for user $user_id");
|
||||||
|
|
||||||
|
return $success_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification to specific token
|
||||||
|
*/
|
||||||
|
private function send_to_token($token, $title, $body, $data = array()) {
|
||||||
|
$notification = array(
|
||||||
|
'title' => $title,
|
||||||
|
'body' => $body,
|
||||||
|
'sound' => 'default',
|
||||||
|
'priority' => 'high',
|
||||||
|
'click_action' => 'FLUTTER_NOTIFICATION_CLICK'
|
||||||
|
);
|
||||||
|
|
||||||
|
$payload = array(
|
||||||
|
'to' => $token,
|
||||||
|
'notification' => $notification,
|
||||||
|
'data' => array_merge($data, array(
|
||||||
|
'title' => $title,
|
||||||
|
'body' => $body,
|
||||||
|
'timestamp' => time()
|
||||||
|
)),
|
||||||
|
'priority' => 'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
$headers = array(
|
||||||
|
'Authorization: key=' . $this->server_key,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
);
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $this->fcm_url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code !== 200) {
|
||||||
|
error_log("TWP FCM: Failed to send notification. HTTP $http_code: $response");
|
||||||
|
|
||||||
|
// Check if token is invalid
|
||||||
|
$response_data = json_decode($response, true);
|
||||||
|
if (isset($response_data['results'][0]['error']) &&
|
||||||
|
in_array($response_data['results'][0]['error'], array('InvalidRegistration', 'NotRegistered'))) {
|
||||||
|
return array('success' => false, 'error' => 'invalid_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('success' => false, 'error' => 'http_error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('success' => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active FCM tokens for a user
|
||||||
|
*/
|
||||||
|
private function get_user_tokens($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
return $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT fcm_token FROM $table
|
||||||
|
WHERE user_id = %d
|
||||||
|
AND is_active = 1
|
||||||
|
AND fcm_token IS NOT NULL
|
||||||
|
AND fcm_token != ''
|
||||||
|
AND expires_at > NOW()",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove invalid FCM token from database
|
||||||
|
*/
|
||||||
|
private function remove_invalid_token($token) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('fcm_token' => null),
|
||||||
|
array('fcm_token' => $token),
|
||||||
|
array('%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
error_log("TWP FCM: Removed invalid token from database");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send incoming call notification
|
||||||
|
*/
|
||||||
|
public function notify_incoming_call($user_id, $from_number, $queue_name, $call_sid) {
|
||||||
|
$title = 'Incoming Call';
|
||||||
|
$body = "Call from $from_number in $queue_name queue";
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'incoming_call',
|
||||||
|
'call_sid' => $call_sid,
|
||||||
|
'from_number' => $from_number,
|
||||||
|
'queue_name' => $queue_name
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send queue timeout notification
|
||||||
|
*/
|
||||||
|
public function notify_queue_timeout($user_id, $queue_name, $waiting_count) {
|
||||||
|
$title = 'Queue Alert';
|
||||||
|
$body = "$queue_name has $waiting_count waiting call" . ($waiting_count > 1 ? 's' : '');
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'queue_timeout',
|
||||||
|
'queue_name' => $queue_name,
|
||||||
|
'waiting_count' => $waiting_count
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send agent status change notification
|
||||||
|
*/
|
||||||
|
public function notify_status_change($user_id, $old_status, $new_status) {
|
||||||
|
$title = 'Status Changed';
|
||||||
|
$body = "Your status changed from $old_status to $new_status";
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'status_change',
|
||||||
|
'old_status' => $old_status,
|
||||||
|
'new_status' => $new_status
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test notification (for settings page)
|
||||||
|
*/
|
||||||
|
public function send_test_notification($user_id) {
|
||||||
|
$title = 'Test Notification';
|
||||||
|
$body = 'This is a test notification from Twilio WordPress Plugin';
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'type' => 'test',
|
||||||
|
'test' => true
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->send_notification($user_id, $title, $body, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
684
includes/class-twp-mobile-api.php
Normal file
684
includes/class-twp-mobile-api.php
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Mobile App REST API Endpoints
|
||||||
|
*
|
||||||
|
* Provides REST API endpoints for mobile app functionality
|
||||||
|
*/
|
||||||
|
class TWP_Mobile_API {
|
||||||
|
|
||||||
|
private $auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
// Initialize auth handler
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'class-twp-mobile-auth.php';
|
||||||
|
$this->auth = new TWP_Mobile_Auth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API endpoints
|
||||||
|
*/
|
||||||
|
public function register_endpoints() {
|
||||||
|
add_action('rest_api_init', function() {
|
||||||
|
// Agent status endpoints
|
||||||
|
register_rest_route('twilio-mobile/v1', '/agent/status', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_agent_status'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('twilio-mobile/v1', '/agent/status', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'update_agent_status'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Queue state endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/queues/state', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_queue_state'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Queue calls (specific queue)
|
||||||
|
register_rest_route('twilio-mobile/v1', '/queues/(?P<id>\d+)/calls', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_queue_calls'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Call control endpoints
|
||||||
|
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/accept', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'accept_call'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/reject', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'reject_call'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/hold', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'hold_call'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/unhold', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'unhold_call'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('twilio-mobile/v1', '/calls/(?P<call_sid>[^/]+)/transfer', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'transfer_call'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// FCM token registration
|
||||||
|
register_rest_route('twilio-mobile/v1', '/fcm/register', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'register_fcm_token'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Agent phone number
|
||||||
|
register_rest_route('twilio-mobile/v1', '/agent/phone', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_agent_phone'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('twilio-mobile/v1', '/agent/phone', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'update_agent_phone'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent status
|
||||||
|
*/
|
||||||
|
public function get_agent_status($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
|
||||||
|
$status = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, is_logged_in, current_call_sid, last_activity, available_for_queues FROM $table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$status) {
|
||||||
|
// Create default status
|
||||||
|
$wpdb->insert(
|
||||||
|
$table,
|
||||||
|
array('user_id' => $user_id, 'status' => 'offline', 'is_logged_in' => 0),
|
||||||
|
array('%d', '%s', '%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = (object) array(
|
||||||
|
'status' => 'offline',
|
||||||
|
'is_logged_in' => 0,
|
||||||
|
'current_call_sid' => null,
|
||||||
|
'last_activity' => current_time('mysql'),
|
||||||
|
'available_for_queues' => 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'status' => $status->status,
|
||||||
|
'is_logged_in' => (bool)$status->is_logged_in,
|
||||||
|
'current_call_sid' => $status->current_call_sid,
|
||||||
|
'last_activity' => $status->last_activity,
|
||||||
|
'available_for_queues' => (bool)$status->available_for_queues
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update agent status
|
||||||
|
*/
|
||||||
|
public function update_agent_status($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$new_status = $request->get_param('status');
|
||||||
|
$is_logged_in = $request->get_param('is_logged_in');
|
||||||
|
|
||||||
|
if (!in_array($new_status, array('available', 'busy', 'offline'))) {
|
||||||
|
return new WP_Error('invalid_status', 'Status must be available, busy, or offline', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
|
||||||
|
// Check if status exists
|
||||||
|
$exists = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM $table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'status' => $new_status,
|
||||||
|
'last_activity' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($is_logged_in !== null) {
|
||||||
|
$data['is_logged_in'] = $is_logged_in ? 1 : 0;
|
||||||
|
if ($is_logged_in) {
|
||||||
|
$data['logged_in_at'] = current_time('mysql');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
$data,
|
||||||
|
array('user_id' => $user_id),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$data['user_id'] = $user_id;
|
||||||
|
$wpdb->insert($table, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Status updated successfully'
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queue state (all queues user has access to)
|
||||||
|
*/
|
||||||
|
public function get_queue_state($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
|
||||||
|
|
||||||
|
// Get queues assigned to this user
|
||||||
|
$queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT queue_id FROM $assignments_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
// Also include personal queues
|
||||||
|
$personal_queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT id FROM $queues_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
|
||||||
|
|
||||||
|
if (empty($all_queue_ids)) {
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'queues' => array()
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
|
||||||
|
|
||||||
|
// Get queue information with call counts
|
||||||
|
$queues = $wpdb->get_results("
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
q.queue_name,
|
||||||
|
q.queue_type,
|
||||||
|
q.extension,
|
||||||
|
COUNT(c.id) as waiting_count
|
||||||
|
FROM $queues_table q
|
||||||
|
LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
|
||||||
|
WHERE q.id IN ($queue_ids_str)
|
||||||
|
GROUP BY q.id
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($queues as $queue) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => (int)$queue->id,
|
||||||
|
'name' => $queue->queue_name,
|
||||||
|
'type' => $queue->queue_type,
|
||||||
|
'extension' => $queue->extension,
|
||||||
|
'waiting_count' => (int)$queue->waiting_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'queues' => $result
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calls in a specific queue
|
||||||
|
*/
|
||||||
|
public function get_queue_calls($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$queue_id = (int)$request['id'];
|
||||||
|
|
||||||
|
// Verify user has access to this queue
|
||||||
|
if (!$this->user_has_queue_access($user_id, $queue_id)) {
|
||||||
|
return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$calls = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT call_sid, from_number, to_number, position, status, joined_at, enqueued_at
|
||||||
|
FROM $table
|
||||||
|
WHERE queue_id = %d AND status = 'waiting'
|
||||||
|
ORDER BY position ASC",
|
||||||
|
$queue_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($calls as $call) {
|
||||||
|
$result[] = array(
|
||||||
|
'call_sid' => $call->call_sid,
|
||||||
|
'from_number' => $call->from_number,
|
||||||
|
'to_number' => $call->to_number,
|
||||||
|
'position' => (int)$call->position,
|
||||||
|
'status' => $call->status,
|
||||||
|
'wait_time' => $this->calculate_wait_time($call->enqueued_at ?: $call->joined_at)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'calls' => $result
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a call (dequeue and connect to agent)
|
||||||
|
*/
|
||||||
|
public function accept_call($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$call_sid = $request['call_sid'];
|
||||||
|
|
||||||
|
// Get agent phone number
|
||||||
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
||||||
|
|
||||||
|
if (empty($agent_number)) {
|
||||||
|
return new WP_Error('no_phone', 'No phone number configured for agent', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Twilio API
|
||||||
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Get call info from queue
|
||||||
|
global $wpdb;
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$call = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'",
|
||||||
|
$call_sid
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$call) {
|
||||||
|
return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user has access to this queue
|
||||||
|
if (!$this->user_has_queue_access($user_id, $call->queue_id)) {
|
||||||
|
return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connect agent to call
|
||||||
|
$agent_call = $twilio->create_call(
|
||||||
|
$agent_number,
|
||||||
|
$call->to_number,
|
||||||
|
array(
|
||||||
|
'url' => site_url('/wp-json/twilio-webhook/v1/connect-agent'),
|
||||||
|
'statusCallback' => site_url('/wp-json/twilio-webhook/v1/agent-call-status'),
|
||||||
|
'statusCallbackEvent' => array('completed', 'no-answer', 'busy', 'failed'),
|
||||||
|
'timeout' => 30
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update call record
|
||||||
|
$wpdb->update(
|
||||||
|
$calls_table,
|
||||||
|
array(
|
||||||
|
'status' => 'connecting',
|
||||||
|
'agent_phone' => $agent_number,
|
||||||
|
'agent_call_sid' => $agent_call->sid
|
||||||
|
),
|
||||||
|
array('call_sid' => $call_sid),
|
||||||
|
array('%s', '%s', '%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update agent status
|
||||||
|
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
$wpdb->update(
|
||||||
|
$status_table,
|
||||||
|
array('status' => 'busy', 'current_call_sid' => $call_sid),
|
||||||
|
array('user_id' => $user_id),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Call accepted, connecting to agent',
|
||||||
|
'agent_call_sid' => $agent_call->sid
|
||||||
|
), 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a call (send to voicemail)
|
||||||
|
*/
|
||||||
|
public function reject_call($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$call_sid = $request['call_sid'];
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$call = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'",
|
||||||
|
$call_sid
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$call) {
|
||||||
|
return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user has access to this queue
|
||||||
|
if (!$this->user_has_queue_access($user_id, $call->queue_id)) {
|
||||||
|
return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize Twilio API
|
||||||
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Redirect call to voicemail
|
||||||
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
$twiml->say('The agent is unavailable. Please leave a message after the tone.');
|
||||||
|
$twiml->record(array(
|
||||||
|
'action' => site_url('/wp-json/twilio-webhook/v1/voicemail-complete'),
|
||||||
|
'maxLength' => 120,
|
||||||
|
'transcribe' => true
|
||||||
|
));
|
||||||
|
$twiml->say('We did not receive a recording. Goodbye.');
|
||||||
|
|
||||||
|
$twilio->update_call($call_sid, array('twiml' => $twiml->asXML()));
|
||||||
|
|
||||||
|
// Update call status
|
||||||
|
$wpdb->update(
|
||||||
|
$calls_table,
|
||||||
|
array('status' => 'voicemail', 'ended_at' => current_time('mysql')),
|
||||||
|
array('call_sid' => $call_sid),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Call sent to voicemail'
|
||||||
|
), 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hold a call
|
||||||
|
*/
|
||||||
|
public function hold_call($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$call_sid = $request['call_sid'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php';
|
||||||
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
|
||||||
|
|
||||||
|
$admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Find customer call leg
|
||||||
|
$customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio);
|
||||||
|
|
||||||
|
if (!$customer_call_sid) {
|
||||||
|
return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's hold queue
|
||||||
|
global $wpdb;
|
||||||
|
$ext_table = $wpdb->prefix . 'twp_user_extensions';
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
|
||||||
|
$extension = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT hold_queue_id FROM $ext_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$extension || !$extension->hold_queue_id) {
|
||||||
|
return new WP_Error('no_hold_queue', 'No hold queue configured', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
$hold_queue = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT queue_name, wait_music_url FROM $queues_table WHERE id = %d",
|
||||||
|
$extension->hold_queue_id
|
||||||
|
));
|
||||||
|
|
||||||
|
// Put call on hold
|
||||||
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
$twiml->say('Please hold while we transfer your call.');
|
||||||
|
$enqueue = $twiml->enqueue($hold_queue->queue_name, array(
|
||||||
|
'waitUrl' => $hold_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait')
|
||||||
|
));
|
||||||
|
|
||||||
|
$twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML()));
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Call placed on hold'
|
||||||
|
), 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return new WP_Error('hold_error', $e->getMessage(), array('status' => 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unhold a call (resume from hold queue)
|
||||||
|
*/
|
||||||
|
public function unhold_call($request) {
|
||||||
|
// Implementation would retrieve from hold queue and reconnect
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Unhold functionality - to be implemented with queue retrieval'
|
||||||
|
), 501);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfer a call to another extension/queue
|
||||||
|
*/
|
||||||
|
public function transfer_call($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$call_sid = $request['call_sid'];
|
||||||
|
$target = $request->get_param('target'); // Extension number or queue ID
|
||||||
|
|
||||||
|
if (empty($target)) {
|
||||||
|
return new WP_Error('missing_target', 'Transfer target is required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php';
|
||||||
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
|
||||||
|
|
||||||
|
$admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Find customer call leg
|
||||||
|
$customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio);
|
||||||
|
|
||||||
|
if (!$customer_call_sid) {
|
||||||
|
return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up target (extension or queue)
|
||||||
|
global $wpdb;
|
||||||
|
$ext_table = $wpdb->prefix . 'twp_user_extensions';
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
|
||||||
|
// Try as extension first
|
||||||
|
$target_queue = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT q.* FROM $queues_table q
|
||||||
|
JOIN $ext_table e ON q.id = e.personal_queue_id
|
||||||
|
WHERE e.extension = %s",
|
||||||
|
$target
|
||||||
|
));
|
||||||
|
|
||||||
|
// If not extension, try as queue ID
|
||||||
|
if (!$target_queue && is_numeric($target)) {
|
||||||
|
$target_queue = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $queues_table WHERE id = %d",
|
||||||
|
$target
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$target_queue) {
|
||||||
|
return new WP_Error('invalid_target', 'Transfer target not found', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer to queue
|
||||||
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
$twiml->say('Transferring your call.');
|
||||||
|
$twiml->enqueue($target_queue->queue_name, array(
|
||||||
|
'waitUrl' => $target_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait')
|
||||||
|
));
|
||||||
|
|
||||||
|
$twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML()));
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Call transferred successfully'
|
||||||
|
), 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return new WP_Error('transfer_error', $e->getMessage(), array('status' => 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register FCM token for push notifications
|
||||||
|
*/
|
||||||
|
public function register_fcm_token($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$fcm_token = $request->get_param('fcm_token');
|
||||||
|
$refresh_token = $request->get_param('refresh_token');
|
||||||
|
|
||||||
|
if (empty($fcm_token)) {
|
||||||
|
return new WP_Error('missing_token', 'FCM token is required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->auth->update_fcm_token($user_id, $refresh_token, $fcm_token);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'FCM token registered successfully'
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent phone number
|
||||||
|
*/
|
||||||
|
public function get_agent_phone($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'phone_number' => $agent_number ?: null
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update agent phone number
|
||||||
|
*/
|
||||||
|
public function update_agent_phone($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
$phone_number = $request->get_param('phone_number');
|
||||||
|
|
||||||
|
if (empty($phone_number)) {
|
||||||
|
return new WP_Error('missing_phone', 'Phone number is required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate E.164 format
|
||||||
|
if (!preg_match('/^\+[1-9]\d{1,14}$/', $phone_number)) {
|
||||||
|
return new WP_Error('invalid_phone', 'Phone number must be in E.164 format (+1XXXXXXXXXX)', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
update_user_meta($user_id, 'twp_agent_phone', $phone_number);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Phone number updated successfully'
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has access to a queue
|
||||||
|
*/
|
||||||
|
private function user_has_queue_access($user_id, $queue_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
|
||||||
|
|
||||||
|
// Check if it's user's personal queue
|
||||||
|
$is_personal = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM $queues_table WHERE id = %d AND user_id = %d",
|
||||||
|
$queue_id, $user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($is_personal) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is assigned to this queue
|
||||||
|
$is_assigned = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM $assignments_table WHERE queue_id = %d AND user_id = %d",
|
||||||
|
$queue_id, $user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
return (bool)$is_assigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate wait time in seconds
|
||||||
|
*/
|
||||||
|
private function calculate_wait_time($start_time) {
|
||||||
|
if (!$start_time) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = strtotime($start_time);
|
||||||
|
$now = current_time('timestamp');
|
||||||
|
|
||||||
|
return max(0, $now - $start);
|
||||||
|
}
|
||||||
|
}
|
||||||
457
includes/class-twp-mobile-auth.php
Normal file
457
includes/class-twp-mobile-auth.php
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Mobile App JWT Authentication Handler
|
||||||
|
*
|
||||||
|
* Handles JWT token generation, validation, and refresh for Android/iOS apps
|
||||||
|
*/
|
||||||
|
class TWP_Mobile_Auth {
|
||||||
|
|
||||||
|
private $secret_key;
|
||||||
|
private $token_expiry = 86400; // 24 hours in seconds
|
||||||
|
private $refresh_expiry = 2592000; // 30 days in seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->secret_key = $this->get_secret_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or generate JWT secret key
|
||||||
|
*/
|
||||||
|
private function get_secret_key() {
|
||||||
|
$key = get_option('twp_mobile_jwt_secret');
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
// Generate a secure random key
|
||||||
|
$key = bin2hex(random_bytes(32));
|
||||||
|
update_option('twp_mobile_jwt_secret', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API endpoints
|
||||||
|
*/
|
||||||
|
public function register_endpoints() {
|
||||||
|
add_action('rest_api_init', function() {
|
||||||
|
// Login endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/login', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_login'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Refresh token endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/refresh', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_refresh'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Logout endpoint
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/logout', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_logout'),
|
||||||
|
'permission_callback' => array($this, 'verify_token')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Validate token endpoint (for debugging)
|
||||||
|
register_rest_route('twilio-mobile/v1', '/auth/validate', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'handle_validate'),
|
||||||
|
'permission_callback' => array($this, 'verify_token')
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle login request
|
||||||
|
*/
|
||||||
|
public function handle_login($request) {
|
||||||
|
$username = $request->get_param('username');
|
||||||
|
$password = $request->get_param('password');
|
||||||
|
$fcm_token = $request->get_param('fcm_token'); // Optional
|
||||||
|
$device_info = $request->get_param('device_info'); // Optional
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
return new WP_Error('missing_credentials', 'Username and password are required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate user
|
||||||
|
$user = wp_authenticate($username, $password);
|
||||||
|
|
||||||
|
if (is_wp_error($user)) {
|
||||||
|
return new WP_Error('invalid_credentials', 'Invalid username or password', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has phone agent capabilities
|
||||||
|
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
|
||||||
|
return new WP_Error('insufficient_permissions', 'User does not have phone agent access', array('status' => 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tokens
|
||||||
|
$access_token = $this->generate_token($user->ID, 'access');
|
||||||
|
$refresh_token = $this->generate_token($user->ID, 'refresh');
|
||||||
|
|
||||||
|
// Store session in database
|
||||||
|
$this->store_session($user->ID, $refresh_token, $fcm_token, $device_info);
|
||||||
|
|
||||||
|
// Get user data
|
||||||
|
$user_data = $this->get_user_data($user->ID);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
'refresh_token' => $refresh_token,
|
||||||
|
'expires_in' => $this->token_expiry,
|
||||||
|
'user' => $user_data
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle token refresh request
|
||||||
|
*/
|
||||||
|
public function handle_refresh($request) {
|
||||||
|
$refresh_token = $request->get_param('refresh_token');
|
||||||
|
|
||||||
|
if (empty($refresh_token)) {
|
||||||
|
return new WP_Error('missing_token', 'Refresh token is required', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify refresh token
|
||||||
|
$payload = $this->decode_token($refresh_token);
|
||||||
|
|
||||||
|
if (!$payload || $payload->type !== 'refresh') {
|
||||||
|
return new WP_Error('invalid_token', 'Invalid refresh token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session exists and is valid
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$session = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table WHERE user_id = %d AND refresh_token = %s AND is_active = 1 AND expires_at > NOW()",
|
||||||
|
$payload->user_id,
|
||||||
|
$refresh_token
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$session) {
|
||||||
|
return new WP_Error('invalid_session', 'Session expired or invalid', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
$access_token = $this->generate_token($payload->user_id, 'access');
|
||||||
|
|
||||||
|
// Update last_used timestamp
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('last_used' => current_time('mysql')),
|
||||||
|
array('id' => $session->id),
|
||||||
|
array('%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
'expires_in' => $this->token_expiry
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle logout request
|
||||||
|
*/
|
||||||
|
public function handle_logout($request) {
|
||||||
|
$user_id = $this->get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get refresh token from request
|
||||||
|
$refresh_token = $request->get_param('refresh_token');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
if ($refresh_token) {
|
||||||
|
// Invalidate specific session
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('is_active' => 0),
|
||||||
|
array('user_id' => $user_id, 'refresh_token' => $refresh_token),
|
||||||
|
array('%d'),
|
||||||
|
array('%d', '%s')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Invalidate all sessions for this user
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('is_active' => 0),
|
||||||
|
array('user_id' => $user_id),
|
||||||
|
array('%d'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Logged out successfully'
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle token validation request
|
||||||
|
*/
|
||||||
|
public function handle_validate($request) {
|
||||||
|
$user_id = $this->get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_data = $this->get_user_data($user_id);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'valid' => true,
|
||||||
|
'user' => $user_data
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JWT token
|
||||||
|
*/
|
||||||
|
private function generate_token($user_id, $type = 'access') {
|
||||||
|
$issued_at = time();
|
||||||
|
$expiry = $type === 'refresh' ? $this->refresh_expiry : $this->token_expiry;
|
||||||
|
|
||||||
|
$payload = array(
|
||||||
|
'iat' => $issued_at,
|
||||||
|
'exp' => $issued_at + $expiry,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->encode_token($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple JWT encoding (header.payload.signature)
|
||||||
|
*/
|
||||||
|
private function encode_token($payload) {
|
||||||
|
$header = array('typ' => 'JWT', 'alg' => 'HS256');
|
||||||
|
|
||||||
|
$segments = array();
|
||||||
|
$segments[] = $this->base64url_encode(json_encode($header));
|
||||||
|
$segments[] = $this->base64url_encode(json_encode($payload));
|
||||||
|
|
||||||
|
$signing_input = implode('.', $segments);
|
||||||
|
$signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
|
||||||
|
$segments[] = $this->base64url_encode($signature);
|
||||||
|
|
||||||
|
return implode('.', $segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple JWT decoding
|
||||||
|
*/
|
||||||
|
private function decode_token($token) {
|
||||||
|
$segments = explode('.', $token);
|
||||||
|
|
||||||
|
if (count($segments) !== 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($header_b64, $payload_b64, $signature_b64) = $segments;
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
$signing_input = $header_b64 . '.' . $payload_b64;
|
||||||
|
$signature = $this->base64url_decode($signature_b64);
|
||||||
|
$expected_signature = hash_hmac('sha256', $signing_input, $this->secret_key, true);
|
||||||
|
|
||||||
|
if (!hash_equals($signature, $expected_signature)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode payload
|
||||||
|
$payload = json_decode($this->base64url_decode($payload_b64));
|
||||||
|
|
||||||
|
if (!$payload) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiration
|
||||||
|
if (isset($payload->exp) && $payload->exp < time()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 URL encode
|
||||||
|
*/
|
||||||
|
private function base64url_encode($data) {
|
||||||
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 URL decode
|
||||||
|
*/
|
||||||
|
private function base64url_decode($data) {
|
||||||
|
return base64_decode(strtr($data, '-_', '+/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify token (permission callback)
|
||||||
|
*/
|
||||||
|
public function verify_token($request) {
|
||||||
|
$auth_header = $request->get_header('Authorization');
|
||||||
|
|
||||||
|
if (empty($auth_header)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from "Bearer <token>"
|
||||||
|
if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
|
||||||
|
$token = $matches[1];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->decode_token($token);
|
||||||
|
|
||||||
|
if (!$payload || $payload->type !== 'access') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user ID for later use
|
||||||
|
$request->set_param('_twp_user_id', $payload->user_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user ID from token
|
||||||
|
*/
|
||||||
|
public function get_current_user_id() {
|
||||||
|
$request = rest_get_server()->get_request();
|
||||||
|
return $request->get_param('_twp_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store session in database
|
||||||
|
*/
|
||||||
|
private function store_session($user_id, $refresh_token, $fcm_token = null, $device_info = null) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
$table,
|
||||||
|
array(
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'refresh_token' => $refresh_token,
|
||||||
|
'fcm_token' => $fcm_token,
|
||||||
|
'device_info' => $device_info,
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'expires_at' => date('Y-m-d H:i:s', time() + $this->refresh_expiry),
|
||||||
|
'last_used' => current_time('mysql'),
|
||||||
|
'is_active' => 1
|
||||||
|
),
|
||||||
|
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user data for response
|
||||||
|
*/
|
||||||
|
private function get_user_data($user_id) {
|
||||||
|
$user = get_userdata($user_id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get agent phone number
|
||||||
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
||||||
|
|
||||||
|
// Get agent status
|
||||||
|
global $wpdb;
|
||||||
|
$status_table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
$status = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, is_logged_in, current_call_sid FROM $status_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
// Get user extension
|
||||||
|
$ext_table = $wpdb->prefix . 'twp_user_extensions';
|
||||||
|
$extension = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT extension, direct_dial_number FROM $ext_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $user->ID,
|
||||||
|
'username' => $user->user_login,
|
||||||
|
'display_name' => $user->display_name,
|
||||||
|
'email' => $user->user_email,
|
||||||
|
'phone_number' => $agent_number,
|
||||||
|
'extension' => $extension ? $extension->extension : null,
|
||||||
|
'direct_dial' => $extension ? $extension->direct_dial_number : null,
|
||||||
|
'status' => $status ? $status->status : 'offline',
|
||||||
|
'is_logged_in' => $status ? (bool)$status->is_logged_in : false,
|
||||||
|
'current_call_sid' => $status ? $status->current_call_sid : null,
|
||||||
|
'capabilities' => array(
|
||||||
|
'can_access_browser_phone' => user_can($user_id, 'twp_access_browser_phone'),
|
||||||
|
'can_access_voicemails' => user_can($user_id, 'twp_access_voicemails'),
|
||||||
|
'can_access_call_log' => user_can($user_id, 'twp_access_call_log'),
|
||||||
|
'can_access_agent_queue' => user_can($user_id, 'twp_access_agent_queue'),
|
||||||
|
'can_access_sms_inbox' => user_can($user_id, 'twp_access_sms_inbox'),
|
||||||
|
'is_admin' => user_can($user_id, 'manage_options')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update FCM token for existing session
|
||||||
|
*/
|
||||||
|
public function update_fcm_token($user_id, $refresh_token, $fcm_token) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$table,
|
||||||
|
array('fcm_token' => $fcm_token),
|
||||||
|
array('user_id' => $user_id, 'refresh_token' => $refresh_token, 'is_active' => 1),
|
||||||
|
array('%s'),
|
||||||
|
array('%d', '%s', '%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active FCM tokens for a user
|
||||||
|
*/
|
||||||
|
public function get_user_fcm_tokens($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
return $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT fcm_token FROM $table WHERE user_id = %d AND is_active = 1 AND fcm_token IS NOT NULL AND expires_at > NOW()",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired sessions
|
||||||
|
*/
|
||||||
|
public static function cleanup_expired_sessions() {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_mobile_sessions';
|
||||||
|
|
||||||
|
$wpdb->query("UPDATE $table SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
308
includes/class-twp-mobile-sse.php
Normal file
308
includes/class-twp-mobile-sse.php
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Server-Sent Events (SSE) Handler for Mobile App
|
||||||
|
*
|
||||||
|
* Provides real-time updates for queue state, incoming calls, and agent status
|
||||||
|
*/
|
||||||
|
class TWP_Mobile_SSE {
|
||||||
|
|
||||||
|
private $auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'class-twp-mobile-auth.php';
|
||||||
|
$this->auth = new TWP_Mobile_Auth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register SSE endpoint
|
||||||
|
*/
|
||||||
|
public function register_endpoints() {
|
||||||
|
add_action('rest_api_init', function() {
|
||||||
|
register_rest_route('twilio-mobile/v1', '/stream/events', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'stream_events'),
|
||||||
|
'permission_callback' => array($this->auth, 'verify_token')
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream events to mobile app
|
||||||
|
*/
|
||||||
|
public function stream_events($request) {
|
||||||
|
$user_id = $this->auth->get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
return new WP_Error('unauthorized', 'Invalid token', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers for SSE
|
||||||
|
header('Content-Type: text/event-stream');
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
header('Connection: keep-alive');
|
||||||
|
header('X-Accel-Buffering: no'); // Disable nginx buffering
|
||||||
|
|
||||||
|
// Disable PHP output buffering
|
||||||
|
if (function_exists('apache_setenv')) {
|
||||||
|
@apache_setenv('no-gzip', '1');
|
||||||
|
}
|
||||||
|
@ini_set('zlib.output_compression', 0);
|
||||||
|
@ini_set('implicit_flush', 1);
|
||||||
|
ob_implicit_flush(1);
|
||||||
|
while (ob_get_level() > 0) {
|
||||||
|
ob_end_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial connection event
|
||||||
|
$this->send_event('connected', array('user_id' => $user_id, 'timestamp' => time()));
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
$last_check = time();
|
||||||
|
$previous_state = $this->get_current_state($user_id);
|
||||||
|
|
||||||
|
// Stream loop - check for changes every 2 seconds
|
||||||
|
$max_duration = 300; // 5 minutes max connection time
|
||||||
|
$start_time = time();
|
||||||
|
|
||||||
|
while (time() - $start_time < $max_duration) {
|
||||||
|
// Check if connection is still alive
|
||||||
|
if (connection_aborted()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
$current_state = $this->get_current_state($user_id);
|
||||||
|
|
||||||
|
// Compare and send updates
|
||||||
|
$this->check_and_send_updates($previous_state, $current_state);
|
||||||
|
|
||||||
|
// Update previous state
|
||||||
|
$previous_state = $current_state;
|
||||||
|
|
||||||
|
// Send heartbeat every 15 seconds
|
||||||
|
if (time() - $last_check >= 15) {
|
||||||
|
$this->send_event('heartbeat', array('timestamp' => time()));
|
||||||
|
$last_check = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep for 2 seconds
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection closing
|
||||||
|
$this->send_event('disconnect', array('reason' => 'timeout', 'timestamp' => time()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current state for agent
|
||||||
|
*/
|
||||||
|
private function get_current_state($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$state = array(
|
||||||
|
'agent_status' => $this->get_agent_status($user_id),
|
||||||
|
'queues' => $this->get_queues_state($user_id),
|
||||||
|
'current_call' => $this->get_current_call($user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent status
|
||||||
|
*/
|
||||||
|
private function get_agent_status($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'twp_agent_status';
|
||||||
|
|
||||||
|
$status = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, is_logged_in, current_call_sid FROM $table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$status) {
|
||||||
|
return array('status' => 'offline', 'is_logged_in' => false, 'current_call_sid' => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'status' => $status->status,
|
||||||
|
'is_logged_in' => (bool)$status->is_logged_in,
|
||||||
|
'current_call_sid' => $status->current_call_sid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queues state
|
||||||
|
*/
|
||||||
|
private function get_queues_state($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
$assignments_table = $wpdb->prefix . 'twp_queue_assignments';
|
||||||
|
|
||||||
|
// Get queue IDs
|
||||||
|
$queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT queue_id FROM $assignments_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$personal_queue_ids = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT id FROM $queues_table WHERE user_id = %d",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids));
|
||||||
|
|
||||||
|
if (empty($all_queue_ids)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_ids_str = implode(',', array_map('intval', $all_queue_ids));
|
||||||
|
|
||||||
|
$queues = $wpdb->get_results("
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
q.queue_name,
|
||||||
|
COUNT(c.id) as waiting_count,
|
||||||
|
MIN(c.enqueued_at) as oldest_call_time
|
||||||
|
FROM $queues_table q
|
||||||
|
LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting'
|
||||||
|
WHERE q.id IN ($queue_ids_str)
|
||||||
|
GROUP BY q.id
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($queues as $queue) {
|
||||||
|
$result[$queue->id] = array(
|
||||||
|
'id' => (int)$queue->id,
|
||||||
|
'name' => $queue->queue_name,
|
||||||
|
'waiting_count' => (int)$queue->waiting_count,
|
||||||
|
'oldest_call_time' => $queue->oldest_call_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current call for agent
|
||||||
|
*/
|
||||||
|
private function get_current_call($user_id) {
|
||||||
|
global $wpdb;
|
||||||
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
||||||
|
|
||||||
|
$agent_number = get_user_meta($user_id, 'twp_agent_phone', true);
|
||||||
|
|
||||||
|
if (!$agent_number) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$call = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT call_sid, from_number, queue_id, status, joined_at
|
||||||
|
FROM $calls_table
|
||||||
|
WHERE agent_phone = %s AND status IN ('connecting', 'in_progress')
|
||||||
|
ORDER BY joined_at DESC
|
||||||
|
LIMIT 1",
|
||||||
|
$agent_number
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$call) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'call_sid' => $call->call_sid,
|
||||||
|
'from_number' => $call->from_number,
|
||||||
|
'queue_id' => (int)$call->queue_id,
|
||||||
|
'status' => $call->status,
|
||||||
|
'duration' => time() - strtotime($call->joined_at)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check state changes and send updates
|
||||||
|
*/
|
||||||
|
private function check_and_send_updates($previous, $current) {
|
||||||
|
// Check agent status changes
|
||||||
|
if ($previous['agent_status'] !== $current['agent_status']) {
|
||||||
|
$this->send_event('agent_status_changed', $current['agent_status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check queue changes
|
||||||
|
$this->check_queue_changes($previous['queues'], $current['queues']);
|
||||||
|
|
||||||
|
// Check current call changes
|
||||||
|
if ($previous['current_call'] !== $current['current_call']) {
|
||||||
|
if ($current['current_call'] && !$previous['current_call']) {
|
||||||
|
// New call started
|
||||||
|
$this->send_event('call_started', $current['current_call']);
|
||||||
|
} elseif (!$current['current_call'] && $previous['current_call']) {
|
||||||
|
// Call ended
|
||||||
|
$this->send_event('call_ended', $previous['current_call']);
|
||||||
|
} elseif ($current['current_call'] && $previous['current_call']) {
|
||||||
|
// Call status changed
|
||||||
|
if ($current['current_call']['status'] !== $previous['current_call']['status']) {
|
||||||
|
$this->send_event('call_status_changed', $current['current_call']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for queue changes
|
||||||
|
*/
|
||||||
|
private function check_queue_changes($previous_queues, $current_queues) {
|
||||||
|
foreach ($current_queues as $queue_id => $current_queue) {
|
||||||
|
$previous_queue = $previous_queues[$queue_id] ?? null;
|
||||||
|
|
||||||
|
if (!$previous_queue) {
|
||||||
|
// New queue added
|
||||||
|
$this->send_event('queue_added', $current_queue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for waiting count changes
|
||||||
|
if ($current_queue['waiting_count'] !== $previous_queue['waiting_count']) {
|
||||||
|
if ($current_queue['waiting_count'] > $previous_queue['waiting_count']) {
|
||||||
|
// New call in queue
|
||||||
|
$this->send_event('call_enqueued', array(
|
||||||
|
'queue_id' => $queue_id,
|
||||||
|
'queue_name' => $current_queue['name'],
|
||||||
|
'waiting_count' => $current_queue['waiting_count']
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Call removed from queue
|
||||||
|
$this->send_event('call_dequeued', array(
|
||||||
|
'queue_id' => $queue_id,
|
||||||
|
'queue_name' => $current_queue['name'],
|
||||||
|
'waiting_count' => $current_queue['waiting_count']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed queues
|
||||||
|
foreach ($previous_queues as $queue_id => $previous_queue) {
|
||||||
|
if (!isset($current_queues[$queue_id])) {
|
||||||
|
$this->send_event('queue_removed', array('queue_id' => $queue_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send SSE event
|
||||||
|
*/
|
||||||
|
private function send_event($event_type, $data) {
|
||||||
|
echo "event: $event_type\n";
|
||||||
|
echo "data: " . json_encode($data) . "\n\n";
|
||||||
|
|
||||||
|
if (ob_get_level() > 0) {
|
||||||
|
ob_flush();
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
150
includes/class-twp-sms-manager.php
Normal file
150
includes/class-twp-sms-manager.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
206
includes/class-twp-sms-provider-sns.php
Normal file
206
includes/class-twp-sms-provider-sns.php
Normal 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()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
includes/class-twp-sms-provider-twilio.php
Normal file
137
includes/class-twp-sms-provider-twilio.php
Normal 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'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
226
includes/class-twp-voicemail-handler.php
Normal file
226
includes/class-twp-voicemail-handler.php
Normal 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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -197,6 +197,48 @@ class TWP_Webhooks {
|
|||||||
'permission_callback' => '__return_true'
|
'permission_callback' => '__return_true'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Agent features webhook (handles DTMF during bridged calls)
|
||||||
|
register_rest_route('twilio-webhook/v1', '/agent-features', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_agent_features'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Forward result webhook (handles dial result for forwards)
|
||||||
|
register_rest_route('twilio-webhook/v1', '/forward-result', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_forward_result'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Agent action webhook (processes DTMF commands from agent)
|
||||||
|
register_rest_route('twilio-webhook/v1', '/agent-action', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_agent_action'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Conference status webhook
|
||||||
|
register_rest_route('twilio-webhook/v1', '/conference-status', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_conference_status'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Agent conference join webhook
|
||||||
|
register_rest_route('twilio-webhook/v1', '/agent-conference-join', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_agent_conference_join'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Agent call status webhook (for conference calls)
|
||||||
|
register_rest_route('twilio-webhook/v1', '/agent-call-status-new', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_agent_call_status_new'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
// Request callback webhook
|
// Request callback webhook
|
||||||
register_rest_route('twilio-webhook/v1', '/request-callback', array(
|
register_rest_route('twilio-webhook/v1', '/request-callback', array(
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
@@ -2618,4 +2660,347 @@ class TWP_Webhooks {
|
|||||||
|
|
||||||
return new WP_REST_Response($response->asXML(), 200, array('Content-Type' => 'text/xml'));
|
return new WP_REST_Response($response->asXML(), 200, array('Content-Type' => 'text/xml'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle agent features during bridged calls
|
||||||
|
*/
|
||||||
|
public function handle_agent_features($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
error_log('TWP Agent Features: Webhook triggered with params: ' . print_r($params, true));
|
||||||
|
|
||||||
|
$response = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
|
||||||
|
// This webhook is called when the agent answers
|
||||||
|
// We set up a Gather to listen for DTMF during the call
|
||||||
|
$gather = $response->gather([
|
||||||
|
'input' => 'dtmf',
|
||||||
|
'numDigits' => 2,
|
||||||
|
'actionOnEmptyResult' => false,
|
||||||
|
'action' => home_url('/wp-json/twilio-webhook/v1/agent-action'),
|
||||||
|
'method' => 'POST',
|
||||||
|
'timeout' => 1,
|
||||||
|
'finishOnKey' => '' // Don't finish on any key, we want to capture patterns like *9
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Connect the call (empty gather continues the call)
|
||||||
|
|
||||||
|
// After gather timeout, continue listening
|
||||||
|
$response->redirect(home_url('/wp-json/twilio-webhook/v1/agent-features'), ['method' => 'POST']);
|
||||||
|
|
||||||
|
error_log('TWP Agent Features: TwiML response: ' . $response->asXML());
|
||||||
|
|
||||||
|
return $this->send_twiml_response($response->asXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle forward result (called after dial completes)
|
||||||
|
*/
|
||||||
|
public function handle_forward_result($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
error_log('TWP Forward Result: Call completed with params: ' . print_r($params, true));
|
||||||
|
|
||||||
|
$dial_call_status = isset($params['DialCallStatus']) ? $params['DialCallStatus'] : '';
|
||||||
|
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
||||||
|
|
||||||
|
// Update call log with result
|
||||||
|
if ($call_sid) {
|
||||||
|
TWP_Call_Logger::log_action($call_sid, 'Forward result: ' . $dial_call_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
|
||||||
|
// Handle different outcomes
|
||||||
|
switch ($dial_call_status) {
|
||||||
|
case 'busy':
|
||||||
|
$response->say('The number is busy. Please try again later.', ['voice' => 'alice']);
|
||||||
|
break;
|
||||||
|
case 'no-answer':
|
||||||
|
$response->say('There was no answer. Please try again later.', ['voice' => 'alice']);
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
$response->say('The call could not be completed. Please try again later.', ['voice' => 'alice']);
|
||||||
|
break;
|
||||||
|
case 'canceled':
|
||||||
|
$response->say('The call was canceled.', ['voice' => 'alice']);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Call completed successfully or caller hung up
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->hangup();
|
||||||
|
|
||||||
|
return $this->send_twiml_response($response->asXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle agent DTMF actions (*9 hold, *0 record, *5 transfer, etc.)
|
||||||
|
*/
|
||||||
|
public function handle_agent_action($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
$digits = isset($params['Digits']) ? $params['Digits'] : '';
|
||||||
|
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
||||||
|
$parent_call_sid = isset($params['ParentCallSid']) ? $params['ParentCallSid'] : '';
|
||||||
|
|
||||||
|
error_log('TWP Agent Action: Received DTMF: ' . $digits . ' for call: ' . $call_sid);
|
||||||
|
|
||||||
|
$response = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
$admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
|
||||||
|
|
||||||
|
// Process DTMF commands
|
||||||
|
switch ($digits) {
|
||||||
|
case '*9':
|
||||||
|
// Hold/Unhold
|
||||||
|
error_log('TWP Agent Action: Processing hold/unhold request');
|
||||||
|
|
||||||
|
// Find the customer call leg
|
||||||
|
$customer_call_sid = $admin->find_customer_call_leg($parent_call_sid, $twilio->get_client());
|
||||||
|
|
||||||
|
if ($customer_call_sid) {
|
||||||
|
// Check if call is on hold by looking at the current state
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_active_calls';
|
||||||
|
$call_info = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table_name WHERE call_sid = %s",
|
||||||
|
$customer_call_sid
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($call_info && $call_info->status === 'on-hold') {
|
||||||
|
// Resume the call
|
||||||
|
$twiml = '<Response><Say voice="alice">Resuming call</Say><Pause length="1"/></Response>';
|
||||||
|
$twilio->update_call($customer_call_sid, ['twiml' => $twiml]);
|
||||||
|
|
||||||
|
$wpdb->update($table_name,
|
||||||
|
['status' => 'in-progress'],
|
||||||
|
['call_sid' => $customer_call_sid]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->say('Call resumed', ['voice' => 'alice']);
|
||||||
|
error_log('TWP Agent Action: Call resumed');
|
||||||
|
} else {
|
||||||
|
// Put on hold
|
||||||
|
$hold_music = get_option('twp_hold_music_url', 'http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
|
||||||
|
$twiml = '<Response><Say voice="alice">You have been placed on hold</Say><Play loop="0">' . $hold_music . '</Play></Response>';
|
||||||
|
$twilio->update_call($customer_call_sid, ['twiml' => $twiml]);
|
||||||
|
|
||||||
|
$wpdb->update($table_name,
|
||||||
|
['status' => 'on-hold'],
|
||||||
|
['call_sid' => $customer_call_sid]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->say('Call on hold', ['voice' => 'alice']);
|
||||||
|
error_log('TWP Agent Action: Call placed on hold');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log('TWP Agent Action: Could not find customer call leg for hold');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '*0':
|
||||||
|
// Start/Stop Recording
|
||||||
|
error_log('TWP Agent Action: Processing recording toggle');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if we're already recording
|
||||||
|
$recordings = $twilio->get_client()->recordings->read(['callSid' => $parent_call_sid, 'status' => 'in-progress'], 1);
|
||||||
|
|
||||||
|
if (!empty($recordings)) {
|
||||||
|
// Stop recording
|
||||||
|
$recording = $recordings[0];
|
||||||
|
$twilio->get_client()->recordings($recording->sid)->update(['status' => 'stopped']);
|
||||||
|
$response->say('Recording stopped', ['voice' => 'alice']);
|
||||||
|
error_log('TWP Agent Action: Recording stopped');
|
||||||
|
} else {
|
||||||
|
// Start recording
|
||||||
|
$twilio->get_client()->calls($parent_call_sid)->recordings->create([
|
||||||
|
'recordingStatusCallback' => home_url('/wp-json/twilio-webhook/v1/recording-status'),
|
||||||
|
'recordingStatusCallbackEvent' => ['completed']
|
||||||
|
]);
|
||||||
|
$response->say('Recording started', ['voice' => 'alice']);
|
||||||
|
error_log('TWP Agent Action: Recording started');
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('TWP Agent Action: Recording error: ' . $e->getMessage());
|
||||||
|
$response->say('Recording feature unavailable', ['voice' => 'alice']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '*5':
|
||||||
|
// Transfer to extension
|
||||||
|
error_log('TWP Agent Action: Initiating transfer');
|
||||||
|
$response->say('Enter extension number followed by pound', ['voice' => 'alice']);
|
||||||
|
|
||||||
|
$gather = $response->gather([
|
||||||
|
'input' => 'dtmf',
|
||||||
|
'finishOnKey' => '#',
|
||||||
|
'action' => home_url('/wp-json/twilio-webhook/v1/transfer-extension'),
|
||||||
|
'method' => 'POST'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Continue call if no input
|
||||||
|
$response->redirect(home_url('/wp-json/twilio-webhook/v1/agent-features'), ['method' => 'POST']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '*1':
|
||||||
|
// Mute/Unmute (agent side)
|
||||||
|
error_log('TWP Agent Action: Processing mute toggle');
|
||||||
|
// This would require conference mode - we'll note this for future implementation
|
||||||
|
$response->say('Mute feature requires conference mode', ['voice' => 'alice']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error_log('TWP Agent Action: Unknown DTMF code: ' . $digits);
|
||||||
|
// Unknown code, just continue
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue listening for more DTMF
|
||||||
|
$response->redirect(home_url('/wp-json/twilio-webhook/v1/agent-features'), ['method' => 'POST']);
|
||||||
|
|
||||||
|
return $this->send_twiml_response($response->asXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle conference status events
|
||||||
|
*/
|
||||||
|
public function handle_conference_status($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
error_log('TWP Conference Status: ' . print_r($params, true));
|
||||||
|
|
||||||
|
$status_callback_event = isset($params['StatusCallbackEvent']) ? $params['StatusCallbackEvent'] : '';
|
||||||
|
$conference_sid = isset($params['ConferenceSid']) ? $params['ConferenceSid'] : '';
|
||||||
|
$friendly_name = isset($params['FriendlyName']) ? $params['FriendlyName'] : '';
|
||||||
|
|
||||||
|
// Log conference events for debugging
|
||||||
|
switch ($status_callback_event) {
|
||||||
|
case 'conference-start':
|
||||||
|
error_log('TWP Conference: Conference started: ' . $friendly_name);
|
||||||
|
break;
|
||||||
|
case 'participant-join':
|
||||||
|
error_log('TWP Conference: Participant joined: ' . $friendly_name);
|
||||||
|
break;
|
||||||
|
case 'participant-leave':
|
||||||
|
error_log('TWP Conference: Participant left: ' . $friendly_name);
|
||||||
|
break;
|
||||||
|
case 'conference-end':
|
||||||
|
error_log('TWP Conference: Conference ended: ' . $friendly_name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response('OK', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle agent joining conference with features
|
||||||
|
*/
|
||||||
|
public function handle_agent_conference_join($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
error_log('TWP Agent Conference Join: ' . print_r($params, true));
|
||||||
|
|
||||||
|
$conference_name = isset($_GET['conference_name']) ? $_GET['conference_name'] : '';
|
||||||
|
$caller_number = isset($_GET['caller_number']) ? $_GET['caller_number'] : '';
|
||||||
|
|
||||||
|
$response = new \Twilio\TwiML\VoiceResponse();
|
||||||
|
|
||||||
|
if (empty($conference_name)) {
|
||||||
|
$response->say('Conference not found', ['voice' => 'alice']);
|
||||||
|
$response->hangup();
|
||||||
|
return $this->send_twiml_response($response->asXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce the incoming call to the agent
|
||||||
|
if (!empty($caller_number)) {
|
||||||
|
$response->say('Incoming call from ' . $this->format_phone_number_for_speech($caller_number), ['voice' => 'alice']);
|
||||||
|
} else {
|
||||||
|
$response->say('Incoming call', ['voice' => 'alice']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up agent conference with features
|
||||||
|
$dial = $response->dial();
|
||||||
|
$conference = $dial->conference($conference_name, [
|
||||||
|
'startConferenceOnEnter' => true, // Start when agent joins
|
||||||
|
'endConferenceOnExit' => false, // Don't end when agent leaves (let caller stay)
|
||||||
|
'muted' => false,
|
||||||
|
'beep' => false,
|
||||||
|
'waitUrl' => '',
|
||||||
|
// Enable DTMF detection for agent features
|
||||||
|
'eventCallbackUrl' => home_url('/wp-json/twilio-webhook/v1/conference-events'),
|
||||||
|
'record' => false // We'll control recording via DTMF
|
||||||
|
]);
|
||||||
|
|
||||||
|
error_log('TWP Agent Conference Join: Joining agent to conference: ' . $conference_name);
|
||||||
|
|
||||||
|
return $this->send_twiml_response($response->asXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle agent call status for conference calls
|
||||||
|
*/
|
||||||
|
public function handle_agent_call_status_new($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
error_log('TWP Agent Call Status: ' . print_r($params, true));
|
||||||
|
|
||||||
|
$call_status = isset($params['CallStatus']) ? $params['CallStatus'] : '';
|
||||||
|
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
||||||
|
|
||||||
|
switch ($call_status) {
|
||||||
|
case 'initiated':
|
||||||
|
error_log('TWP Agent Call: Call initiated to agent: ' . $call_sid);
|
||||||
|
break;
|
||||||
|
case 'ringing':
|
||||||
|
error_log('TWP Agent Call: Agent phone ringing: ' . $call_sid);
|
||||||
|
break;
|
||||||
|
case 'answered':
|
||||||
|
error_log('TWP Agent Call: Agent answered: ' . $call_sid);
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
error_log('TWP Agent Call: Agent call completed: ' . $call_sid);
|
||||||
|
break;
|
||||||
|
case 'busy':
|
||||||
|
case 'no-answer':
|
||||||
|
case 'failed':
|
||||||
|
error_log('TWP Agent Call: Agent call failed (' . $call_status . '): ' . $call_sid);
|
||||||
|
// TODO: Could implement fallback logic here (try next agent, voicemail, etc.)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response('OK', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format phone number for speech (reads each digit individually)
|
||||||
|
*/
|
||||||
|
private function format_phone_number_for_speech($number) {
|
||||||
|
// Remove +1 country code and any formatting
|
||||||
|
$cleaned = preg_replace('/^\+1/', '', $number);
|
||||||
|
$cleaned = preg_replace('/[^0-9]/', '', $cleaned);
|
||||||
|
|
||||||
|
if (strlen($cleaned) == 10) {
|
||||||
|
// Break into individual digits with pauses for natural speech
|
||||||
|
// Area code: 9-0-9, Main number: 5-7-3-7-3-7-2
|
||||||
|
$area_code = str_split(substr($cleaned, 0, 3));
|
||||||
|
$exchange = str_split(substr($cleaned, 3, 3));
|
||||||
|
$number_part = str_split(substr($cleaned, 6, 4));
|
||||||
|
|
||||||
|
// Join with spaces so TTS reads each digit individually
|
||||||
|
return implode(' ', $area_code) . ', ' . implode(' ', $exchange) . ', ' . implode(' ', $number_part);
|
||||||
|
} elseif (strlen($cleaned) == 11 && substr($cleaned, 0, 1) == '1') {
|
||||||
|
// Handle numbers that still have the 1 prefix
|
||||||
|
$area_code = str_split(substr($cleaned, 1, 3));
|
||||||
|
$exchange = str_split(substr($cleaned, 4, 3));
|
||||||
|
$number_part = str_split(substr($cleaned, 7, 4));
|
||||||
|
|
||||||
|
return implode(' ', $area_code) . ', ' . implode(' ', $exchange) . ', ' . implode(' ', $number_part);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: just break into individual digits
|
||||||
|
return implode(' ', str_split($cleaned));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,9 @@ class TWP_Workflow {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'forward':
|
case 'forward':
|
||||||
|
error_log('TWP Workflow: Processing forward step: ' . json_encode($step));
|
||||||
$step_twiml = self::create_forward_twiml($step);
|
$step_twiml = self::create_forward_twiml($step);
|
||||||
|
error_log('TWP Workflow: Forward step TwiML generated: ' . $step_twiml);
|
||||||
$stop_after_step = true; // Forward ends the workflow
|
$stop_after_step = true; // Forward ends the workflow
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -135,6 +137,9 @@ class TWP_Workflow {
|
|||||||
|
|
||||||
// Add step TwiML to combined response
|
// Add step TwiML to combined response
|
||||||
if ($step_twiml) {
|
if ($step_twiml) {
|
||||||
|
error_log('TWP Workflow: Appending step TwiML to combined response');
|
||||||
|
error_log('TWP Workflow: Step TwiML before append: ' . $step_twiml);
|
||||||
|
|
||||||
// Parse the step TwiML and append to combined response
|
// Parse the step TwiML and append to combined response
|
||||||
$step_xml = simplexml_load_string($step_twiml);
|
$step_xml = simplexml_load_string($step_twiml);
|
||||||
if ($step_xml) {
|
if ($step_xml) {
|
||||||
@@ -142,10 +147,15 @@ class TWP_Workflow {
|
|||||||
self::append_twiml_element($response, $element);
|
self::append_twiml_element($response, $element);
|
||||||
}
|
}
|
||||||
$has_response = true;
|
$has_response = true;
|
||||||
|
|
||||||
|
error_log('TWP Workflow: Combined response after append: ' . $response->asXML());
|
||||||
|
} else {
|
||||||
|
error_log('TWP Workflow: ERROR - Failed to parse step TwiML: ' . $step_twiml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop processing if this step type should end the workflow
|
// Stop processing if this step type should end the workflow
|
||||||
if ($stop_after_step) {
|
if ($stop_after_step) {
|
||||||
|
error_log('TWP Workflow: Stopping after this step (stop_after_step = true)');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,10 +163,13 @@ class TWP_Workflow {
|
|||||||
|
|
||||||
// Return combined response or default
|
// Return combined response or default
|
||||||
if ($has_response) {
|
if ($has_response) {
|
||||||
return $response->asXML();
|
$final_twiml = $response->asXML();
|
||||||
|
error_log('TWP Workflow: Final workflow TwiML response: ' . $final_twiml);
|
||||||
|
return $final_twiml;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default response
|
// Default response
|
||||||
|
error_log('TWP Workflow: No response generated, returning default response');
|
||||||
return self::create_default_response();
|
return self::create_default_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,11 +209,33 @@ class TWP_Workflow {
|
|||||||
$response->record($attributes);
|
$response->record($attributes);
|
||||||
break;
|
break;
|
||||||
case 'Dial':
|
case 'Dial':
|
||||||
$response->dial((string) $element, $attributes);
|
// Create dial instance
|
||||||
|
$dial = $response->dial('', $attributes);
|
||||||
|
// Add child Number elements
|
||||||
|
foreach ($element->children() as $child) {
|
||||||
|
$child_name = $child->getName();
|
||||||
|
$child_attrs = self::get_attributes($child);
|
||||||
|
|
||||||
|
if ($child_name === 'Number') {
|
||||||
|
// Number can have url, method attributes for agent features
|
||||||
|
$dial->number((string) $child, $child_attrs);
|
||||||
|
} elseif ($child_name === 'Client') {
|
||||||
|
$dial->client((string) $child, $child_attrs);
|
||||||
|
} elseif ($child_name === 'Queue') {
|
||||||
|
$dial->queue((string) $child, $child_attrs);
|
||||||
|
} elseif ($child_name === 'Conference') {
|
||||||
|
$dial->conference((string) $child, $child_attrs);
|
||||||
|
} elseif ($child_name === 'Sip') {
|
||||||
|
$dial->sip((string) $child, $child_attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Queue':
|
case 'Queue':
|
||||||
$response->queue((string) $element, $attributes);
|
$response->queue((string) $element, $attributes);
|
||||||
break;
|
break;
|
||||||
|
case 'Conference':
|
||||||
|
$response->dial()->conference((string) $element, $attributes);
|
||||||
|
break;
|
||||||
case 'Redirect':
|
case 'Redirect':
|
||||||
$response->redirect((string) $element, $attributes);
|
$response->redirect((string) $element, $attributes);
|
||||||
break;
|
break;
|
||||||
@@ -485,25 +520,162 @@ class TWP_Workflow {
|
|||||||
* Create forward TwiML
|
* Create forward TwiML
|
||||||
*/
|
*/
|
||||||
private static function create_forward_twiml($step) {
|
private static function create_forward_twiml($step) {
|
||||||
|
error_log('TWP Workflow Forward: Creating forward TwiML with step data: ' . print_r($step, true));
|
||||||
|
|
||||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
|
// Check if data is nested (workflow steps have data nested)
|
||||||
|
$step_data = isset($step['data']) ? $step['data'] : $step;
|
||||||
|
|
||||||
|
// Get the forward number(s) from the proper location
|
||||||
|
$forward_numbers = array();
|
||||||
|
|
||||||
|
// Check various possible locations for forward numbers
|
||||||
|
if (isset($step_data['forward_numbers']) && is_array($step_data['forward_numbers'])) {
|
||||||
|
$forward_numbers = $step_data['forward_numbers'];
|
||||||
|
error_log('TWP Workflow Forward: Found forward_numbers array in data: ' . print_r($forward_numbers, true));
|
||||||
|
} elseif (isset($step_data['forward_number']) && !empty($step_data['forward_number'])) {
|
||||||
|
$forward_numbers = array($step_data['forward_number']);
|
||||||
|
error_log('TWP Workflow Forward: Found single forward_number in data: ' . $step_data['forward_number']);
|
||||||
|
} elseif (isset($step_data['number']) && !empty($step_data['number'])) {
|
||||||
|
$forward_numbers = array($step_data['number']);
|
||||||
|
error_log('TWP Workflow Forward: Found number in data: ' . $step_data['number']);
|
||||||
|
} elseif (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
|
||||||
|
$forward_numbers = $step['forward_numbers'];
|
||||||
|
error_log('TWP Workflow Forward: Found forward_numbers array: ' . print_r($forward_numbers, true));
|
||||||
|
} elseif (isset($step['forward_number']) && !empty($step['forward_number'])) {
|
||||||
|
$forward_numbers = array($step['forward_number']);
|
||||||
|
error_log('TWP Workflow Forward: Found single forward_number: ' . $step['forward_number']);
|
||||||
|
} elseif (isset($step['number']) && !empty($step['number'])) {
|
||||||
|
$forward_numbers = array($step['number']);
|
||||||
|
error_log('TWP Workflow Forward: Found number: ' . $step['number']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out empty numbers
|
||||||
|
$forward_numbers = array_filter($forward_numbers, function($num) {
|
||||||
|
return !empty($num);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($forward_numbers)) {
|
||||||
|
error_log('TWP Workflow Forward: ERROR - No forward numbers found in step data');
|
||||||
|
// Return error message instead of empty dial
|
||||||
|
$say = $twiml->addChild('Say', 'Sorry, the forwarding destination is not configured. Please try again later.');
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
$twiml->addChild('Hangup');
|
||||||
|
return $twiml->asXML();
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers));
|
||||||
|
|
||||||
|
// Check if we should use conference mode with agent features
|
||||||
|
$use_conference_mode = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true;
|
||||||
|
|
||||||
|
if ($use_conference_mode) {
|
||||||
|
// Use conference mode for better call control
|
||||||
|
error_log('TWP Workflow Forward: Using conference mode for forwarding');
|
||||||
|
|
||||||
|
// Generate a unique conference name using call SID
|
||||||
|
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] :
|
||||||
|
(isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] :
|
||||||
|
(isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : uniqid('conf_')));
|
||||||
|
$conference_name = 'Forward_' . $call_sid;
|
||||||
|
|
||||||
|
// Put the caller into a conference
|
||||||
|
$dial = $twiml->addChild('Dial');
|
||||||
|
$dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result'));
|
||||||
|
$dial->addAttribute('method', 'POST');
|
||||||
|
|
||||||
|
$conference = $dial->addChild('Conference', $conference_name);
|
||||||
|
$conference->addAttribute('startConferenceOnEnter', 'false'); // Wait for agent
|
||||||
|
$conference->addAttribute('endConferenceOnExit', 'true'); // End when caller leaves
|
||||||
|
$conference->addAttribute('waitUrl', 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical');
|
||||||
|
$conference->addAttribute('beep', 'false');
|
||||||
|
|
||||||
|
// Add event callbacks to monitor conference
|
||||||
|
$conference->addAttribute('statusCallback', home_url('/wp-json/twilio-webhook/v1/conference-status'));
|
||||||
|
$conference->addAttribute('statusCallbackEvent', 'start join leave end');
|
||||||
|
$conference->addAttribute('statusCallbackMethod', 'POST');
|
||||||
|
|
||||||
|
error_log('TWP Workflow Forward: Placing caller in conference: ' . $conference_name);
|
||||||
|
|
||||||
|
// Immediately call the agent to join the conference
|
||||||
|
// We'll use the Twilio API to make an outbound call
|
||||||
|
$twilio = new TWP_Twilio_API();
|
||||||
|
|
||||||
|
// Set caller ID to the business number
|
||||||
|
$caller_id = isset($_POST['To']) ? $_POST['To'] : null;
|
||||||
|
if (!$caller_id && isset($GLOBALS['call_data']['To'])) {
|
||||||
|
$caller_id = $GLOBALS['call_data']['To'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TwiML for the agent call that joins them to conference with features
|
||||||
|
$agent_twiml_url = home_url('/wp-json/twilio-webhook/v1/agent-conference-join?' . http_build_query([
|
||||||
|
'conference_name' => $conference_name,
|
||||||
|
'caller_number' => isset($_POST['From']) ? $_POST['From'] : ''
|
||||||
|
]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
error_log('TWP Workflow Forward: Calling agent ' . $forward_numbers[0] . ' to join conference');
|
||||||
|
|
||||||
|
$agent_call = $twilio->get_client()->calls->create(
|
||||||
|
$forward_numbers[0], // to
|
||||||
|
$caller_id, // from (business number)
|
||||||
|
[
|
||||||
|
'url' => $agent_twiml_url,
|
||||||
|
'method' => 'POST',
|
||||||
|
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'),
|
||||||
|
'statusCallbackEvent' => ['initiated', 'ringing', 'answered', 'completed'],
|
||||||
|
'statusCallbackMethod' => 'POST'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
error_log('TWP Workflow Forward: Agent call created with SID: ' . $agent_call->sid);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('TWP Workflow Forward: Failed to create agent call: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Use standard dial forwarding (simpler but less features)
|
||||||
|
error_log('TWP Workflow Forward: Using standard dial forwarding');
|
||||||
|
|
||||||
$dial = $twiml->addChild('Dial');
|
$dial = $twiml->addChild('Dial');
|
||||||
$dial->addAttribute('answerOnBridge', 'true');
|
$dial->addAttribute('answerOnBridge', 'true');
|
||||||
|
|
||||||
if (isset($step['timeout'])) {
|
// Set timeout (default to 30 seconds if not specified)
|
||||||
$dial->addAttribute('timeout', $step['timeout']);
|
$timeout = isset($step_data['timeout']) ? $step_data['timeout'] :
|
||||||
|
(isset($step['timeout']) ? $step['timeout'] : '30');
|
||||||
|
$dial->addAttribute('timeout', $timeout);
|
||||||
|
|
||||||
|
// Set caller ID to the number that was called
|
||||||
|
$caller_id = null;
|
||||||
|
if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) {
|
||||||
|
$caller_id = $GLOBALS['call_data']['To'];
|
||||||
|
} elseif (isset($_POST['To']) && !empty($_POST['To'])) {
|
||||||
|
$caller_id = $_POST['To'];
|
||||||
|
} elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) {
|
||||||
|
$caller_id = $_REQUEST['To'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
|
if ($caller_id) {
|
||||||
// Sequential forwarding
|
$dial->addAttribute('callerId', $caller_id);
|
||||||
foreach ($step['forward_numbers'] as $number) {
|
}
|
||||||
|
|
||||||
|
// Add action URL to handle dial result
|
||||||
|
$dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result'));
|
||||||
|
$dial->addAttribute('method', 'POST');
|
||||||
|
|
||||||
|
// Add all forward numbers
|
||||||
|
foreach ($forward_numbers as $number) {
|
||||||
|
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
|
||||||
$dial->addChild('Number', $number);
|
$dial->addChild('Number', $number);
|
||||||
}
|
}
|
||||||
} elseif (isset($step['forward_number'])) {
|
|
||||||
$dial->addChild('Number', $step['forward_number']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $twiml->asXML();
|
$result = $twiml->asXML();
|
||||||
|
error_log('TWP Workflow Forward: Final Forward TwiML: ' . $result);
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -677,8 +849,23 @@ class TWP_Workflow {
|
|||||||
$dial->addAttribute('timeout', '30');
|
$dial->addAttribute('timeout', '30');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($step['caller_id'])) {
|
// Set caller ID - use provided value or default to the incoming number
|
||||||
|
if (isset($step['caller_id']) && !empty($step['caller_id'])) {
|
||||||
$dial->addAttribute('callerId', $step['caller_id']);
|
$dial->addAttribute('callerId', $step['caller_id']);
|
||||||
|
} else {
|
||||||
|
// Use the number that was called (To number) as default caller ID
|
||||||
|
$caller_id = null;
|
||||||
|
if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) {
|
||||||
|
$caller_id = $GLOBALS['call_data']['To'];
|
||||||
|
} elseif (isset($_POST['To']) && !empty($_POST['To'])) {
|
||||||
|
$caller_id = $_POST['To'];
|
||||||
|
} elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) {
|
||||||
|
$caller_id = $_REQUEST['To'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($caller_id) {
|
||||||
|
$dial->addAttribute('callerId', $caller_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set action URL to handle no-answer scenarios
|
// Set action URL to handle no-answer scenarios
|
||||||
|
|||||||
32
includes/interface-twp-sms-provider.php
Normal file
32
includes/interface-twp-sms-provider.php
Normal 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();
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Twilio WP Plugin
|
* Plugin Name: Twilio WP Plugin
|
||||||
* Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin
|
* Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin
|
||||||
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
|
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
|
||||||
* Version: 2.2.0
|
* Version: {auto_update_value_on_deploy}
|
||||||
* Author: Josh Knapp
|
* Author: Josh Knapp
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
* Text Domain: twilio-wp-plugin
|
* Text Domain: twilio-wp-plugin
|
||||||
@@ -15,7 +15,7 @@ if (!defined('WPINC')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('TWP_VERSION', '2.8.6');
|
define('TWP_VERSION', '{auto_update_value_on_deploy}');
|
||||||
define('TWP_DB_VERSION', '1.6.2'); // Track database version separately
|
define('TWP_DB_VERSION', '1.6.2'); // Track database version separately
|
||||||
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user