forked from hesabix/arc
Issabel/Rocky 8 ships Python 3.6 which crashes the connector; install/update now detect or install python39 and point systemd at it. Co-authored-by: Cursor <cursoragent@cursor.com>
1597 lines
58 KiB
Shell
Executable file
1597 lines
58 KiB
Shell
Executable file
#!/usr/bin/env bash
|
||
# hesabix-pbx — نصب و مدیریت Hesabix Telephony Connector روی Issabel/Asterisk
|
||
# استفاده: hesabix-pbx <command>
|
||
set -euo pipefail
|
||
|
||
VERSION="1.3.1"
|
||
INSTALL_DIR="${HESABIX_PBX_DIR:-/opt/HesabixTelephonyConnector}"
|
||
ENV_FILE="${INSTALL_DIR}/.env"
|
||
SERVICE_NAME="hesabix-telephony-connector"
|
||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||
BIN_LINK="/usr/local/bin/hesabix-pbx"
|
||
# مفسر پایتون کانکتور (باید ≥3.8 باشد؛ روی Issabel/Rocky8 پیشفرض سیستم 3.6 است)
|
||
PYTHON_BIN="${HESABIX_PYTHON:-}"
|
||
PYTHON_BIN_FILE="${INSTALL_DIR}/.python-bin"
|
||
MIN_PYTHON_MAJOR=3
|
||
MIN_PYTHON_MINOR=8
|
||
|
||
REPO_URL="${HESABIX_PBX_REPO:-https://source.hesabix.ir/hesabix/arc.git}"
|
||
REPO_BRANCH="${HESABIX_PBX_BRANCH:-master}"
|
||
RAW_BASE="${HESABIX_PBX_RAW_BASE:-https://source.hesabix.ir/hesabix/arc/raw/branch/${REPO_BRANCH}/extraScripts/HesabixTelephonyConnector}"
|
||
CONN_REL_PATH="extraScripts/HesabixTelephonyConnector"
|
||
|
||
# پیشفرض آدرس API حسابیکس (قابل بازنویسی با متغیر محیطی قبل از ویزارد)
|
||
HESABIX_DEFAULT_API_URL="https://hsxn.hesabix.ir"
|
||
|
||
# تشخیص توزیع (با detect_os پر میشود)
|
||
OS_ID=""
|
||
OS_ID_LIKE=""
|
||
OS_NAME=""
|
||
OS_FAMILY="" # debian | rhel | unknown
|
||
PKG_MGR="" # apt-get | dnf | yum | ""
|
||
IS_ISSABEL=0
|
||
|
||
# فایلهای ضروری کانکتور (برای حالت بدون git)
|
||
CONNECTOR_FILES=(
|
||
"app/main.py"
|
||
"app/media_audiosocket.py"
|
||
"app/media_tunnel.py"
|
||
"bin/hesabix-pbx"
|
||
"config.example.env"
|
||
"requirements-softphone.txt"
|
||
"README.md"
|
||
"VERSION"
|
||
"systemd/hesabix-telephony-connector.service"
|
||
"install.sh"
|
||
"dialplan/hesabix-softphone-relay.conf.sample"
|
||
)
|
||
|
||
# ─── helpers ────────────────────────────────────────────────────────────────
|
||
|
||
c_reset=$(printf '\033[0m')
|
||
c_green=$(printf '\033[0;32m')
|
||
c_yellow=$(printf '\033[0;33m')
|
||
c_red=$(printf '\033[0;31m')
|
||
c_cyan=$(printf '\033[0;36m')
|
||
c_bold=$(printf '\033[1m')
|
||
|
||
log() { printf "%b[*]%b %s\n" "$c_cyan" "$c_reset" "$*"; }
|
||
ok() { printf "%b[✔]%b %s\n" "$c_green" "$c_reset" "$*"; }
|
||
warn() { printf "%b[!]%b %s\n" "$c_yellow" "$c_reset" "$*"; }
|
||
err() { printf "%b[✖]%b %s\n" "$c_red" "$c_reset" "$*" >&2; }
|
||
die() { err "$*"; exit 1; }
|
||
|
||
need_root() {
|
||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||
if have sudo; then
|
||
die "این دستور را با root اجرا کنید: sudo hesabix-pbx $*"
|
||
fi
|
||
die "این دستور را با کاربر root اجرا کنید (sudo در این سیستم نیست): hesabix-pbx $*"
|
||
fi
|
||
}
|
||
|
||
have() { command -v "$1" >/dev/null 2>&1; }
|
||
|
||
# ─── Python runtime (≥3.8) ───────────────────────────────────────────────────
|
||
# Issabel / Rocky / CentOS 8 معمولاً فقط python3.6 دارند؛ کد کانکتور به ≥3.8 نیاز دارد.
|
||
|
||
py_meets_min() {
|
||
local bin="$1"
|
||
[[ -n "$bin" && -x "$bin" ]] || return 1
|
||
"$bin" -c "import sys; raise SystemExit(0 if sys.version_info >= (${MIN_PYTHON_MAJOR}, ${MIN_PYTHON_MINOR}) else 1)" 2>/dev/null
|
||
}
|
||
|
||
py_version_str() {
|
||
local bin="${1:-}"
|
||
[[ -n "$bin" ]] || return 1
|
||
"$bin" -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])' 2>/dev/null
|
||
}
|
||
|
||
# پیدا کردن اولین مفسر مناسب در PATH / مسیرهای رایج
|
||
find_suitable_python() {
|
||
local c p
|
||
for c in \
|
||
"${HESABIX_PYTHON:-}" \
|
||
python3.12 python3.11 python3.10 python3.9 python3.8 \
|
||
/usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 \
|
||
/usr/bin/python3.9 /usr/bin/python3.8 \
|
||
/usr/local/bin/python3.12 /usr/local/bin/python3.11 \
|
||
/usr/local/bin/python3.10 /usr/local/bin/python3.9 /usr/local/bin/python3.8 \
|
||
python3 /usr/bin/python3
|
||
do
|
||
[[ -n "$c" ]] || continue
|
||
if [[ "$c" == /* ]]; then
|
||
p="$c"
|
||
else
|
||
p="$(command -v "$c" 2>/dev/null || true)"
|
||
fi
|
||
if py_meets_min "$p"; then
|
||
printf '%s\n' "$p"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
persist_python_bin() {
|
||
[[ -n "${PYTHON_BIN:-}" ]] || return 1
|
||
mkdir -p "$INSTALL_DIR"
|
||
printf '%s\n' "$PYTHON_BIN" > "$PYTHON_BIN_FILE"
|
||
chmod 644 "$PYTHON_BIN_FILE" 2>/dev/null || true
|
||
}
|
||
|
||
# بارگذاری مفسر ذخیرهشده یا پیدا کردن روی سیستم (بدون نصب)
|
||
load_python_bin() {
|
||
if [[ -n "${PYTHON_BIN:-}" ]] && py_meets_min "$PYTHON_BIN"; then
|
||
return 0
|
||
fi
|
||
if [[ -f "$PYTHON_BIN_FILE" ]]; then
|
||
local saved
|
||
saved="$(tr -d '[:space:]' < "$PYTHON_BIN_FILE" 2>/dev/null || true)"
|
||
if py_meets_min "$saved"; then
|
||
PYTHON_BIN="$saved"
|
||
return 0
|
||
fi
|
||
fi
|
||
local found
|
||
if found="$(find_suitable_python)"; then
|
||
PYTHON_BIN="$found"
|
||
persist_python_bin
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# نصب بستهٔ پایتون جدید روی RHEL/Issabel وقتی فقط 3.6 موجود است
|
||
_install_modern_python_rhel() {
|
||
local pkg
|
||
# ترتیب: 3.9 (AppStream متداول Rocky/Alma/Issabel 8) سپس 3.8
|
||
for pkg in python39 python3.9 python38 python3.8; do
|
||
log "تلاش برای نصب ${pkg} …"
|
||
if pkg_install "$pkg" 2>/dev/null; then
|
||
# pip متناظر
|
||
case "$pkg" in
|
||
python39|python3.9) pkg_install python39-pip 2>/dev/null || true ;;
|
||
python38|python3.8) pkg_install python38-pip 2>/dev/null || true ;;
|
||
esac
|
||
if find_suitable_python >/dev/null; then
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
# ماژول dnf (Rocky/RHEL 8+)
|
||
if have dnf; then
|
||
for stream in python39:3.9 python38:3.8; do
|
||
log "تلاش dnf module enable ${stream} …"
|
||
dnf -y module enable "$stream" 2>/dev/null || true
|
||
dnf -y module install "$stream" 2>/dev/null || true
|
||
if find_suitable_python >/dev/null; then
|
||
return 0
|
||
fi
|
||
done
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
_install_modern_python_debian() {
|
||
# روی Debian/Ubuntu معمولاً python3 ≥3.8 است؛ در صورت قدیمی بودن، نسخهدار نصب کن
|
||
pkg_install python3 python3-pip 2>/dev/null || true
|
||
if find_suitable_python >/dev/null; then
|
||
return 0
|
||
fi
|
||
local pkg
|
||
for pkg in python3.12 python3.11 python3.10 python3.9 python3.8; do
|
||
log "تلاش برای نصب ${pkg} …"
|
||
if pkg_install "$pkg" 2>/dev/null; then
|
||
pkg_install "${pkg}-venv" 2>/dev/null || true
|
||
if find_suitable_python >/dev/null; then
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# تضمین مفسر ≥3.8 — در install/update/softphone اجباری است
|
||
ensure_python_runtime() {
|
||
detect_os
|
||
if load_python_bin; then
|
||
ok "پایتون مناسب: $PYTHON_BIN ($(py_version_str "$PYTHON_BIN"))"
|
||
return 0
|
||
fi
|
||
|
||
warn "پایتون ≥${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} پیدا نشد (روی Issabel/Rocky8 اغلب فقط 3.6 است)."
|
||
if [[ -z "$PKG_MGR" ]] || ! have "$PKG_MGR"; then
|
||
die "نمیتوان پایتون جدید نصب کرد — مدیر بسته نیست.
|
||
دستی نصب کنید، سپس دوباره: hesabix-pbx update
|
||
RHEL/Rocky/Issabel: dnf install -y python39 python39-pip
|
||
Debian/Ubuntu: apt-get install -y python3.9"
|
||
fi
|
||
|
||
log "نصب پایتون ≥${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} …"
|
||
case "$OS_FAMILY" in
|
||
rhel)
|
||
_install_modern_python_rhel || true
|
||
;;
|
||
debian)
|
||
_install_modern_python_debian || true
|
||
;;
|
||
*)
|
||
_install_modern_python_rhel || _install_modern_python_debian || true
|
||
;;
|
||
esac
|
||
|
||
local found
|
||
if found="$(find_suitable_python)"; then
|
||
PYTHON_BIN="$found"
|
||
persist_python_bin
|
||
ok "پایتون مناسب نصب/انتخاب شد: $PYTHON_BIN ($(py_version_str "$PYTHON_BIN"))"
|
||
return 0
|
||
fi
|
||
|
||
die "پس از نصب، هنوز پایتون ≥${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} در سیستم نیست.
|
||
روی Issabel/Rocky 8:
|
||
dnf install -y python39 python39-pip
|
||
hesabix-pbx update
|
||
سپس سرویس با همین مفسر اجرا میشود (نه python3.6 سیستم)."
|
||
}
|
||
|
||
# تشخیص سیستمعامل و مدیر بسته (Debian/Ubuntu vs RHEL/CentOS/Issabel)
|
||
# توجه: /etc/os-release را در سابشل میخوانیم تا VERSION اسکریپت بازنویسی نشود.
|
||
detect_os() {
|
||
OS_ID=""
|
||
OS_ID_LIKE=""
|
||
OS_NAME=""
|
||
OS_FAMILY="unknown"
|
||
PKG_MGR=""
|
||
IS_ISSABEL=0
|
||
|
||
if [[ -r /etc/os-release ]]; then
|
||
# سابشل جدا تا VERSION/NAME اسکریپت آلوده نشود؛ بدون null-byte (هشدار bash)
|
||
# shellcheck disable=SC1091
|
||
local os_vars
|
||
os_vars="$(. /etc/os-release; printf '%s\n%s\n%s' "${ID:-}" "${ID_LIKE:-}" "${PRETTY_NAME:-${NAME:-}}")"
|
||
OS_ID="$(printf '%s\n' "$os_vars" | sed -n '1p')"
|
||
OS_ID_LIKE="$(printf '%s\n' "$os_vars" | sed -n '2p')"
|
||
OS_NAME="$(printf '%s\n' "$os_vars" | sed -n '3p')"
|
||
[[ -n "$OS_NAME" ]] || OS_NAME="$OS_ID"
|
||
elif [[ -r /etc/redhat-release ]]; then
|
||
OS_ID="rhel"
|
||
OS_NAME="$(tr -d '\n' < /etc/redhat-release)"
|
||
elif [[ -r /etc/debian_version ]]; then
|
||
OS_ID="debian"
|
||
OS_NAME="Debian $(tr -d '\n' < /etc/debian_version)"
|
||
fi
|
||
|
||
if [[ -f /etc/issabel.conf ]] || [[ -d /etc/issabel ]] \
|
||
|| [[ -f /etc/asterisk/issabel.conf ]] \
|
||
|| grep -qi issabel /etc/redhat-release 2>/dev/null \
|
||
|| grep -qi issabel /etc/os-release 2>/dev/null; then
|
||
IS_ISSABEL=1
|
||
OS_NAME="${OS_NAME:-Issabel}"
|
||
fi
|
||
|
||
local fingerprint
|
||
fingerprint=" ${OS_ID} ${OS_ID_LIKE} "
|
||
if [[ "$fingerprint" =~ (debian|ubuntu|raspbian|linuxmint|pop) ]]; then
|
||
OS_FAMILY="debian"
|
||
elif [[ "$fingerprint" =~ (rhel|centos|fedora|rocky|almalinux|alma|ol|scientific|amzn|oracle|sangoma|freepbx) ]]; then
|
||
OS_FAMILY="rhel"
|
||
elif [[ -f /etc/debian_version ]]; then
|
||
OS_FAMILY="debian"
|
||
elif [[ -f /etc/redhat-release ]] || [[ $IS_ISSABEL -eq 1 ]]; then
|
||
OS_FAMILY="rhel"
|
||
fi
|
||
|
||
if have dnf; then
|
||
PKG_MGR="dnf"
|
||
elif have yum; then
|
||
PKG_MGR="yum"
|
||
elif have apt-get; then
|
||
PKG_MGR="apt-get"
|
||
elif [[ "$OS_FAMILY" == "rhel" ]]; then
|
||
# Issabel/CentOS قدیمی ممکن است yum داشته باشد ولی هنوز در PATH نباشد
|
||
PKG_MGR="yum"
|
||
elif [[ "$OS_FAMILY" == "debian" ]]; then
|
||
PKG_MGR="apt-get"
|
||
fi
|
||
}
|
||
|
||
# نصب بسته بدون اتکا به sudo (فرض: همینالان root هستیم)
|
||
pkg_install() {
|
||
[[ $# -gt 0 ]] || return 0
|
||
[[ -n "$PKG_MGR" ]] || detect_os
|
||
case "$PKG_MGR" in
|
||
dnf)
|
||
dnf install -y "$@"
|
||
;;
|
||
yum)
|
||
yum install -y "$@"
|
||
;;
|
||
apt-get)
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get install -y "$@"
|
||
;;
|
||
*)
|
||
die "مدیر بسته شناخته نشد. بستههای لازم را دستی نصب کنید: $*"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# نصب پیشنیازهای اجرا بر اساس توزیع
|
||
ensure_prerequisites() {
|
||
detect_os
|
||
local label="$OS_NAME"
|
||
if [[ $IS_ISSABEL -eq 1 ]]; then
|
||
label="${label} [Issabel]"
|
||
fi
|
||
log "سیستمعامل: ${label:-نامشخص} | خانواده: ${OS_FAMILY} | مدیر بسته: ${PKG_MGR:-نامشخص}"
|
||
|
||
local missing=()
|
||
have rsync || missing+=(rsync)
|
||
if ! have curl && ! have wget; then
|
||
missing+=(curl)
|
||
fi
|
||
have git || missing+=(git)
|
||
|
||
if ((${#missing[@]} > 0)); then
|
||
if [[ -z "$PKG_MGR" ]] || ! have "$PKG_MGR"; then
|
||
die "پیشنیازهای زیر نصب نیستند و مدیر بسته در دسترس نیست: ${missing[*]}
|
||
روی Debian/Ubuntu: apt-get install -y ${missing[*]}
|
||
روی Issabel/CentOS/RHEL: yum install -y ${missing[*]} یا dnf install -y ${missing[*]}"
|
||
fi
|
||
log "نصب پیشنیازها (${PKG_MGR}): ${missing[*]}"
|
||
if ! pkg_install "${missing[@]}"; then
|
||
die "نصب پیشنیازها ناموفق بود: ${missing[*]}"
|
||
fi
|
||
else
|
||
ok "ابزارهای پایه (rsync/curl/git) آمادهاند."
|
||
fi
|
||
|
||
if ! have curl && ! have wget; then
|
||
die "curl یا wget پس از نصب در PATH نیست."
|
||
fi
|
||
|
||
# پایتون ≥3.8 — حیاتی برای اجرای کانکتور روی Issabel/Rocky8
|
||
ensure_python_runtime
|
||
ok "پیشنیازها آماده است."
|
||
}
|
||
|
||
# ورودی تعاملی: وقتی اسکریپت با curl|bash اجرا شود stdin لوله است؛ از /dev/tty بخوان.
|
||
read_tty() {
|
||
# استفاده: read_tty [-s] var_name
|
||
local secret=0
|
||
if [[ "${1:-}" == "-s" ]]; then
|
||
secret=1
|
||
shift
|
||
fi
|
||
local __var="$1"
|
||
local __ans=""
|
||
if [[ -r /dev/tty ]]; then
|
||
if [[ $secret -eq 1 ]]; then
|
||
read -r -s __ans < /dev/tty || true
|
||
else
|
||
read -r __ans < /dev/tty || true
|
||
fi
|
||
else
|
||
if [[ $secret -eq 1 ]]; then
|
||
read -r -s __ans || true
|
||
else
|
||
read -r __ans || true
|
||
fi
|
||
fi
|
||
printf -v "$__var" '%s' "$__ans"
|
||
}
|
||
|
||
prompt() {
|
||
# prompt "سؤال" "پیشفرض"
|
||
local q="$1" d="${2:-}" ans
|
||
if [[ -n "$d" ]]; then
|
||
printf '%b%s%b [%s]: ' "$c_bold" "$q" "$c_reset" "$d" > /dev/tty 2>/dev/null || printf '%b%s%b [%s]: ' "$c_bold" "$q" "$c_reset" "$d"
|
||
else
|
||
printf '%b%s%b: ' "$c_bold" "$q" "$c_reset" > /dev/tty 2>/dev/null || printf '%b%s%b: ' "$c_bold" "$q" "$c_reset"
|
||
fi
|
||
read_tty ans
|
||
echo "${ans:-$d}"
|
||
}
|
||
|
||
prompt_secret() {
|
||
local q="$1" d="${2:-}" ans
|
||
if [[ -n "$d" ]]; then
|
||
printf '%b%s%b [پنهان/Enter=همان]: ' "$c_bold" "$q" "$c_reset" > /dev/tty 2>/dev/null || printf '%b%s%b [پنهان/Enter=همان]: ' "$c_bold" "$q" "$c_reset"
|
||
else
|
||
printf '%b%s%b: ' "$c_bold" "$q" "$c_reset" > /dev/tty 2>/dev/null || printf '%b%s%b: ' "$c_bold" "$q" "$c_reset"
|
||
fi
|
||
read_tty -s ans
|
||
# خط جدید بعد از ورودی مخفی
|
||
if [[ -w /dev/tty ]]; then
|
||
printf '\n' > /dev/tty
|
||
else
|
||
echo
|
||
fi
|
||
echo "${ans:-$d}"
|
||
}
|
||
|
||
load_env() {
|
||
[[ -f "$ENV_FILE" ]] || return 1
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE"
|
||
set +a
|
||
}
|
||
|
||
download() {
|
||
local url="$1" out="$2"
|
||
if have curl; then
|
||
curl -fsSL --connect-timeout 20 --max-time 120 "$url" -o "$out"
|
||
elif have wget; then
|
||
wget -q -O "$out" "$url"
|
||
else
|
||
die "curl یا wget لازم است."
|
||
fi
|
||
}
|
||
|
||
# ─── fetch connector files ──────────────────────────────────────────────────
|
||
|
||
fetch_via_git() {
|
||
# مسیر temp را فوری در trap میگنجانیم تا با set -u بعد از خروج تابع unbound نشود
|
||
local tmp src env_bak=""
|
||
tmp="$(mktemp -d /tmp/hesabix-pbx.XXXXXX)"
|
||
# shellcheck disable=SC2064
|
||
trap "rm -rf '${tmp}'" RETURN
|
||
|
||
log "دریافت کانکتور از مخزن (sparse git)…"
|
||
log "منبع: $REPO_URL شاخه: $REPO_BRANCH"
|
||
|
||
if ! git clone --depth 1 --filter=blob:none --sparse \
|
||
-b "$REPO_BRANCH" "$REPO_URL" "$tmp/repo" >/dev/null 2>&1; then
|
||
# بعضی سرورها filter را پشتیبانی نمیکنند
|
||
git clone --depth 1 -b "$REPO_BRANCH" "$REPO_URL" "$tmp/repo" >/dev/null 2>&1 \
|
||
|| { warn "کلون مخزن ناموفق بود."; return 1; }
|
||
fi
|
||
|
||
(
|
||
cd "$tmp/repo"
|
||
if git sparse-checkout set "$CONN_REL_PATH" >/dev/null 2>&1; then
|
||
:
|
||
fi
|
||
)
|
||
|
||
src="$tmp/repo/$CONN_REL_PATH"
|
||
if [[ ! -d "$src" ]]; then
|
||
warn "پوشه کانکتور در مخزن پیدا نشد: $CONN_REL_PATH"
|
||
return 1
|
||
fi
|
||
|
||
mkdir -p "$INSTALL_DIR"
|
||
# حفظ .env
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
env_bak="$(mktemp)"
|
||
cp -a "$ENV_FILE" "$env_bak"
|
||
fi
|
||
|
||
if have rsync; then
|
||
rsync -a --delete \
|
||
--exclude '.env' \
|
||
--exclude '.env.*' \
|
||
--exclude '__pycache__/' \
|
||
--exclude '*.pyc' \
|
||
"$src/" "$INSTALL_DIR/"
|
||
else
|
||
# بدون rsync: کپی فایلها بدون پاک کردن .env
|
||
find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 ! -name '.env' ! -name '.env.*' -exec rm -rf {} +
|
||
cp -a "$src/." "$INSTALL_DIR/"
|
||
fi
|
||
|
||
if [[ -n "$env_bak" ]]; then
|
||
mv -f "$env_bak" "$ENV_FILE"
|
||
fi
|
||
|
||
ok "فایلهای کانکتور در $INSTALL_DIR بهروز شد."
|
||
return 0
|
||
}
|
||
|
||
fetch_via_raw() {
|
||
log "git در دسترس نیست یا کلون ناموفق — دانلود فایلبهفایل…"
|
||
mkdir -p "$INSTALL_DIR/app" "$INSTALL_DIR/bin" "$INSTALL_DIR/systemd"
|
||
|
||
local env_bak=""
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
env_bak="$(mktemp)"
|
||
cp -a "$ENV_FILE" "$env_bak"
|
||
fi
|
||
|
||
local f
|
||
for f in "${CONNECTOR_FILES[@]}"; do
|
||
log " ↓ $f"
|
||
mkdir -p "$(dirname "$INSTALL_DIR/$f")"
|
||
download "${RAW_BASE}/${f}" "$INSTALL_DIR/$f" \
|
||
|| die "دانلود ناموفق: $f"
|
||
done
|
||
|
||
if [[ -n "$env_bak" ]]; then
|
||
mv -f "$env_bak" "$ENV_FILE"
|
||
fi
|
||
ok "دانلود خام کامل شد."
|
||
}
|
||
|
||
sync_connector() {
|
||
if have git; then
|
||
if fetch_via_git; then
|
||
return 0
|
||
fi
|
||
warn "sparse/git ناموفق — تلاش با دانلود خام…"
|
||
fi
|
||
fetch_via_raw
|
||
}
|
||
|
||
install_cli() {
|
||
local src="$INSTALL_DIR/bin/hesabix-pbx"
|
||
[[ -f "$src" ]] || die "فایل CLI پیدا نشد: $src"
|
||
chmod +x "$src" "$INSTALL_DIR/install.sh" 2>/dev/null || true
|
||
chmod +x "$INSTALL_DIR/app/main.py" 2>/dev/null || true
|
||
ln -sfn "$src" "$BIN_LINK"
|
||
ok "دستور سیستمعامل آماده است: hesabix-pbx"
|
||
}
|
||
|
||
install_systemd() {
|
||
local unit_src="$INSTALL_DIR/systemd/hesabix-telephony-connector.service"
|
||
[[ -f "$unit_src" ]] || die "فایل systemd پیدا نشد."
|
||
|
||
load_python_bin || ensure_python_runtime
|
||
[[ -n "${PYTHON_BIN:-}" ]] || die "مفسر پایتون برای سرویس مشخص نشد."
|
||
|
||
# بازنویسی مسیرها مطابق INSTALL_DIR و مفسر ≥3.8
|
||
sed -e "s|/opt/HesabixTelephonyConnector|${INSTALL_DIR}|g" \
|
||
-e "s|WorkingDirectory=.*|WorkingDirectory=${INSTALL_DIR}|g" \
|
||
-e "s|EnvironmentFile=.*|EnvironmentFile=${ENV_FILE}|g" \
|
||
-e "s|^ExecStart=.*|ExecStart=${PYTHON_BIN} ${INSTALL_DIR}/app/main.py|g" \
|
||
"$unit_src" > "$SERVICE_FILE"
|
||
|
||
# اطمینان نهایی از مسیر مفسر
|
||
sed -i "s|^ExecStart=.*|ExecStart=${PYTHON_BIN} ${INSTALL_DIR}/app/main.py|" "$SERVICE_FILE"
|
||
|
||
persist_python_bin
|
||
systemctl daemon-reload
|
||
systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||
ok "سرویس systemd ثبت شد: $SERVICE_NAME (Python: $PYTHON_BIN)"
|
||
}
|
||
|
||
# ─── Softphone Media Relay helpers ───────────────────────────────────────────
|
||
|
||
ensure_python_pip() {
|
||
load_python_bin || ensure_python_runtime
|
||
[[ -n "${PYTHON_BIN:-}" ]] || die "مفسر پایتون مشخص نیست."
|
||
if "$PYTHON_BIN" -m pip --version >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
log "pip برای $PYTHON_BIN پیدا نشد — تلاش برای نصب…"
|
||
detect_os
|
||
local ver_minor
|
||
ver_minor="$("$PYTHON_BIN" -c 'import sys; print(sys.version_info[1])' 2>/dev/null || echo "")"
|
||
case "$PKG_MGR" in
|
||
apt-get)
|
||
pkg_install python3-pip || true
|
||
if [[ -n "$ver_minor" ]]; then
|
||
pkg_install "python3.${ver_minor}-distutils" 2>/dev/null || true
|
||
fi
|
||
;;
|
||
dnf|yum)
|
||
# روی RHEL بستههای pip نسخهدار: python39-pip / python38-pip
|
||
if [[ -n "$ver_minor" ]]; then
|
||
pkg_install "python3${ver_minor}-pip" 2>/dev/null \
|
||
|| pkg_install "python3.${ver_minor}-pip" 2>/dev/null \
|
||
|| true
|
||
fi
|
||
pkg_install python3-pip 2>/dev/null || pkg_install python-pip 2>/dev/null || true
|
||
;;
|
||
esac
|
||
if ! "$PYTHON_BIN" -m pip --version >/dev/null 2>&1; then
|
||
"$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 || true
|
||
fi
|
||
"$PYTHON_BIN" -m pip --version >/dev/null 2>&1 \
|
||
|| die "pip برای $PYTHON_BIN نصب نشد. دستی: dnf install -y python39-pip یا apt install python3-pip"
|
||
}
|
||
|
||
ensure_softphone_python_deps() {
|
||
ensure_python_pip
|
||
local req="$INSTALL_DIR/requirements-softphone.txt"
|
||
log "نصب وابستگی Softphone (websocket-client) با $PYTHON_BIN …"
|
||
if [[ -f "$req" ]]; then
|
||
"$PYTHON_BIN" -m pip install -r "$req" \
|
||
|| "$PYTHON_BIN" -m pip install --break-system-packages -r "$req" \
|
||
|| return 1
|
||
else
|
||
"$PYTHON_BIN" -m pip install "websocket-client>=1.6.0" \
|
||
|| "$PYTHON_BIN" -m pip install --break-system-packages "websocket-client>=1.6.0" \
|
||
|| return 1
|
||
fi
|
||
"$PYTHON_BIN" -c "import websocket; print(websocket.__version__)" >/dev/null 2>&1 \
|
||
|| return 1
|
||
return 0
|
||
}
|
||
|
||
ensure_softphone_env_keys() {
|
||
[[ -f "$ENV_FILE" ]] || die "فایل .env نیست. اول: hesabix-pbx configure"
|
||
load_python_bin || ensure_python_runtime
|
||
log "همگامسازی کلیدهای Softphone در .env …"
|
||
"$PYTHON_BIN" - "$ENV_FILE" <<'PY'
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
path = Path(sys.argv[1])
|
||
text = path.read_text(encoding="utf-8", errors="replace")
|
||
defaults = {
|
||
"AUDIOSOCKET_HOST": "127.0.0.1",
|
||
"AUDIOSOCKET_PORT": "9092",
|
||
"MEDIA_TUNNEL_ENABLED": "1",
|
||
}
|
||
lines = text.splitlines()
|
||
keys_present = set()
|
||
out = []
|
||
for line in lines:
|
||
raw = line.strip()
|
||
if raw and not raw.startswith("#") and "=" in raw:
|
||
k = raw.split("=", 1)[0].strip()
|
||
keys_present.add(k)
|
||
if k in defaults and (raw.split("=", 1)[1].strip().strip("'\"") == ""):
|
||
out.append(f"{k}={defaults[k]}")
|
||
continue
|
||
out.append(line)
|
||
|
||
missing = [k for k in defaults if k not in keys_present]
|
||
if missing:
|
||
if out and out[-1].strip() != "":
|
||
out.append("")
|
||
out.append("# Softphone Media Relay (auto by hesabix-pbx softphone-setup)")
|
||
for k in missing:
|
||
out.append(f"{k}={defaults[k]}")
|
||
|
||
path.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||
print("env softphone keys ok")
|
||
PY
|
||
chmod 600 "$ENV_FILE" 2>/dev/null || true
|
||
ok "کلیدهای Softphone در $ENV_FILE تنظیم شد."
|
||
}
|
||
|
||
verify_softphone_ready() {
|
||
log "بررسی آمادگی Softphone…"
|
||
load_python_bin || ensure_python_runtime
|
||
if "$PYTHON_BIN" -c "import websocket" >/dev/null 2>&1; then
|
||
local ver
|
||
ver="$("$PYTHON_BIN" -c "import websocket; print(getattr(websocket, '__version__', 'ok'))" 2>/dev/null || echo ok)"
|
||
ok "websocket-client نصب است (نسخه: $ver)"
|
||
else
|
||
err "websocket-client هنوز import نمیشود."
|
||
return 1
|
||
fi
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
grep -q '^MEDIA_TUNNEL_ENABLED=' "$ENV_FILE" && ok "MEDIA_TUNNEL_ENABLED در .env هست" || warn "MEDIA_TUNNEL_ENABLED در .env نیست"
|
||
grep -q '^AUDIOSOCKET_PORT=' "$ENV_FILE" && ok "AUDIOSOCKET_PORT در .env هست" || warn "AUDIOSOCKET_PORT در .env نیست"
|
||
fi
|
||
if [[ -f "$INSTALL_DIR/app/media_tunnel.py" && -f "$INSTALL_DIR/app/media_audiosocket.py" ]]; then
|
||
ok "ماژولهای Softphone در $INSTALL_DIR/app موجودند."
|
||
else
|
||
warn "فایلهای media_*.py پیدا نشد — ابتدا hesabix-pbx update بزنید."
|
||
fi
|
||
if [[ -f "$INSTALL_DIR/dialplan/hesabix-softphone-relay.conf.sample" ]]; then
|
||
ok "نمونه دیالپلن: $INSTALL_DIR/dialplan/hesabix-softphone-relay.conf.sample"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
cmd_softphone_setup() {
|
||
need_root softphone-setup
|
||
[[ -d "$INSTALL_DIR" ]] || die "نصب پیدا نشد. اول: hesabix-pbx install یا hesabix-pbx update"
|
||
ensure_prerequisites
|
||
|
||
log "راهاندازی خودکار Softphone Media Relay…"
|
||
# اگر فایلهای softphone نیستند، یک sync سبک انجام بده
|
||
if [[ ! -f "$INSTALL_DIR/app/media_tunnel.py" ]]; then
|
||
warn "کد Softphone روی دیسک نیست — در حال بهروزرسانی از مخزن…"
|
||
sync_connector
|
||
install_cli
|
||
install_systemd
|
||
fi
|
||
|
||
ensure_softphone_python_deps || die "نصب websocket-client ناموفق بود."
|
||
ok "websocket-client نصب شد."
|
||
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
ensure_softphone_env_keys
|
||
else
|
||
warn ".env نیست. اول configure را بزنید، بعد دوباره softphone-setup."
|
||
cmd_configure
|
||
ensure_softphone_env_keys
|
||
fi
|
||
|
||
systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME" || die "ریاستارت سرویس ناموفق"
|
||
sleep 1
|
||
verify_softphone_ready || true
|
||
|
||
echo
|
||
ok "Softphone Relay آماده شد."
|
||
echo " بعدی در حسابیکس:"
|
||
echo " ۱) تنظیمات مرکز تماس → نگاشت کاربر → حالت Relay"
|
||
echo " ۲) صفحه Softphone → آنلاین شدن"
|
||
echo " لاگ:"
|
||
echo " hesabix-pbx logs"
|
||
echo " hesabix-pbx follow"
|
||
echo
|
||
cmd_status
|
||
}
|
||
|
||
# ─── Hesabix API helpers (ویزارد تعاملی) ─────────────────────────────────────
|
||
|
||
api_request_json() {
|
||
# api_request_json METHOD URL API_KEY [JSON_BODY]
|
||
# خروجی: بدنه JSON؛ کد HTTP در متغیر سراسری API_HTTP_CODE
|
||
local method="$1" url="$2" api_key="$3" body="${4:-}"
|
||
API_HTTP_CODE="0"
|
||
load_python_bin || ensure_python_runtime
|
||
HESABIX_API_KEY="$api_key" HESABIX_HTTP_METHOD="$method" HESABIX_HTTP_URL="$url" HESABIX_HTTP_BODY="$body" \
|
||
"$PYTHON_BIN" - <<'PY'
|
||
import json, os, sys, urllib.error, urllib.request
|
||
method = os.environ["HESABIX_HTTP_METHOD"].upper()
|
||
url = os.environ["HESABIX_HTTP_URL"]
|
||
key = os.environ["HESABIX_API_KEY"]
|
||
body = os.environ.get("HESABIX_HTTP_BODY") or ""
|
||
data = body.encode("utf-8") if body else None
|
||
req = urllib.request.Request(
|
||
url,
|
||
data=data,
|
||
method=method,
|
||
headers={
|
||
"Authorization": f"ApiKey {key}",
|
||
"Accept": "application/json",
|
||
"Content-Type": "application/json",
|
||
"User-Agent": "hesabix-pbx/1.3",
|
||
},
|
||
)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
raw = resp.read().decode("utf-8", "replace")
|
||
print(resp.status, file=sys.stderr)
|
||
sys.stdout.write(raw)
|
||
except urllib.error.HTTPError as e:
|
||
raw = e.read().decode("utf-8", "replace")
|
||
print(e.code, file=sys.stderr)
|
||
sys.stdout.write(raw or json.dumps({"success": False, "error": {"message": str(e)}}))
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print(0, file=sys.stderr)
|
||
sys.stdout.write(json.dumps({"success": False, "error": {"message": str(e)}}))
|
||
sys.exit(0)
|
||
PY
|
||
}
|
||
|
||
api_extract() {
|
||
# api_extract JSON PYTHON_EXPR → چاپ نتیجه
|
||
local json="$1" expr="$2"
|
||
load_python_bin || ensure_python_runtime
|
||
HESABIX_JSON="$json" HESABIX_EXPR="$expr" "$PYTHON_BIN" - <<'PY'
|
||
import json, os, sys
|
||
data = json.loads(os.environ["HESABIX_JSON"] or "{}")
|
||
expr = os.environ["HESABIX_EXPR"]
|
||
try:
|
||
val = eval(expr, {"data": data, "json": json}) # noqa: S307 — ورودی خودمان
|
||
except Exception as e:
|
||
print("", end="")
|
||
sys.exit(0)
|
||
if val is None:
|
||
print("", end="")
|
||
elif isinstance(val, (dict, list)):
|
||
print(json.dumps(val, ensure_ascii=False))
|
||
else:
|
||
print(val, end="")
|
||
PY
|
||
}
|
||
|
||
# ─── configure ──────────────────────────────────────────────────────────────
|
||
|
||
cmd_configure() {
|
||
need_root configure
|
||
mkdir -p "$INSTALL_DIR"
|
||
load_python_bin || ensure_python_runtime
|
||
|
||
echo
|
||
echo -e "${c_bold}پیکربندی اتصال حسابیکس ↔ Issabel/Asterisk${c_reset}"
|
||
echo "۱) در حسابیکس بروید به: پروفایل ← کلیدهای API و یک کلید بسازید/کپی کنید"
|
||
echo "۲) اینجا وارد کنید؛ کسبوکار و مرکز تلفن را از لیست انتخاب میکنید"
|
||
echo "آدرس API پیشفرض: ${HESABIX_DEFAULT_API_URL}"
|
||
echo
|
||
|
||
local env_override_api="${HESABIX_API_URL:-}"
|
||
local cur_api="" cur_bid="" cur_pid="" cur_token="" cur_ami_host="127.0.0.1" cur_ami_port="5038" cur_ami_user="hesabix" cur_ami_secret=""
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE" || true
|
||
cur_api="${HESABIX_API_URL:-}"
|
||
cur_bid="${HESABIX_BUSINESS_ID:-}"
|
||
cur_pid="${HESABIX_PBX_ID:-}"
|
||
cur_token="${HESABIX_CONNECTOR_TOKEN:-}"
|
||
cur_ami_host="${AMI_HOST:-127.0.0.1}"
|
||
cur_ami_port="${AMI_PORT:-5038}"
|
||
cur_ami_user="${AMI_USER:-hesabix}"
|
||
cur_ami_secret="${AMI_SECRET:-}"
|
||
fi
|
||
cur_api="${cur_api:-${env_override_api:-$HESABIX_DEFAULT_API_URL}}"
|
||
|
||
local api user_key
|
||
api="$(prompt "آدرس API حسابیکس" "$cur_api")"
|
||
api="${api%/}"
|
||
user_key="$(prompt_secret "کلید API حسابیکس (از پروفایل)" "")"
|
||
[[ -n "$api" && -n "$user_key" ]] || die "آدرس API و کلید API الزامی است."
|
||
|
||
log "در حال دریافت لیست کسبوکارها…"
|
||
local biz_raw biz_code
|
||
local tmp_http tmp_body
|
||
tmp_http="$(mktemp)"
|
||
tmp_body="$(mktemp)"
|
||
api_request_json POST "${api}/api/v1/businesses/list?take=100&skip=0" "$user_key" "{}" \
|
||
>"$tmp_body" 2>"$tmp_http" || true
|
||
biz_code="$(tr -d '[:space:]' < "$tmp_http")"
|
||
biz_raw="$(cat "$tmp_body")"
|
||
rm -f "$tmp_http" "$tmp_body"
|
||
|
||
if [[ "$biz_code" != "200" ]]; then
|
||
err "ورود با کلید API ناموفق بود (HTTP ${biz_code:-?})."
|
||
echo "$(api_extract "$biz_raw" "data.get('error',{}).get('message') or data.get('message') or ''")"
|
||
echo "کلید را از: پروفایل ← کلیدهای API بررسی کنید."
|
||
exit 1
|
||
fi
|
||
|
||
local biz_json
|
||
# normalize items
|
||
biz_json="$(HESABIX_JSON="$biz_raw" "$PYTHON_BIN" - <<'PY'
|
||
import json, os
|
||
root = json.loads(os.environ["HESABIX_JSON"])
|
||
payload = root.get("data", root)
|
||
items = payload.get("items") if isinstance(payload, dict) else payload
|
||
if not isinstance(items, list):
|
||
items = []
|
||
print(json.dumps(items, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
local biz_count
|
||
biz_count="$(HESABIX_JSON="$biz_json" "$PYTHON_BIN" -c 'import json,os; print(len(json.loads(os.environ["HESABIX_JSON"])))')"
|
||
if [[ "$biz_count" -eq 0 ]]; then
|
||
die "هیچ کسبوکاری برای این کاربر پیدا نشد."
|
||
fi
|
||
|
||
local -a biz_labels=()
|
||
local i label
|
||
for ((i = 0; i < biz_count; i++)); do
|
||
label="$(HESABIX_JSON="$biz_json" HESABIX_I="$i" "$PYTHON_BIN" - <<'PY'
|
||
import json, os
|
||
items = json.loads(os.environ["HESABIX_JSON"])
|
||
b = items[int(os.environ["HESABIX_I"])]
|
||
print(f"{b.get('name') or 'بدوننام'} (ID: {b.get('id')})")
|
||
PY
|
||
)"
|
||
biz_labels+=("$label")
|
||
done
|
||
|
||
local biz_choice biz_id biz_name
|
||
# پیشفرض روی کسبوکار قبلی اگر در لیست باشد
|
||
local default_biz="1"
|
||
if [[ -n "$cur_bid" ]]; then
|
||
default_biz="$(HESABIX_JSON="$biz_json" HESABIX_BID="$cur_bid" "$PYTHON_BIN" - <<'PY'
|
||
import json, os
|
||
items = json.loads(os.environ["HESABIX_JSON"])
|
||
want = str(os.environ.get("HESABIX_BID") or "")
|
||
for i, b in enumerate(items):
|
||
if str(b.get("id")) == want:
|
||
print(i + 1)
|
||
break
|
||
else:
|
||
print(1)
|
||
PY
|
||
)"
|
||
fi
|
||
echo
|
||
echo -e "${c_bold}انتخاب کسبوکار${c_reset}"
|
||
for i in "${!biz_labels[@]}"; do
|
||
printf " %b%d)%b %s\n" "$c_cyan" "$((i + 1))" "$c_reset" "${biz_labels[$i]}"
|
||
done
|
||
biz_choice="$(prompt "شماره کسبوکار" "$default_biz")"
|
||
[[ "$biz_choice" =~ ^[0-9]+$ ]] && (( biz_choice >= 1 && biz_choice <= biz_count )) \
|
||
|| die "انتخاب کسبوکار نامعتبر است."
|
||
biz_id="$(HESABIX_JSON="$biz_json" HESABIX_I="$((biz_choice - 1))" "$PYTHON_BIN" -c 'import json,os; print(json.loads(os.environ["HESABIX_JSON"])[int(os.environ["HESABIX_I"])]["id"])')"
|
||
biz_name="$(HESABIX_JSON="$biz_json" HESABIX_I="$((biz_choice - 1))" "$PYTHON_BIN" -c 'import json,os; print(json.loads(os.environ["HESABIX_JSON"])[int(os.environ["HESABIX_I"])].get("name") or "")')"
|
||
ok "کسبوکار انتخاب شد: $biz_name (#$biz_id)"
|
||
|
||
log "دریافت مراکز تلفن…"
|
||
local pbx_raw pbx_code pbx_json pbx_count
|
||
tmp_http="$(mktemp)"; tmp_body="$(mktemp)"
|
||
api_request_json GET "${api}/api/v1/telephony/business/${biz_id}/pbx" "$user_key" \
|
||
>"$tmp_body" 2>"$tmp_http" || true
|
||
pbx_code="$(tr -d '[:space:]' < "$tmp_http")"
|
||
pbx_raw="$(cat "$tmp_body")"
|
||
rm -f "$tmp_http" "$tmp_body"
|
||
|
||
if [[ "$pbx_code" == "403" ]]; then
|
||
err "افزونه تلفن برای این کسبوکار فعال نیست یا مجوز ندارید (HTTP 403)."
|
||
echo "در حسابیکس: مارکتپلیس ← اتصال به استریسک و ایزابل را فعال کنید."
|
||
exit 1
|
||
fi
|
||
if [[ "$pbx_code" != "200" ]]; then
|
||
err "دریافت مراکز تلفن ناموفق (HTTP ${pbx_code:-?})."
|
||
echo "$(api_extract "$pbx_raw" "data.get('error',{}).get('message') or data.get('message') or ''")"
|
||
exit 1
|
||
fi
|
||
|
||
pbx_json="$(HESABIX_JSON="$pbx_raw" "$PYTHON_BIN" - <<'PY'
|
||
import json, os
|
||
root = json.loads(os.environ["HESABIX_JSON"])
|
||
payload = root.get("data", root)
|
||
items = payload.get("items") if isinstance(payload, dict) else []
|
||
print(json.dumps(items if isinstance(items, list) else [], ensure_ascii=False))
|
||
PY
|
||
)"
|
||
pbx_count="$(HESABIX_JSON="$pbx_json" "$PYTHON_BIN" -c 'import json,os; print(len(json.loads(os.environ["HESABIX_JSON"])))')"
|
||
|
||
local pbx_id="" token="" pbx_name=""
|
||
local create_new="N"
|
||
if [[ "$pbx_count" -eq 0 ]]; then
|
||
warn "مرکز تلفنی تعریف نشده."
|
||
create_new="$(prompt "یک مرکز تلفن جدید بسازم؟ (Y/n)" "Y")"
|
||
[[ "$create_new" =~ ^[Yy]$ ]] || die "بدون مرکز تلفن نمیتوان ادامه داد. در UI بسازید یا دوباره configure بزنید."
|
||
else
|
||
local -a pbx_labels=()
|
||
for ((i = 0; i < pbx_count; i++)); do
|
||
label="$(HESABIX_JSON="$pbx_json" HESABIX_I="$i" "$PYTHON_BIN" - <<'PY'
|
||
import json, os
|
||
p = json.loads(os.environ["HESABIX_JSON"])[int(os.environ["HESABIX_I"])]
|
||
print(f"{p.get('name') or 'PBX'} (ID: {p.get('id')}) · وضعیت: {p.get('status') or '—'} · توکن: {p.get('connector_token_prefix') or ''}…")
|
||
PY
|
||
)"
|
||
pbx_labels+=("$label")
|
||
done
|
||
pbx_labels+=("➕ ایجاد مرکز تلفن جدید")
|
||
echo
|
||
echo -e "${c_bold}انتخاب مرکز تلفن (PBX)${c_reset}"
|
||
for i in "${!pbx_labels[@]}"; do
|
||
printf " %b%d)%b %s\n" "$c_cyan" "$((i + 1))" "$c_reset" "${pbx_labels[$i]}"
|
||
done
|
||
local pbx_choice default_pbx="1"
|
||
if [[ -n "$cur_pid" ]]; then
|
||
default_pbx="$(HESABIX_JSON="$pbx_json" HESABIX_PID="$cur_pid" "$PYTHON_BIN" - <<'PY'
|
||
import json, os
|
||
items = json.loads(os.environ["HESABIX_JSON"])
|
||
want = str(os.environ.get("HESABIX_PID") or "")
|
||
for i, p in enumerate(items):
|
||
if str(p.get("id")) == want:
|
||
print(i + 1)
|
||
break
|
||
else:
|
||
print(1)
|
||
PY
|
||
)"
|
||
fi
|
||
pbx_choice="$(prompt "شماره مرکز تلفن" "$default_pbx")"
|
||
[[ "$pbx_choice" =~ ^[0-9]+$ ]] || die "انتخاب نامعتبر"
|
||
if (( pbx_choice == pbx_count + 1 )); then
|
||
create_new="Y"
|
||
elif (( pbx_choice >= 1 && pbx_choice <= pbx_count )); then
|
||
pbx_id="$(HESABIX_JSON="$pbx_json" HESABIX_I="$((pbx_choice - 1))" "$PYTHON_BIN" -c 'import json,os; print(json.loads(os.environ["HESABIX_JSON"])[int(os.environ["HESABIX_I"])]["id"])')"
|
||
pbx_name="$(HESABIX_JSON="$pbx_json" HESABIX_I="$((pbx_choice - 1))" "$PYTHON_BIN" -c 'import json,os; print(json.loads(os.environ["HESABIX_JSON"])[int(os.environ["HESABIX_I"])].get("name") or "")')"
|
||
ok "مرکز تلفن: $pbx_name (#$pbx_id)"
|
||
else
|
||
die "انتخاب مرکز تلفن نامعتبر است."
|
||
fi
|
||
fi
|
||
|
||
if [[ "$create_new" =~ ^[Yy]$ ]]; then
|
||
local new_name
|
||
new_name="$(prompt "نام مرکز تلفن" "Issabel اصلی")"
|
||
log "ایجاد مرکز تلفن…"
|
||
tmp_http="$(mktemp)"; tmp_body="$(mktemp)"
|
||
api_request_json POST "${api}/api/v1/telephony/business/${biz_id}/pbx" "$user_key" \
|
||
"$(HESABIX_N="$new_name" "$PYTHON_BIN" -c 'import json,os; print(json.dumps({"name":os.environ["HESABIX_N"],"pbx_type":"issabel"},ensure_ascii=False))')" \
|
||
>"$tmp_body" 2>"$tmp_http" || true
|
||
local create_code create_raw
|
||
create_code="$(tr -d '[:space:]' < "$tmp_http")"
|
||
create_raw="$(cat "$tmp_body")"
|
||
rm -f "$tmp_http" "$tmp_body"
|
||
[[ "$create_code" == "200" ]] || die "ایجاد PBX ناموفق (HTTP ${create_code}). $(api_extract "$create_raw" "data.get('error',{}).get('message') or ''")"
|
||
pbx_id="$(HESABIX_JSON="$create_raw" "$PYTHON_BIN" -c 'import json,os; d=json.loads(os.environ["HESABIX_JSON"]).get("data",{}); print(d.get("id") or "")')"
|
||
token="$(HESABIX_JSON="$create_raw" "$PYTHON_BIN" -c 'import json,os; d=json.loads(os.environ["HESABIX_JSON"]).get("data",{}); print(d.get("connector_token") or "")')"
|
||
pbx_name="$new_name"
|
||
[[ -n "$pbx_id" && -n "$token" ]] || die "پاسخ ایجاد PBX ناقص بود."
|
||
ok "مرکز تلفن ساخته شد (#$pbx_id) و توکن Connector دریافت شد."
|
||
else
|
||
# توکن قبلی فقط یکبار نمایش داده میشود؛ برای اتصال باید rotate کنیم یا از .env قبلی استفاده کنیم
|
||
if [[ -n "$cur_token" && "$cur_pid" == "$pbx_id" && "$cur_bid" == "$biz_id" ]]; then
|
||
local keep
|
||
keep="$(prompt "توکن فعلی .env را نگه دارم؟ (Y=نگه دار / n=توکن جدید)" "Y")"
|
||
if [[ "$keep" =~ ^[Yy]$ ]]; then
|
||
token="$cur_token"
|
||
ok "همان توکن قبلی استفاده میشود."
|
||
fi
|
||
fi
|
||
if [[ -z "$token" ]]; then
|
||
log "صدور توکن جدید Connector (rotate)…"
|
||
tmp_http="$(mktemp)"; tmp_body="$(mktemp)"
|
||
api_request_json POST "${api}/api/v1/telephony/business/${biz_id}/pbx/${pbx_id}/rotate-token" "$user_key" "{}" \
|
||
>"$tmp_body" 2>"$tmp_http" || true
|
||
local rot_code rot_raw
|
||
rot_code="$(tr -d '[:space:]' < "$tmp_http")"
|
||
rot_raw="$(cat "$tmp_body")"
|
||
rm -f "$tmp_http" "$tmp_body"
|
||
[[ "$rot_code" == "200" ]] || die "صدور توکن ناموفق (HTTP ${rot_code}). مجوز telephony.manage لازم است."
|
||
token="$(HESABIX_JSON="$rot_raw" "$PYTHON_BIN" -c 'import json,os; d=json.loads(os.environ["HESABIX_JSON"]).get("data",{}); print(d.get("connector_token") or "")')"
|
||
[[ -n "$token" ]] || die "توکن در پاسخ نبود."
|
||
ok "توکن Connector جدید دریافت شد."
|
||
fi
|
||
fi
|
||
|
||
echo
|
||
echo -e "${c_bold}تنظیمات AMI روی همین سرور تلفن${c_reset}"
|
||
local ami_host ami_port ami_user ami_secret
|
||
ami_host="$(prompt "آدرس AMI" "$cur_ami_host")"
|
||
ami_port="$(prompt "پورت AMI" "$cur_ami_port")"
|
||
ami_user="$(prompt "کاربر AMI" "$cur_ami_user")"
|
||
ami_secret="$(prompt_secret "رمز AMI" "$cur_ami_secret")"
|
||
|
||
[[ -n "$api" && -n "$biz_id" && -n "$pbx_id" && -n "$token" && -n "$ami_user" && -n "$ami_secret" ]] \
|
||
|| die "مقادیر ضروری ناقص است."
|
||
|
||
umask 077
|
||
# مقادیر را برای EnvironmentFile systemd با کوتیشن امن مینویسیم
|
||
qenv() { "$PYTHON_BIN" -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }
|
||
cat > "$ENV_FILE" <<EOF
|
||
# generated by hesabix-pbx $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
# business_id=${biz_id} pbx_id=${pbx_id}
|
||
HESABIX_API_URL=$(qenv "$api")
|
||
HESABIX_BUSINESS_ID=$(qenv "$biz_id")
|
||
HESABIX_PBX_ID=$(qenv "$pbx_id")
|
||
HESABIX_CONNECTOR_TOKEN=$(qenv "$token")
|
||
AMI_HOST=$(qenv "$ami_host")
|
||
AMI_PORT=$(qenv "$ami_port")
|
||
AMI_USER=$(qenv "$ami_user")
|
||
AMI_SECRET=$(qenv "$ami_secret")
|
||
# Softphone Media Relay
|
||
AUDIOSOCKET_HOST="127.0.0.1"
|
||
AUDIOSOCKET_PORT="9092"
|
||
MEDIA_TUNNEL_ENABLED="1"
|
||
EOF
|
||
chmod 600 "$ENV_FILE"
|
||
ok "پیکربندی ذخیره شد: $ENV_FILE"
|
||
echo " Business ID = $biz_id"
|
||
echo " PBX ID = $pbx_id"
|
||
|
||
local setup_ami
|
||
setup_ami="$(prompt "کاربر AMI را در manager.conf بسازم؟ (y/N)" "N")"
|
||
if [[ "$setup_ami" =~ ^[Yy]$ ]]; then
|
||
setup_ami_user "$ami_user" "$ami_secret"
|
||
fi
|
||
}
|
||
|
||
enable_ami_general() {
|
||
# فعالسازی AMI در [general] — بدون این، پورت 5038 بسته است (Connection refused)
|
||
local conf="${1:-/etc/asterisk/manager.conf}"
|
||
local port="${2:-5038}"
|
||
local bind="${3:-127.0.0.1}"
|
||
[[ -f "$conf" ]] || { warn "فایل $conf نیست."; return 1; }
|
||
load_python_bin || ensure_python_runtime
|
||
|
||
cp -a "$conf" "${conf}.bak.hesabix-enable-$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||
|
||
"$PYTHON_BIN" - "$conf" "$port" "$bind" <<'PY'
|
||
import re, sys
|
||
path, port, bind = sys.argv[1], sys.argv[2], sys.argv[3]
|
||
text = open(path, "r", encoding="utf-8", errors="replace").read()
|
||
# بخش general را پیدا کن
|
||
m = re.search(r"(?ms)^\[general\]\s*\n(.*?)(?=^\[|\Z)", text)
|
||
if not m:
|
||
block = f"[general]\nenabled = yes\nport = {port}\nbindaddr = {bind}\nwebenabled = no\n\n"
|
||
text = block + text
|
||
else:
|
||
body = m.group(1)
|
||
def set_kv(body, key, val):
|
||
pat = re.compile(rf"(?im)^(\s*{re.escape(key)}\s*=\s*).*$")
|
||
if pat.search(body):
|
||
return pat.sub(rf"\1{val}", body, count=1)
|
||
return body.rstrip() + f"\n{key} = {val}\n"
|
||
body = set_kv(body, "enabled", "yes")
|
||
body = set_kv(body, "port", port)
|
||
body = set_kv(body, "bindaddr", bind)
|
||
# بعضی نصبها bindaddr را جدا دارند
|
||
text = text[: m.start(1)] + body + text[m.end(1) :]
|
||
open(path, "w", encoding="utf-8").write(text)
|
||
print(f"AMI enabled in {path} (bind={bind}:{port})")
|
||
PY
|
||
}
|
||
|
||
setup_ami_user() {
|
||
local user="$1" secret="$2"
|
||
local port="${AMI_PORT:-5038}"
|
||
local bind="${AMI_HOST:-127.0.0.1}"
|
||
# اگر bind روی 0.0.0.0 باشد برای listen مناسبتر است؛ برای کلاینت 127.0.0.1 بماند
|
||
local listen_bind="127.0.0.1"
|
||
if [[ "$bind" != "127.0.0.1" && "$bind" != "localhost" ]]; then
|
||
listen_bind="0.0.0.0"
|
||
fi
|
||
load_python_bin || ensure_python_runtime
|
||
|
||
local main_conf="/etc/asterisk/manager.conf"
|
||
if [[ ! -f "$main_conf" ]]; then
|
||
warn "manager.conf پیدا نشد — آیا Asterisk نصب است؟"
|
||
return 1
|
||
fi
|
||
|
||
log "فعالسازی AMI در manager.conf …"
|
||
enable_ami_general "$main_conf" "$port" "$listen_bind" || return 1
|
||
|
||
local conf=""
|
||
if [[ -d /etc/asterisk/manager.d ]]; then
|
||
conf="/etc/asterisk/manager.d/hesabix.conf"
|
||
elif [[ -f /etc/asterisk/manager_custom.conf ]]; then
|
||
conf="/etc/asterisk/manager_custom.conf"
|
||
else
|
||
conf="$main_conf"
|
||
fi
|
||
|
||
# کاربر را بساز یا secret را بهروز کن
|
||
if grep -q "^\[${user}\]" "$conf" 2>/dev/null || grep -q "^\[${user}\]" "$main_conf" 2>/dev/null; then
|
||
log "کاربر [$user] هست — secret را در $conf همگام میکنم…"
|
||
local target="$conf"
|
||
if ! grep -q "^\[${user}\]" "$conf" 2>/dev/null; then
|
||
target="$main_conf"
|
||
fi
|
||
cp -a "$target" "${target}.bak.hesabix-user-$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||
"$PYTHON_BIN" - "$target" "$user" "$secret" <<'PY'
|
||
import re, sys
|
||
path, user, secret = sys.argv[1], sys.argv[2], sys.argv[3]
|
||
text = open(path, "r", encoding="utf-8", errors="replace").read()
|
||
pat = re.compile(rf"(?ms)^\[{re.escape(user)}\]\s*\n(.*?)(?=^\[|\Z)")
|
||
m = pat.search(text)
|
||
if not m:
|
||
sys.exit(0)
|
||
body = m.group(1)
|
||
if re.search(r"(?im)^\s*secret\s*=", body):
|
||
body = re.sub(r"(?im)^(\s*secret\s*=\s*).*$", rf"\1{secret}", body, count=1)
|
||
else:
|
||
body = f"secret = {secret}\n" + body
|
||
# اطمینان از permit لوکال
|
||
if not re.search(r"(?im)^\s*permit\s*=", body):
|
||
body += "permit = 127.0.0.1/255.255.255.255\n"
|
||
text = text[: m.start(1)] + body + text[m.end(1) :]
|
||
open(path, "w", encoding="utf-8").write(text)
|
||
print(f"updated [{user}] in {path}")
|
||
PY
|
||
ok "کاربر AMI [$user] بهروز شد ($target)."
|
||
else
|
||
cp -a "$conf" "${conf}.bak.hesabix-$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||
mkdir -p "$(dirname "$conf")"
|
||
cat >> "$conf" <<EOF
|
||
|
||
; --- Hesabix Telephony Connector ($(date +%Y-%m-%d)) ---
|
||
[${user}]
|
||
secret = ${secret}
|
||
deny = 0.0.0.0/0.0.0.0
|
||
permit = 127.0.0.1/255.255.255.255
|
||
read = system,call,log,verbose,command,agent,user,originate,dialplan
|
||
write = system,call,log,verbose,command,agent,user,originate,dialplan
|
||
writetimeout = 5000
|
||
EOF
|
||
ok "کاربر AMI به $conf اضافه شد."
|
||
fi
|
||
|
||
# Asterisk در حال اجرا باشد
|
||
if have systemctl; then
|
||
if ! systemctl is-active --quiet asterisk 2>/dev/null \
|
||
&& ! systemctl is-active --quiet asterisk.service 2>/dev/null; then
|
||
log "سرویس asterisk در حال اجرا نیست — تلاش برای start…"
|
||
systemctl start asterisk 2>/dev/null || systemctl start asterisk.service 2>/dev/null || true
|
||
sleep 2
|
||
fi
|
||
fi
|
||
|
||
if have asterisk; then
|
||
asterisk -rx "manager reload" >/dev/null 2>&1 && ok "manager reload شد." \
|
||
|| warn "manager reload ناموفق"
|
||
sleep 1
|
||
local ami_state
|
||
ami_state="$(asterisk -rx "manager show settings" 2>/dev/null | grep -i "Manager (AMI)" || true)"
|
||
echo " $ami_state"
|
||
if echo "$ami_state" | grep -qi "No"; then
|
||
warn "هنوز AMI=No است. اگر Issabel/FreePBX دارید از پنل Settings → Asterisk Manager هم Enabled کنید."
|
||
warn "یا فایل /etc/asterisk/manager.conf را دستی چک کنید و: fwconsole reload"
|
||
fi
|
||
else
|
||
warn "دستور asterisk پیدا نشد."
|
||
fi
|
||
|
||
# بررسی پورت
|
||
if have ss; then
|
||
if ss -lnt | grep -q ":${port} "; then
|
||
ok "پورت AMI ${port} در حال listen است."
|
||
else
|
||
warn "پورت ${port} هنوز listen نیست — Connection refused ادامه خواهد داشت."
|
||
echo " ss -lntp | grep ${port}"
|
||
echo " asterisk -rx 'manager show settings'"
|
||
fi
|
||
elif have netstat; then
|
||
netstat -lnt 2>/dev/null | grep -q ":${port} " && ok "پورت ${port} listen است." \
|
||
|| warn "پورت ${port} listen نیست."
|
||
fi
|
||
}
|
||
|
||
# ─── service ops ────────────────────────────────────────────────────────────
|
||
|
||
cmd_start() {
|
||
need_root start
|
||
[[ -f "$ENV_FILE" ]] || die "ابتدا پیکربندی کنید: hesabix-pbx configure"
|
||
[[ -f "$SERVICE_FILE" ]] || install_systemd
|
||
systemctl start "$SERVICE_NAME"
|
||
ok "سرویس شروع شد."
|
||
cmd_status
|
||
}
|
||
|
||
cmd_stop() {
|
||
need_root stop
|
||
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||
ok "سرویس متوقف شد."
|
||
}
|
||
|
||
cmd_restart() {
|
||
need_root restart
|
||
[[ -f "$ENV_FILE" ]] || die "ابتدا پیکربندی کنید: hesabix-pbx configure"
|
||
[[ -f "$SERVICE_FILE" ]] || install_systemd
|
||
systemctl restart "$SERVICE_NAME"
|
||
ok "سرویس ریاستارت شد."
|
||
sleep 1
|
||
cmd_status
|
||
}
|
||
|
||
cmd_status() {
|
||
echo -e "${c_bold}وضعیت Hesabix PBX Connector${c_reset}"
|
||
echo "نصب: $INSTALL_DIR"
|
||
if [[ -f "$INSTALL_DIR/VERSION" ]]; then
|
||
echo "نسخه: $(tr -d '\n' < "$INSTALL_DIR/VERSION")"
|
||
fi
|
||
if [[ -z "$OS_FAMILY" || "$OS_FAMILY" == "unknown" ]]; then
|
||
detect_os || true
|
||
fi
|
||
if [[ -n "$OS_NAME" || -n "$OS_FAMILY" ]]; then
|
||
local os_line="${OS_NAME:-—} (${OS_FAMILY}"
|
||
[[ -n "$PKG_MGR" ]] && os_line+=", ${PKG_MGR}"
|
||
[[ $IS_ISSABEL -eq 1 ]] && os_line+=", Issabel"
|
||
os_line+=")"
|
||
echo "سیستم: $os_line"
|
||
fi
|
||
if load_python_bin 2>/dev/null; then
|
||
echo "Python: $PYTHON_BIN ($(py_version_str "$PYTHON_BIN"))"
|
||
else
|
||
warn "Python ≥${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR} پیدا نشد — hesabix-pbx update"
|
||
fi
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE" || true
|
||
echo "API: ${HESABIX_API_URL:-—}"
|
||
echo "Business: ${HESABIX_BUSINESS_ID:-—}"
|
||
echo "PBX ID: ${HESABIX_PBX_ID:-—}"
|
||
echo "AMI: ${AMI_USER:-—}@${AMI_HOST:-—}:${AMI_PORT:-—}"
|
||
else
|
||
warn "فایل .env وجود ندارد — hesabix-pbx configure"
|
||
fi
|
||
echo
|
||
if have systemctl && [[ -f "$SERVICE_FILE" ]]; then
|
||
systemctl --no-pager --full status "$SERVICE_NAME" 2>/dev/null | sed -n '1,12p' || true
|
||
else
|
||
warn "سرویس systemd نصب نشده."
|
||
fi
|
||
}
|
||
|
||
cmd_logs() {
|
||
local n="${1:-80}"
|
||
if have journalctl; then
|
||
journalctl -u "$SERVICE_NAME" -n "$n" --no-pager -l
|
||
else
|
||
die "journalctl در دسترس نیست."
|
||
fi
|
||
}
|
||
|
||
cmd_follow() {
|
||
journalctl -u "$SERVICE_NAME" -f -l
|
||
}
|
||
|
||
# ─── test ───────────────────────────────────────────────────────────────────
|
||
|
||
cmd_test() {
|
||
[[ -f "$ENV_FILE" ]] || die "ابتدا: hesabix-pbx configure"
|
||
load_python_bin || ensure_python_runtime
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE"
|
||
set +a
|
||
|
||
echo -e "${c_bold}۱) تست AMI${c_reset}"
|
||
"$PYTHON_BIN" - <<'PY' || die "تست AMI ناموفق"
|
||
import os, socket, sys
|
||
host = os.environ.get("AMI_HOST", "127.0.0.1")
|
||
port = int(os.environ.get("AMI_PORT", "5038"))
|
||
user = os.environ.get("AMI_USER") or ""
|
||
secret = os.environ.get("AMI_SECRET") or ""
|
||
if not user or not secret:
|
||
print("AMI_USER/AMI_SECRET در محیط نیست. .env را بررسی کنید:", os.environ.get("HESABIX_PBX_ENV", "/opt/HesabixTelephonyConnector/.env"))
|
||
sys.exit(1)
|
||
try:
|
||
s = socket.create_connection((host, port), timeout=5)
|
||
except Exception as e:
|
||
print("AMI connect failed:", e)
|
||
sys.exit(1)
|
||
banner = s.recv(256).decode("utf-8", "ignore")
|
||
payload = f"Action: Login\r\nUsername: {user}\r\nSecret: {secret}\r\n\r\n"
|
||
s.sendall(payload.encode())
|
||
s.settimeout(5)
|
||
data = b""
|
||
while b"\r\n\r\n" not in data:
|
||
chunk = s.recv(4096)
|
||
if not chunk:
|
||
break
|
||
data += chunk
|
||
s.close()
|
||
text = data.decode("utf-8", "ignore")
|
||
if "Success" in text or "Authentication accepted" in text:
|
||
print("AMI OK —", banner.strip().splitlines()[0] if banner.strip() else "connected")
|
||
sys.exit(0)
|
||
print("AMI login failed:\n", text[:400])
|
||
sys.exit(1)
|
||
PY
|
||
ok "AMI پذیرفته شد."
|
||
|
||
echo
|
||
echo -e "${c_bold}۲) تست Heartbeat حسابیکس${c_reset}"
|
||
"$PYTHON_BIN" - <<'PY' || die "تست API ناموفق"
|
||
import json, os, sys, urllib.request, urllib.error
|
||
base = (os.environ.get("HESABIX_API_URL") or "").rstrip("/")
|
||
token = os.environ.get("HESABIX_CONNECTOR_TOKEN") or ""
|
||
bid = os.environ.get("HESABIX_BUSINESS_ID") or ""
|
||
pid = os.environ.get("HESABIX_PBX_ID") or ""
|
||
if not all([base, token, bid, pid]):
|
||
print("متغیرهای Hesabix ناقص است")
|
||
sys.exit(1)
|
||
url = f"{base}/api/v1/telephony/connector/heartbeat"
|
||
body = json.dumps({"status": "ok", "connector_version": "cli-test"}).encode()
|
||
req = urllib.request.Request(
|
||
url,
|
||
data=body,
|
||
headers={
|
||
"Authorization": f"Bearer {token}",
|
||
"Content-Type": "application/json",
|
||
"X-Hesabix-Business-Id": str(bid),
|
||
"X-Hesabix-Pbx-Id": str(pid),
|
||
},
|
||
method="POST",
|
||
)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||
raw = resp.read().decode()
|
||
print("HTTP", resp.status, raw[:300])
|
||
if resp.status >= 400:
|
||
sys.exit(1)
|
||
except urllib.error.HTTPError as e:
|
||
print("HTTP", e.code, e.read().decode()[:400])
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print("API error:", e)
|
||
sys.exit(1)
|
||
print("Hesabix heartbeat OK")
|
||
PY
|
||
ok "اتصال به حسابیکس برقرار است."
|
||
echo
|
||
ok "هر دو تست موفق بودند. در UI وضعیت مرکز تلفن باید online شود."
|
||
}
|
||
|
||
cmd_fix_ami() {
|
||
need_root fix-ami
|
||
[[ -f "$ENV_FILE" ]] || die "ابتدا: hesabix-pbx configure"
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE"
|
||
set +a
|
||
local user="${AMI_USER:-hesabix}" secret="${AMI_SECRET:-}"
|
||
[[ -n "$secret" ]] || die "AMI_SECRET در .env خالی است — hesabix-pbx configure"
|
||
setup_ami_user "$user" "$secret"
|
||
systemctl restart "$SERVICE_NAME" || true
|
||
sleep 1
|
||
cmd_test
|
||
}
|
||
|
||
cmd_add_extension() {
|
||
need_root add-extension
|
||
local ext="${1:-}"
|
||
[[ -n "$ext" ]] || ext="$(prompt "شماره داخلی" "500")"
|
||
[[ -f "$ENV_FILE" ]] || die "ابتدا: hesabix-pbx configure"
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE"
|
||
set +a
|
||
local api="${HESABIX_API_URL%/}" bid="${HESABIX_BUSINESS_ID}" pid="${HESABIX_PBX_ID}"
|
||
local user_key display
|
||
user_key="$(prompt_secret "کلید API حسابیکس (پروفایل)" "")"
|
||
display="$(prompt "نام نمایشی داخلی" "داخلی $ext")"
|
||
[[ -n "$user_key" ]] || die "کلید API لازم است."
|
||
|
||
log "ثبت داخلی $ext در حسابیکس…"
|
||
local tmp_http tmp_body code raw
|
||
tmp_http="$(mktemp)"; tmp_body="$(mktemp)"
|
||
api_request_json POST "${api}/api/v1/telephony/business/${bid}/extensions" "$user_key" \
|
||
"$(EXT="$ext" PID="$pid" DN="$display" "$PYTHON_BIN" -c 'import json,os; print(json.dumps({"pbx_id":int(os.environ["PID"]),"extension":os.environ["EXT"],"display_name":os.environ["DN"]},ensure_ascii=False))')" \
|
||
>"$tmp_body" 2>"$tmp_http" || true
|
||
code="$(tr -d '[:space:]' < "$tmp_http")"
|
||
raw="$(cat "$tmp_body")"
|
||
rm -f "$tmp_http" "$tmp_body"
|
||
[[ "$code" == "200" ]] || die "ثبت داخلی ناموفق (HTTP $code). $raw"
|
||
ok "داخلی $ext در حسابیکس ثبت شد."
|
||
echo
|
||
warn "اگر این داخلی روی خود Issabel/Asterisk هنوز وجود ندارد، از پنل Issabel (PBX → Extensions) هم بسازید تا تلفن واقعی زنگ بخورد."
|
||
}
|
||
# ─── install / update / uninstall ───────────────────────────────────────────
|
||
|
||
cmd_install() {
|
||
need_root install
|
||
echo
|
||
echo -e "${c_bold}نصب Hesabix Telephony Connector${c_reset}"
|
||
echo "مسیر نصب: $INSTALL_DIR"
|
||
echo
|
||
|
||
ensure_prerequisites
|
||
|
||
sync_connector
|
||
install_cli
|
||
install_systemd
|
||
|
||
if [[ ! -f "$ENV_FILE" ]]; then
|
||
cmd_configure
|
||
else
|
||
warn ".env موجود است — برای تغییر: hesabix-pbx configure"
|
||
fi
|
||
|
||
systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME"
|
||
echo
|
||
ok "نصب کامل شد."
|
||
echo
|
||
echo "دستورات مفید:"
|
||
echo " hesabix-pbx status"
|
||
echo " hesabix-pbx test"
|
||
echo " hesabix-pbx logs"
|
||
echo " hesabix-pbx update"
|
||
echo
|
||
cmd_status
|
||
}
|
||
|
||
cmd_update() {
|
||
need_root update
|
||
[[ -d "$INSTALL_DIR" ]] || die "نصب پیدا نشد. اول: hesabix-pbx install"
|
||
ensure_prerequisites
|
||
log "بهروزرسانی کانکتور از مخزن…"
|
||
sync_connector
|
||
install_cli
|
||
install_systemd
|
||
# Softphone deps را در update هم نصب کن (اگر شکست خورد فقط هشدار)
|
||
if ensure_softphone_python_deps; then
|
||
ok "وابستگی Softphone (websocket-client) آماده است."
|
||
else
|
||
warn "نصب websocket-client ناموفق بود. بعداً بزنید: hesabix-pbx softphone-setup"
|
||
fi
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
ensure_softphone_env_keys
|
||
systemctl restart "$SERVICE_NAME" || true
|
||
fi
|
||
ok "بهروزرسانی انجام شد."
|
||
cmd_status
|
||
}
|
||
|
||
cmd_reinstall() {
|
||
need_root reinstall
|
||
warn "نصب مجدد — .env حفظ میشود."
|
||
cmd_update
|
||
if [[ ! -f "$ENV_FILE" ]]; then
|
||
cmd_configure
|
||
fi
|
||
systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME"
|
||
ok "نصب مجدد کامل شد."
|
||
}
|
||
|
||
cmd_uninstall() {
|
||
need_root uninstall
|
||
local keep
|
||
keep="$(prompt "فایل .env نگه داشته شود؟ (Y/n)" "Y")"
|
||
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||
rm -f "$SERVICE_FILE"
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
rm -f "$BIN_LINK"
|
||
if [[ "$keep" =~ ^[Nn]$ ]]; then
|
||
rm -rf "$INSTALL_DIR"
|
||
ok "همه چیز حذف شد."
|
||
else
|
||
# فقط کد را پاک نکن — فقط نشان بده متوقف شده
|
||
ok "سرویس و دستور حذف شد. پوشه $INSTALL_DIR (با .env) باقی ماند."
|
||
fi
|
||
}
|
||
|
||
cmd_help() {
|
||
cat <<EOF
|
||
${c_bold}hesabix-pbx${c_reset} v${VERSION} — مدیریت اتصال حسابیکس به Issabel/Asterisk
|
||
|
||
${c_bold}نصب اول (با root):${c_reset}
|
||
# اگر sudo دارید:
|
||
curl -fsSL ${RAW_BASE}/install.sh | sudo bash
|
||
# اگر sudo ندارید (Debian/Issabel با ورود root):
|
||
curl -fsSL ${RAW_BASE}/install.sh | bash
|
||
|
||
${c_bold}دستورات:${c_reset}
|
||
hesabix-pbx install نصب کامل (پیشنیاز + دانلود + پیکربندی + سرویس)
|
||
hesabix-pbx update بهروزرسانی از مخزن حسابیکس (+ وابستگی Softphone)
|
||
hesabix-pbx softphone-setup نصب خودکار Softphone Relay (websocket-client + .env + restart)
|
||
hesabix-pbx reinstall نصب مجدد (حفظ .env)
|
||
hesabix-pbx configure ویزارد پیکربندی / تغییر توکن، AMI و آدرس API
|
||
hesabix-pbx start شروع سرویس
|
||
hesabix-pbx stop توقف سرویس
|
||
hesabix-pbx restart ریاستارت
|
||
hesabix-pbx status وضعیت اتصال و سرویس
|
||
hesabix-pbx test تست AMI + Heartbeat حسابیکس
|
||
hesabix-pbx fix-ami ساخت/تعمیر کاربر AMI در manager.conf
|
||
hesabix-pbx add-extension ثبت داخلی در حسابیکس (مثلاً 500)
|
||
hesabix-pbx logs [N] آخرین لاگها (پیشفرض ۸۰ خط)
|
||
hesabix-pbx follow دنبال کردن زنده لاگ
|
||
hesabix-pbx uninstall حذف سرویس
|
||
hesabix-pbx help همین راهنما
|
||
|
||
${c_bold}متغیرهای اختیاری:${c_reset}
|
||
HESABIX_PBX_DIR=/opt/HesabixTelephonyConnector
|
||
HESABIX_PBX_REPO=${REPO_URL}
|
||
HESABIX_PBX_BRANCH=${REPO_BRANCH}
|
||
HESABIX_API_URL=${HESABIX_DEFAULT_API_URL} # پیشفرض ویزارد پیکربندی
|
||
|
||
${c_bold}توزیعهای پشتیبانیشده:${c_reset}
|
||
Debian / Ubuntu (apt-get) — بدون نیاز به sudo اگر با root اجرا شود
|
||
CentOS / RHEL / Rocky / Alma / Issabel (yum یا dnf)
|
||
نیازمندی: Python ≥3.8 (روی Issabel/Rocky8 در صورت نبود، python39 خودکار نصب میشود)
|
||
|
||
مخزن:
|
||
https://source.hesabix.ir/hesabix/arc/src/branch/master/extraScripts/HesabixTelephonyConnector
|
||
EOF
|
||
}
|
||
|
||
# ─── main ───────────────────────────────────────────────────────────────────
|
||
|
||
main() {
|
||
local cmd="${1:-help}"
|
||
shift || true
|
||
case "$cmd" in
|
||
install) cmd_install "$@" ;;
|
||
update) cmd_update "$@" ;;
|
||
softphone-setup|softphone|media-setup)
|
||
cmd_softphone_setup "$@" ;;
|
||
reinstall) cmd_reinstall "$@" ;;
|
||
configure|config|setup) cmd_configure "$@" ;;
|
||
start) cmd_start "$@" ;;
|
||
stop) cmd_stop "$@" ;;
|
||
restart) cmd_restart "$@" ;;
|
||
status) cmd_status "$@" ;;
|
||
test|check) cmd_test "$@" ;;
|
||
fix-ami|ami-fix) cmd_fix_ami "$@" ;;
|
||
add-extension|extension) cmd_add_extension "$@" ;;
|
||
logs|log) cmd_logs "$@" ;;
|
||
follow|tail) cmd_follow "$@" ;;
|
||
uninstall|remove) cmd_uninstall "$@" ;;
|
||
help|-h|--help) cmd_help ;;
|
||
version|-V|--version) echo "hesabix-pbx $VERSION" ;;
|
||
*)
|
||
err "دستور ناشناخته: $cmd"
|
||
echo
|
||
cmd_help
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|