From 9321aeb263a70bbfb8bc8444948dfc631d2c7b25 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Tue, 31 Mar 2026 11:04:14 -0600 Subject: [PATCH] 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 --- admin/src/pages/sms/SmsSetupPage.tsx | 6 +- campaign_connector | 1 - mkdocs/docs/docs/admin/broadcast/sms.md | 8 +- termux-sms/app.py | 117 ++++ termux-sms/network-monitor.sh | 58 ++ termux-sms/services/sms-api/log/run | 8 + termux-sms/services/sms-api/run | 19 + termux-sms/services/sshd/run | 19 + termux-sms/setup-api-key.sh | 151 +++++ termux-sms/setup-services.sh | 94 +++ termux-sms/setup.sh | 193 ++++++ termux-sms/sms-service.sh | 43 ++ termux-sms/sms-watchdog.sh | 78 +++ termux-sms/start-all-services.sh | 32 + termux-sms/start-monitoring.sh | 16 + termux-sms/start-sms-api.sh | 11 + termux-sms/termux-boot-start.sh | 16 + termux-sms/termux-sms-api-server.py | 746 ++++++++++++++++++++++++ 18 files changed, 1609 insertions(+), 7 deletions(-) delete mode 160000 campaign_connector create mode 100644 termux-sms/app.py create mode 100755 termux-sms/network-monitor.sh create mode 100644 termux-sms/services/sms-api/log/run create mode 100644 termux-sms/services/sms-api/run create mode 100644 termux-sms/services/sshd/run create mode 100755 termux-sms/setup-api-key.sh create mode 100644 termux-sms/setup-services.sh create mode 100755 termux-sms/setup.sh create mode 100755 termux-sms/sms-service.sh create mode 100755 termux-sms/sms-watchdog.sh create mode 100755 termux-sms/start-all-services.sh create mode 100755 termux-sms/start-monitoring.sh create mode 100755 termux-sms/start-sms-api.sh create mode 100644 termux-sms/termux-boot-start.sh create mode 100644 termux-sms/termux-sms-api-server.py diff --git a/admin/src/pages/sms/SmsSetupPage.tsx b/admin/src/pages/sms/SmsSetupPage.tsx index 051ddd71..a404163d 100644 --- a/admin/src/pages/sms/SmsSetupPage.tsx +++ b/admin/src/pages/sms/SmsSetupPage.tsx @@ -460,8 +460,8 @@ export default function SmsSetupPage() { configures auto-start, and launches the server.
- - + +
The script will: @@ -517,7 +517,7 @@ export default function SmsSetupPage() { > ~/.bashrc && source ~/.bashrc && sv restart sms-api`} /> - If sv is not installed yet, run the full setup: cd ~/sms-server && git pull && bash android/setup-services.sh + If sv is not installed yet, run the full setup: cd ~/sms-server && git pull && bash termux-sms/setup-services.sh } diff --git a/campaign_connector b/campaign_connector deleted file mode 160000 index d9be9c96..00000000 --- a/campaign_connector +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d9be9c961d4ffcf32abac81fd32589abfb146fd3 diff --git a/mkdocs/docs/docs/admin/broadcast/sms.md b/mkdocs/docs/docs/admin/broadcast/sms.md index ed09010b..1d5dc8c9 100644 --- a/mkdocs/docs/docs/admin/broadcast/sms.md +++ b/mkdocs/docs/docs/admin/broadcast/sms.md @@ -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: diff --git a/termux-sms/app.py b/termux-sms/app.py new file mode 100644 index 00000000..fa0c86bd --- /dev/null +++ b/termux-sms/app.py @@ -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(''' + + + + + SMS Campaign Manager - Android Monitor + + + + + + +
+
+
+
+

๐Ÿ“Š Android Monitor

+

SMS Campaign Manager โ€ข Android Interface

+
+ +
+
+
+ +
+ +
+

โœ… Server Status

+
+
+
Flask Server
+
Active
+
10.0.0.193:5000
+
+
+
Environment
+
Termux
+
Android Runtime
+
+
+
SMS API
+
Ready
+
Port 5001
+
+
+
+ + + + + + ''') + +@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""" +

๐Ÿ”‹ Battery Status

+
{json.dumps(battery_data, indent=2)}
+

โ† Back

+ """ + except Exception as e: + return f"

Error

{str(e)}

โ† Back

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

๐Ÿ”” Notification Sent!

+

Check your Android notifications.

+

โ† Back

+ """ + except Exception as e: + return f"

Error

{str(e)}

โ† Back

" + +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) diff --git a/termux-sms/network-monitor.sh b/termux-sms/network-monitor.sh new file mode 100755 index 00000000..476b59b4 --- /dev/null +++ b/termux-sms/network-monitor.sh @@ -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 diff --git a/termux-sms/services/sms-api/log/run b/termux-sms/services/sms-api/log/run new file mode 100644 index 00000000..5b22e372 --- /dev/null +++ b/termux-sms/services/sms-api/log/run @@ -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" diff --git a/termux-sms/services/sms-api/run b/termux-sms/services/sms-api/run new file mode 100644 index 00000000..e2c18bbd --- /dev/null +++ b/termux-sms/services/sms-api/run @@ -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 diff --git a/termux-sms/services/sshd/run b/termux-sms/services/sshd/run new file mode 100644 index 00000000..101ca0e6 --- /dev/null +++ b/termux-sms/services/sshd/run @@ -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 diff --git a/termux-sms/setup-api-key.sh b/termux-sms/setup-api-key.sh new file mode 100755 index 00000000..54a8e86c --- /dev/null +++ b/termux-sms/setup-api-key.sh @@ -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 "" diff --git a/termux-sms/setup-services.sh b/termux-sms/setup-services.sh new file mode 100644 index 00000000..aef5422e --- /dev/null +++ b/termux-sms/setup-services.sh @@ -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}" diff --git a/termux-sms/setup.sh b/termux-sms/setup.sh new file mode 100755 index 00000000..45a54b62 --- /dev/null +++ b/termux-sms/setup.sh @@ -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 diff --git a/termux-sms/sms-service.sh b/termux-sms/sms-service.sh new file mode 100755 index 00000000..f5de2ace --- /dev/null +++ b/termux-sms/sms-service.sh @@ -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 diff --git a/termux-sms/sms-watchdog.sh b/termux-sms/sms-watchdog.sh new file mode 100755 index 00000000..b60b9b5e --- /dev/null +++ b/termux-sms/sms-watchdog.sh @@ -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 diff --git a/termux-sms/start-all-services.sh b/termux-sms/start-all-services.sh new file mode 100755 index 00000000..36751fab --- /dev/null +++ b/termux-sms/start-all-services.sh @@ -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" diff --git a/termux-sms/start-monitoring.sh b/termux-sms/start-monitoring.sh new file mode 100755 index 00000000..50d5c7c7 --- /dev/null +++ b/termux-sms/start-monitoring.sh @@ -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" diff --git a/termux-sms/start-sms-api.sh b/termux-sms/start-sms-api.sh new file mode 100755 index 00000000..ecf80c0a --- /dev/null +++ b/termux-sms/start-sms-api.sh @@ -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 diff --git a/termux-sms/termux-boot-start.sh b/termux-sms/termux-boot-start.sh new file mode 100644 index 00000000..6f62f55b --- /dev/null +++ b/termux-sms/termux-boot-start.sh @@ -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 diff --git a/termux-sms/termux-sms-api-server.py b/termux-sms/termux-sms-api-server.py new file mode 100644 index 00000000..1f8d388e --- /dev/null +++ b/termux-sms/termux-sms-api-server.py @@ -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/', 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)