- Fix active_calls and extensions count to output clean integers - Remove tr -cd which was causing duplicate values in JSON - Use simpler variable assignment with fallback to 0 - Prevents malformed JSON output from cmd_status() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1064 lines
24 KiB
Bash
Executable File
1064 lines
24 KiB
Bash
Executable File
#!/bin/sh
|
|
# voipctl - SecuBox VoIP PBX Control Script
|
|
# Manages Asterisk PBX in LXC container with OVH SIP trunk
|
|
|
|
set -e
|
|
|
|
CONTAINER_NAME="voip"
|
|
CONTAINER_PATH="/srv/lxc/$CONTAINER_NAME"
|
|
DATA_PATH="/srv/voip"
|
|
LIB_PATH="/usr/lib/secubox/voip"
|
|
|
|
# Source helper libraries
|
|
[ -f "$LIB_PATH/ovh-telephony.sh" ] && . "$LIB_PATH/ovh-telephony.sh"
|
|
[ -f "$LIB_PATH/asterisk-config.sh" ] && . "$LIB_PATH/asterisk-config.sh"
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
|
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
log_err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: voipctl <command> [options]
|
|
|
|
Container Management:
|
|
install Create LXC container and install Asterisk
|
|
uninstall Remove container and all data
|
|
start Start VoIP services
|
|
stop Stop VoIP services
|
|
restart Restart VoIP services
|
|
status Show status (JSON)
|
|
logs [-f] View Asterisk logs
|
|
shell Open shell in container
|
|
|
|
Extension Management:
|
|
ext add <num> <name> [password] Add extension
|
|
ext del <num> Delete extension
|
|
ext list List extensions
|
|
ext passwd <num> [password] Set extension password
|
|
|
|
Trunk Management:
|
|
trunk add ovh Auto-provision OVH SIP trunk
|
|
trunk add manual Configure trunk manually
|
|
trunk test Test trunk connectivity
|
|
trunk status Show registration status
|
|
|
|
Call Operations:
|
|
call <from> <to> Originate call (click-to-call)
|
|
hangup <channel> Hang up active call
|
|
calls List active calls
|
|
|
|
Voicemail:
|
|
vm list [ext] List voicemails
|
|
vm play <ext> <id> Play voicemail
|
|
vm delete <ext> <id> Delete voicemail
|
|
|
|
Call Recording:
|
|
rec enable Enable call recording
|
|
rec disable Disable call recording
|
|
rec status Show recording status
|
|
rec list [date] List recordings (YYYYMMDD)
|
|
rec play <file> Play recording
|
|
rec download <file> Get download path
|
|
rec delete <file> Delete recording
|
|
rec cleanup [days] Delete recordings older than N days
|
|
|
|
Configuration:
|
|
configure-haproxy Setup WebRTC proxy in HAProxy
|
|
emancipate <domain> Full exposure with SSL
|
|
reload Reload Asterisk configuration
|
|
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
# Check if container exists
|
|
container_exists() {
|
|
[ -d "$CONTAINER_PATH/rootfs" ]
|
|
}
|
|
|
|
# Check if container is running
|
|
container_running() {
|
|
lxc-info -n "$CONTAINER_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
|
}
|
|
|
|
# Execute command in container
|
|
container_exec() {
|
|
if ! container_running; then
|
|
log_err "Container not running"
|
|
return 1
|
|
fi
|
|
lxc-attach -n "$CONTAINER_NAME" -- "$@"
|
|
}
|
|
|
|
# Generate random password
|
|
gen_password() {
|
|
head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16
|
|
}
|
|
|
|
#
|
|
# Container Installation
|
|
#
|
|
cmd_install() {
|
|
if container_exists; then
|
|
log_warn "Container already exists. Use 'uninstall' first to reinstall."
|
|
return 1
|
|
fi
|
|
|
|
log_info "Creating VoIP container..."
|
|
mkdir -p "$CONTAINER_PATH" "$DATA_PATH"
|
|
|
|
# Create LXC config
|
|
cat > "$CONTAINER_PATH/config" <<EOF
|
|
lxc.uts.name = $CONTAINER_NAME
|
|
lxc.rootfs.path = dir:$CONTAINER_PATH/rootfs
|
|
lxc.include = /usr/share/lxc/config/common.conf
|
|
|
|
# Use host network for SIP/RTP
|
|
lxc.net.0.type = none
|
|
|
|
# Mount data directory
|
|
lxc.mount.entry = $DATA_PATH srv/voip none bind,create=dir 0 0
|
|
|
|
# Init with tini
|
|
lxc.init.cmd = /usr/bin/tini -- /start-voip.sh
|
|
|
|
# Capabilities for audio
|
|
lxc.cap.keep = sys_nice
|
|
EOF
|
|
|
|
log_info "Downloading Debian rootfs..."
|
|
local arch="arm64"
|
|
local release="bookworm"
|
|
local mirror="http://deb.debian.org/debian"
|
|
|
|
# Use debootstrap if available, otherwise download tarball
|
|
if command -v debootstrap >/dev/null 2>&1; then
|
|
debootstrap --arch="$arch" --variant=minbase "$release" "$CONTAINER_PATH/rootfs" "$mirror"
|
|
else
|
|
# Download pre-built rootfs
|
|
local rootfs_url="https://images.linuxcontainers.org/images/debian/$release/$arch/default/"
|
|
local latest=$(wget -qO- "$rootfs_url" | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | tail -1)
|
|
wget -O /tmp/rootfs.tar.xz "${rootfs_url}${latest}/rootfs.tar.xz"
|
|
mkdir -p "$CONTAINER_PATH/rootfs"
|
|
tar -xJf /tmp/rootfs.tar.xz -C "$CONTAINER_PATH/rootfs"
|
|
rm -f /tmp/rootfs.tar.xz
|
|
fi
|
|
|
|
log_info "Installing Asterisk packages..."
|
|
|
|
# Setup resolv.conf
|
|
cp /etc/resolv.conf "$CONTAINER_PATH/rootfs/etc/resolv.conf"
|
|
|
|
# Install packages
|
|
cat > "$CONTAINER_PATH/rootfs/tmp/setup.sh" <<'SETUP'
|
|
#!/bin/bash
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends \
|
|
asterisk \
|
|
asterisk-core-sounds-en \
|
|
asterisk-moh-opsound-wav \
|
|
asterisk-modules \
|
|
tini \
|
|
curl \
|
|
jq \
|
|
ca-certificates
|
|
|
|
# Clean up
|
|
apt-get clean
|
|
rm -rf /var/lib/apt/lists/*
|
|
|
|
# Create voip data directory
|
|
mkdir -p /srv/voip/{sounds,recordings,voicemail,logs}
|
|
SETUP
|
|
chmod +x "$CONTAINER_PATH/rootfs/tmp/setup.sh"
|
|
|
|
# Start container temporarily to run setup
|
|
lxc-start -n "$CONTAINER_NAME" -d -F -- /bin/bash /tmp/setup.sh
|
|
|
|
# Wait for setup to complete
|
|
local timeout=300
|
|
while [ $timeout -gt 0 ] && lxc-info -n "$CONTAINER_NAME" -s 2>/dev/null | grep -q "RUNNING"; do
|
|
sleep 5
|
|
timeout=$((timeout - 5))
|
|
done
|
|
|
|
# Create startup script
|
|
cat > "$CONTAINER_PATH/rootfs/start-voip.sh" <<'STARTUP'
|
|
#!/bin/bash
|
|
# VoIP container startup script
|
|
|
|
# Clean stale PID files
|
|
rm -f /var/run/asterisk/asterisk.pid 2>/dev/null
|
|
|
|
# Start Asterisk
|
|
/usr/sbin/asterisk -f -vvvg -c
|
|
STARTUP
|
|
chmod +x "$CONTAINER_PATH/rootfs/start-voip.sh"
|
|
|
|
# Generate initial Asterisk config
|
|
generate_asterisk_config
|
|
|
|
log_ok "VoIP container installed successfully"
|
|
log_info "Run 'voipctl start' to start services"
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
log_warn "This will remove the VoIP container and all data!"
|
|
echo -n "Continue? [y/N] "
|
|
read -r confirm
|
|
[ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || return 1
|
|
|
|
if container_running; then
|
|
log_info "Stopping container..."
|
|
lxc-stop -n "$CONTAINER_NAME" -k 2>/dev/null || true
|
|
fi
|
|
|
|
log_info "Removing container..."
|
|
rm -rf "$CONTAINER_PATH"
|
|
|
|
log_ok "Container removed"
|
|
log_info "Data directory preserved at $DATA_PATH"
|
|
}
|
|
|
|
cmd_start() {
|
|
if ! container_exists; then
|
|
log_err "Container not installed. Run 'voipctl install' first."
|
|
return 1
|
|
fi
|
|
|
|
if container_running; then
|
|
log_warn "Container already running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Starting VoIP container..."
|
|
lxc-start -n "$CONTAINER_NAME" -d
|
|
|
|
# Wait for Asterisk to be ready
|
|
local timeout=30
|
|
while [ $timeout -gt 0 ]; do
|
|
if container_exec asterisk -rx "core show version" >/dev/null 2>&1; then
|
|
log_ok "Asterisk started"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
timeout=$((timeout - 1))
|
|
done
|
|
|
|
log_err "Asterisk failed to start in time"
|
|
return 1
|
|
}
|
|
|
|
cmd_stop() {
|
|
if ! container_running; then
|
|
log_warn "Container not running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Stopping VoIP container..."
|
|
lxc-stop -n "$CONTAINER_NAME"
|
|
log_ok "Container stopped"
|
|
}
|
|
|
|
cmd_restart() {
|
|
cmd_stop
|
|
sleep 2
|
|
cmd_start
|
|
}
|
|
|
|
cmd_status() {
|
|
local running=0
|
|
local registered=0
|
|
local active_calls=0
|
|
local extensions=0
|
|
|
|
if container_running; then
|
|
running=1
|
|
|
|
# Check trunk registration
|
|
if container_exec asterisk -rx "pjsip show registrations" 2>/dev/null | grep -q "Registered"; then
|
|
registered=1
|
|
fi
|
|
|
|
# Count active calls
|
|
local calls_output
|
|
calls_output=$(container_exec asterisk -rx "core show channels" 2>/dev/null | grep -oE "^[0-9]+ active" | head -1 | cut -d' ' -f1) || true
|
|
active_calls=${calls_output:-0}
|
|
|
|
# Count extensions
|
|
local ext_output
|
|
ext_output=$(container_exec asterisk -rx "pjsip show endpoints" 2>/dev/null | wc -l) || true
|
|
extensions=${ext_output:-0}
|
|
fi
|
|
|
|
cat <<EOF
|
|
{
|
|
"running": $running,
|
|
"trunk_registered": $registered,
|
|
"active_calls": ${active_calls:-0},
|
|
"extensions": ${extensions:-0},
|
|
"container": "$CONTAINER_NAME",
|
|
"data_path": "$DATA_PATH"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
cmd_logs() {
|
|
local follow=""
|
|
[ "$1" = "-f" ] && follow="-f"
|
|
|
|
if [ -n "$follow" ]; then
|
|
container_exec tail -f /var/log/asterisk/messages
|
|
else
|
|
container_exec tail -100 /var/log/asterisk/messages
|
|
fi
|
|
}
|
|
|
|
cmd_shell() {
|
|
container_exec /bin/bash
|
|
}
|
|
|
|
#
|
|
# Extension Management
|
|
#
|
|
cmd_ext_add() {
|
|
local num="$1"
|
|
local name="$2"
|
|
local secret="${3:-$(gen_password)}"
|
|
|
|
[ -z "$num" ] || [ -z "$name" ] && {
|
|
log_err "Usage: voipctl ext add <number> <name> [password]"
|
|
return 1
|
|
}
|
|
|
|
# Validate extension number
|
|
if ! echo "$num" | grep -qE '^[0-9]{3,6}$'; then
|
|
log_err "Extension must be 3-6 digits"
|
|
return 1
|
|
fi
|
|
|
|
# Add to UCI
|
|
local section="ext_$num"
|
|
uci set voip.$section=extension
|
|
uci set voip.$section.name="$name"
|
|
uci set voip.$section.secret="$secret"
|
|
uci set voip.$section.context="internal"
|
|
uci set voip.$section.voicemail="1"
|
|
uci commit voip
|
|
|
|
# Regenerate Asterisk config
|
|
generate_pjsip_extensions
|
|
|
|
log_ok "Extension $num created for $name"
|
|
echo "Password: $secret"
|
|
}
|
|
|
|
cmd_ext_del() {
|
|
local num="$1"
|
|
[ -z "$num" ] && {
|
|
log_err "Usage: voipctl ext del <number>"
|
|
return 1
|
|
}
|
|
|
|
uci delete "voip.ext_$num" 2>/dev/null || {
|
|
log_err "Extension $num not found"
|
|
return 1
|
|
}
|
|
uci commit voip
|
|
|
|
generate_pjsip_extensions
|
|
log_ok "Extension $num deleted"
|
|
}
|
|
|
|
cmd_ext_list() {
|
|
echo "Extensions:"
|
|
echo "----------"
|
|
|
|
uci show voip 2>/dev/null | grep "=extension" | while read -r line; do
|
|
local section=$(echo "$line" | cut -d'.' -f2 | cut -d'=' -f1)
|
|
local num=$(echo "$section" | sed 's/ext_//')
|
|
local name=$(uci -q get "voip.$section.name")
|
|
echo " $num: $name"
|
|
done
|
|
}
|
|
|
|
cmd_ext_passwd() {
|
|
local num="$1"
|
|
local secret="${2:-$(gen_password)}"
|
|
|
|
[ -z "$num" ] && {
|
|
log_err "Usage: voipctl ext passwd <number> [password]"
|
|
return 1
|
|
}
|
|
|
|
uci set "voip.ext_$num.secret=$secret" 2>/dev/null || {
|
|
log_err "Extension $num not found"
|
|
return 1
|
|
}
|
|
uci commit voip
|
|
|
|
generate_pjsip_extensions
|
|
log_ok "Password updated for extension $num"
|
|
echo "New password: $secret"
|
|
}
|
|
|
|
#
|
|
# Trunk Management
|
|
#
|
|
cmd_trunk_add_ovh() {
|
|
log_info "Provisioning OVH SIP trunk..."
|
|
|
|
# Check OVH credentials
|
|
local app_key=$(uci -q get voip.ovh_telephony.app_key)
|
|
local app_secret=$(uci -q get voip.ovh_telephony.app_secret)
|
|
local consumer_key=$(uci -q get voip.ovh_telephony.consumer_key)
|
|
|
|
if [ -z "$app_key" ] || [ -z "$app_secret" ] || [ -z "$consumer_key" ]; then
|
|
log_err "OVH API credentials not configured"
|
|
log_info "Set credentials in /etc/config/voip under ovh_telephony section"
|
|
return 1
|
|
fi
|
|
|
|
# Fetch billing accounts
|
|
log_info "Fetching OVH telephony accounts..."
|
|
local accounts=$(ovh_api_get "/telephony")
|
|
|
|
if [ -z "$accounts" ] || [ "$accounts" = "[]" ]; then
|
|
log_err "No telephony accounts found"
|
|
return 1
|
|
fi
|
|
|
|
echo "Available billing accounts:"
|
|
echo "$accounts" | jsonfilter -e '@[*]' | nl -w2 -s'. '
|
|
|
|
echo -n "Select account number: "
|
|
read -r account_num
|
|
local billing_account=$(echo "$accounts" | jsonfilter -e "@[$((account_num-1))]")
|
|
|
|
# Fetch SIP lines
|
|
log_info "Fetching SIP lines..."
|
|
local lines=$(ovh_api_get "/telephony/$billing_account/line")
|
|
|
|
echo "Available SIP lines:"
|
|
echo "$lines" | jsonfilter -e '@[*]' | nl -w2 -s'. '
|
|
|
|
echo -n "Select line number: "
|
|
read -r line_num
|
|
local service_name=$(echo "$lines" | jsonfilter -e "@[$((line_num-1))]")
|
|
|
|
# Get SIP credentials
|
|
log_info "Fetching SIP credentials..."
|
|
local sip_accounts=$(ovh_api_get "/telephony/$billing_account/line/$service_name/sipAccounts")
|
|
local sip_username=$(echo "$sip_accounts" | jsonfilter -e '@[0]')
|
|
|
|
# Save to UCI
|
|
uci set voip.ovh_telephony.billing_account="$billing_account"
|
|
uci set voip.ovh_telephony.service_name="$service_name"
|
|
uci set voip.sip_trunk.enabled="1"
|
|
uci set voip.sip_trunk.provider="ovh"
|
|
uci set voip.sip_trunk.host="sip.ovh.net"
|
|
uci set voip.sip_trunk.username="$sip_username"
|
|
uci commit voip
|
|
|
|
log_info "Enter SIP password (from OVH manager):"
|
|
read -rs sip_password
|
|
uci set voip.sip_trunk.password="$sip_password"
|
|
uci commit voip
|
|
|
|
# Generate PJSIP config
|
|
generate_pjsip_trunk
|
|
|
|
log_ok "OVH trunk configured: $sip_username"
|
|
log_info "Run 'voipctl restart' to apply changes"
|
|
}
|
|
|
|
cmd_trunk_test() {
|
|
if ! container_running; then
|
|
log_err "Container not running"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Testing trunk registration..."
|
|
container_exec asterisk -rx "pjsip show registrations"
|
|
}
|
|
|
|
cmd_trunk_status() {
|
|
if ! container_running; then
|
|
log_err "Container not running"
|
|
return 1
|
|
fi
|
|
|
|
container_exec asterisk -rx "pjsip show registrations"
|
|
echo
|
|
container_exec asterisk -rx "pjsip show endpoints" | head -20
|
|
}
|
|
|
|
#
|
|
# Call Operations
|
|
#
|
|
cmd_call() {
|
|
local from="$1"
|
|
local to="$2"
|
|
|
|
[ -z "$from" ] || [ -z "$to" ] && {
|
|
log_err "Usage: voipctl call <from_ext> <to_number>"
|
|
return 1
|
|
}
|
|
|
|
if ! container_running; then
|
|
log_err "Container not running"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Originating call: $from -> $to"
|
|
|
|
# First ring the extension, then connect to destination
|
|
local channel=$(container_exec asterisk -rx "channel originate PJSIP/$from application Dial PJSIP/$to@ovh-trunk" 2>&1)
|
|
|
|
if echo "$channel" | grep -qi "error"; then
|
|
log_err "Failed to originate call: $channel"
|
|
return 1
|
|
fi
|
|
|
|
log_ok "Call initiated"
|
|
echo "$channel"
|
|
}
|
|
|
|
cmd_hangup() {
|
|
local channel="$1"
|
|
[ -z "$channel" ] && {
|
|
log_err "Usage: voipctl hangup <channel>"
|
|
return 1
|
|
}
|
|
|
|
container_exec asterisk -rx "channel request hangup $channel"
|
|
log_ok "Hangup requested"
|
|
}
|
|
|
|
cmd_calls() {
|
|
if ! container_running; then
|
|
log_err "Container not running"
|
|
return 1
|
|
fi
|
|
|
|
container_exec asterisk -rx "core show channels"
|
|
}
|
|
|
|
#
|
|
# Voicemail
|
|
#
|
|
cmd_vm_list() {
|
|
local ext="$1"
|
|
|
|
if [ -n "$ext" ]; then
|
|
find "$DATA_PATH/voicemail/default/$ext" -name "msg*.wav" 2>/dev/null | while read -r msg; do
|
|
local id=$(basename "$msg" .wav)
|
|
local info=$(cat "${msg%.wav}.txt" 2>/dev/null | grep -E "^(callerid|origdate)=" | tr '\n' ' ')
|
|
echo "$id: $info"
|
|
done
|
|
else
|
|
find "$DATA_PATH/voicemail/default" -type d -name "[0-9]*" 2>/dev/null | while read -r dir; do
|
|
local ext=$(basename "$dir")
|
|
local count=$(find "$dir" -name "msg*.wav" 2>/dev/null | wc -l)
|
|
echo "$ext: $count messages"
|
|
done
|
|
fi
|
|
}
|
|
|
|
cmd_vm_play() {
|
|
local ext="$1"
|
|
local id="$2"
|
|
[ -z "$ext" ] || [ -z "$id" ] && {
|
|
log_err "Usage: voipctl vm play <extension> <msg_id>"
|
|
return 1
|
|
}
|
|
|
|
local file="$DATA_PATH/voicemail/default/$ext/$id.wav"
|
|
if [ -f "$file" ]; then
|
|
# Play via aplay or just output path
|
|
if command -v aplay >/dev/null; then
|
|
aplay "$file"
|
|
else
|
|
echo "Voicemail file: $file"
|
|
fi
|
|
else
|
|
log_err "Message not found: $id"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
cmd_vm_delete() {
|
|
local ext="$1"
|
|
local id="$2"
|
|
[ -z "$ext" ] || [ -z "$id" ] && {
|
|
log_err "Usage: voipctl vm delete <extension> <msg_id>"
|
|
return 1
|
|
}
|
|
|
|
rm -f "$DATA_PATH/voicemail/default/$ext/$id".*
|
|
log_ok "Message deleted"
|
|
}
|
|
|
|
#
|
|
# Call Recording
|
|
#
|
|
RECORDINGS_PATH="$DATA_PATH/recordings"
|
|
|
|
cmd_rec_enable() {
|
|
log_info "Enabling call recording..."
|
|
uci set voip.recording=recording
|
|
uci set voip.recording.enabled="1"
|
|
uci set voip.recording.format="wav"
|
|
uci set voip.recording.retention_days="30"
|
|
uci commit voip
|
|
|
|
# Create recordings directory
|
|
mkdir -p "$RECORDINGS_PATH"
|
|
|
|
# Regenerate dialplan with recording enabled
|
|
generate_dialplan
|
|
|
|
if container_running; then
|
|
container_exec asterisk -rx "dialplan reload"
|
|
fi
|
|
|
|
log_ok "Call recording enabled"
|
|
log_info "Recordings will be saved to: $RECORDINGS_PATH"
|
|
}
|
|
|
|
cmd_rec_disable() {
|
|
log_info "Disabling call recording..."
|
|
uci set voip.recording.enabled="0"
|
|
uci commit voip
|
|
|
|
generate_dialplan
|
|
|
|
if container_running; then
|
|
container_exec asterisk -rx "dialplan reload"
|
|
fi
|
|
|
|
log_ok "Call recording disabled"
|
|
}
|
|
|
|
cmd_rec_status() {
|
|
local enabled=$(uci -q get voip.recording.enabled)
|
|
local format=$(uci -q get voip.recording.format || echo "wav")
|
|
local retention=$(uci -q get voip.recording.retention_days || echo "30")
|
|
local total_count=0
|
|
local total_size=0
|
|
local today_count=0
|
|
|
|
if [ -d "$RECORDINGS_PATH" ]; then
|
|
total_count=$(find "$RECORDINGS_PATH" -type f -name "*.$format" 2>/dev/null | wc -l)
|
|
total_size=$(du -sh "$RECORDINGS_PATH" 2>/dev/null | cut -f1 || echo "0")
|
|
|
|
local today=$(date +%Y%m%d)
|
|
if [ -d "$RECORDINGS_PATH/$today" ]; then
|
|
today_count=$(find "$RECORDINGS_PATH/$today" -type f -name "*.$format" 2>/dev/null | wc -l)
|
|
fi
|
|
fi
|
|
|
|
cat <<EOF
|
|
{
|
|
"enabled": ${enabled:-0},
|
|
"format": "$format",
|
|
"retention_days": $retention,
|
|
"path": "$RECORDINGS_PATH",
|
|
"total_recordings": $total_count,
|
|
"total_size": "$total_size",
|
|
"today_recordings": $today_count
|
|
}
|
|
EOF
|
|
}
|
|
|
|
cmd_rec_list() {
|
|
local date_filter="$1"
|
|
local format=$(uci -q get voip.recording.format || echo "wav")
|
|
|
|
if [ -n "$date_filter" ]; then
|
|
# List recordings for specific date
|
|
local dir="$RECORDINGS_PATH/$date_filter"
|
|
if [ -d "$dir" ]; then
|
|
echo "Recordings for $date_filter:"
|
|
echo "-------------------------"
|
|
find "$dir" -type f -name "*.$format" 2>/dev/null | while read -r file; do
|
|
local name=$(basename "$file")
|
|
local size=$(du -h "$file" 2>/dev/null | cut -f1)
|
|
local time=$(echo "$name" | cut -d'-' -f1)
|
|
local caller=$(echo "$name" | cut -d'-' -f2)
|
|
local dest=$(echo "$name" | cut -d'-' -f3 | sed "s/\.$format//")
|
|
printf " %s %s -> %s (%s)\n" "$time" "$caller" "$dest" "$size"
|
|
done
|
|
else
|
|
log_warn "No recordings found for $date_filter"
|
|
fi
|
|
else
|
|
# List all recording dates with counts
|
|
echo "Recording dates:"
|
|
echo "---------------"
|
|
find "$RECORDINGS_PATH" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -r | while read -r dir; do
|
|
local date=$(basename "$dir")
|
|
local count=$(find "$dir" -type f -name "*.$format" 2>/dev/null | wc -l)
|
|
local size=$(du -sh "$dir" 2>/dev/null | cut -f1)
|
|
printf " %s: %d recordings (%s)\n" "$date" "$count" "$size"
|
|
done
|
|
fi
|
|
}
|
|
|
|
cmd_rec_play() {
|
|
local file="$1"
|
|
[ -z "$file" ] && {
|
|
log_err "Usage: voipctl rec play <filename>"
|
|
return 1
|
|
}
|
|
|
|
# Find the file (could be full path or just filename)
|
|
local fullpath
|
|
if [ -f "$file" ]; then
|
|
fullpath="$file"
|
|
else
|
|
fullpath=$(find "$RECORDINGS_PATH" -name "$file" -type f 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [ -z "$fullpath" ] || [ ! -f "$fullpath" ]; then
|
|
log_err "Recording not found: $file"
|
|
return 1
|
|
fi
|
|
|
|
# Try to play the file
|
|
if command -v aplay >/dev/null 2>&1; then
|
|
log_info "Playing: $fullpath"
|
|
aplay "$fullpath"
|
|
elif command -v ffplay >/dev/null 2>&1; then
|
|
ffplay -nodisp -autoexit "$fullpath"
|
|
else
|
|
echo "File: $fullpath"
|
|
log_info "No audio player available. Download the file to play."
|
|
fi
|
|
}
|
|
|
|
cmd_rec_download() {
|
|
local file="$1"
|
|
[ -z "$file" ] && {
|
|
log_err "Usage: voipctl rec download <filename>"
|
|
return 1
|
|
}
|
|
|
|
local fullpath
|
|
if [ -f "$file" ]; then
|
|
fullpath="$file"
|
|
else
|
|
fullpath=$(find "$RECORDINGS_PATH" -name "$file" -type f 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [ -z "$fullpath" ] || [ ! -f "$fullpath" ]; then
|
|
log_err "Recording not found: $file"
|
|
return 1
|
|
fi
|
|
|
|
echo "$fullpath"
|
|
}
|
|
|
|
cmd_rec_delete() {
|
|
local file="$1"
|
|
[ -z "$file" ] && {
|
|
log_err "Usage: voipctl rec delete <filename>"
|
|
return 1
|
|
}
|
|
|
|
local fullpath
|
|
if [ -f "$file" ]; then
|
|
fullpath="$file"
|
|
else
|
|
fullpath=$(find "$RECORDINGS_PATH" -name "$file" -type f 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [ -z "$fullpath" ] || [ ! -f "$fullpath" ]; then
|
|
log_err "Recording not found: $file"
|
|
return 1
|
|
fi
|
|
|
|
rm -f "$fullpath"
|
|
log_ok "Deleted: $(basename "$fullpath")"
|
|
|
|
# Clean up empty directories
|
|
find "$RECORDINGS_PATH" -type d -empty -delete 2>/dev/null
|
|
}
|
|
|
|
cmd_rec_cleanup() {
|
|
local days="${1:-30}"
|
|
|
|
log_info "Cleaning up recordings older than $days days..."
|
|
|
|
local count=0
|
|
local freed=0
|
|
|
|
if [ -d "$RECORDINGS_PATH" ]; then
|
|
# Calculate size before
|
|
local before=$(du -s "$RECORDINGS_PATH" 2>/dev/null | cut -f1 || echo 0)
|
|
|
|
# Delete old recordings
|
|
count=$(find "$RECORDINGS_PATH" -type f -mtime +$days 2>/dev/null | wc -l)
|
|
find "$RECORDINGS_PATH" -type f -mtime +$days -delete 2>/dev/null
|
|
|
|
# Remove empty directories
|
|
find "$RECORDINGS_PATH" -type d -empty -delete 2>/dev/null
|
|
|
|
# Calculate freed space
|
|
local after=$(du -s "$RECORDINGS_PATH" 2>/dev/null | cut -f1 || echo 0)
|
|
freed=$(( (before - after) / 1024 ))
|
|
fi
|
|
|
|
log_ok "Deleted $count recordings, freed ${freed}MB"
|
|
}
|
|
|
|
cmd_rec_list_json() {
|
|
local date_filter="$1"
|
|
local format=$(uci -q get voip.recording.format || echo "wav")
|
|
|
|
echo "["
|
|
local first=1
|
|
|
|
if [ -n "$date_filter" ]; then
|
|
local dir="$RECORDINGS_PATH/$date_filter"
|
|
if [ -d "$dir" ]; then
|
|
find "$dir" -type f -name "*.$format" 2>/dev/null | sort -r | while read -r file; do
|
|
local name=$(basename "$file")
|
|
local size=$(stat -c%s "$file" 2>/dev/null || echo 0)
|
|
local mtime=$(stat -c%Y "$file" 2>/dev/null || echo 0)
|
|
local time=$(echo "$name" | cut -d'-' -f1)
|
|
local caller=$(echo "$name" | cut -d'-' -f2)
|
|
local dest=$(echo "$name" | cut -d'-' -f3 | sed "s/\.$format//")
|
|
|
|
[ $first -eq 0 ] && echo ","
|
|
first=0
|
|
|
|
cat <<ENTRY
|
|
{
|
|
"filename": "$name",
|
|
"path": "$file",
|
|
"date": "$date_filter",
|
|
"time": "$time",
|
|
"caller": "$caller",
|
|
"destination": "$dest",
|
|
"size": $size,
|
|
"timestamp": $mtime
|
|
}
|
|
ENTRY
|
|
done
|
|
fi
|
|
else
|
|
# List recent recordings (last 7 days)
|
|
find "$RECORDINGS_PATH" -type f -name "*.$format" -mtime -7 2>/dev/null | sort -r | head -50 | while read -r file; do
|
|
local name=$(basename "$file")
|
|
local dir=$(dirname "$file")
|
|
local date=$(basename "$dir")
|
|
local size=$(stat -c%s "$file" 2>/dev/null || echo 0)
|
|
local mtime=$(stat -c%Y "$file" 2>/dev/null || echo 0)
|
|
local time=$(echo "$name" | cut -d'-' -f1)
|
|
local caller=$(echo "$name" | cut -d'-' -f2)
|
|
local dest=$(echo "$name" | cut -d'-' -f3 | sed "s/\.$format//")
|
|
|
|
[ $first -eq 0 ] && echo ","
|
|
first=0
|
|
|
|
cat <<ENTRY
|
|
{
|
|
"filename": "$name",
|
|
"path": "$file",
|
|
"date": "$date",
|
|
"time": "$time",
|
|
"caller": "$caller",
|
|
"destination": "$dest",
|
|
"size": $size,
|
|
"timestamp": $mtime
|
|
}
|
|
ENTRY
|
|
done
|
|
fi
|
|
|
|
echo "]"
|
|
}
|
|
|
|
#
|
|
# Configuration
|
|
#
|
|
cmd_configure_haproxy() {
|
|
log_info "Configuring HAProxy for WebRTC..."
|
|
|
|
local domain=$(uci -q get voip.ssl.domain)
|
|
[ -z "$domain" ] && {
|
|
log_err "Domain not configured. Set voip.ssl.domain first."
|
|
return 1
|
|
}
|
|
|
|
# Add HAProxy backend for WebRTC
|
|
# This assumes secubox-app-haproxy is installed
|
|
if [ -f /usr/sbin/haproxyctl ]; then
|
|
# Generate HAProxy config snippet
|
|
cat >> /etc/haproxy/conf.d/voip.cfg <<EOF
|
|
# VoIP WebRTC Backend
|
|
backend voip_websocket
|
|
mode http
|
|
option forwardfor
|
|
server asterisk 127.0.0.1:8088 check
|
|
|
|
# Route for /ws WebSocket
|
|
acl is_voip_ws path_beg /ws
|
|
use_backend voip_websocket if is_voip_ws
|
|
EOF
|
|
|
|
haproxyctl reload
|
|
log_ok "HAProxy configured for WebRTC"
|
|
else
|
|
log_warn "HAProxy not found. Install secubox-app-haproxy first."
|
|
fi
|
|
}
|
|
|
|
cmd_emancipate() {
|
|
local domain="$1"
|
|
[ -z "$domain" ] && {
|
|
log_err "Usage: voipctl emancipate <domain>"
|
|
return 1
|
|
}
|
|
|
|
log_info "Emancipating VoIP at $domain..."
|
|
|
|
# Configure domain
|
|
uci set voip.ssl.enabled="1"
|
|
uci set voip.ssl.domain="$domain"
|
|
uci commit voip
|
|
|
|
# Configure HAProxy
|
|
cmd_configure_haproxy
|
|
|
|
# Request SSL certificate
|
|
if [ -f /usr/sbin/acmectl ]; then
|
|
acmectl issue "$domain"
|
|
fi
|
|
|
|
log_ok "VoIP exposed at https://$domain"
|
|
}
|
|
|
|
cmd_reload() {
|
|
if ! container_running; then
|
|
log_err "Container not running"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Reloading Asterisk configuration..."
|
|
generate_asterisk_config
|
|
container_exec asterisk -rx "core reload"
|
|
log_ok "Configuration reloaded"
|
|
}
|
|
|
|
#
|
|
# Main
|
|
#
|
|
case "$1" in
|
|
install)
|
|
cmd_install
|
|
;;
|
|
uninstall)
|
|
cmd_uninstall
|
|
;;
|
|
start)
|
|
cmd_start
|
|
;;
|
|
stop)
|
|
cmd_stop
|
|
;;
|
|
restart)
|
|
cmd_restart
|
|
;;
|
|
status)
|
|
cmd_status
|
|
;;
|
|
logs)
|
|
shift
|
|
cmd_logs "$@"
|
|
;;
|
|
shell)
|
|
cmd_shell
|
|
;;
|
|
ext)
|
|
case "$2" in
|
|
add) shift 2; cmd_ext_add "$@" ;;
|
|
del) shift 2; cmd_ext_del "$@" ;;
|
|
list) cmd_ext_list ;;
|
|
passwd) shift 2; cmd_ext_passwd "$@" ;;
|
|
*) usage ;;
|
|
esac
|
|
;;
|
|
trunk)
|
|
case "$2" in
|
|
add)
|
|
case "$3" in
|
|
ovh) cmd_trunk_add_ovh ;;
|
|
manual) log_info "Edit /etc/config/voip sip_trunk section" ;;
|
|
*) usage ;;
|
|
esac
|
|
;;
|
|
test) cmd_trunk_test ;;
|
|
status) cmd_trunk_status ;;
|
|
*) usage ;;
|
|
esac
|
|
;;
|
|
call)
|
|
shift
|
|
cmd_call "$@"
|
|
;;
|
|
hangup)
|
|
shift
|
|
cmd_hangup "$@"
|
|
;;
|
|
calls)
|
|
cmd_calls
|
|
;;
|
|
vm)
|
|
case "$2" in
|
|
list) shift 2; cmd_vm_list "$@" ;;
|
|
play) shift 2; cmd_vm_play "$@" ;;
|
|
delete) shift 2; cmd_vm_delete "$@" ;;
|
|
*) usage ;;
|
|
esac
|
|
;;
|
|
rec)
|
|
case "$2" in
|
|
enable) cmd_rec_enable ;;
|
|
disable) cmd_rec_disable ;;
|
|
status) cmd_rec_status ;;
|
|
list) shift 2; cmd_rec_list "$@" ;;
|
|
list-json) shift 2; cmd_rec_list_json "$@" ;;
|
|
play) shift 2; cmd_rec_play "$@" ;;
|
|
download) shift 2; cmd_rec_download "$@" ;;
|
|
delete) shift 2; cmd_rec_delete "$@" ;;
|
|
cleanup) shift 2; cmd_rec_cleanup "$@" ;;
|
|
*) usage ;;
|
|
esac
|
|
;;
|
|
configure-haproxy)
|
|
cmd_configure_haproxy
|
|
;;
|
|
emancipate)
|
|
shift
|
|
cmd_emancipate "$@"
|
|
;;
|
|
reload)
|
|
cmd_reload
|
|
;;
|
|
*)
|
|
usage
|
|
;;
|
|
esac
|