Just a few updates using claude to the connnection system

This commit is contained in:
admin 2025-12-30 07:35:45 -07:00
parent 9050d8f9b0
commit f6092e8351
9 changed files with 836 additions and 97 deletions

154
CLAUDE.md Normal file
View File

@ -0,0 +1,154 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
SMS Campaign Manager - A Dockerized SMS automation system with Android device integration via Termux API and ADB. The system manages SMS campaigns, tracks responses, and provides real-time analytics through a Flask web interface.
## Key Commands
### Development
```bash
# Start the application using Docker Compose
docker-compose up -d
# Start using convenience script
./run.sh start
# View logs
docker-compose logs -f sms-campaign
# Stop services
docker-compose down
# Rebuild after code changes
docker-compose build && docker-compose up -d
# Access container shell for debugging
docker-compose exec sms-campaign bash
```
### Testing
```bash
# Run test script (if available)
python test_refactoring.py
# Test phone connectivity
curl http://localhost:5000/api/phone/status
# Test SMS functionality
curl -X POST http://localhost:5000/api/sms/test \
-H "Content-Type: application/json" \
-d '{"phone":"YOUR_NUMBER","message":"Test message"}'
# Test Termux API health
curl http://10.0.0.193:5001/health
```
### Android Device Management
```bash
# Deploy Android services
scp -P 8022 android/*.sh android-dev@10.0.0.193:~/bin/
scp -P 8022 android/*.py android-dev@10.0.0.193:~/projects/sms-campaign-manager/
# Start all Android services
ssh android-dev "~/bin/start-all-services.sh"
# Check service status
ssh android-dev "~/bin/sms-service.sh status"
# Auto-connect phone via ADB
./scripts/auto.sh
```
## Architecture
### Three-Tier System Architecture
```
Ubuntu Homelab (Port 5000) → Flask Web Application
↓ HTTP API calls
Android SMS API Server (Port 5001) → Termux SMS API
Android Monitor Dashboard (Port 5000) → Device monitoring
```
### Core Service Components
1. **Flask Application** (`src/app.py`)
- Main web application orchestrator
- Manages campaign creation, scheduling, and monitoring
- Coordinates with Android services via HTTP APIs
2. **Database Layer** (`src/database/`)
- SQLite database at `data/campaign.db`
- Manages campaigns, contacts, messages, and responses
- DatabaseManager handles initialization and migrations
- DatabaseHelper provides query abstractions
3. **SMS Services** (`src/services/sms/`)
- **SMSConnectionManager**: Manages dual connection (Termux API + ADB fallback)
- **SMSSender**: Handles message sending with retry logic
- Automatic failover between Termux (fast) and ADB (reliable)
4. **Campaign Services** (`src/services/campaign/`)
- **CampaignManager**: Campaign lifecycle management
- **CampaignExecutor**: Message sending orchestration
- **MessageUtils**: Template processing and variable substitution
5. **Background Services** (`src/services/background/`)
- **PhoneMonitor**: Device connectivity and health monitoring
- **ResponseSyncService**: SMS reply synchronization
6. **API Routes** (`src/routes/api/`)
- Campaign management endpoints
- SMS sending and testing endpoints
- File upload and CSV processing
- Analytics and reporting
### Configuration Management
Configuration is centralized in `src/core/config.py`:
- Phone IP and ports (ADB, Termux API)
- SMS retry settings (max retries, delays)
- Flask settings (secret key, upload limits)
- Database and file paths
Environment variables are loaded from `.env` file (see `.env.example` template).
### Key Design Patterns
1. **Dual Connection Strategy**: Primary Termux API with automatic ADB fallback
2. **Retry Mechanism**: Exponential backoff for SMS sending failures
3. **Modular Architecture**: Clear separation of concerns across services
4. **Factory Pattern**: `create_app()` for Flask application initialization
5. **Service Layer**: Business logic separated from routes/controllers
## Dependencies
Python 3.x with key packages:
- Flask 3.0.0 - Web framework
- flask-socketio 5.3.5 - WebSocket support
- requests 2.31.0 - HTTP client
- aiohttp 3.9.1 - Async HTTP support
Install with: `pip install -r src/requirements.txt`
## File Structure Notes
- `src/` - Main application code (Flask app, services, routes)
- `android/` - Android-side Python servers (Termux API server, monitoring app)
- `data/` - SQLite database (created at runtime)
- `uploads/` - CSV contact uploads
- `logs/` - Application logs
- `docker/` - Docker configuration files
- `scripts/` - Shell automation scripts
## Important Considerations
- The system requires an Android device with Termux and Termux:API installed
- Device IP is configured in `.env` (default: 10.0.0.193)
- Uses network_mode: host in Docker for ADB connectivity
- Requires privileged mode for USB device access
- SMS retry logic uses exponential backoff (2s, 4s, 8s)
- WebSocket service provides real-time updates to web interface

View File

@ -0,0 +1,143 @@
# Termux API Retry Logic Improvements
## Problem Analysis
Your logs showed that the Termux API was consistently failing during campaigns with the message:
```
services.sms.connection_manager - WARNING - Termux API failed, falling back to ADB
```
The issue was that there was no retry logic for transient failures. Even though the health check passed, individual SMS requests would fail immediately and fall back to ADB.
## Solution Implemented
### 1. Added Comprehensive Retry Logic
The `send_sms_termux_api` method now includes:
- **Configurable retry attempts** (default: 3)
- **Exponential backoff** with maximum delay
- **Health checks between retries**
- **Smart error handling** for different failure types
- **Detailed HTTP 400 error logging** to identify validation issues
### 2. Fixed Message Length Limits
**Root Cause Identified:**
The Termux API server had a `MAX_MESSAGE_LENGTH` of only 160 characters, but campaign messages are typically longer.
**Fix Applied:**
- Increased `MAX_MESSAGE_LENGTH` from 160 to 1600 characters
- Added detailed message length logging
- Improved validation error messages
### 3. Improved Rate Limiting
**Changes:**
- Reduced `RATE_LIMIT_DELAY` from 2.0 to 1.0 seconds for faster campaigns
- Better rate limiting error handling
**Retry Strategy:**
- Attempt 1: Immediate
- Attempt 2: Wait 2 seconds
- Attempt 3: Wait 4 seconds
- Maximum delay: 8 seconds (configurable)
**Error-Specific Handling:**
- **Rate limiting (HTTP 429)**: Extra delay with longer backoff
- **Server errors (5xx)**: Retry with standard backoff
- **Connection/timeout errors**: Retry with standard backoff
- **Permission/invalid errors**: Don't retry (permanent failures)
- **Client errors (4xx)**: Don't retry on first attempt
### 3. Health Checks Between Retries
Before each retry, the system:
1. Checks if the Termux API `/health` endpoint is responding
2. Skips retry if API is completely down
3. Continues with retry if API is healthy
### 4. Configuration Options
New environment variables you can set:
```bash
# Maximum number of retry attempts for Termux API
SMS_MAX_RETRIES=3
# Base delay for exponential backoff (seconds)
SMS_RETRY_BASE_DELAY=2
# Maximum delay between retries (seconds)
SMS_MAX_RETRY_DELAY=8
```
### 5. Enhanced Logging
The system now logs:
- Each retry attempt with delay time
- Specific error types and HTTP status codes
- Success on retry attempts
- Final failure after all retries exhausted
- Combined error messages when both Termux API and ADB fail
## Expected Improvements
**Before:**
- Termux API fails → Immediate fallback to ADB
- No retry for transient network issues
- No visibility into failure reasons
**After:**
- Termux API fails → 3 retry attempts with smart delays
- Health checks prevent unnecessary retries
- Detailed error logging for troubleshooting
- Only falls back to ADB after genuine Termux API failure
## Testing the Changes
1. **Monitor the logs** during your next campaign to see retry attempts:
```bash
docker compose logs -f sms-campaign-manager | grep -E "(retry|Termux API)"
```
2. **Expected log patterns:**
```
Termux API retry 1/3 for 7802921731 (delay: 2s)
Termux API retry 2/3 for 7802921731 (delay: 4s)
✅ Termux API succeeded on retry 2 for 7802921731
SMS Result - Success: True, Connection: termux_api, Retries: 1
```
3. **Configuration tuning** - If you see many retries, you can:
- Increase `SMS_MAX_RETRIES` for more attempts
- Increase `SMS_MAX_RETRY_DELAY` for longer waits
- Check Termux API server stability on your Android device
## Common Issues & Solutions
### Termux API Still Failing
1. Check if termux-sms-api-server.py is running on Android
2. Verify SMS permissions for Termux:API app
3. Check Android battery optimization settings
4. Restart the Termux API server
### High Retry Rates
1. Increase base delay: `SMS_RETRY_BASE_DELAY=4`
2. Check Android device performance/memory
3. Verify network stability between homelab and phone
4. Consider reducing campaign send rate
### ADB Still Being Used Frequently
1. Check `/health` endpoint: `curl http://10.0.0.193:5001/health`
2. Review Termux API server logs on Android
3. Verify Android device isn't in power saving mode
4. Check if multiple campaigns are running simultaneously
## Implementation Details
The retry logic is implemented in:
- `src/services/sms/connection_manager.py` - Main retry logic
- `src/services/sms/sms_sender.py` - Enhanced SMS sending
- `src/core/config.py` - Configuration management
The changes maintain full backward compatibility while adding robustness for production campaigns.

View File

@ -35,8 +35,8 @@ app = Flask(__name__)
# Configuration
CONFIG = {
'SECRET_KEY': os.environ.get('SMS_API_SECRET', 'termux-sms-campaign-2025'),
'MAX_MESSAGE_LENGTH': 160,
'RATE_LIMIT_DELAY': 2.0, # Seconds between messages
'MAX_MESSAGE_LENGTH': 1600, # Increased from 160 to support longer messages (SMS can be up to 1600 chars)
'RATE_LIMIT_DELAY': 1.0, # Reduced from 2.0 to 1.0 seconds between messages for faster campaigns
'ALLOWED_COMMANDS': [
'termux-sms-send',
'termux-sms-list',
@ -162,22 +162,34 @@ class SMSApiServer:
"""Send SMS using Termux API"""
# Input validation
if not phone or not message:
return {'success': False, 'error': 'Phone and message required'}
error_msg = 'Phone and message required'
logger.error(f"SMS validation failed: {error_msg}")
return {'success': False, 'error': error_msg}
# Clean phone number
clean_phone = phone.replace('+', '').replace('-', '').replace(' ', '').replace('(', '').replace(')', '')
if len(message) > CONFIG['MAX_MESSAGE_LENGTH']:
return {'success': False, 'error': f'Message too long (max {CONFIG["MAX_MESSAGE_LENGTH"]} chars)'}
error_msg = f'Message too long ({len(message)} chars, max {CONFIG["MAX_MESSAGE_LENGTH"]})'
logger.error(f"SMS validation failed: {error_msg}")
return {'success': False, 'error': error_msg}
# Rate limiting
if not self.rate_limit_check():
return {'success': False, 'error': 'Rate limit exceeded, please wait'}
error_msg = 'Rate limit exceeded, please wait'
logger.warning(f"SMS rate limited for {phone}")
return {'success': False, 'error': error_msg}
# Log the SMS attempt
logger.info(f"Attempting to send SMS to {phone} (length: {len(message)} chars)")
# Execute SMS send command
command = ['termux-sms-send', '-n', phone, message]
command = ['termux-sms-send', '-n', clean_phone, message]
result = self.execute_termux_command(command)
if result['success']:
self.message_count += 1
logger.info(f"SMS sent to {phone}: {message[:50]}...")
logger.info(f"SMS sent successfully to {phone}: {message[:50]}...")
# Send confirmation notification
self.execute_termux_command([
@ -185,6 +197,8 @@ class SMSApiServer:
'--title', 'SMS Sent',
'--content', f'Message sent to {phone}'
])
else:
logger.error(f"❌ SMS send failed to {phone}: {result.get('error', 'Unknown error')}")
return {
'success': result['success'],
@ -319,6 +333,7 @@ def send_sms():
try:
data = request.get_json()
if not data:
logger.error("SMS send endpoint: No JSON data provided")
return jsonify({'success': False, 'error': 'JSON data required'}), 400
# Extract parameters
@ -326,6 +341,17 @@ def send_sms():
message = data.get('message', '').strip()
name = data.get('name', '')
logger.info(f"SMS send request: phone={phone}, name={name}, message_length={len(message) if message else 0}")
# Validate required fields
if not phone:
logger.error("SMS send validation: Missing phone number")
return jsonify({'success': False, 'error': 'Phone number required'}), 400
if not message:
logger.error("SMS send validation: Missing message")
return jsonify({'success': False, 'error': 'Message required'}), 400
# Message template substitution (like existing ui.sh)
if name and '{name}' in message:
message = message.replace('{name}', name)
@ -334,10 +360,11 @@ def send_sms():
result = sms_server.send_sms(phone, message)
status_code = 200 if result['success'] else 400
logger.info(f"SMS send result: success={result['success']}, error={result.get('error', 'None')}")
return jsonify(result), status_code
except Exception as e:
logger.error(f"SMS send error: {e}")
logger.error(f"SMS send endpoint error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/sms/list', methods=['GET'])

172
deploy-android.sh Executable file
View File

@ -0,0 +1,172 @@
#!/bin/bash
# Android Services Deployment Script
# Deploys SMS Campaign Manager services to Android device via Tailscale
set -e
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Configuration
ANDROID_IP="100.107.173.66"
SSH_PORT="8022"
SSH_USER="${SSH_USER:-$(whoami)}"
ANDROID_HOST="${SSH_USER}@${ANDROID_IP}"
echo -e "${GREEN}📱 SMS Campaign Manager - Android Deployment${NC}"
echo "=============================================="
echo -e "${BLUE}Target Device:${NC} ${ANDROID_HOST}:${SSH_PORT}"
echo -e "${BLUE}Via Tailscale:${NC} ${ANDROID_IP}"
echo ""
# Test connectivity
echo -e "${YELLOW}⏳ Testing connectivity...${NC}"
if ! ping -c 1 -W 2 ${ANDROID_IP} > /dev/null 2>&1; then
echo -e "${RED}❌ Cannot reach Android device at ${ANDROID_IP}${NC}"
echo " Make sure Tailscale is running on both devices"
exit 1
fi
echo -e "${GREEN}✅ Device reachable${NC}"
# Test SSH
echo -e "${YELLOW}⏳ Testing SSH connection...${NC}"
if ! ssh -p ${SSH_PORT} -o ConnectTimeout=5 -o BatchMode=yes ${ANDROID_HOST} "exit" 2>/dev/null; then
echo -e "${YELLOW}⚠️ SSH requires password authentication${NC}"
echo " You'll be prompted for your password during deployment"
else
echo -e "${GREEN}✅ SSH connection ready${NC}"
fi
echo ""
# Check required files
echo -e "${YELLOW}⏳ Checking deployment files...${NC}"
if [ ! -d "android" ]; then
echo -e "${RED}❌ android/ directory not found${NC}"
echo " Please run this script from the project root"
exit 1
fi
SHELL_SCRIPTS=(android/*.sh)
PYTHON_APPS=(android/*.py)
if [ ${#SHELL_SCRIPTS[@]} -eq 0 ] || [ ${#PYTHON_APPS[@]} -eq 0 ]; then
echo -e "${RED}❌ Missing deployment files in android/ directory${NC}"
exit 1
fi
echo -e "${GREEN}✅ Found ${#SHELL_SCRIPTS[@]} shell scripts and ${#PYTHON_APPS[@]} Python apps${NC}"
echo ""
# Deployment confirmation
echo -e "${BLUE}Ready to deploy:${NC}"
echo " • Shell scripts → ~/bin/"
echo " • Python apps → ~/projects/sms-campaign-manager/"
echo ""
read -p "Continue with deployment? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Deployment cancelled${NC}"
exit 0
fi
# Step 1: Create directories
echo ""
echo -e "${GREEN}[1/5]${NC} Creating directories on Android..."
ssh -p ${SSH_PORT} ${ANDROID_HOST} "mkdir -p ~/bin ~/projects/sms-campaign-manager ~/logs"
echo -e "${GREEN}✅ Directories created${NC}"
# Step 2: Deploy shell scripts
echo ""
echo -e "${GREEN}[2/5]${NC} Deploying shell scripts to ~/bin/..."
scp -P ${SSH_PORT} android/*.sh ${ANDROID_HOST}:~/bin/
echo -e "${GREEN}✅ Shell scripts deployed${NC}"
# Step 3: Make scripts executable
echo ""
echo -e "${GREEN}[3/5]${NC} Setting execute permissions..."
ssh -p ${SSH_PORT} ${ANDROID_HOST} "chmod +x ~/bin/*.sh"
echo -e "${GREEN}✅ Permissions set${NC}"
# Step 4: Deploy Python applications
echo ""
echo -e "${GREEN}[4/5]${NC} Deploying Python applications..."
scp -P ${SSH_PORT} android/*.py ${ANDROID_HOST}:~/projects/sms-campaign-manager/
echo -e "${GREEN}✅ Python apps deployed${NC}"
# Step 5: Start services
echo ""
echo -e "${GREEN}[5/5]${NC} Starting Android services..."
echo -e "${YELLOW}⏳ This may take a moment...${NC}"
ssh -p ${SSH_PORT} ${ANDROID_HOST} "~/bin/start-all-services.sh" || {
echo -e "${YELLOW}⚠️ Service startup script completed with warnings${NC}"
echo " Services may still be starting up"
}
# Verification
echo ""
echo -e "${GREEN}🔍 Verifying deployment...${NC}"
sleep 3
# Check if processes are running
echo -e "${YELLOW}⏳ Checking service processes...${NC}"
PROCESSES=$(ssh -p ${SSH_PORT} ${ANDROID_HOST} "ps aux | grep -E '(termux-sms-api-server|python.*app.py)' | grep -v grep" || echo "")
if [ -n "$PROCESSES" ]; then
echo -e "${GREEN}✅ Services are running:${NC}"
echo "$PROCESSES" | sed 's/^/ /'
else
echo -e "${YELLOW}⚠️ Services may still be starting up${NC}"
fi
# Check API health
echo ""
echo -e "${YELLOW}⏳ Testing API endpoints...${NC}"
sleep 2
if curl -s --max-time 3 http://${ANDROID_IP}:5001/health > /dev/null 2>&1; then
echo -e "${GREEN}✅ SMS API Server (port 5001): Responding${NC}"
else
echo -e "${YELLOW}⚠️ SMS API Server (port 5001): Not responding yet${NC}"
fi
if curl -s --max-time 3 http://${ANDROID_IP}:5000/ > /dev/null 2>&1; then
echo -e "${GREEN}✅ Monitoring Dashboard (port 5000): Responding${NC}"
else
echo -e "${YELLOW}⚠️ Monitoring Dashboard (port 5000): Not responding yet${NC}"
fi
# Final summary
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}✅ Deployment Complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${BLUE}Deployed Files:${NC}"
echo " ~/bin/start-all-services.sh"
echo " ~/bin/sms-service.sh"
echo " ~/bin/start-sms-api.sh"
echo " ~/bin/start-monitoring.sh"
echo " ~/bin/network-monitor.sh"
echo " ~/projects/sms-campaign-manager/termux-sms-api-server.py"
echo " ~/projects/sms-campaign-manager/app.py"
echo ""
echo -e "${BLUE}Service URLs:${NC}"
echo " 🌐 Ubuntu Homelab: http://localhost:5000"
echo " 📡 Android SMS API: http://${ANDROID_IP}:5001"
echo " 📊 Android Monitor: http://${ANDROID_IP}:5000"
echo ""
echo -e "${BLUE}Useful Commands:${NC}"
echo " # Check service status"
echo " ssh -p ${SSH_PORT} ${ANDROID_HOST} '~/bin/sms-service.sh status'"
echo ""
echo " # View logs"
echo " ssh -p ${SSH_PORT} ${ANDROID_HOST} 'tail -f ~/logs/sms-api.log'"
echo ""
echo " # Restart services"
echo " ssh -p ${SSH_PORT} ${ANDROID_HOST} '~/bin/start-all-services.sh'"
echo ""
echo -e "${YELLOW}Note:${NC} If services aren't responding yet, wait 10-20 seconds for startup"
echo ""

View File

@ -27,6 +27,11 @@ class AppConfig:
SEND_Y: int = 2900
DELAY_SECONDS: int = 3
# SMS Retry Configuration
SMS_MAX_RETRIES: int = int(os.environ.get('SMS_MAX_RETRIES', '3'))
SMS_RETRY_BASE_DELAY: int = int(os.environ.get('SMS_RETRY_BASE_DELAY', '2'))
SMS_MAX_RETRY_DELAY: int = int(os.environ.get('SMS_MAX_RETRY_DELAY', '8'))
# Flask Settings
SECRET_KEY: str = os.environ.get('SECRET_KEY', 'dev-key-change-in-production')
UPLOAD_FOLDER: str = './uploads'
@ -41,7 +46,13 @@ class AppConfig:
return {
'PHONE_IP': self.PHONE_IP,
'ADB_PORT': int(self.ADB_PORT),
'TERMUX_API_PORT': self.TERMUX_API_PORT
'TERMUX_API_PORT': self.TERMUX_API_PORT,
'SEND_X': self.SEND_X,
'SEND_Y': self.SEND_Y,
'DELAY_SECONDS': self.DELAY_SECONDS,
'SMS_MAX_RETRIES': self.SMS_MAX_RETRIES,
'SMS_RETRY_BASE_DELAY': self.SMS_RETRY_BASE_DELAY,
'SMS_MAX_RETRY_DELAY': self.SMS_MAX_RETRY_DELAY
}
config = AppConfig()

View File

@ -26,7 +26,19 @@ class PhoneMonitor:
self.shutdown_event = Event()
def check_phone_connection(self) -> bool:
"""Check if phone is connected via ADB"""
"""Check if phone is connected via ADB or Termux API"""
# Skip ADB check if Termux API is available (preferred method over Tailscale)
try:
# Check Termux API first (faster and more reliable over Tailscale)
import requests
termux_url = f"http://{self.phone_ip}:{getattr(self.sms_manager, 'termux_api_port', 5001)}"
response = requests.get(f"{termux_url}/health", timeout=3)
if response.status_code == 200:
return True
except Exception:
pass # Fall through to ADB check
# Only try ADB if Termux API is unavailable
try:
# First try to connect
subprocess.run(
@ -34,7 +46,7 @@ class PhoneMonitor:
capture_output=True,
timeout=5
)
# Check if connected
result = subprocess.run(
['adb', 'devices'],
@ -42,14 +54,14 @@ class PhoneMonitor:
text=True,
timeout=5
)
devices = result.stdout.strip().split('\n')[1:]
for device in devices:
if self.phone_ip in device and 'device' in device:
return True
return False
except Exception as e:
logger.error(f"Error checking phone connection: {e}")
logger.debug(f"ADB check failed (this is normal if using Termux API): {e}")
return False
def start(self):

View File

@ -54,6 +54,11 @@ class SMSConnectionManager:
self.send_y = config.get('SEND_Y', 2900)
self.delay_seconds = config.get('DELAY_SECONDS', 3)
# Retry configuration
self.default_max_retries = config.get('SMS_MAX_RETRIES', 3)
self.retry_base_delay = config.get('SMS_RETRY_BASE_DELAY', 2) # Base delay for exponential backoff
self.max_retry_delay = config.get('SMS_MAX_RETRY_DELAY', 8) # Maximum delay between retries
def check_connections(self) -> Dict[ConnectionType, bool]:
"""Check availability of both connection methods"""
current_time = time.time()
@ -88,36 +93,44 @@ class SMSConnectionManager:
logger.warning(f"Termux API became unavailable: {e}")
self.connection_status[ConnectionType.TERMUX_API] = current_status
# Check ADB connection
# Check ADB connection (only if Termux API is unavailable)
prev_adb_status = self.connection_status.get(ConnectionType.ADB, None)
try:
result = subprocess.run([
'adb', 'connect', f"{self.phone_ip}:{self.adb_port}"
], capture_output=True, text=True, timeout=10)
# Check if device is connected
list_result = subprocess.run([
'adb', 'devices'
], capture_output=True, text=True, timeout=5)
current_status = (
f"{self.phone_ip}:{self.adb_port}" in list_result.stdout and
"device" in list_result.stdout
)
self.connection_status[ConnectionType.ADB] = current_status
# Only log if status changed or every 5 minutes
if prev_adb_status != current_status or not hasattr(self, '_last_adb_log') or \
(current_time - getattr(self, '_last_adb_log', 0)) > 300:
logger.info(f"ADB connection check: {current_status}")
self._last_adb_log = current_time
else:
logger.debug(f"ADB connection check: {current_status}")
except Exception as e:
current_status = False
if prev_adb_status != current_status:
logger.warning(f"ADB became unavailable: {e}")
self.connection_status[ConnectionType.ADB] = current_status
# Skip ADB check if Termux API is working (save resources over Tailscale)
if self.connection_status.get(ConnectionType.TERMUX_API, False):
self.connection_status[ConnectionType.ADB] = False
if prev_adb_status is not False:
logger.debug("Skipping ADB check - Termux API is available")
else:
# Only try ADB if Termux API is unavailable
try:
result = subprocess.run([
'adb', 'connect', f"{self.phone_ip}:{self.adb_port}"
], capture_output=True, text=True, timeout=10)
# Check if device is connected
list_result = subprocess.run([
'adb', 'devices'
], capture_output=True, text=True, timeout=5)
current_status = (
f"{self.phone_ip}:{self.adb_port}" in list_result.stdout and
"device" in list_result.stdout
)
self.connection_status[ConnectionType.ADB] = current_status
# Only log if status changed or every 5 minutes
if prev_adb_status != current_status or not hasattr(self, '_last_adb_log') or \
(current_time - getattr(self, '_last_adb_log', 0)) > 300:
logger.info(f"ADB connection check: {current_status}")
self._last_adb_log = current_time
else:
logger.debug(f"ADB connection check: {current_status}")
except Exception as e:
current_status = False
if prev_adb_status not in (False, None):
logger.debug(f"ADB unavailable (using Termux API): {e}")
self.connection_status[ConnectionType.ADB] = current_status
return self.connection_status
@ -136,50 +149,142 @@ class SMSConnectionManager:
# No connections available
return None
def send_sms_termux_api(self, phone: str, message: str, name: Optional[str] = None) -> SMSResult:
"""Send SMS via Termux API"""
try:
payload = {
'phone': phone,
'message': message,
'name': name
}
response = requests.post(
f"{self.termux_api_url}/api/sms/send",
json=payload,
timeout=30
)
result_data = response.json()
return SMSResult(
success=result_data.get('success', False),
message=message,
phone=phone,
timestamp=time.time(),
connection_type=ConnectionType.TERMUX_API,
error=result_data.get('error')
)
except requests.exceptions.RequestException as e:
return SMSResult(
success=False,
message=message,
phone=phone,
timestamp=time.time(),
connection_type=ConnectionType.TERMUX_API,
error=f'Request failed: {str(e)}'
)
except Exception as e:
return SMSResult(
success=False,
message=message,
phone=phone,
timestamp=time.time(),
connection_type=ConnectionType.TERMUX_API,
error=str(e)
)
def send_sms_termux_api(self, phone: str, message: str, name: Optional[str] = None, max_retries: Optional[int] = None) -> SMSResult:
"""Send SMS via Termux API with retry logic"""
effective_max_retries = max_retries if max_retries is not None else self.default_max_retries
last_error = None
for attempt in range(effective_max_retries):
try:
# Add small delay between retries (except first attempt)
if attempt > 0:
retry_delay = min(self.retry_base_delay ** attempt, self.max_retry_delay)
logger.info(f"Termux API retry {attempt + 1}/{effective_max_retries} for {phone} (delay: {retry_delay}s)")
time.sleep(retry_delay)
# Check if API is still healthy on retries
if attempt > 0:
try:
health_response = requests.get(f"{self.termux_api_url}/health", timeout=5)
if health_response.status_code != 200:
logger.warning(f"Termux API health check failed on retry {attempt + 1}")
continue
except Exception as e:
logger.warning(f"Health check failed during retry {attempt + 1}: {e}")
continue
payload = {
'phone': phone,
'message': message,
'name': name
}
# Increase timeout for retries
timeout = 30 + (attempt * 10) # 30s, 40s, 50s
response = requests.post(
f"{self.termux_api_url}/api/sms/send",
json=payload,
timeout=timeout
)
# Handle different HTTP status codes
if response.status_code == 200:
result_data = response.json()
success = result_data.get('success', False)
if success:
if attempt > 0:
logger.info(f"✅ Termux API succeeded on retry {attempt + 1} for {phone}")
return SMSResult(
success=True,
message=message,
phone=phone,
timestamp=time.time(),
connection_type=ConnectionType.TERMUX_API,
error=None,
retry_count=attempt
)
else:
# API returned success=false - might be recoverable
api_error = result_data.get('error', 'Unknown API error')
logger.warning(f"Termux API returned error on attempt {attempt + 1}: {api_error}")
last_error = f"API error: {api_error}"
# Don't retry certain permanent errors
if 'permission' in api_error.lower() or 'invalid' in api_error.lower():
break
continue
elif response.status_code == 400:
# Bad request - log the response for debugging
try:
error_data = response.json()
api_error = error_data.get('error', 'Bad request')
logger.error(f"Termux API HTTP 400 on attempt {attempt + 1}: {api_error}")
logger.error(f"Request payload: phone={phone}, message_length={len(message)}")
last_error = f"HTTP 400: {api_error}"
except:
logger.error(f"Termux API HTTP 400 on attempt {attempt + 1} (no JSON response)")
last_error = "HTTP 400: Bad request"
# For 400 errors, don't retry - it's likely a validation issue
break
elif response.status_code == 429:
# Rate limited - definitely retry with longer delay
rate_limit_delay = 5 + (attempt * 2)
logger.warning(f"Termux API rate limited, waiting {rate_limit_delay}s before retry {attempt + 1}")
time.sleep(rate_limit_delay)
last_error = "Rate limited"
continue
elif response.status_code >= 500:
# Server error - retry
logger.warning(f"Termux API server error {response.status_code} on attempt {attempt + 1}")
last_error = f"Server error: {response.status_code}"
continue
else:
# Other HTTP errors - might not be recoverable
logger.error(f"Termux API HTTP {response.status_code} on attempt {attempt + 1}")
last_error = f"HTTP {response.status_code}"
if attempt == 0: # Don't retry client errors on first attempt
break
continue
except requests.exceptions.Timeout:
logger.warning(f"Termux API timeout on attempt {attempt + 1}/{effective_max_retries}")
last_error = "Request timeout"
continue
except requests.exceptions.ConnectionError as e:
logger.warning(f"Termux API connection error on attempt {attempt + 1}: {e}")
last_error = f"Connection error: {str(e)}"
continue
except requests.exceptions.RequestException as e:
logger.warning(f"Termux API request error on attempt {attempt + 1}: {e}")
last_error = f"Request failed: {str(e)}"
continue
except Exception as e:
logger.error(f"Unexpected Termux API error on attempt {attempt + 1}: {e}")
last_error = str(e)
# Don't retry unexpected errors
break
# All retries failed
logger.error(f"❌ Termux API failed after {effective_max_retries} attempts for {phone}: {last_error}")
return SMSResult(
success=False,
message=message,
phone=phone,
timestamp=time.time(),
connection_type=ConnectionType.TERMUX_API,
error=f"Failed after {effective_max_retries} retries: {last_error}",
retry_count=effective_max_retries
)
def send_sms_adb(self, phone: str, message: str, name: Optional[str] = None) -> SMSResult:
"""Send SMS via ADB automation"""
@ -238,9 +343,11 @@ class SMSConnectionManager:
)
def send_sms(self, phone: str, message: str, name: Optional[str] = None,
prefer_connection: Optional[ConnectionType] = None) -> SMSResult:
prefer_connection: Optional[ConnectionType] = None, max_retries: Optional[int] = None) -> SMSResult:
"""Send SMS with automatic connection selection and failover"""
effective_max_retries = max_retries if max_retries is not None else self.default_max_retries
# Determine connection method
if prefer_connection and self.connection_status.get(prefer_connection, False):
connection_type = prefer_connection
@ -257,13 +364,27 @@ class SMSConnectionManager:
error="No SMS connections available"
)
# Send via selected method
# Send via selected method with retry support
if connection_type == ConnectionType.TERMUX_API:
result = self.send_sms_termux_api(phone, message, name)
# Fallback to ADB if Termux fails
result = self.send_sms_termux_api(phone, message, name, effective_max_retries)
# Fallback to ADB if Termux fails after all retries
if not result.success and self.connection_status.get(ConnectionType.ADB, False):
logger.warning("Termux API failed, falling back to ADB")
result = self.send_sms_adb(phone, message, name)
logger.warning(f"Termux API failed after {result.retry_count} retries, falling back to ADB")
adb_result = self.send_sms_adb(phone, message, name)
# Preserve original error details but use ADB result
if adb_result.success:
return adb_result
else:
# Both methods failed - return combined error
return SMSResult(
success=False,
message=message,
phone=phone,
timestamp=time.time(),
connection_type=ConnectionType.ADB,
error=f"Termux API failed ({result.error}), ADB also failed ({adb_result.error})",
retry_count=result.retry_count
)
else:
result = self.send_sms_adb(phone, message, name)

View File

@ -16,17 +16,19 @@ class SMSSender:
def __init__(self, connection_manager: SMSConnectionManager):
self.connection_manager = connection_manager
def send_sms_enhanced(self, phone: str, message: str, name: Optional[str] = None, prefer_termux: bool = True) -> SMSResult:
def send_sms_enhanced(self, phone: str, message: str, name: Optional[str] = None,
prefer_termux: bool = True, max_retries: Optional[int] = None) -> SMSResult:
"""Enhanced SMS sending with dual connection support"""
# Use connection manager
# Use connection manager with retry support
prefer_connection = ConnectionType.TERMUX_API if prefer_termux else ConnectionType.ADB
result = self.connection_manager.send_sms(phone, message, name, prefer_connection)
result = self.connection_manager.send_sms(phone, message, name, prefer_connection, max_retries)
# Log the result
# Log the result with retry information
retry_info = f", Retries: {result.retry_count}" if result.retry_count > 0 else ""
logger.info(f"SMS Result - Success: {result.success}, "
f"Connection: {result.connection_type.value}, "
f"Phone: {phone}, Error: {result.error}")
f"Phone: {phone}{retry_info}, Error: {result.error}")
return result

97
update-termux-server.sh Executable file
View File

@ -0,0 +1,97 @@
#!/bin/bash
# Update Termux SMS API Server on Android Device
# This script updates the Termux API server with improved error handling and higher message limits
set -e
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${GREEN}📱 Updating Termux SMS API Server${NC}"
echo "======================================"
# Configuration
ANDROID_IP="10.0.0.193"
ANDROID_PORT="8022"
ANDROID_USER="android-dev"
REMOTE_PATH="~/projects/sms-campaign-manager"
echo -e "${BLUE}📋 Configuration:${NC}"
echo " Android IP: $ANDROID_IP"
echo " SSH Port: $ANDROID_PORT"
echo " User: $ANDROID_USER"
echo " Remote Path: $REMOTE_PATH"
echo
# Check if Android device is reachable
echo -e "${BLUE}🔍 Checking Android device connectivity...${NC}"
if ! ping -c 1 $ANDROID_IP >/dev/null 2>&1; then
echo -e "${RED}❌ Cannot reach Android device at $ANDROID_IP${NC}"
exit 1
fi
echo -e "${GREEN}✅ Android device is reachable${NC}"
# Stop existing API server
echo -e "${BLUE}🛑 Stopping existing Termux API server...${NC}"
ssh -p $ANDROID_PORT $ANDROID_USER@$ANDROID_IP "pkill -f termux-sms-api-server.py || true"
sleep 2
# Copy updated server file
echo -e "${BLUE}📤 Uploading updated Termux API server...${NC}"
scp -P $ANDROID_PORT android/termux-sms-api-server.py $ANDROID_USER@$ANDROID_IP:$REMOTE_PATH/
# Set permissions
echo -e "${BLUE}🔧 Setting permissions...${NC}"
ssh -p $ANDROID_PORT $ANDROID_USER@$ANDROID_IP "chmod +x $REMOTE_PATH/termux-sms-api-server.py"
# Create logs directory
ssh -p $ANDROID_PORT $ANDROID_USER@$ANDROID_IP "mkdir -p ~/logs"
# Start updated API server
echo -e "${BLUE}🚀 Starting updated Termux API server...${NC}"
ssh -p $ANDROID_PORT $ANDROID_USER@$ANDROID_IP "cd $REMOTE_PATH && nohup python termux-sms-api-server.py > ~/logs/sms-api-startup.log 2>&1 &"
# Wait a moment for startup
sleep 3
# Check if server is running
echo -e "${BLUE}🔍 Checking if API server is running...${NC}"
if ssh -p $ANDROID_PORT $ANDROID_USER@$ANDROID_IP "pgrep -f termux-sms-api-server.py" >/dev/null; then
echo -e "${GREEN}✅ Termux API server is running${NC}"
else
echo -e "${RED}❌ Failed to start Termux API server${NC}"
echo -e "${YELLOW}📋 Check startup log:${NC}"
ssh -p $ANDROID_PORT $ANDROID_USER@$ANDROID_IP "cat ~/logs/sms-api-startup.log"
exit 1
fi
# Test API endpoint
echo -e "${BLUE}🧪 Testing API endpoint...${NC}"
if curl -s -f http://$ANDROID_IP:5001/health >/dev/null; then
echo -e "${GREEN}✅ API endpoint is responding${NC}"
else
echo -e "${YELLOW}⚠️ API endpoint may still be starting up${NC}"
fi
echo
echo -e "${GREEN}🎉 Update Complete!${NC}"
echo
echo -e "${BLUE}📊 What was updated:${NC}"
echo " ✅ Increased max message length from 160 to 1600 characters"
echo " ✅ Reduced rate limit delay from 2.0 to 1.0 seconds"
echo " ✅ Added detailed error logging and validation"
echo " ✅ Improved phone number cleaning"
echo " ✅ Better debugging information"
echo
echo -e "${BLUE}📋 Next steps:${NC}"
echo " 1. Rebuild Docker container: docker compose build"
echo " 2. Restart Docker container: docker compose up"
echo " 3. Test campaign again"
echo
echo -e "${BLUE}🔍 Check logs:${NC}"
echo " Android API logs: ssh -p 8022 android-dev@10.0.0.193 'tail -f ~/logs/sms-api.log'"
echo " Docker logs: docker compose logs -f"