Move SMS phone bridge from campaign_connector submodule into main repo
Consolidates the Termux SMS server code (previously in a separate campaign_connector git submodule) into termux-sms/ at repo root. Updates phone clone commands to use sparse checkout so only the termux-sms/ directory is downloaded onto the Android device. Bunker Admin
This commit is contained in:
parent
5d15b4cffa
commit
9321aeb263
@ -460,8 +460,8 @@ export default function SmsSetupPage() {
|
||||
configures auto-start, and launches the server.
|
||||
</Paragraph>
|
||||
<div style={{ background: 'rgba(0,0,0,0.1)', padding: 12, borderRadius: 6 }}>
|
||||
<CmdLine comment="Clone the SMS server (first time only)" cmd="pkg install -y git && git clone https://gitea.bnkops.com/admin/campaign_connector.git ~/sms-server" />
|
||||
<CmdLine comment="Run the setup script with your API key" cmd={`bash ~/sms-server/android/setup.sh ${generatedKey}`} />
|
||||
<CmdLine comment="Clone the SMS server (first time only)" cmd="pkg install -y git && git clone --depth 1 --filter=blob:none --sparse https://gitea.bnkops.com/admin/changemaker.lite.git ~/sms-server && cd ~/sms-server && git sparse-checkout set termux-sms" />
|
||||
<CmdLine comment="Run the setup script with your API key" cmd={`bash ~/sms-server/termux-sms/setup.sh ${generatedKey}`} />
|
||||
</div>
|
||||
<Paragraph style={{ marginTop: 12 }}>
|
||||
The script will:
|
||||
@ -517,7 +517,7 @@ export default function SmsSetupPage() {
|
||||
<CmdLine comment="Update key and restart service" cmd={`sed -i '/SMS_API_SECRET/d' ~/.bashrc && echo 'export SMS_API_SECRET="${generatedKey}"' >> ~/.bashrc && source ~/.bashrc && sv restart sms-api`} />
|
||||
</div>
|
||||
<Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||
If <Text code>sv</Text> is not installed yet, run the full setup: <Text code copyable={{ text: `cd ~/sms-server && git pull && bash android/setup-services.sh` }}>cd ~/sms-server && git pull && bash android/setup-services.sh</Text>
|
||||
If <Text code>sv</Text> is not installed yet, run the full setup: <Text code copyable={{ text: `cd ~/sms-server && git pull && bash termux-sms/setup-services.sh` }}>cd ~/sms-server && git pull && bash termux-sms/setup-services.sh</Text>
|
||||
</Paragraph>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit d9be9c961d4ffcf32abac81fd32589abfb146fd3
|
||||
@ -92,10 +92,12 @@ Open Termux on the phone and run:
|
||||
|
||||
```bash
|
||||
# Clone the SMS server (first time only)
|
||||
pkg install -y git && git clone https://gitea.bnkops.com/admin/campaign_connector.git ~/sms-server
|
||||
pkg install -y git && git clone --depth 1 --filter=blob:none --sparse \
|
||||
https://gitea.bnkops.com/admin/changemaker.lite.git ~/sms-server \
|
||||
&& cd ~/sms-server && git sparse-checkout set termux-sms
|
||||
|
||||
# Run the setup script — paste your API key at the end
|
||||
bash ~/sms-server/android/setup.sh YOUR_API_KEY_HERE
|
||||
bash ~/sms-server/termux-sms/setup.sh YOUR_API_KEY_HERE
|
||||
```
|
||||
|
||||
The setup script automatically:
|
||||
@ -113,7 +115,7 @@ When done, note the **Phone URL** displayed (e.g. `http://100.64.0.5:5001`).
|
||||
After initial setup, install `termux-services` for reliable process management. This uses runit, a proper UNIX service supervisor that automatically restarts the server if it crashes:
|
||||
|
||||
```bash
|
||||
cd ~/sms-server && bash android/setup-services.sh
|
||||
cd ~/sms-server && bash termux-sms/setup-services.sh
|
||||
```
|
||||
|
||||
This registers two supervised services:
|
||||
|
||||
117
termux-sms/app.py
Normal file
117
termux-sms/app.py
Normal file
@ -0,0 +1,117 @@
|
||||
from flask import Flask, jsonify, render_template_string
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template_string('''
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SMS Campaign Manager - Android Monitor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||
.gradient-bg { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
|
||||
.nav-link:hover { transform: translateY(-1px); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<!-- Header -->
|
||||
<div class="gradient-bg text-white p-6 mb-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">📊 Android Monitor</h1>
|
||||
<p class="text-green-100 text-lg">SMS Campaign Manager • Android Interface</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a href="http://localhost:5000/"
|
||||
class="nav-link bg-white/20 px-4 py-2 rounded-full text-white no-underline hover:bg-white/30 transition-all">
|
||||
🏠 Homelab
|
||||
</a>
|
||||
<a href="http://10.0.0.193:5001"
|
||||
class="nav-link bg-white/20 px-4 py-2 rounded-full text-white no-underline hover:bg-white/30 transition-all">
|
||||
📡 SMS API
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-6">
|
||||
<!-- Status Card -->
|
||||
<div class="card p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-800 mb-4">✅ Server Status</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||
<div class="text-sm text-green-600 font-medium">Flask Server</div>
|
||||
<div class="text-lg font-bold text-green-700">Active</div>
|
||||
<div class="text-xs text-green-600">10.0.0.193:5000</div>
|
||||
</div>
|
||||
<div class="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<div class="text-sm text-blue-600 font-medium">Environment</div>
|
||||
<div class="text-lg font-bold text-blue-700">Termux</div>
|
||||
<div class="text-xs text-blue-600">Android Runtime</div>
|
||||
</div>
|
||||
<div class="bg-purple-50 p-4 rounded-lg border border-purple-200">
|
||||
<div class="text-sm text-purple-600 font-medium">SMS API</div>
|
||||
<div class="text-lg font-bold text-purple-700">Ready</div>
|
||||
<div class="text-xs text-purple-600">Port 5001</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Tests Card -->
|
||||
<div class="card p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-800 mb-4">🧪 Termux API Tests</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<a href="/battery" class="block p-4 bg-yellow-50 rounded-lg border border-yellow-200 hover:bg-yellow-100 transition-colors no-underline">
|
||||
<div class="text-lg">🔋 Battery Status</div>
|
||||
<div class="text-sm text-gray-600 mt-1">Check device battery level and health</div>
|
||||
</a>
|
||||
<a href="/notification" class="block p-4 bg-blue-50 rounded-lg border border-blue-200 hover:bg-blue-100 transition-colors no-underline">
|
||||
<div class="text-lg">🔔 Test Notification</div>
|
||||
<div class="text-sm text-gray-600 mt-1">Send a test system notification</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
@app.route('/battery')
|
||||
def battery():
|
||||
try:
|
||||
result = subprocess.run(['termux-battery-status'], capture_output=True, text=True)
|
||||
battery_data = json.loads(result.stdout)
|
||||
return f"""
|
||||
<h2>🔋 Battery Status</h2>
|
||||
<pre>{json.dumps(battery_data, indent=2)}</pre>
|
||||
<p><a href='/'>← Back</a></p>
|
||||
"""
|
||||
except Exception as e:
|
||||
return f"<h2>Error</h2><pre>{str(e)}</pre><p><a href='/'>← Back</a></p>"
|
||||
|
||||
@app.route('/notification')
|
||||
def notification():
|
||||
try:
|
||||
subprocess.run(['termux-notification', '--title', 'Flask Test', '--content', 'Hello from SMS Campaign Manager!'], capture_output=True, text=True)
|
||||
return f"""
|
||||
<h2>🔔 Notification Sent!</h2>
|
||||
<p>Check your Android notifications.</p>
|
||||
<p><a href='/'>← Back</a></p>
|
||||
"""
|
||||
except Exception as e:
|
||||
return f"<h2>Error</h2><pre>{str(e)}</pre><p><a href='/'>← Back</a></p>"
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🚀 Starting SMS Campaign Manager on Termux...")
|
||||
print("📱 Device IP: 10.0.0.193")
|
||||
print("🌐 Access from Ubuntu: http://10.0.0.193:5000")
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
58
termux-sms/network-monitor.sh
Executable file
58
termux-sms/network-monitor.sh
Executable file
@ -0,0 +1,58 @@
|
||||
# Network monitor and auto-service starter
|
||||
# Monitors for home network connection and starts services
|
||||
|
||||
HOME_NETWORK_SSID="The Bunker V3" # Replace with your home network name
|
||||
HOME_NETWORK_IP_RANGE="10.0.0" # Your home network IP prefix
|
||||
LOG_FILE="$HOME/logs/network-monitor.log"
|
||||
SERVICES_STARTED=false
|
||||
|
||||
# Ensure logs directory exists
|
||||
mkdir -p ~/logs
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
check_home_network() {
|
||||
# Check if connected to home network by IP range
|
||||
current_ip=$(ifconfig 2>/dev/null | grep -A1 wlan0 | grep inet | awk '{print $2}' | cut -d: -f2)
|
||||
|
||||
if [[ "$current_ip" == $HOME_NETWORK_IP_RANGE* ]]; then
|
||||
return 0 # On home network
|
||||
else
|
||||
return 1 # Not on home network
|
||||
fi
|
||||
}
|
||||
|
||||
start_services() {
|
||||
if [ "$SERVICES_STARTED" = false ]; then
|
||||
log "🏠 Home network detected - starting services..."
|
||||
~/bin/start-all-services.sh >> "$LOG_FILE" 2>&1
|
||||
SERVICES_STARTED=true
|
||||
log "✅ Services startup completed"
|
||||
fi
|
||||
}
|
||||
|
||||
stop_services() {
|
||||
if [ "$SERVICES_STARTED" = true ]; then
|
||||
log "🌍 Left home network - stopping services..."
|
||||
pkill -f "termux-sms-api-server.py" 2>/dev/null
|
||||
pkill -f "python app.py" 2>/dev/null
|
||||
SERVICES_STARTED=false
|
||||
log "⏹️ Services stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
log "📡 Network monitor started"
|
||||
|
||||
# Main monitoring loop
|
||||
while true; do
|
||||
if check_home_network; then
|
||||
start_services
|
||||
else
|
||||
stop_services
|
||||
fi
|
||||
|
||||
# Check every 30 seconds
|
||||
sleep 30
|
||||
done
|
||||
8
termux-sms/services/sms-api/log/run
Normal file
8
termux-sms/services/sms-api/log/run
Normal file
@ -0,0 +1,8 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# runit log service for sms-api
|
||||
# Captures stdout/stderr via svlogd (automatic rotation)
|
||||
#
|
||||
|
||||
mkdir -p "$HOME/logs/sms-api-sv"
|
||||
exec svlogd -tt "$HOME/logs/sms-api-sv"
|
||||
19
termux-sms/services/sms-api/run
Normal file
19
termux-sms/services/sms-api/run
Normal file
@ -0,0 +1,19 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# runit service: sms-api
|
||||
# Runs the Flask SMS API server under termux-services supervision.
|
||||
# Install: ln -s ~/sms-server/termux-sms/services/sms-api $PREFIX/var/service/
|
||||
#
|
||||
|
||||
# Source environment (API key, etc.)
|
||||
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
||||
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$HOME/logs"
|
||||
|
||||
# Acquire wake lock (idempotent — safe to call multiple times)
|
||||
termux-wake-lock 2>/dev/null
|
||||
|
||||
# exec replaces the shell with python so runit tracks the real PID
|
||||
exec python "$HOME/sms-server/termux-sms/termux-sms-api-server.py" \
|
||||
>> "$HOME/logs/sms-api.log" 2>&1
|
||||
19
termux-sms/services/sshd/run
Normal file
19
termux-sms/services/sshd/run
Normal file
@ -0,0 +1,19 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# runit service: sshd
|
||||
# Keeps SSH daemon running so the server can manage the phone remotely.
|
||||
# Install: ln -s ~/sms-server/termux-sms/services/sshd $PREFIX/var/service/
|
||||
#
|
||||
|
||||
# Generate host keys if missing
|
||||
if [ ! -f "$PREFIX/etc/ssh/ssh_host_rsa_key" ]; then
|
||||
ssh-keygen -A
|
||||
fi
|
||||
|
||||
# Kill any standalone sshd first so we can bind the port
|
||||
# (only kills the listener, not active sessions)
|
||||
pkill -x sshd 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
# sshd -D = foreground (required for runit), -e = log to stderr
|
||||
exec sshd -D -e 2>&1
|
||||
151
termux-sms/setup-api-key.sh
Executable file
151
termux-sms/setup-api-key.sh
Executable file
@ -0,0 +1,151 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# Termux SMS API Server - Security Setup Script
|
||||
# Generates and configures the required SMS_API_SECRET environment variable
|
||||
#
|
||||
|
||||
# Color codes for pretty output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Banner
|
||||
clear
|
||||
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}🔐 Termux SMS API Server - Security Setup${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if Python is available
|
||||
if ! command -v python &> /dev/null; then
|
||||
echo -e "${RED}❌ ERROR: Python is not installed${NC}"
|
||||
echo -e "${YELLOW}Please install Python: pkg install python${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📝 Step 1: Generating secure API key...${NC}"
|
||||
echo ""
|
||||
|
||||
# Generate the API key
|
||||
API_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
|
||||
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo -e "${RED}❌ Failed to generate API key${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Secure API key generated successfully!${NC}"
|
||||
echo ""
|
||||
|
||||
# Determine shell config file
|
||||
SHELL_RC=""
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
SHELL_RC="$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.zshrc" ]; then
|
||||
SHELL_RC="$HOME/.zshrc"
|
||||
else
|
||||
SHELL_RC="$HOME/.bashrc"
|
||||
touch "$SHELL_RC"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📝 Step 2: Saving to ${SHELL_RC}...${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if SMS_API_SECRET already exists in config
|
||||
if grep -q "SMS_API_SECRET" "$SHELL_RC" 2>/dev/null; then
|
||||
echo -e "${YELLOW}⚠️ SMS_API_SECRET already exists in ${SHELL_RC}${NC}"
|
||||
echo -e "${YELLOW}Do you want to replace it? (y/n)${NC}"
|
||||
read -r response
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
# Remove old entry
|
||||
sed -i '/export SMS_API_SECRET=/d' "$SHELL_RC"
|
||||
echo "export SMS_API_SECRET=\"$API_KEY\"" >> "$SHELL_RC"
|
||||
echo -e "${GREEN}✅ Updated existing API key${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⏭️ Skipping - keeping existing key${NC}"
|
||||
# Use existing key for display
|
||||
API_KEY=$(grep "SMS_API_SECRET" "$SHELL_RC" | cut -d'"' -f2)
|
||||
fi
|
||||
else
|
||||
# Add new entry
|
||||
echo "" >> "$SHELL_RC"
|
||||
echo "# Termux SMS API Server Authentication" >> "$SHELL_RC"
|
||||
echo "export SMS_API_SECRET=\"$API_KEY\"" >> "$SHELL_RC"
|
||||
echo -e "${GREEN}✅ API key saved to ${SHELL_RC}${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📝 Step 3: Activating in current session...${NC}"
|
||||
echo ""
|
||||
|
||||
# Export for current session
|
||||
export SMS_API_SECRET="$API_KEY"
|
||||
|
||||
echo -e "${GREEN}✅ API key activated${NC}"
|
||||
echo ""
|
||||
|
||||
# Display summary box
|
||||
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}🎉 Setup Complete!${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╠════════════════════════════════════════════════════════════════════════╣${NC}"
|
||||
echo -e "${CYAN}║${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}Your API Key:${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${GREEN}${API_KEY}${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}Saved to:${NC} ${YELLOW}${SHELL_RC}${NC}"
|
||||
# Pad the line to align with box
|
||||
printf "${CYAN}║${NC}\n"
|
||||
echo -e "${CYAN}║${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}Status:${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${GREEN}✅ Active in current session${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${GREEN}✅ Will persist across restarts${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# Next steps
|
||||
echo -e "${BOLD}📋 Next Steps:${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}1.${NC} ${BOLD}Copy this key to your Ubuntu homelab .env file:${NC}"
|
||||
echo -e " ${CYAN}TERMUX_API_KEY=${API_KEY}${NC}"
|
||||
echo -e " ${CYAN}SMS_API_SECRET=${API_KEY}${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}2.${NC} ${BOLD}Start the SMS API server:${NC}"
|
||||
echo -e " ${CYAN}python ~/projects/sms-campaign-manager/android/termux-sms-api-server.py${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}3.${NC} ${BOLD}Or use the service manager:${NC}"
|
||||
echo -e " ${CYAN}~/bin/sms-service.sh start${NC}"
|
||||
echo ""
|
||||
|
||||
# Verification section
|
||||
echo -e "${BOLD}🔍 Verification:${NC}"
|
||||
echo ""
|
||||
echo -e " Check if key is set: ${CYAN}echo \$SMS_API_SECRET${NC}"
|
||||
echo -e " Expected output: ${GREEN}${API_KEY}${NC}"
|
||||
echo ""
|
||||
|
||||
# Test the environment variable
|
||||
CURRENT_VALUE="${SMS_API_SECRET}"
|
||||
if [ "$CURRENT_VALUE" == "$API_KEY" ]; then
|
||||
echo -e "${GREEN}✅ Verification passed - API key is correctly set!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Note: New terminal sessions will have the key automatically loaded${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}${BOLD} Setup complete! Your SMS API server is now secure. 🔒${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Optional: Save key to a file for easy copying (secured with permissions)
|
||||
KEY_FILE="$HOME/.sms-api-key"
|
||||
echo "$API_KEY" > "$KEY_FILE"
|
||||
chmod 600 "$KEY_FILE"
|
||||
|
||||
echo -e "${BLUE}💾 API key also saved to: ${KEY_FILE} (readable only by you)${NC}"
|
||||
echo ""
|
||||
94
termux-sms/setup-services.sh
Normal file
94
termux-sms/setup-services.sh
Normal file
@ -0,0 +1,94 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# One-time setup: install termux-services and register sms-api + sshd
|
||||
# Run this ON THE PHONE in Termux.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${CYAN}╔═══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}SMS Server — Service Setup${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚═══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# 1. Install termux-services (provides runit supervisor)
|
||||
echo -e "${YELLOW}[1/5]${NC} Installing termux-services..."
|
||||
pkg install -y termux-services 2>/dev/null || {
|
||||
echo -e "${RED}Failed to install termux-services${NC}"
|
||||
exit 1
|
||||
}
|
||||
echo -e "${GREEN} OK${NC}"
|
||||
|
||||
# 2. Source sv helper (adds sv command to PATH)
|
||||
echo -e "${YELLOW}[2/5]${NC} Loading service helpers..."
|
||||
source "$PREFIX/etc/profile.d/start-services.sh" 2>/dev/null || true
|
||||
echo -e "${GREEN} OK${NC}"
|
||||
|
||||
# 3. Make run scripts executable
|
||||
echo -e "${YELLOW}[3/5]${NC} Setting permissions..."
|
||||
SVC_DIR="$HOME/sms-server/termux-sms/services"
|
||||
chmod +x "$SVC_DIR/sms-api/run"
|
||||
chmod +x "$SVC_DIR/sms-api/log/run"
|
||||
chmod +x "$SVC_DIR/sshd/run"
|
||||
echo -e "${GREEN} OK${NC}"
|
||||
|
||||
# 4. Symlink services into runit service directory
|
||||
echo -e "${YELLOW}[4/5]${NC} Registering services with runit..."
|
||||
mkdir -p "$PREFIX/var/service"
|
||||
|
||||
# Remove old symlinks if they exist
|
||||
rm -f "$PREFIX/var/service/sms-api" 2>/dev/null
|
||||
rm -f "$PREFIX/var/service/sshd-custom" 2>/dev/null
|
||||
|
||||
ln -s "$SVC_DIR/sms-api" "$PREFIX/var/service/sms-api"
|
||||
ln -s "$SVC_DIR/sshd" "$PREFIX/var/service/sshd-custom"
|
||||
echo -e "${GREEN} OK${NC}"
|
||||
|
||||
# 5. Kill old watchdog and standalone processes
|
||||
echo -e "${YELLOW}[5/5]${NC} Cleaning up old processes..."
|
||||
pkill -f "sms-watchdog.sh" 2>/dev/null || true
|
||||
pkill -f "termux-sms-api-server.py" 2>/dev/null || true
|
||||
# Don't kill sshd — runit will take it over
|
||||
echo -e "${GREEN} OK${NC}"
|
||||
|
||||
# Acquire wake lock
|
||||
termux-wake-lock 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}${BOLD} Setup complete!${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "${BOLD}Services registered:${NC}"
|
||||
echo -e " ${GREEN}sms-api${NC} — Flask SMS API server (port 5001)"
|
||||
echo -e " ${GREEN}sshd-custom${NC} — SSH daemon (port 8022)"
|
||||
echo ""
|
||||
echo -e "${BOLD}Management commands:${NC}"
|
||||
echo -e " ${CYAN}sv status sms-api${NC} — Check status"
|
||||
echo -e " ${CYAN}sv restart sms-api${NC} — Restart"
|
||||
echo -e " ${CYAN}sv down sms-api${NC} — Stop"
|
||||
echo -e " ${CYAN}sv up sms-api${NC} — Start"
|
||||
echo -e " ${CYAN}cat ~/logs/sms-api.log${NC} — View logs"
|
||||
echo ""
|
||||
echo -e "${BOLD}runit will automatically restart services if they crash.${NC}"
|
||||
echo ""
|
||||
|
||||
# Wait a moment for runit to pick up the new services
|
||||
sleep 3
|
||||
|
||||
# Show status
|
||||
echo -e "${BOLD}Current status:${NC}"
|
||||
sv status sms-api 2>/dev/null || echo " sms-api: starting..."
|
||||
sv status sshd-custom 2>/dev/null || echo " sshd-custom: starting..."
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Health check:${NC}"
|
||||
sleep 2
|
||||
curl -s http://127.0.0.1:5001/health && echo "" || echo -e "${YELLOW} Server still starting — wait a few seconds and retry${NC}"
|
||||
193
termux-sms/setup.sh
Executable file
193
termux-sms/setup.sh
Executable file
@ -0,0 +1,193 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# Changemaker SMS Server — One-Command Phone Setup
|
||||
#
|
||||
# Usage:
|
||||
# bash setup.sh YOUR_API_KEY
|
||||
#
|
||||
# This script:
|
||||
# 1. Installs required packages (python, termux-api, flask)
|
||||
# 2. Saves the API key to ~/.bashrc
|
||||
# 3. Requests SMS & Contacts permissions
|
||||
# 4. Sets up Termux:Boot auto-start (if installed)
|
||||
# 5. Starts the SMS server
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
step() { echo -e "\n${CYAN}[$1/6]${NC} ${BOLD}$2${NC}"; }
|
||||
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
||||
fail() { echo -e " ${RED}✗${NC} $1"; exit 1; }
|
||||
|
||||
# --- Banner ---
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD} Changemaker SMS Server — Phone Setup${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
|
||||
|
||||
# --- Validate API key ---
|
||||
API_KEY="$1"
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo ""
|
||||
echo -e "${RED}Usage: bash setup.sh YOUR_API_KEY${NC}"
|
||||
echo ""
|
||||
echo "Get the API key from the admin dashboard:"
|
||||
echo " SMS Setup → Step 3 → Generate API Key → Copy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ${#API_KEY} -lt 32 ]; then
|
||||
fail "API key looks too short (expected 64 hex characters). Check you copied the full key."
|
||||
fi
|
||||
|
||||
# --- Step 1: Install packages ---
|
||||
step 1 "Installing packages"
|
||||
|
||||
pkg update -y 2>/dev/null || true
|
||||
pkg install -y python git termux-api openssh 2>/dev/null
|
||||
|
||||
if ! command -v python &>/dev/null; then
|
||||
fail "Python failed to install"
|
||||
fi
|
||||
ok "python, git, termux-api, openssh installed"
|
||||
|
||||
if ! pip show flask &>/dev/null; then
|
||||
pip install flask 2>/dev/null
|
||||
fi
|
||||
ok "Flask installed"
|
||||
|
||||
# --- Step 2: Save API key ---
|
||||
step 2 "Saving API key"
|
||||
|
||||
export SMS_API_SECRET="$API_KEY"
|
||||
|
||||
SHELL_RC="$HOME/.bashrc"
|
||||
[ -f "$HOME/.zshrc" ] && ! [ -f "$HOME/.bashrc" ] && SHELL_RC="$HOME/.zshrc"
|
||||
|
||||
# Remove any existing SMS_API_SECRET lines
|
||||
if grep -q "SMS_API_SECRET" "$SHELL_RC" 2>/dev/null; then
|
||||
sed -i '/SMS_API_SECRET/d' "$SHELL_RC"
|
||||
warn "Replaced existing API key"
|
||||
fi
|
||||
|
||||
echo "" >> "$SHELL_RC"
|
||||
echo "# Changemaker SMS Server" >> "$SHELL_RC"
|
||||
echo "export SMS_API_SECRET=\"$API_KEY\"" >> "$SHELL_RC"
|
||||
ok "Key saved to $SHELL_RC"
|
||||
ok "Key active in current session"
|
||||
|
||||
# --- Step 3: Grant permissions ---
|
||||
step 3 "Requesting Android permissions"
|
||||
echo -e " ${YELLOW}Tap 'Allow' if Android shows permission prompts${NC}"
|
||||
|
||||
# These trigger permission dialogs; capture output to suppress noise
|
||||
termux-sms-list -l 1 >/dev/null 2>&1 && ok "SMS permission granted" || warn "SMS permission — check Android Settings → Termux:API → Permissions"
|
||||
termux-contact-list >/dev/null 2>&1 && ok "Contacts permission granted" || warn "Contacts permission — check Android Settings → Termux:API → Permissions"
|
||||
termux-battery-status >/dev/null 2>&1 && ok "Battery status accessible" || true
|
||||
|
||||
# --- Step 4: Create logs directory ---
|
||||
step 4 "Setting up directories"
|
||||
|
||||
mkdir -p ~/logs
|
||||
ok "~/logs created"
|
||||
|
||||
# --- Step 5: Install service supervisor ---
|
||||
step 5 "Installing termux-services (runit)"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
pkg install -y termux-services 2>/dev/null
|
||||
ok "termux-services (runit) installed"
|
||||
|
||||
# Source runit helpers
|
||||
source "$PREFIX/etc/profile.d/start-services.sh" 2>/dev/null || true
|
||||
|
||||
# Make run scripts executable
|
||||
chmod +x "$SCRIPT_DIR/services/sms-api/run" 2>/dev/null || true
|
||||
chmod +x "$SCRIPT_DIR/services/sms-api/log/run" 2>/dev/null || true
|
||||
chmod +x "$SCRIPT_DIR/services/sshd/run" 2>/dev/null || true
|
||||
|
||||
# Register services with runit
|
||||
mkdir -p "$PREFIX/var/service"
|
||||
rm -f "$PREFIX/var/service/sms-api" 2>/dev/null
|
||||
rm -f "$PREFIX/var/service/sshd-custom" 2>/dev/null
|
||||
ln -s "$SCRIPT_DIR/services/sms-api" "$PREFIX/var/service/sms-api"
|
||||
ln -s "$SCRIPT_DIR/services/sshd" "$PREFIX/var/service/sshd-custom"
|
||||
ok "sms-api and sshd-custom services registered"
|
||||
|
||||
# Set up Termux:Boot auto-start
|
||||
if dpkg -l 2>/dev/null | grep -q termux-boot 2>/dev/null || [ -d "$HOME/.termux/boot" ]; then
|
||||
mkdir -p ~/.termux/boot
|
||||
cp "$SCRIPT_DIR/termux-boot-start.sh" ~/.termux/boot/start-sms-server 2>/dev/null || {
|
||||
cat > ~/.termux/boot/start-sms-server << 'BOOT'
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
termux-wake-lock
|
||||
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
||||
. "$PREFIX/etc/profile.d/start-services.sh" 2>/dev/null || true
|
||||
BOOT
|
||||
}
|
||||
chmod +x ~/.termux/boot/start-sms-server
|
||||
ok "Boot script installed (auto-start on reboot)"
|
||||
else
|
||||
warn "Termux:Boot not detected — install from F-Droid for auto-start on reboot"
|
||||
fi
|
||||
|
||||
# --- Step 6: Start the server ---
|
||||
step 6 "Starting SMS server"
|
||||
|
||||
# Kill any old watchdog/standalone instances
|
||||
pkill -f sms-watchdog.sh 2>/dev/null || true
|
||||
pkill -f termux-sms-api-server.py 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Acquire wake lock
|
||||
termux-wake-lock 2>/dev/null || true
|
||||
|
||||
# runit auto-starts services — just wait for it
|
||||
sleep 3
|
||||
|
||||
# Check if it's running
|
||||
if curl -s --max-time 5 http://127.0.0.1:5001/health >/dev/null 2>&1; then
|
||||
ok "Server is running via runit!"
|
||||
sv status sms-api 2>/dev/null || true
|
||||
else
|
||||
warn "Server starting up... check: sv status sms-api"
|
||||
fi
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}${BOLD} Setup complete!${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Get device IP
|
||||
DEVICE_IP=$(ip -4 addr show 2>/dev/null | grep -oP '(?<=inet\s)(?!127\.)\d+\.\d+\.\d+\.\d+' | head -1)
|
||||
if [ -n "$DEVICE_IP" ]; then
|
||||
echo -e " Phone URL: ${BOLD}http://${DEVICE_IP}:5001${NC}"
|
||||
fi
|
||||
echo -e " Health: ${BOLD}http://127.0.0.1:5001/health${NC}"
|
||||
echo -e " Status: ${BOLD}sv status sms-api${NC}"
|
||||
echo -e " Restart: ${BOLD}sv restart sms-api${NC}"
|
||||
echo -e " Logs: ${BOLD}tail -f ~/logs/sms-api.log${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Next:${NC} Go back to the admin dashboard SMS Setup wizard"
|
||||
echo -e " and enter this phone's URL in Step 2 (Connect)."
|
||||
echo ""
|
||||
|
||||
# Show Tailscale IP if available
|
||||
TS_IP=$(ip -4 addr show tailscale0 2>/dev/null | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+' || true)
|
||||
if [ -n "$TS_IP" ]; then
|
||||
echo -e " ${GREEN}Tailscale IP: ${BOLD}http://${TS_IP}:5001${NC} (recommended)"
|
||||
echo ""
|
||||
fi
|
||||
43
termux-sms/sms-service.sh
Executable file
43
termux-sms/sms-service.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# SMS API Service management script
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starting SMS API Server..."
|
||||
nohup ~/bin/start-sms-api.sh > ~/logs/sms-api-service.log 2>&1 &
|
||||
echo $! > ~/.sms-api.pid
|
||||
echo "Service started (PID: $(cat ~/.sms-api.pid))"
|
||||
;;
|
||||
stop)
|
||||
if [ -f ~/.sms-api.pid ]; then
|
||||
PID=$(cat ~/.sms-api.pid)
|
||||
kill $PID 2>/dev/null || true
|
||||
rm -f ~/.sms-api.pid
|
||||
echo "Service stopped"
|
||||
else
|
||||
echo "Service not running"
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
if [ -f ~/.sms-api.pid ]; then
|
||||
PID=$(cat ~/.sms-api.pid)
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
echo "Service running (PID: $PID)"
|
||||
else
|
||||
echo "Service dead (stale PID file)"
|
||||
rm -f ~/.sms-api.pid
|
||||
fi
|
||||
else
|
||||
echo "Service not running"
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
sleep 2
|
||||
$0 start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|status|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
78
termux-sms/sms-watchdog.sh
Executable file
78
termux-sms/sms-watchdog.sh
Executable file
@ -0,0 +1,78 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# SMS API Server Watchdog
|
||||
# Monitors the Flask server and restarts it if it crashes.
|
||||
# Run this from ~/.termux/boot/start-sms-server or manually.
|
||||
#
|
||||
|
||||
SERVER_SCRIPT="$HOME/sms-server/android/termux-sms-api-server.py"
|
||||
LOG_DIR="$HOME/logs"
|
||||
LOG_FILE="$LOG_DIR/sms-api.log"
|
||||
PID_FILE="$LOG_DIR/sms-api.pid"
|
||||
CHECK_INTERVAL=30 # seconds between health checks
|
||||
|
||||
# Source environment variables (SMS_API_SECRET etc.)
|
||||
# Non-interactive shells (nohup, boot scripts) don't source .bashrc automatically
|
||||
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') [watchdog] $1" >> "$LOG_FILE"
|
||||
echo "[watchdog] $1"
|
||||
}
|
||||
|
||||
start_server() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
local old_pid
|
||||
old_pid=$(cat "$PID_FILE")
|
||||
if kill -0 "$old_pid" 2>/dev/null; then
|
||||
log "Server already running (PID $old_pid)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Starting SMS API server..."
|
||||
nohup python "$SERVER_SCRIPT" >> "$LOG_FILE" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
log "Server started (PID $!)"
|
||||
sleep 3 # Give it time to start
|
||||
}
|
||||
|
||||
check_health() {
|
||||
curl -s --max-time 5 http://127.0.0.1:5001/health > /dev/null 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
# Acquire wake lock to prevent Android from killing us
|
||||
termux-wake-lock 2>/dev/null
|
||||
|
||||
log "Watchdog starting..."
|
||||
start_server
|
||||
|
||||
# Main watchdog loop
|
||||
while true; do
|
||||
sleep "$CHECK_INTERVAL"
|
||||
|
||||
if ! check_health; then
|
||||
log "Health check FAILED — restarting server"
|
||||
|
||||
# Kill old process if it exists
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
kill "$(cat "$PID_FILE")" 2>/dev/null
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Also kill any orphaned instances
|
||||
pkill -f "termux-sms-api-server.py" 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
start_server
|
||||
|
||||
if check_health; then
|
||||
log "Server restarted successfully"
|
||||
else
|
||||
log "Server failed to restart — will retry in ${CHECK_INTERVAL}s"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
32
termux-sms/start-all-services.sh
Executable file
32
termux-sms/start-all-services.sh
Executable file
@ -0,0 +1,32 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# Start all SMS Campaign Manager services via runit (termux-services)
|
||||
# Prerequisite: run setup-services.sh first to install and register services
|
||||
#
|
||||
|
||||
echo "Starting SMS Campaign Manager Services..."
|
||||
echo ""
|
||||
|
||||
# Acquire wake lock
|
||||
termux-wake-lock 2>/dev/null
|
||||
|
||||
# Source runit helpers
|
||||
source "$PREFIX/etc/profile.d/start-services.sh" 2>/dev/null || true
|
||||
|
||||
# Ensure services are up
|
||||
sv up sms-api 2>/dev/null && echo " sms-api: started" || echo " sms-api: failed (run setup-services.sh first)"
|
||||
sv up sshd-custom 2>/dev/null && echo " sshd-custom: started" || echo " sshd-custom: failed (run setup-services.sh first)"
|
||||
|
||||
echo ""
|
||||
|
||||
# Wait for startup
|
||||
sleep 3
|
||||
|
||||
# Status
|
||||
echo "Service Status:"
|
||||
sv status sms-api 2>/dev/null || echo " sms-api: not registered"
|
||||
sv status sshd-custom 2>/dev/null || echo " sshd-custom: not registered"
|
||||
|
||||
echo ""
|
||||
echo "Health Check:"
|
||||
curl -s http://localhost:5001/health > /dev/null && echo " SMS API: Healthy" || echo " SMS API: Not responding"
|
||||
16
termux-sms/start-monitoring.sh
Executable file
16
termux-sms/start-monitoring.sh
Executable file
@ -0,0 +1,16 @@
|
||||
# Monitoring Interface startup script
|
||||
|
||||
cd ~/projects/sms-campaign-manager
|
||||
|
||||
echo "🚀 Starting Monitoring Interface..."
|
||||
echo "📱 Device IP: $(ifconfig 2>/dev/null | grep -A1 wlan0 | grep inet | awk '{print $2}' | cut -d: -f2)"
|
||||
echo "🌐 Monitoring URL: http://$(ifconfig 2>/dev/null | grep -A1 wlan0 | grep inet | awk '{print $2}' | cut -d: -f2):5000"
|
||||
|
||||
# Kill any existing monitoring process
|
||||
pkill -f "python app.py" 2>/dev/null
|
||||
|
||||
# Start the monitoring interface in background
|
||||
nohup python app.py > ~/logs/monitoring.log 2>&1 &
|
||||
|
||||
echo "✅ Monitoring interface started (PID: $!)"
|
||||
echo "📊 Access: http://10.0.0.193:5000"
|
||||
11
termux-sms/start-sms-api.sh
Executable file
11
termux-sms/start-sms-api.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# SMS API Server startup script
|
||||
|
||||
cd ~/projects/sms-campaign-manager
|
||||
|
||||
echo "🚀 Starting SMS API Server..."
|
||||
echo "📱 Device IP: $(ifconfig 2>/dev/null | grep -A1 wlan0 | grep inet | awk '{print $2}' | cut -d: -f2)"
|
||||
echo "🌐 API URL: http://$(ifconfig 2>/dev/null | grep -A1 wlan0 | grep inet | awk '{print $2}' | cut -d: -f2):5001"
|
||||
|
||||
# Start the server
|
||||
python termux-sms-api-server.py
|
||||
16
termux-sms/termux-boot-start.sh
Normal file
16
termux-sms/termux-boot-start.sh
Normal file
@ -0,0 +1,16 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# Termux:Boot startup script
|
||||
# Install: cp this to ~/.termux/boot/start-sms-server
|
||||
# Requires: Termux:Boot app from F-Droid
|
||||
#
|
||||
|
||||
# Prevent Android from killing background processes
|
||||
termux-wake-lock
|
||||
|
||||
# Source env (API key, PATH, etc.)
|
||||
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
||||
|
||||
# Source runit helpers — this starts the runit supervisor
|
||||
# which automatically picks up all services in $PREFIX/var/service/
|
||||
. "$PREFIX/etc/profile.d/start-services.sh" 2>/dev/null || true
|
||||
746
termux-sms/termux-sms-api-server.py
Normal file
746
termux-sms/termux-sms-api-server.py
Normal file
@ -0,0 +1,746 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Termux SMS API Server
|
||||
Bridges SMS Campaign Manager (Ubuntu) with Termux API (Android)
|
||||
|
||||
This server runs on the Android device in Termux and provides REST API
|
||||
endpoints for the main SMS Campaign Manager to send messages using
|
||||
native Android SMS capabilities instead of ADB automation.
|
||||
|
||||
All endpoints require API key authentication via X-API-Key header.
|
||||
Localhost requests (127.0.0.1, ::1) are exempt for watchdog health checks.
|
||||
"""
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import hmac
|
||||
import hashlib
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
# Configure logging — ensure log directory exists before creating FileHandler
|
||||
_log_dir = os.path.expanduser('~/logs')
|
||||
os.makedirs(_log_dir, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.join(_log_dir, 'sms-api.log')),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configuration
|
||||
CONFIG = {
|
||||
'SECRET_KEY': os.environ.get('SMS_API_SECRET') or os.environ.get('TERMUX_API_KEY', ''),
|
||||
'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',
|
||||
'termux-battery-status',
|
||||
'termux-location',
|
||||
'termux-notification',
|
||||
'termux-contact-list'
|
||||
]
|
||||
}
|
||||
|
||||
class SMSApiServer:
|
||||
"""Main SMS API server class"""
|
||||
|
||||
def __init__(self):
|
||||
self.last_send_time = 0
|
||||
self.message_count = 0
|
||||
self.start_time = time.time()
|
||||
|
||||
def authenticate_request(self, request_data: str, signature: str) -> bool:
|
||||
"""Verify HMAC signature for request authentication"""
|
||||
try:
|
||||
expected_signature = hmac.new(
|
||||
CONFIG['SECRET_KEY'].encode(),
|
||||
request_data.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(signature, expected_signature)
|
||||
except Exception as e:
|
||||
logger.error(f"Authentication error: {e}")
|
||||
return False
|
||||
|
||||
def execute_termux_command(self, command: List[str]) -> Dict[str, Any]:
|
||||
"""Execute Termux API command with error handling"""
|
||||
if not command or command[0] not in CONFIG['ALLOWED_COMMANDS']:
|
||||
return {'success': False, 'error': 'Command not allowed'}
|
||||
|
||||
try:
|
||||
logger.info(f"Executing: {' '.join(command)}")
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
return {
|
||||
'success': result.returncode == 0,
|
||||
'stdout': result.stdout.strip(),
|
||||
'stderr': result.stderr.strip(),
|
||||
'return_code': result.returncode
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {'success': False, 'error': 'Command timeout'}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def get_sms_history(self, phone: Optional[str] = None, limit: int = 100) -> Dict[str, Any]:
|
||||
"""Get SMS history for a specific phone number or all messages"""
|
||||
try:
|
||||
command = ['termux-sms-list']
|
||||
if limit:
|
||||
command.extend(['-l', str(limit)])
|
||||
|
||||
result = self.execute_termux_command(command)
|
||||
|
||||
if result['success'] and result['stdout']:
|
||||
try:
|
||||
messages = json.loads(result['stdout'])
|
||||
|
||||
# Filter by phone number if specified
|
||||
if phone:
|
||||
# Clean phone number for comparison
|
||||
clean_phone = phone.replace('+', '').replace('-', '').replace(' ', '').replace('(', '').replace(')', '')
|
||||
messages = [msg for msg in messages
|
||||
if msg.get('number', '').replace('+', '').replace('-', '').replace(' ', '').replace('(', '').replace(')', '') == clean_phone]
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'messages': messages,
|
||||
'count': len(messages)
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
return {'success': False, 'error': 'Failed to parse SMS data'}
|
||||
|
||||
return {'success': False, 'error': 'Failed to retrieve SMS history'}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting SMS history: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def get_contact_name(self, phone: str) -> Optional[str]:
|
||||
"""Get contact name from phone's contact list"""
|
||||
try:
|
||||
# Use termux-contact-list command if available
|
||||
result = self.execute_termux_command(['termux-contact-list'])
|
||||
|
||||
if result['success'] and result['stdout']:
|
||||
try:
|
||||
contacts = json.loads(result['stdout'])
|
||||
clean_phone = phone.replace('+', '').replace('-', '').replace(' ', '').replace('(', '').replace(')', '')
|
||||
|
||||
for contact in contacts:
|
||||
if 'phoneNumbers' in contact:
|
||||
for phone_entry in contact['phoneNumbers']:
|
||||
contact_phone = phone_entry.get('number', '').replace('+', '').replace('-', '').replace(' ', '').replace('(', '').replace(')', '')
|
||||
if contact_phone == clean_phone:
|
||||
return contact.get('name')
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting contact name for {phone}: {e}")
|
||||
return None
|
||||
|
||||
def rate_limit_check(self) -> bool:
|
||||
"""Check if enough time has passed since last message"""
|
||||
current_time = time.time()
|
||||
if current_time - self.last_send_time < CONFIG['RATE_LIMIT_DELAY']:
|
||||
return False
|
||||
self.last_send_time = current_time
|
||||
return True
|
||||
|
||||
def send_sms(self, phone: str, message: str) -> Dict[str, Any]:
|
||||
"""Send SMS using Termux API"""
|
||||
# Input validation
|
||||
if not phone or not message:
|
||||
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']:
|
||||
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():
|
||||
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', clean_phone, message]
|
||||
result = self.execute_termux_command(command)
|
||||
|
||||
if result['success']:
|
||||
self.message_count += 1
|
||||
logger.info(f"SMS sent successfully to {phone}: {message[:50]}...")
|
||||
|
||||
# Send confirmation notification
|
||||
self.execute_termux_command([
|
||||
'termux-notification',
|
||||
'--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'],
|
||||
'error': result.get('error') or result.get('stderr'),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'phone': phone,
|
||||
'message_length': len(message),
|
||||
'total_sent': self.message_count
|
||||
}
|
||||
|
||||
# Global server instance
|
||||
sms_server = SMSApiServer()
|
||||
|
||||
def verify_api_key():
|
||||
"""Verify API key from request headers"""
|
||||
api_key = request.headers.get('X-API-Key') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
expected_key = CONFIG['SECRET_KEY']
|
||||
|
||||
if not api_key:
|
||||
return False
|
||||
|
||||
if not hmac.compare_digest(api_key, expected_key):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Global auth: every request from a remote client must provide a valid API key.
|
||||
# Localhost (127.0.0.1 / ::1) is exempt so the watchdog health check works.
|
||||
# ---------------------------------------------------------------------------
|
||||
@app.before_request
|
||||
def require_api_key():
|
||||
"""Enforce API key authentication on all endpoints for remote clients."""
|
||||
if request.remote_addr in ('127.0.0.1', '::1'):
|
||||
return None # allow localhost (watchdog health checks)
|
||||
|
||||
if not verify_api_key():
|
||||
logger.warning(f"Auth failed: {request.method} {request.path} from {request.remote_addr}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Authentication required',
|
||||
'message': 'Please provide valid API key via X-API-Key header'
|
||||
}), 401
|
||||
|
||||
return None
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""Health check endpoint"""
|
||||
uptime = time.time() - sms_server.start_time
|
||||
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'uptime_seconds': int(uptime),
|
||||
'messages_sent': sms_server.message_count,
|
||||
'version': '1.0.0'
|
||||
})
|
||||
|
||||
@app.route('/api/sms/send', methods=['POST'])
|
||||
def send_sms():
|
||||
"""Send SMS message via Termux API"""
|
||||
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
|
||||
phone = data.get('phone', '').strip()
|
||||
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)
|
||||
|
||||
# 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 endpoint error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/sms/list', methods=['GET'])
|
||||
def list_sms():
|
||||
"""List SMS messages"""
|
||||
try:
|
||||
limit = request.args.get('limit', '10')
|
||||
offset = request.args.get('offset', '0')
|
||||
|
||||
command = ['termux-sms-list', '-l', limit, '-o', offset]
|
||||
result = sms_server.execute_termux_command(command)
|
||||
|
||||
if result['success']:
|
||||
try:
|
||||
sms_data = json.loads(result['stdout']) if result['stdout'] else []
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'messages': sms_data,
|
||||
'count': len(sms_data)
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'messages': result['stdout'],
|
||||
'raw_output': True
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': result['error']}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SMS list error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/device/battery', methods=['GET'])
|
||||
def get_battery_status():
|
||||
"""Get device battery status"""
|
||||
try:
|
||||
result = sms_server.execute_termux_command(['termux-battery-status'])
|
||||
|
||||
if result['success']:
|
||||
battery_data = json.loads(result['stdout'])
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'battery': battery_data,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': result['error']}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Battery status error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/device/location', methods=['GET'])
|
||||
def get_location():
|
||||
"""Get device GPS location"""
|
||||
try:
|
||||
result = sms_server.execute_termux_command(['termux-location'])
|
||||
|
||||
if result['success'] and result['stdout']:
|
||||
try:
|
||||
location_data = json.loads(result['stdout'])
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'location': location_data,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'location': result['stdout'],
|
||||
'raw_output': True
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Location unavailable'}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Location error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/device/info', methods=['GET'])
|
||||
def get_device_info():
|
||||
"""Get general device information"""
|
||||
try:
|
||||
# Get multiple device stats
|
||||
battery_result = sms_server.execute_termux_command(['termux-battery-status'])
|
||||
|
||||
info = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'uptime': time.time() - sms_server.start_time,
|
||||
'messages_sent': sms_server.message_count,
|
||||
'api_version': '1.0.0',
|
||||
'termux_api_available': True
|
||||
}
|
||||
|
||||
if battery_result['success']:
|
||||
try:
|
||||
battery_data = json.loads(battery_result['stdout'])
|
||||
info['battery'] = battery_data
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return jsonify({'success': True, 'device_info': info})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Device info error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/sms/history', methods=['GET'])
|
||||
def get_sms_history():
|
||||
"""Get SMS history for conversation sync"""
|
||||
try:
|
||||
phone = request.args.get('phone')
|
||||
limit = request.args.get('limit', 100, type=int)
|
||||
|
||||
result = sms_server.get_sms_history(phone, limit)
|
||||
|
||||
if result['success']:
|
||||
return jsonify(result)
|
||||
else:
|
||||
return jsonify(result), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SMS history error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/sms/inbox', methods=['GET'])
|
||||
def get_sms_inbox():
|
||||
"""Get SMS inbox messages (compatibility endpoint)"""
|
||||
try:
|
||||
since = request.args.get('since', type=int)
|
||||
limit = request.args.get('limit', 100, type=int)
|
||||
|
||||
result = sms_server.get_sms_history(None, limit)
|
||||
|
||||
if result['success']:
|
||||
messages = result['messages']
|
||||
|
||||
# Filter by timestamp if 'since' parameter provided
|
||||
if since:
|
||||
filtered_messages = []
|
||||
for msg in messages:
|
||||
# Convert received timestamp to compare with since
|
||||
try:
|
||||
msg_time = datetime.strptime(msg.get('received', ''), '%Y-%m-%d %H:%M:%S').timestamp()
|
||||
if msg_time > since:
|
||||
filtered_messages.append(msg)
|
||||
except:
|
||||
# If timestamp parsing fails, include the message
|
||||
filtered_messages.append(msg)
|
||||
messages = filtered_messages
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'messages': messages,
|
||||
'count': len(messages)
|
||||
})
|
||||
else:
|
||||
return jsonify(result), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SMS inbox error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/contact/<phone>', methods=['GET'])
|
||||
def get_contact_info(phone):
|
||||
"""Get contact information for a phone number"""
|
||||
try:
|
||||
contact_name = sms_server.get_contact_name(phone)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'phone': phone,
|
||||
'name': contact_name,
|
||||
'has_name': contact_name is not None
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Contact info error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/sms/send-reply', methods=['POST'])
|
||||
def send_reply():
|
||||
"""Send a reply message with enhanced tracking"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'No JSON data provided'}), 400
|
||||
|
||||
phone = data.get('phone')
|
||||
message = data.get('message')
|
||||
conversation_id = data.get('conversation_id')
|
||||
|
||||
if not phone or not message:
|
||||
return jsonify({'success': False, 'error': 'Phone and message required'}), 400
|
||||
|
||||
# Rate limiting check
|
||||
if not sms_server.rate_limit_check():
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Rate limited. Wait {CONFIG["RATE_LIMIT_DELAY"]} seconds between messages'
|
||||
}), 429
|
||||
|
||||
# Send via termux-sms-send
|
||||
command = ['termux-sms-send', '-n', phone, message]
|
||||
result = sms_server.execute_termux_command(command)
|
||||
|
||||
if result['success']:
|
||||
sms_server.message_count += 1
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Reply sent successfully',
|
||||
'conversation_id': conversation_id,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'phone': phone,
|
||||
'message_sent': message
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Failed to send SMS: {result.get("stderr", "Unknown error")}'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Send reply error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/campaign/notify', methods=['POST'])
|
||||
def campaign_notification():
|
||||
"""Send notification about campaign status"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
title = data.get('title', 'SMS Campaign')
|
||||
message = data.get('message', 'Campaign update')
|
||||
|
||||
result = sms_server.execute_termux_command([
|
||||
'termux-notification',
|
||||
'--title', title,
|
||||
'--content', message
|
||||
])
|
||||
|
||||
return jsonify({
|
||||
'success': result['success'],
|
||||
'error': result.get('error')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Notification error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/logs/tail', methods=['GET'])
|
||||
def tail_logs():
|
||||
"""Get last N lines from the SMS API log file"""
|
||||
try:
|
||||
lines_param = request.args.get('lines', '100', type=str)
|
||||
try:
|
||||
num_lines = min(500, max(1, int(lines_param)))
|
||||
except (ValueError, TypeError):
|
||||
num_lines = 100
|
||||
|
||||
log_path = os.path.expanduser('~/logs/sms-api.log')
|
||||
|
||||
if not os.path.isfile(log_path):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'lines': [],
|
||||
'total_lines': 0,
|
||||
'file_size': 0
|
||||
})
|
||||
|
||||
file_size = os.path.getsize(log_path)
|
||||
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
all_lines = f.readlines()
|
||||
|
||||
total_lines = len(all_lines)
|
||||
tail_lines = [line.rstrip('\n') for line in all_lines[-num_lines:]]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'lines': tail_lines,
|
||||
'total_lines': total_lines,
|
||||
'file_size': file_size
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Log tail error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/contacts/test', methods=['GET'])
|
||||
def test_contacts():
|
||||
"""Test endpoint to fetch and examine raw contact list structure"""
|
||||
try:
|
||||
logger.info("Testing termux-contact-list command...")
|
||||
|
||||
result = sms_server.execute_termux_command(['termux-contact-list'])
|
||||
|
||||
if not result['success']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': result.get('error', 'Unknown error'),
|
||||
'stderr': result.get('stderr', ''),
|
||||
'return_code': result.get('return_code')
|
||||
}), 400
|
||||
|
||||
# Try to parse JSON
|
||||
raw_output = result['stdout']
|
||||
contacts_data = None
|
||||
parse_error = None
|
||||
|
||||
try:
|
||||
contacts_data = json.loads(raw_output)
|
||||
except json.JSONDecodeError as e:
|
||||
parse_error = str(e)
|
||||
|
||||
# Analyze structure
|
||||
analysis = {
|
||||
'total_contacts': len(contacts_data) if isinstance(contacts_data, list) else 'N/A',
|
||||
'is_array': isinstance(contacts_data, list),
|
||||
'sample_contact': None,
|
||||
'all_fields': set()
|
||||
}
|
||||
|
||||
if isinstance(contacts_data, list) and len(contacts_data) > 0:
|
||||
# Get first contact as sample
|
||||
analysis['sample_contact'] = contacts_data[0]
|
||||
|
||||
# Collect all unique field names across contacts
|
||||
for contact in contacts_data[:10]: # Check first 10
|
||||
if isinstance(contact, dict):
|
||||
analysis['all_fields'].update(contact.keys())
|
||||
|
||||
analysis['all_fields'] = list(analysis['all_fields'])
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'raw_output': raw_output,
|
||||
'parsed_data': contacts_data,
|
||||
'parse_error': parse_error,
|
||||
'analysis': analysis,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Contact test error: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'traceback': str(e.__traceback__)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/contacts/list', methods=['GET'])
|
||||
def list_contacts():
|
||||
"""List all contacts from phone"""
|
||||
try:
|
||||
result = sms_server.execute_termux_command(['termux-contact-list'])
|
||||
|
||||
if result['success'] and result['stdout']:
|
||||
try:
|
||||
contacts = json.loads(result['stdout'])
|
||||
|
||||
# Optional filtering
|
||||
search = request.args.get('search', '').lower()
|
||||
if search:
|
||||
contacts = [c for c in contacts
|
||||
if search in str(c.get('name', '')).lower()
|
||||
or search in str(c.get('number', '')).lower()]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'contacts': contacts,
|
||||
'count': len(contacts),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
except json.JSONDecodeError as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Failed to parse contact data: {str(e)}',
|
||||
'raw_output': result['stdout']
|
||||
}), 500
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': result.get('error', 'Failed to retrieve contacts'),
|
||||
'stderr': result.get('stderr')
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Contact list error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# Validate required configuration
|
||||
if not CONFIG['SECRET_KEY']:
|
||||
logger.critical("SECURITY ERROR: SMS_API_SECRET or TERMUX_API_KEY environment variable is required!")
|
||||
logger.critical("Set SMS_API_SECRET environment variable before starting the server")
|
||||
logger.critical("Generate a secure key with: python -c \"import secrets; print(secrets.token_hex(32))\"")
|
||||
print("\n" + "="*80)
|
||||
print("FATAL ERROR: Missing required security configuration")
|
||||
print("="*80)
|
||||
print("SMS_API_SECRET environment variable must be set for authentication.")
|
||||
print("This server cannot start without proper API key configuration.")
|
||||
print("\nTo fix this:")
|
||||
print("1. Generate a secure key: python -c \"import secrets; print(secrets.token_hex(32))\"")
|
||||
print("2. Set environment variable: export SMS_API_SECRET='your-generated-key'")
|
||||
print("3. Add to your shell profile or systemd service file")
|
||||
print("="*80 + "\n")
|
||||
exit(1)
|
||||
|
||||
logger.info("Starting Termux SMS API Server...")
|
||||
logger.info("API authentication configured (all remote endpoints protected)")
|
||||
logger.info(f"Available commands: {CONFIG['ALLOWED_COMMANDS']}")
|
||||
|
||||
# Get local IP for display (secure method without shell=True)
|
||||
try:
|
||||
import socket
|
||||
# Get IP by connecting to external host (doesn't actually send data)
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(('8.8.8.8', 80))
|
||||
local_ip = s.getsockname()[0]
|
||||
s.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not determine IP address: {e}")
|
||||
local_ip = '0.0.0.0'
|
||||
|
||||
print(f"""
|
||||
Termux SMS API Server Starting
|
||||
Device IP: {local_ip}
|
||||
API Base URL: http://{local_ip}:5001
|
||||
All endpoints require X-API-Key header (except localhost)
|
||||
|
||||
Access from server:
|
||||
curl -H "X-API-Key: $SMS_API_SECRET" http://{local_ip}:5001/health
|
||||
""")
|
||||
|
||||
app.run(host='0.0.0.0', port=5001, debug=False)
|
||||
Loading…
x
Reference in New Issue
Block a user