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:
bunker-admin 2026-03-31 11:04:14 -06:00
parent 5d15b4cffa
commit 9321aeb263
18 changed files with 1609 additions and 7 deletions

View File

@ -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

View File

@ -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
View 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
View 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

View 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"

View 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

View 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
View 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 ""

View 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
View 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
View 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
View 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

View 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
View 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
View 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

View 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

View 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)