#!/bin/sh # SecuBox Jabber Manager - LXC Debian container with Prosody XMPP Server CONFIG="jabber" LXC_NAME="jabber" LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONF="$LXC_PATH/$LXC_NAME/config" DATA_PATH_DEFAULT="/srv/jabber" PROSODY_VERSION="0.12" OPKG_UPDATED=0 usage() { cat <<'USAGE' Usage: jabberctl Installation: install Create LXC container with Prosody XMPP server uninstall Remove container (preserves data) update Update Prosody to latest version check Run prerequisite checks Service: start Start Jabber/XMPP server (via init) stop Stop Jabber/XMPP server restart Restart Jabber/XMPP server status Show container and service status logs [N] Show last N lines of logs (default: 50) shell Open interactive shell in container Users: user add [password] Create user (e.g. user@domain) user del Delete user user passwd [password] Change password user list List all users Rooms (MUC): room create Create conference room room delete Delete conference room room list List all rooms Exposure: configure-haproxy Setup HAProxy vhost for HTTPS/WSS emancipate Full exposure (HAProxy + ACME + DNS + S2S) VoIP Integration: jingle enable Enable Jingle VoIP (XMPP calls) jingle disable Disable Jingle VoIP jingle status Show Jingle configuration sms config Configure OVH SMS relay sms send Send SMS via OVH voicemail-notify Configure Asterisk voicemail notifications Backup: backup [path] Backup database and config restore Restore from backup Internal: service-run Run container via procd service-stop Stop container USAGE } # ---------- helpers ---------- require_root() { [ "$(id -u)" -eq 0 ]; } uci_get() { local key="$1" local section="${2:-main}" uci -q get ${CONFIG}.${section}.$key } uci_set() { local key="$1" local value="$2" local section="${3:-main}" uci set ${CONFIG}.${section}.$key="$value" } log_info() { echo "[INFO] $*"; logger -t jabberctl "$*"; } log_warn() { echo "[WARN] $*"; logger -t jabberctl -p warning "$*"; } log_error() { echo "[ERROR] $*" >&2; logger -t jabberctl -p err "$*"; } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } ensure_packages() { for pkg in "$@"; do if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then if [ "$OPKG_UPDATED" -eq 0 ]; then opkg update || return 1 OPKG_UPDATED=1 fi opkg install "$pkg" || return 1 fi done } defaults() { data_path="$(uci_get data_path || echo $DATA_PATH_DEFAULT)" memory_limit="$(uci_get memory_limit || echo 512)" hostname="$(uci_get hostname server || echo jabber.local)" c2s_port="$(uci_get c2s_port server || echo 5222)" s2s_port="$(uci_get s2s_port server || echo 5269)" http_port="$(uci_get http_port server || echo 5280)" https_port="$(uci_get https_port server || echo 5281)" } detect_arch() { case "$(uname -m)" in aarch64) echo "aarch64" ;; armv7l) echo "armv7" ;; x86_64) echo "x86_64" ;; *) echo "x86_64" ;; esac } generate_password() { head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16 } # ---------- LXC helpers ---------- lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" } lxc_exists() { [ -f "$LXC_CONF" ] && [ -d "$LXC_ROOTFS" ] } lxc_exec() { lxc-attach -n "$LXC_NAME" -- "$@" } lxc_stop() { if lxc_running; then lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true sleep 2 fi } # ---------- prosodyctl wrapper ---------- prosodyctl() { lxc_exec prosodyctl "$@" } # ---------- rootfs creation ---------- lxc_create_rootfs() { local arch=$(detect_arch) # Map to Debian architecture names local debian_arch case "$arch" in aarch64) debian_arch="arm64" ;; armv7) debian_arch="armhf" ;; x86_64) debian_arch="amd64" ;; *) debian_arch="amd64" ;; esac ensure_dir "$LXC_ROOTFS" # Minimal Debian rootfs via tarball from LXC image server local rootfs_url="https://images.linuxcontainers.org/images/debian/bookworm/${debian_arch}/default/" log_info "Downloading Debian bookworm rootfs for ${debian_arch}..." # Get latest build directory local latest_path latest_path=$(wget -q -O - "$rootfs_url" 2>/dev/null | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | tail -1) if [ -z "$latest_path" ]; then log_error "Failed to find latest Debian rootfs build" return 1 fi local tarball="/tmp/debian-jabber.tar.xz" local tarball_url="${rootfs_url}${latest_path}/rootfs.tar.xz" wget -q -O "$tarball" "$tarball_url" || { log_error "Failed to download Debian rootfs from $tarball_url" return 1 } tar -xJf "$tarball" -C "$LXC_ROOTFS" || { log_error "Failed to extract Debian rootfs" return 1 } rm -f "$tarball" # DNS cp /etc/resolv.conf "$LXC_ROOTFS/etc/resolv.conf" 2>/dev/null || \ echo "nameserver 8.8.8.8" > "$LXC_ROOTFS/etc/resolv.conf" # Create minimal /dev for chroot operations mkdir -p "$LXC_ROOTFS/dev" [ -c "$LXC_ROOTFS/dev/null" ] || mknod -m 666 "$LXC_ROOTFS/dev/null" c 1 3 2>/dev/null [ -c "$LXC_ROOTFS/dev/zero" ] || mknod -m 666 "$LXC_ROOTFS/dev/zero" c 1 5 2>/dev/null [ -c "$LXC_ROOTFS/dev/random" ] || mknod -m 666 "$LXC_ROOTFS/dev/random" c 1 8 2>/dev/null [ -c "$LXC_ROOTFS/dev/urandom" ] || mknod -m 666 "$LXC_ROOTFS/dev/urandom" c 1 9 2>/dev/null # Configure apt sources cat > "$LXC_ROOTFS/etc/apt/sources.list" <<'SOURCES' deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware SOURCES # Install Prosody XMPP server log_info "Installing Prosody XMPP server..." chroot "$LXC_ROOTFS" /bin/sh -c " export DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get install -y --no-install-recommends \ prosody \ prosody-modules \ lua-sec \ lua-event \ lua-dbi-sqlite3 \ lua-zlib \ ca-certificates \ openssl \ procps " || { log_error "Failed to install Prosody" return 1 } # Create directories mkdir -p "$LXC_ROOTFS/var/lib/prosody" mkdir -p "$LXC_ROOTFS/var/log/prosody" mkdir -p "$LXC_ROOTFS/etc/prosody/conf.d" mkdir -p "$LXC_ROOTFS/var/lib/prosody/http_upload" # Create startup script create_startup_script # Create webchat interface create_webchat # Clean up apt cache chroot "$LXC_ROOTFS" /bin/sh -c " apt-get clean rm -rf /var/lib/apt/lists/* " log_info "Rootfs created successfully" } create_startup_script() { cat > "$LXC_ROOTFS/opt/start-jabber.sh" <<'STARTUP' #!/bin/bash export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin # Get hostname from environment or default XMPP_DOMAIN="${XMPP_HOSTNAME:-jabber.local}" ADMIN_USER="${XMPP_ADMIN:-admin}" # Generate Prosody config if not exists if [ ! -f /etc/prosody/prosody.cfg.lua.configured ]; then echo "[JABBER] Generating Prosody configuration..." # Create main config FIRST (before cert generation) cat > /etc/prosody/prosody.cfg.lua </dev/null chown prosody:prosody "${XMPP_DOMAIN}.crt" "${XMPP_DOMAIN}.key" chmod 600 "${XMPP_DOMAIN}.key" touch /etc/prosody/prosody.cfg.lua.configured echo "[JABBER] Configuration generated" fi # Ensure proper permissions chown -R prosody:prosody /var/lib/prosody chown -R prosody:prosody /var/log/prosody chown -R prosody:prosody /var/www/prosody 2>/dev/null echo "[JABBER] Starting Prosody XMPP server..." # Run Prosody as prosody user in foreground exec su -s /bin/sh prosody -c "/usr/bin/prosody" STARTUP chmod +x "$LXC_ROOTFS/opt/start-jabber.sh" } create_webchat() { # Create webchat directory and Converse.js files mkdir -p "$LXC_ROOTFS/var/www/prosody" # Download Converse.js if not exists if [ ! -f "$LXC_ROOTFS/var/www/prosody/converse.min.js" ]; then log_info "Downloading Converse.js web client..." local converse_version="10.1.6" local cdn_base="https://cdn.conversejs.org/10.1.6/dist" wget -q -O "$LXC_ROOTFS/var/www/prosody/converse.min.js" \ "${cdn_base}/converse.min.js" || true wget -q -O "$LXC_ROOTFS/var/www/prosody/converse.min.css" \ "${cdn_base}/converse.min.css" || true fi # Create index.html defaults cat > "$LXC_ROOTFS/var/www/prosody/index.html" < SecuBox Chat - XMPP Web Client WEBCHAT log_info "Webchat interface created at /chat/" } lxc_create_config() { defaults local mem_bytes=$((memory_limit * 1024 * 1024)) ensure_dir "$LXC_PATH/$LXC_NAME" ensure_dir "$data_path" ensure_dir "$data_path/data" ensure_dir "$data_path/certs" ensure_dir "$data_path/webchat" cat > "$LXC_CONF" </dev/null || true fi local lan_ip=$(uci -q get network.lan.ipaddr || echo '192.168.255.1') log_info "" log_info "==============================================" log_info " Jabber/XMPP Server installed!" log_info "==============================================" log_info "" log_info " Domain: $hostname" log_info " C2S Port: $c2s_port (client connections)" log_info " S2S Port: $s2s_port (server federation)" log_info " HTTP/BOSH: http://${lan_ip}:$http_port/http-bind" log_info " WebSocket: ws://${lan_ip}:$http_port/xmpp-websocket" log_info " Webchat: http://${lan_ip}:$http_port/chat/" log_info "" log_info " Admin JID: ${admin_user}@${hostname}" log_info " Password: $admin_pass" log_info "" log_info " Clients: Conversations (Android), Monal (iOS)," log_info " Gajim (Desktop), Dino (Linux)," log_info " Web: http://${lan_ip}:$http_port/chat/" log_info "" log_info " Expose externally:" log_info " jabberctl emancipate xmpp.example.com" log_info "" } cmd_uninstall() { require_root || { log_error "Must run as root"; return 1; } log_info "Uninstalling Jabber/XMPP server..." # Stop and disable /etc/init.d/jabber stop 2>/dev/null /etc/init.d/jabber disable 2>/dev/null lxc_stop # Remove container but keep data rm -rf "$LXC_ROOTFS" "$LXC_CONF" uci_set enabled '0' uci commit "$CONFIG" defaults log_info "Container removed. Data preserved in $data_path" } cmd_update() { require_root || { log_error "Must run as root"; return 1; } log_info "Updating Prosody..." if lxc_running; then lxc_exec apt-get update lxc_exec apt-get upgrade -y prosody prosody-modules lxc_exec prosodyctl restart log_info "Prosody updated successfully" else log_error "Container not running" return 1 fi } cmd_check() { echo "Jabber/XMPP Prerequisites Check" echo "================================" # LXC if command -v lxc-start >/dev/null 2>&1; then echo "[OK] LXC installed" else echo "[FAIL] LXC not installed" fi # Container exists if lxc_exists; then echo "[OK] Container exists" else echo "[--] Container not created" fi # Container running if lxc_running; then echo "[OK] Container running" else echo "[--] Container not running" fi # Prosody ports defaults for port in $c2s_port $s2s_port $http_port; do if netstat -tln 2>/dev/null | grep -q ":${port} " || \ grep -q ":$(printf '%04X' $port) " /proc/net/tcp 2>/dev/null; then echo "[OK] Port $port listening" else echo "[--] Port $port not listening" fi done # Prosody process (runs as lua5.4) if lxc_running; then if lxc_exec pgrep -f "lua.*prosody" >/dev/null 2>&1; then echo "[OK] Prosody process running" else echo "[FAIL] Prosody process not running" fi fi } cmd_status() { defaults # JSON output for RPCD if [ "$1" = "--json" ]; then local running=0 local prosody_proc=0 local user_count=0 lxc_running && running=1 if [ "$running" = "1" ]; then lxc_exec pgrep -f "lua.*prosody" >/dev/null 2>&1 && prosody_proc=1 user_count=$(lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) fi cat </dev/null 2>&1 && echo " Prosody: UP" || echo " Prosody: DOWN" # User count local users=$(lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) echo "" echo "Users: $users registered" else echo "State: STOPPED" fi echo "" local lan_ip=$(uci -q get network.lan.ipaddr || echo '192.168.255.1') echo "Connection:" echo " XMPP: ${hostname}:${c2s_port}" echo " BOSH: http://${lan_ip}:${http_port}/http-bind" echo " WebSocket: ws://${lan_ip}:${http_port}/xmpp-websocket" } cmd_logs() { local lines="${1:-50}" if lxc_running; then echo "=== Prosody logs ===" lxc_exec tail -n "$lines" /var/log/prosody/prosody.log 2>/dev/null || \ echo "No Prosody logs found" else echo "Container not running" fi } cmd_shell() { if lxc_running; then lxc_exec /bin/bash || lxc_exec /bin/sh else log_error "Container not running" return 1 fi } cmd_start() { require_root || { log_error "Must run as root"; return 1; } /etc/init.d/jabber start } cmd_stop() { require_root || { log_error "Must run as root"; return 1; } /etc/init.d/jabber stop } cmd_restart() { require_root || { log_error "Must run as root"; return 1; } /etc/init.d/jabber restart } # ---------- user management ---------- cmd_user() { local subcmd="$1" shift case "$subcmd" in add) cmd_user_add "$@" ;; del|delete) cmd_user_del "$@" ;; passwd|password) cmd_user_passwd "$@" ;; list) cmd_user_list ;; *) echo "Usage: jabberctl user " return 1 ;; esac } cmd_user_add() { local jid="$1" local password="$2" [ -z "$jid" ] && { echo "Usage: jabberctl user add [password]" return 1 } lxc_running || { log_error "Container not running"; return 1; } # Parse JID local user=$(echo "$jid" | cut -d@ -f1) local domain=$(echo "$jid" | cut -d@ -f2) [ -z "$domain" ] && { defaults domain="$hostname" } [ -z "$password" ] && password=$(generate_password) prosodyctl register "$user" "$domain" "$password" log_info "User created: ${user}@${domain}" log_info "Password: $password" } cmd_user_del() { local jid="$1" [ -z "$jid" ] && { echo "Usage: jabberctl user del " return 1 } lxc_running || { log_error "Container not running"; return 1; } # Parse JID local user=$(echo "$jid" | cut -d@ -f1) local domain=$(echo "$jid" | cut -d@ -f2) [ -z "$domain" ] && { defaults domain="$hostname" } prosodyctl deluser "${user}@${domain}" log_info "User deleted: ${user}@${domain}" } cmd_user_passwd() { local jid="$1" local password="$2" [ -z "$jid" ] && { echo "Usage: jabberctl user passwd [password]" return 1 } lxc_running || { log_error "Container not running"; return 1; } # Parse JID local user=$(echo "$jid" | cut -d@ -f1) local domain=$(echo "$jid" | cut -d@ -f2) [ -z "$domain" ] && { defaults domain="$hostname" } [ -z "$password" ] && password=$(generate_password) # Delete and recreate user with new password prosodyctl deluser "${user}@${domain}" 2>/dev/null prosodyctl register "$user" "$domain" "$password" log_info "Password changed for: ${user}@${domain}" log_info "New password: $password" } cmd_user_list() { lxc_running || { log_error "Container not running"; return 1; } defaults echo "Users for $hostname:" echo "====================" # List all account files lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | while read f; do user=$(basename "$f" .dat) domain=$(echo "$f" | grep -oE '[^/]+/accounts' | cut -d/ -f1 | tr '%' '.') echo " ${user}@${domain}" done } # ---------- room management ---------- cmd_room() { local subcmd="$1" shift case "$subcmd" in create) cmd_room_create "$@" ;; delete) cmd_room_delete "$@" ;; list) cmd_room_list ;; *) echo "Usage: jabberctl room " return 1 ;; esac } cmd_room_create() { local name="$1" [ -z "$name" ] && { echo "Usage: jabberctl room create " return 1 } lxc_running || { log_error "Container not running"; return 1; } defaults local muc_host=$(uci_get host muc || echo conference) # Create room via telnet/adhoc (Prosody doesn't have CLI for this) log_info "Room: ${name}@${muc_host}.${hostname}" log_info "Rooms are created automatically when first user joins." log_info "Or create via XMPP client's room creation dialog." } cmd_room_delete() { local name="$1" [ -z "$name" ] && { echo "Usage: jabberctl room delete " return 1 } lxc_running || { log_error "Container not running"; return 1; } defaults local muc_host=$(uci_get host muc || echo conference) # Remove room data directory local room_path="/var/lib/prosody/${muc_host}%2e${hostname}/rooms/${name}.dat" lxc_exec rm -f "$room_path" 2>/dev/null log_info "Room deleted: ${name}@${muc_host}.${hostname}" } cmd_room_list() { lxc_running || { log_error "Container not running"; return 1; } defaults local muc_host=$(uci_get host muc || echo conference) echo "Rooms on ${muc_host}.${hostname}:" echo "=================================" lxc_exec find /var/lib/prosody -name "*.dat" -path "*rooms*" 2>/dev/null | while read f; do room=$(basename "$f" .dat) echo " ${room}@${muc_host}.${hostname}" done } # ---------- HAProxy integration ---------- cmd_configure_haproxy() { require_root || { log_error "Must run as root"; return 1; } defaults local domain=$(uci_get domain network) [ -z "$domain" ] && domain="$hostname" # Create backend for BOSH/WebSocket local backend_name="jabber_http" uci set haproxy.${backend_name}=backend uci set haproxy.${backend_name}.name="$backend_name" uci set haproxy.${backend_name}.mode='http' uci set haproxy.${backend_name}.balance='roundrobin' uci set haproxy.${backend_name}.enabled='1' uci set haproxy.${backend_name}.timeout_server='3600s' uci set haproxy.${backend_name}.timeout_tunnel='3600s' uci set haproxy.${backend_name}.server="jabber 127.0.0.1:${http_port} check" # Create vhost local vhost_name=$(echo "$domain" | tr '.-' '_') uci set haproxy.${vhost_name}=vhost uci set haproxy.${vhost_name}.domain="$domain" uci set haproxy.${vhost_name}.backend="$backend_name" uci set haproxy.${vhost_name}.ssl='1' uci set haproxy.${vhost_name}.ssl_redirect='1' uci set haproxy.${vhost_name}.acme='1' uci set haproxy.${vhost_name}.enabled='1' uci commit haproxy # Update network config uci_set haproxy '1' network uci_set domain "$domain" network uci commit "$CONFIG" # Regenerate and reload if command -v haproxyctl >/dev/null 2>&1; then haproxyctl generate /etc/init.d/haproxy reload fi log_info "HAProxy configured for $domain" log_info "BOSH: https://$domain/http-bind" log_info "WebSocket: wss://$domain/xmpp-websocket" } cmd_emancipate() { local domain="$1" [ -z "$domain" ] && { echo "Usage: jabberctl emancipate " return 1 } require_root || { log_error "Must run as root"; return 1; } log_info "Emancipating Jabber at $domain..." # Update hostname uci_set hostname "$domain" server uci_set domain "$domain" network uci commit "$CONFIG" # Update Prosody config if lxc_running; then # Regenerate certs for new domain lxc_exec prosodyctl cert generate "$domain" # Update config file with new domain lxc_exec sed -i "s/XMPP_HOSTNAME=.*/XMPP_HOSTNAME=$domain/" /opt/start-jabber.sh # Remove old config marker to trigger regeneration lxc_exec rm -f /etc/prosody/prosody.cfg.lua.configured # Restart to apply cmd_restart fi # Configure HAProxy cmd_configure_haproxy # Enable S2S federation uci_set enabled '1' s2s uci commit "$CONFIG" # Open firewall ports local wan_open=$(uci_get firewall_wan network) if [ "$wan_open" = "1" ]; then # C2S port uci add firewall rule uci set firewall.@rule[-1].name='Jabber-C2S' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port="${c2s_port}" uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' # S2S port uci add firewall rule uci set firewall.@rule[-1].name='Jabber-S2S' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port="${s2s_port}" uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' uci commit firewall /etc/init.d/firewall reload fi log_info "" log_info "==============================================" log_info " Jabber/XMPP Emancipated!" log_info "==============================================" log_info "" log_info " Domain: $domain" log_info " XMPP C2S: ${domain}:${c2s_port}" log_info " XMPP S2S: ${domain}:${s2s_port}" log_info " BOSH: https://$domain/http-bind" log_info " WebSocket: wss://$domain/xmpp-websocket" log_info " Webchat: https://$domain/chat/" log_info "" log_info " DNS Records needed:" log_info " A $domain -> your-ip" log_info " SRV _xmpp-client._tcp.$domain 5222" log_info " SRV _xmpp-server._tcp.$domain 5269" log_info "" } # ---------- backup/restore ---------- cmd_backup() { local backup_path="${1:-/srv/jabber/backup}" require_root || { log_error "Must run as root"; return 1; } lxc_running || { log_error "Container must be running"; return 1; } ensure_dir "$backup_path" local timestamp=$(date +%Y%m%d_%H%M%S) local backup_file="$backup_path/jabber_${timestamp}.tar.gz" log_info "Creating backup..." # Create tarball with data defaults tar -czf "$backup_file" \ -C "$data_path" data certs log_info "Backup created: $backup_file" } cmd_restore() { local backup_file="$1" [ -z "$backup_file" ] || [ ! -f "$backup_file" ] && { echo "Usage: jabberctl restore " return 1 } require_root || { log_error "Must run as root"; return 1; } log_info "Restoring from $backup_file..." # Stop container lxc_stop # Restore data defaults tar -xzf "$backup_file" -C "$data_path" # Start container cmd_start log_info "Restore complete." } # ---------- VoIP Integration (Jingle, SMS, Voicemail) ---------- cmd_jingle() { local subcmd="$1" shift case "$subcmd" in enable) cmd_jingle_enable "$@" ;; disable) cmd_jingle_disable ;; status) cmd_jingle_status ;; *) echo "Usage: jabberctl jingle " return 1 ;; esac } cmd_jingle_enable() { require_root || { log_error "Must run as root"; return 1; } lxc_running || { log_error "Container not running"; return 1; } defaults local stun_server="${1:-stun.l.google.com:19302}" local turn_server=$(uci_get turn_server jingle) local turn_user=$(uci_get turn_user jingle) local turn_password=$(uci_get turn_password jingle) log_info "Enabling Jingle VoIP support..." # Create Prosody config for external_services local jingle_conf="$LXC_ROOTFS/etc/prosody/conf.d/jingle.cfg.lua" cat > "$jingle_conf" <> "$jingle_conf" <> "$jingle_conf" <> "$jingle_conf" # Save config uci set ${CONFIG}.jingle=jingle uci set ${CONFIG}.jingle.enabled='1' uci set ${CONFIG}.jingle.stun_server="$stun_server" [ -n "$turn_server" ] && uci set ${CONFIG}.jingle.turn_server="$turn_server" [ -n "$turn_user" ] && uci set ${CONFIG}.jingle.turn_user="$turn_user" [ -n "$turn_password" ] && uci set ${CONFIG}.jingle.turn_password="$turn_password" uci commit "$CONFIG" # Reload Prosody lxc_exec prosodyctl reload log_info "Jingle VoIP enabled" log_info " STUN: $stun_server" [ -n "$turn_server" ] && log_info " TURN: $turn_server" log_info "" log_info "XMPP clients with Jingle support:" log_info " - Conversations (Android)" log_info " - Dino (Linux)" log_info " - Gajim with Jingle plugin" } cmd_jingle_disable() { require_root || { log_error "Must run as root"; return 1; } lxc_running || { log_error "Container not running"; return 1; } rm -f "$LXC_ROOTFS/etc/prosody/conf.d/jingle.cfg.lua" uci set ${CONFIG}.jingle.enabled='0' uci commit "$CONFIG" lxc_exec prosodyctl reload log_info "Jingle VoIP disabled" } cmd_jingle_status() { local enabled=$(uci_get enabled jingle || echo '0') local stun=$(uci_get stun_server jingle) local turn=$(uci_get turn_server jingle) echo "Jingle VoIP Status" echo "==================" echo " Enabled: $enabled" [ -n "$stun" ] && echo " STUN: $stun" [ -n "$turn" ] && echo " TURN: $turn" if [ -f "$LXC_ROOTFS/etc/prosody/conf.d/jingle.cfg.lua" ]; then echo "" echo "Config: /etc/prosody/conf.d/jingle.cfg.lua" fi } # ---------- SMS Relay (OVH) ---------- cmd_sms() { local subcmd="$1" shift case "$subcmd" in config) cmd_sms_config "$@" ;; send) cmd_sms_send "$@" ;; status) cmd_sms_status ;; *) echo "Usage: jabberctl sms " return 1 ;; esac } cmd_sms_config() { local sender="$1" require_root || { log_error "Must run as root"; return 1; } # Check if OVH credentials are configured in voip config local ovh_app_key=$(uci -q get voip.ovh_telephony.app_key) if [ -z "$ovh_app_key" ]; then log_error "OVH API credentials not configured" log_error "Configure via: uci set voip.ovh_telephony.app_key=..." return 1 fi # Save SMS config uci set ${CONFIG}.sms=sms_relay uci set ${CONFIG}.sms.enabled='1' uci set ${CONFIG}.sms.provider='ovh' [ -n "$sender" ] && uci set ${CONFIG}.sms.sender="$sender" uci commit "$CONFIG" # Create Prosody SMS gateway module log_info "Creating SMS relay module..." lxc_running || { log_error "Container not running"; return 1; } mkdir -p "$LXC_ROOTFS/usr/lib/prosody/modules" cat > "$LXC_ROOTFS/usr/lib/prosody/modules/mod_sms_ovh.lua" <<'SMSMOD' -- mod_sms_ovh: OVH SMS Gateway for Prosody -- Allows sending SMS via XMPP to sms@domain local st = require "util.stanza"; local http = require "socket.http"; local sha1 = require "util.hashes".sha1; local json = require "cjson.safe"; local sms_host = module:get_host(); -- OVH API credentials from environment or config local app_key = os.getenv("OVH_APP_KEY") or ""; local app_secret = os.getenv("OVH_APP_SECRET") or ""; local consumer_key = os.getenv("OVH_CONSUMER_KEY") or ""; local sms_account = os.getenv("OVH_SMS_ACCOUNT") or ""; local sender = os.getenv("OVH_SMS_SENDER") or "SecuBox"; module:hook("message/bare", function(event) local stanza = event.stanza; local to = stanza.attr.to; -- Only handle messages to sms@domain if not to or not to:match("^sms@") then return; end local body = stanza:get_child_text("body"); if not body then return true; end -- Parse: +33612345678 Message text here local phone, text = body:match("^(%+?%d+)%s+(.+)$"); if not phone or not text then local reply = st.reply(stanza):tag("body"):text("Format: +33612345678 Your message"); module:send(reply); return true; end -- Send via OVH API (simplified - real impl would need proper signing) module:log("info", "SMS to %s: %s", phone, text); -- Confirm to user local reply = st.reply(stanza):tag("body"):text("SMS sent to " .. phone); module:send(reply); return true; end); SMSMOD # Create SMS gateway component config defaults cat > "$LXC_ROOTFS/etc/prosody/conf.d/sms.cfg.lua" < " return 1 } # Use OVH API directly if [ -f "/usr/lib/secubox/voip/ovh-telephony.sh" ]; then . /usr/lib/secubox/voip/ovh-telephony.sh ovh_init || return 1 local sms_account=$(uci -q get voip.ovh_telephony.sms_account) [ -z "$sms_account" ] && { log_info "Detecting SMS account..." sms_account=$(ovh_get_sms_accounts | jsonfilter -e '@[0]' 2>/dev/null) } local sender=$(uci_get sender sms || echo "SecuBox") ovh_send_sms "$sms_account" "$sender" "$to" "$message" log_info "SMS sent to $to" else log_error "OVH telephony library not installed" log_error "Install secubox-app-voip for SMS support" return 1 fi } cmd_sms_status() { local enabled=$(uci_get enabled sms || echo '0') local sender=$(uci_get sender sms || echo 'SecuBox') echo "SMS Relay Status" echo "================" echo " Enabled: $enabled" echo " Sender: $sender" echo " Provider: OVH" # Check OVH config local ovh_key=$(uci -q get voip.ovh_telephony.app_key) if [ -n "$ovh_key" ]; then echo " OVH API: Configured" else echo " OVH API: Not configured" fi } # ---------- Voicemail Notifications ---------- cmd_voicemail_notify() { require_root || { log_error "Must run as root"; return 1; } local ami_host="${1:-127.0.0.1}" local ami_port="${2:-5038}" local notify_jid=$(uci_get notify_jid voicemail) [ -z "$notify_jid" ] && { echo "Usage: jabberctl voicemail-notify" echo "" echo "Configure notification JID first:" echo " uci set jabber.voicemail.notify_jid='admin@xchat.example.com'" echo " uci commit jabber" return 1 } log_info "Configuring voicemail notifications..." # Create AMI listener script mkdir -p "$LXC_ROOTFS/usr/local/bin" cat > "$LXC_ROOTFS/usr/local/bin/voicemail-notify.sh" <<'VMSCRIPT' #!/bin/bash # Asterisk AMI -> XMPP Voicemail Notifier AMI_HOST="${AMI_HOST:-127.0.0.1}" AMI_PORT="${AMI_PORT:-5038}" AMI_USER="${AMI_USER:-jabber}" AMI_SECRET="${AMI_SECRET:-}" NOTIFY_JID="${NOTIFY_JID:-}" connect_ami() { exec 3<>/dev/tcp/$AMI_HOST/$AMI_PORT # Login echo -e "Action: Login\r\nUsername: $AMI_USER\r\nSecret: $AMI_SECRET\r\n\r" >&3 # Subscribe to events echo -e "Action: Events\r\nEventMask: call\r\n\r" >&3 # Read events while read -r line <&3; do if [[ "$line" == "Event: VoicemailUserEntry"* ]]; then # Parse voicemail event read_vm_event fi done } read_vm_event() { local mailbox="" local newmessages="" while read -r line <&3; do [[ -z "$line" || "$line" == $'\r' ]] && break case "$line" in Mailbox:*) mailbox="${line#*: }" ;; NewMessageCount:*) newmessages="${line#*: }" ;; esac done if [ -n "$newmessages" ] && [ "$newmessages" -gt 0 ]; then send_xmpp_notification "$mailbox" "$newmessages" fi } send_xmpp_notification() { local mailbox="$1" local count="$2" prosodyctl shell <