feat(voip): Add VoIP packages with OVH provisioning and Jabber integration
New packages: - secubox-app-voip: Asterisk PBX in LXC container - luci-app-voip: Dashboard with extensions, trunks, click-to-call VoIP features: - voipctl CLI for container, extensions, trunks, calls, voicemail - OVH Telephony API auto-provisioning for SIP trunks - Click-to-call web interface with quick dial - RPCD backend with 15 methods Jabber VoIP integration: - Jingle VoIP support (STUN/TURN via mod_external_services) - SMS relay via OVH (messages to sms@domain) - Voicemail notifications via Asterisk AMI → XMPP - 9 new RPCD methods for VoIP features Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3c992026ed
commit
4ca46b61e2
@ -1,6 +1,6 @@
|
|||||||
# SecuBox UI & Theme History
|
# SecuBox UI & Theme History
|
||||||
|
|
||||||
_Last updated: 2026-02-17_
|
_Last updated: 2026-02-19_
|
||||||
|
|
||||||
1. **Unified Dashboard Refresh (2025-12-20)**
|
1. **Unified Dashboard Refresh (2025-12-20)**
|
||||||
- Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs.
|
- Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs.
|
||||||
@ -2334,3 +2334,76 @@ git checkout HEAD -- index.html
|
|||||||
- Fixed SSL certificate generation (openssl instead of prosodyctl)
|
- Fixed SSL certificate generation (openssl instead of prosodyctl)
|
||||||
- Added xchat.gk2.secubox.in route to mitmproxy-in haproxy-routes.json
|
- Added xchat.gk2.secubox.in route to mitmproxy-in haproxy-routes.json
|
||||||
- Fixed route IP from 127.0.0.1 to 192.168.255.1 for container accessibility
|
- Fixed route IP from 127.0.0.1 to 192.168.255.1 for container accessibility
|
||||||
|
|
||||||
|
### 2026-02-19: VoIP + Jabber Integration (Asterisk PBX)
|
||||||
|
|
||||||
|
**New Packages:**
|
||||||
|
- `secubox-app-voip` - LXC-based Asterisk PBX server
|
||||||
|
- `luci-app-voip` - LuCI dashboard for VoIP management
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Debian 12 (Bookworm) LXC container with Asterisk PBX
|
||||||
|
- OVH Telephony API integration for SIP trunk auto-provisioning
|
||||||
|
- SIP extension management with PJSIP
|
||||||
|
- Asterisk ARI/AMI support for call control
|
||||||
|
- Click-to-call web interface
|
||||||
|
- HAProxy integration with WebRTC support
|
||||||
|
- Procd service management
|
||||||
|
|
||||||
|
**CLI Commands (voipctl):**
|
||||||
|
- `install/uninstall` - Container lifecycle
|
||||||
|
- `start/stop/restart/status` - Service control
|
||||||
|
- `ext add/del/passwd/list` - Extension management
|
||||||
|
- `trunk add ovh/manual` - SIP trunk configuration
|
||||||
|
- `trunk test/status` - Trunk connectivity testing
|
||||||
|
- `call/hangup/calls` - Call origination and control
|
||||||
|
- `vm list/play/delete` - Voicemail management
|
||||||
|
- `configure-haproxy` - WebRTC proxy setup
|
||||||
|
- `emancipate <domain>` - Public exposure
|
||||||
|
|
||||||
|
**OVH Telephony Integration (ovh-telephony.sh):**
|
||||||
|
- API signature generation (HMAC-SHA1)
|
||||||
|
- Billing accounts and SIP lines discovery
|
||||||
|
- SIP credentials retrieval and password reset
|
||||||
|
- SMS sending via OVH SMS API
|
||||||
|
- Auto-provisioning flow for trunk configuration
|
||||||
|
|
||||||
|
**LuCI Dashboard (luci-app-voip):**
|
||||||
|
- Overview with container/Asterisk/trunk status
|
||||||
|
- Extensions management (add/delete)
|
||||||
|
- Trunks configuration (OVH auto-provision, manual)
|
||||||
|
- Click-to-call dialer with extension selector
|
||||||
|
- Active calls display with live polling
|
||||||
|
- Quick dial buttons for extensions
|
||||||
|
- Logs viewer
|
||||||
|
|
||||||
|
**Jabber VoIP Integration (Phase 3):**
|
||||||
|
- Jingle VoIP support via mod_external_services
|
||||||
|
- STUN/TURN server configuration
|
||||||
|
- SMS relay via OVH (messages to sms@domain)
|
||||||
|
- Voicemail notifications via Asterisk AMI → XMPP
|
||||||
|
- New jabberctl commands: jingle enable/disable/status, sms config/send, voicemail-notify
|
||||||
|
- New RPCD methods: jingle_status/enable/disable, sms_status/config/send, voicemail_status/config
|
||||||
|
- Updated UCI config with jingle, sms, and voicemail sections
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
- `package/secubox/secubox-app-voip/Makefile`
|
||||||
|
- `package/secubox/secubox-app-voip/files/etc/config/voip`
|
||||||
|
- `package/secubox/secubox-app-voip/files/etc/init.d/voip`
|
||||||
|
- `package/secubox/secubox-app-voip/files/usr/sbin/voipctl`
|
||||||
|
- `package/secubox/secubox-app-voip/files/usr/lib/secubox/voip/ovh-telephony.sh`
|
||||||
|
- `package/secubox/luci-app-voip/Makefile`
|
||||||
|
- `package/secubox/luci-app-voip/root/usr/libexec/rpcd/luci.voip`
|
||||||
|
- `package/secubox/luci-app-voip/root/usr/share/luci/menu.d/luci-app-voip.json`
|
||||||
|
- `package/secubox/luci-app-voip/root/usr/share/rpcd/acl.d/luci-app-voip.json`
|
||||||
|
- `package/secubox/luci-app-voip/htdocs/.../voip/api.js`
|
||||||
|
- `package/secubox/luci-app-voip/htdocs/.../view/voip/overview.js`
|
||||||
|
- `package/secubox/luci-app-voip/htdocs/.../view/voip/extensions.js`
|
||||||
|
- `package/secubox/luci-app-voip/htdocs/.../view/voip/trunks.js`
|
||||||
|
- `package/secubox/luci-app-voip/htdocs/.../view/voip/click-to-call.js`
|
||||||
|
|
||||||
|
**Files Modified:**
|
||||||
|
- `package/secubox/secubox-app-jabber/files/usr/sbin/jabberctl` (added VoIP integration)
|
||||||
|
- `package/secubox/secubox-app-jabber/files/etc/config/jabber` (jingle/sms/voicemail sections)
|
||||||
|
- `package/secubox/luci-app-jabber/root/usr/libexec/rpcd/luci.jabber` (VoIP methods)
|
||||||
|
- `package/secubox/luci-app-jabber/root/usr/share/rpcd/acl.d/luci-app-jabber.json` (VoIP ACL)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Work In Progress (Claude)
|
# Work In Progress (Claude)
|
||||||
|
|
||||||
_Last updated: 2026-02-17 (v0.21.0 - Nextcloud LXC + WebRadio)_
|
_Last updated: 2026-02-19 (v0.22.0 - VoIP + Jabber Integration)_
|
||||||
|
|
||||||
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
|
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
|
||||||
|
|
||||||
@ -62,6 +62,22 @@ _Last updated: 2026-02-17 (v0.21.0 - Nextcloud LXC + WebRadio)_
|
|||||||
- Gossip-based exposure config sync via secubox-p2p
|
- Gossip-based exposure config sync via secubox-p2p
|
||||||
- Created `luci-app-vortex-dns` dashboard
|
- Created `luci-app-vortex-dns` dashboard
|
||||||
|
|
||||||
|
### Just Completed (2026-02-19)
|
||||||
|
|
||||||
|
- **VoIP (Asterisk PBX) + Jabber Integration** — DONE (2026-02-19)
|
||||||
|
- Created `secubox-app-voip` package with Asterisk PBX in LXC container
|
||||||
|
- OVH Telephony API integration for SIP trunk auto-provisioning
|
||||||
|
- `voipctl` CLI: install/uninstall, ext add/del, trunk add ovh, call, vm list
|
||||||
|
- Created `luci-app-voip` with 4 views: Overview, Extensions, Trunks, Click-to-Call
|
||||||
|
- RPCD backend with 15 methods for VoIP management
|
||||||
|
- Jabber VoIP integration:
|
||||||
|
- Jingle VoIP support (STUN/TURN via mod_external_services)
|
||||||
|
- SMS relay via OVH (messages to sms@domain)
|
||||||
|
- Voicemail notifications via Asterisk AMI → XMPP
|
||||||
|
- Updated jabberctl with `jingle enable/disable`, `sms config/send`, `voicemail-notify`
|
||||||
|
- Updated luci.jabber RPCD with 9 new VoIP methods
|
||||||
|
- UCI config sections: jingle, sms, voicemail
|
||||||
|
|
||||||
### Just Completed (2026-02-17)
|
### Just Completed (2026-02-17)
|
||||||
|
|
||||||
- **PeerTube yt-dlp Video Import** — DONE (2026-02-17)
|
- **PeerTube yt-dlp Video Import** — DONE (2026-02-17)
|
||||||
|
|||||||
@ -420,6 +420,188 @@ method_room_list() {
|
|||||||
json_dump
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------- VoIP Integration Methods ----------
|
||||||
|
|
||||||
|
# Method: jingle_status
|
||||||
|
method_jingle_status() {
|
||||||
|
local enabled stun_server turn_server
|
||||||
|
|
||||||
|
enabled=$(uci_get jingle enabled 0)
|
||||||
|
stun_server=$(uci_get jingle stun_server "stun.l.google.com:19302")
|
||||||
|
turn_server=$(uci_get jingle turn_server "")
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "enabled" "$enabled"
|
||||||
|
json_add_string "stun_server" "$stun_server"
|
||||||
|
json_add_string "turn_server" "$turn_server"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: jingle_enable
|
||||||
|
method_jingle_enable() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var stun_server stun_server
|
||||||
|
|
||||||
|
local output
|
||||||
|
if [ -n "$stun_server" ]; then
|
||||||
|
output=$($JABBERCTL jingle enable "$stun_server" 2>&1)
|
||||||
|
else
|
||||||
|
output=$($JABBERCTL jingle enable 2>&1)
|
||||||
|
fi
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Jingle VoIP enabled"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: jingle_disable
|
||||||
|
method_jingle_disable() {
|
||||||
|
local output
|
||||||
|
output=$($JABBERCTL jingle disable 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Jingle VoIP disabled"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: sms_status
|
||||||
|
method_sms_status() {
|
||||||
|
local enabled sender provider
|
||||||
|
|
||||||
|
enabled=$(uci_get sms enabled 0)
|
||||||
|
sender=$(uci_get sms sender "SecuBox")
|
||||||
|
provider=$(uci_get sms provider "ovh")
|
||||||
|
|
||||||
|
# Check OVH API configured
|
||||||
|
local ovh_configured="0"
|
||||||
|
local ovh_key=$(uci -q get voip.ovh_telephony.app_key 2>/dev/null)
|
||||||
|
[ -n "$ovh_key" ] && ovh_configured="1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "enabled" "$enabled"
|
||||||
|
json_add_string "sender" "$sender"
|
||||||
|
json_add_string "provider" "$provider"
|
||||||
|
json_add_string "ovh_configured" "$ovh_configured"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: sms_config
|
||||||
|
method_sms_config() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var sender sender
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$($JABBERCTL sms config "$sender" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "SMS relay configured"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: sms_send
|
||||||
|
method_sms_send() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var to to
|
||||||
|
json_get_var message message
|
||||||
|
|
||||||
|
if [ -z "$to" ] || [ -z "$message" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Phone number and message are required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$($JABBERCTL sms send "$to" "$message" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "SMS sent to $to"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: voicemail_status
|
||||||
|
method_voicemail_status() {
|
||||||
|
local enabled ami_host ami_port notify_jid
|
||||||
|
|
||||||
|
enabled=$(uci_get voicemail enabled 0)
|
||||||
|
ami_host=$(uci_get voicemail ami_host "127.0.0.1")
|
||||||
|
ami_port=$(uci_get voicemail ami_port "5038")
|
||||||
|
notify_jid=$(uci_get voicemail notify_jid "")
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "enabled" "$enabled"
|
||||||
|
json_add_string "ami_host" "$ami_host"
|
||||||
|
json_add_string "ami_port" "$ami_port"
|
||||||
|
json_add_string "notify_jid" "$notify_jid"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: voicemail_config
|
||||||
|
method_voicemail_config() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var notify_jid notify_jid
|
||||||
|
|
||||||
|
if [ -z "$notify_jid" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Notification JID is required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save to UCI first
|
||||||
|
uci set jabber.voicemail=voicemail_notify
|
||||||
|
uci set jabber.voicemail.notify_jid="$notify_jid"
|
||||||
|
uci commit jabber
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$($JABBERCTL voicemail-notify 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Voicemail notifications configured"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
# List available methods
|
# List available methods
|
||||||
list_methods() {
|
list_methods() {
|
||||||
json_init
|
json_init
|
||||||
@ -464,6 +646,27 @@ list_methods() {
|
|||||||
json_close_object
|
json_close_object
|
||||||
json_add_object "room_list"
|
json_add_object "room_list"
|
||||||
json_close_object
|
json_close_object
|
||||||
|
json_add_object "jingle_status"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "jingle_enable"
|
||||||
|
json_add_string "stun_server" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "jingle_disable"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "sms_status"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "sms_config"
|
||||||
|
json_add_string "sender" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "sms_send"
|
||||||
|
json_add_string "to" ""
|
||||||
|
json_add_string "message" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "voicemail_status"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "voicemail_config"
|
||||||
|
json_add_string "notify_jid" ""
|
||||||
|
json_close_object
|
||||||
json_dump
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,6 +728,30 @@ case "$1" in
|
|||||||
room_list)
|
room_list)
|
||||||
method_room_list
|
method_room_list
|
||||||
;;
|
;;
|
||||||
|
jingle_status)
|
||||||
|
method_jingle_status
|
||||||
|
;;
|
||||||
|
jingle_enable)
|
||||||
|
method_jingle_enable
|
||||||
|
;;
|
||||||
|
jingle_disable)
|
||||||
|
method_jingle_disable
|
||||||
|
;;
|
||||||
|
sms_status)
|
||||||
|
method_sms_status
|
||||||
|
;;
|
||||||
|
sms_config)
|
||||||
|
method_sms_config
|
||||||
|
;;
|
||||||
|
sms_send)
|
||||||
|
method_sms_send
|
||||||
|
;;
|
||||||
|
voicemail_status)
|
||||||
|
method_voicemail_status
|
||||||
|
;;
|
||||||
|
voicemail_config)
|
||||||
|
method_voicemail_config
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo '{"error":"Method not found"}'
|
echo '{"error":"Method not found"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -3,13 +3,39 @@
|
|||||||
"description": "Grant access to Jabber/XMPP management",
|
"description": "Grant access to Jabber/XMPP management",
|
||||||
"read": {
|
"read": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.jabber": ["status", "logs", "user_list", "room_list"]
|
"luci.jabber": [
|
||||||
|
"status",
|
||||||
|
"logs",
|
||||||
|
"user_list",
|
||||||
|
"room_list",
|
||||||
|
"jingle_status",
|
||||||
|
"sms_status",
|
||||||
|
"voicemail_status"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"uci": ["jabber"]
|
"uci": ["jabber"]
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.jabber": ["start", "stop", "install", "uninstall", "update", "emancipate", "configure_haproxy", "user_add", "user_del", "user_passwd", "room_create", "room_delete"]
|
"luci.jabber": [
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"install",
|
||||||
|
"uninstall",
|
||||||
|
"update",
|
||||||
|
"emancipate",
|
||||||
|
"configure_haproxy",
|
||||||
|
"user_add",
|
||||||
|
"user_del",
|
||||||
|
"user_passwd",
|
||||||
|
"room_create",
|
||||||
|
"room_delete",
|
||||||
|
"jingle_enable",
|
||||||
|
"jingle_disable",
|
||||||
|
"sms_config",
|
||||||
|
"sms_send",
|
||||||
|
"voicemail_config"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"uci": ["jabber"]
|
"uci": ["jabber"]
|
||||||
}
|
}
|
||||||
|
|||||||
33
package/secubox/luci-app-voip/Makefile
Normal file
33
package/secubox/luci-app-voip/Makefile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-voip
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=SecuBox Team
|
||||||
|
PKG_LICENSE:=GPL-3.0
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI VoIP (Asterisk PBX) Management
|
||||||
|
LUCI_DEPENDS:=+secubox-app-voip +luci-base
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/luci-app-voip/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.voip $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-voip.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-voip.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/voip
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/voip/*.js $(1)/www/luci-static/resources/view/voip/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/voip
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/voip/*.js $(1)/www/luci-static/resources/voip/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,luci-app-voip))
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require voip.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.listExtensions(),
|
||||||
|
api.listCalls()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
parseExtensions: function(extString) {
|
||||||
|
if (!extString) return [];
|
||||||
|
|
||||||
|
return extString.split(',').filter(function(e) { return e; }).map(function(entry) {
|
||||||
|
var parts = entry.split(':');
|
||||||
|
return {
|
||||||
|
ext: parts[0],
|
||||||
|
name: parts[1] || parts[0]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
var status = data[0];
|
||||||
|
var extData = data[1];
|
||||||
|
var callsData = data[2];
|
||||||
|
var running = status.running === 'true';
|
||||||
|
var trunkRegistered = status.trunk_registered === 'true';
|
||||||
|
|
||||||
|
if (!running) {
|
||||||
|
return E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'Click to Call'),
|
||||||
|
E('p', { 'class': 'alert-message warning' },
|
||||||
|
'VoIP service is not running. Start it from the Overview page.')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var extensions = this.parseExtensions(extData.extensions);
|
||||||
|
|
||||||
|
if (extensions.length === 0) {
|
||||||
|
return E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'Click to Call'),
|
||||||
|
E('p', { 'class': 'alert-message warning' },
|
||||||
|
'No extensions configured. Add extensions first.')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension selector
|
||||||
|
var extOptions = extensions.map(function(ext) {
|
||||||
|
return E('option', { 'value': ext.ext }, ext.ext + ' - ' + ext.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'Click to Call'),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Make a Call'),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Your Extension'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', { 'id': 'from-ext', 'class': 'cbi-input-select' }, extOptions)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Number to Call'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'tel',
|
||||||
|
'id': 'to-number',
|
||||||
|
'class': 'cbi-input-text',
|
||||||
|
'placeholder': '+33612345678 or extension number',
|
||||||
|
'style': 'width: 300px;'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-positive',
|
||||||
|
'style': 'font-size: 16px; padding: 10px 30px;',
|
||||||
|
'disabled': !trunkRegistered,
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleCall();
|
||||||
|
})
|
||||||
|
}, 'Call'),
|
||||||
|
!trunkRegistered ? E('span', {
|
||||||
|
'style': 'margin-left: 10px; color: #c00;'
|
||||||
|
}, '(SIP trunk not registered)') : ''
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Active Calls'),
|
||||||
|
E('div', { 'id': 'active-calls' }, this.renderActiveCalls(callsData.calls))
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Quick Dial'),
|
||||||
|
E('p', {}, 'Click an extension to call directly:'),
|
||||||
|
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px;' },
|
||||||
|
extensions.map(function(ext) {
|
||||||
|
return E('button', {
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'data-ext': ext.ext,
|
||||||
|
'click': function() {
|
||||||
|
document.getElementById('to-number').value = ext.ext;
|
||||||
|
}
|
||||||
|
}, ext.ext + ' (' + ext.name + ')');
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Poll for active calls
|
||||||
|
poll.add(L.bind(function() {
|
||||||
|
return api.listCalls().then(L.bind(function(res) {
|
||||||
|
var container = document.getElementById('active-calls');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.appendChild(this.renderActiveCalls(res.calls));
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
}, this), 3);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderActiveCalls: function(callsStr) {
|
||||||
|
if (!callsStr || callsStr.trim() === '') {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No active calls');
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = callsStr.split('\n').filter(function(l) { return l.trim(); });
|
||||||
|
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No active calls');
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('pre', {
|
||||||
|
'style': 'background: #f5f5f5; padding: 10px; font-size: 12px; overflow-x: auto;'
|
||||||
|
}, callsStr);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCall: function() {
|
||||||
|
var fromExt = document.getElementById('from-ext').value;
|
||||||
|
var toNumber = document.getElementById('to-number').value.trim();
|
||||||
|
|
||||||
|
if (!toNumber) {
|
||||||
|
ui.addNotification(null, E('p', 'Enter a number to call'), 'error');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal('Calling...', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Connecting ' + fromExt + ' to ' + toNumber + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return api.originateCall(fromExt, toNumber).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Call initiated. Your phone will ring first.'), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Call failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(e) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Call failed: ' + e.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,184 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
'require voip.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.listExtensions()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
parseExtensions: function(extString) {
|
||||||
|
if (!extString) return [];
|
||||||
|
|
||||||
|
return extString.split(',').filter(function(e) { return e; }).map(function(entry) {
|
||||||
|
var parts = entry.split(':');
|
||||||
|
return {
|
||||||
|
ext: parts[0],
|
||||||
|
name: parts[1] || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderExtensionTable: function(extensions) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!extensions || extensions.length === 0) {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No extensions configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = extensions.map(function(ext) {
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, ext.ext),
|
||||||
|
E('td', { 'class': 'td' }, ext.name),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-remove',
|
||||||
|
'click': ui.createHandlerFn(self, function() {
|
||||||
|
return self.handleDelete(ext.ext);
|
||||||
|
})
|
||||||
|
}, 'Delete')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'Extension'),
|
||||||
|
E('th', { 'class': 'th' }, 'Name'),
|
||||||
|
E('th', { 'class': 'th' }, 'Actions')
|
||||||
|
])
|
||||||
|
].concat(rows));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderAddForm: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Add Extension'),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Extension Number'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'new-ext',
|
||||||
|
'placeholder': '100',
|
||||||
|
'class': 'cbi-input-text'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Display Name'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'new-name',
|
||||||
|
'placeholder': 'John Doe',
|
||||||
|
'class': 'cbi-input-text'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Password'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'password',
|
||||||
|
'id': 'new-password',
|
||||||
|
'placeholder': '(auto-generated if empty)',
|
||||||
|
'class': 'cbi-input-text'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-positive',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleAdd();
|
||||||
|
})
|
||||||
|
}, 'Add Extension')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAdd: function() {
|
||||||
|
var ext = document.getElementById('new-ext').value.trim();
|
||||||
|
var name = document.getElementById('new-name').value.trim();
|
||||||
|
var password = document.getElementById('new-password').value;
|
||||||
|
|
||||||
|
if (!ext) {
|
||||||
|
ui.addNotification(null, E('p', 'Extension number is required'), 'error');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
ui.addNotification(null, E('p', 'Display name is required'), 'error');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(ext)) {
|
||||||
|
ui.addNotification(null, E('p', 'Extension must be numeric'), 'error');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.addExtension(ext, name, password).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
var msg = 'Extension ' + ext + ' created';
|
||||||
|
if (res.password) {
|
||||||
|
msg += '. Password: ' + res.password;
|
||||||
|
}
|
||||||
|
ui.addNotification(null, E('p', msg), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDelete: function(ext) {
|
||||||
|
if (!confirm('Delete extension ' + ext + '?')) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.deleteExtension(ext).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Extension deleted'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0];
|
||||||
|
var extData = data[1];
|
||||||
|
var running = status.running === 'true';
|
||||||
|
|
||||||
|
if (!running) {
|
||||||
|
return E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'Extensions'),
|
||||||
|
E('p', { 'class': 'alert-message warning' },
|
||||||
|
'VoIP service is not running. Start it from the Overview page.')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var extensions = this.parseExtensions(extData.extensions);
|
||||||
|
|
||||||
|
return E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'Extensions'),
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Configured Extensions'),
|
||||||
|
E('div', { 'id': 'ext-table' }, this.renderExtensionTable(extensions))
|
||||||
|
]),
|
||||||
|
this.renderAddForm()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,227 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require dom';
|
||||||
|
'require voip.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return api.getStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatusTable: function(status) {
|
||||||
|
var containerState = status.container_state || 'not_installed';
|
||||||
|
var running = status.running === 'true';
|
||||||
|
var asteriskUp = status.asterisk === 'true';
|
||||||
|
var trunkRegistered = status.trunk_registered === 'true';
|
||||||
|
var activeCalls = parseInt(status.active_calls) || 0;
|
||||||
|
var extensions = parseInt(status.extensions) || 0;
|
||||||
|
|
||||||
|
var stateClass = running ? 'success' : (containerState === 'installed' ? 'warning' : 'danger');
|
||||||
|
var stateText = running ? 'Running' : (containerState === 'installed' ? 'Stopped' : 'Not Installed');
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Container Status'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('span', { 'class': 'badge ' + stateClass }, stateText)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Asterisk PBX'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('span', { 'class': 'badge ' + (asteriskUp ? 'success' : 'danger') },
|
||||||
|
asteriskUp ? 'Running' : 'Stopped')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'SIP Trunk'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('span', { 'class': 'badge ' + (trunkRegistered ? 'success' : 'warning') },
|
||||||
|
trunkRegistered ? 'Registered' : 'Not Registered')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Active Calls'),
|
||||||
|
E('td', { 'class': 'td' }, String(activeCalls))
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Extensions'),
|
||||||
|
E('td', { 'class': 'td' }, String(extensions))
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'SIP Port'),
|
||||||
|
E('td', { 'class': 'td' }, status.sip_port || '5060')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Domain'),
|
||||||
|
E('td', { 'class': 'td' }, status.domain || '(not configured)')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderActions: function(status) {
|
||||||
|
var self = this;
|
||||||
|
var containerState = status.container_state || 'not_installed';
|
||||||
|
var running = status.running === 'true';
|
||||||
|
|
||||||
|
var buttons = [];
|
||||||
|
|
||||||
|
if (containerState === 'not_installed') {
|
||||||
|
buttons.push(E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-positive',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleInstall();
|
||||||
|
})
|
||||||
|
}, 'Install VoIP'));
|
||||||
|
} else {
|
||||||
|
if (running) {
|
||||||
|
buttons.push(E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-negative',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleStop();
|
||||||
|
})
|
||||||
|
}, 'Stop'));
|
||||||
|
} else {
|
||||||
|
buttons.push(E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-positive',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleStart();
|
||||||
|
})
|
||||||
|
}, 'Start'));
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(' ');
|
||||||
|
|
||||||
|
buttons.push(E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-remove',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleUninstall();
|
||||||
|
})
|
||||||
|
}, 'Uninstall'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'cbi-page-actions' }, buttons);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleInstall: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal('Installing VoIP', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Installing Asterisk PBX in LXC container... This may take several minutes.')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return api.install().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message || 'VoIP installed successfully'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Installation failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(e) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Installation failed: ' + e.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleStart: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return api.start().then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', 'VoIP started'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Start failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleStop: function() {
|
||||||
|
return api.stop().then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', 'VoIP stopped'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Stop failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUninstall: function() {
|
||||||
|
if (!confirm('Are you sure you want to uninstall VoIP? All data will be lost.')) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal('Uninstalling VoIP', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Removing VoIP container...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return api.uninstall().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', 'VoIP uninstalled'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Uninstall failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(e) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Uninstall failed: ' + e.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLogs: function() {
|
||||||
|
var logsContainer = E('pre', {
|
||||||
|
'id': 'voip-logs',
|
||||||
|
'style': 'max-height: 300px; overflow-y: auto; background: #1a1a1a; color: #0f0; padding: 10px; font-family: monospace; font-size: 12px;'
|
||||||
|
}, 'Loading logs...');
|
||||||
|
|
||||||
|
api.getLogs(50).then(function(res) {
|
||||||
|
logsContainer.textContent = res.logs || 'No logs available';
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Recent Logs'),
|
||||||
|
logsContainer,
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'click': function() {
|
||||||
|
api.getLogs(100).then(function(res) {
|
||||||
|
document.getElementById('voip-logs').textContent = res.logs || 'No logs available';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'Refresh Logs')
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(status) {
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'VoIP Overview'),
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Status'),
|
||||||
|
this.renderStatusTable(status)
|
||||||
|
]),
|
||||||
|
this.renderActions(status),
|
||||||
|
this.renderLogs()
|
||||||
|
]);
|
||||||
|
|
||||||
|
poll.add(L.bind(function() {
|
||||||
|
return api.getStatus().then(L.bind(function(newStatus) {
|
||||||
|
var table = this.renderStatusTable(newStatus);
|
||||||
|
var oldTable = view.querySelector('.cbi-section table');
|
||||||
|
if (oldTable) {
|
||||||
|
dom.content(oldTable.parentNode, table);
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
}, this), 5);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require uci';
|
||||||
|
'require form';
|
||||||
|
'require voip.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.getTrunkStatus(),
|
||||||
|
uci.load('voip')
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0];
|
||||||
|
var trunkStatus = data[1];
|
||||||
|
var running = status.running === 'true';
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'SIP Trunks')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Trunk status section
|
||||||
|
var registered = trunkStatus.registered === 'true';
|
||||||
|
view.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Trunk Status'),
|
||||||
|
E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Registration'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('span', { 'class': 'badge ' + (registered ? 'success' : 'warning') },
|
||||||
|
registered ? 'Registered' : 'Not Registered')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
trunkStatus.status ? E('pre', {
|
||||||
|
'style': 'background: #f5f5f5; padding: 10px; font-size: 12px; overflow-x: auto;'
|
||||||
|
}, trunkStatus.status) : ''
|
||||||
|
]));
|
||||||
|
|
||||||
|
// OVH Auto-provision section
|
||||||
|
var ovhEnabled = uci.get('voip', 'ovh_telephony', 'enabled') === '1';
|
||||||
|
var ovhAppKey = uci.get('voip', 'ovh_telephony', 'app_key') || '';
|
||||||
|
|
||||||
|
view.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'OVH Telephony'),
|
||||||
|
E('p', {}, 'Auto-provision SIP trunk from OVH Telephony API'),
|
||||||
|
E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'API Credentials'),
|
||||||
|
E('td', { 'class': 'td' }, ovhAppKey ? 'Configured' : 'Not configured')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-positive',
|
||||||
|
'disabled': !ovhAppKey || !running,
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.handleOvhProvision();
|
||||||
|
})
|
||||||
|
}, 'Auto-Provision OVH Trunk')
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
|
||||||
|
// UCI configuration form
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('voip', null, null);
|
||||||
|
|
||||||
|
s = m.section(form.NamedSection, 'ovh_telephony', 'ovh', 'OVH API Settings');
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', 'Enable OVH Integration');
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'endpoint', 'API Endpoint');
|
||||||
|
o.value('ovh-eu', 'OVH Europe');
|
||||||
|
o.value('ovh-ca', 'OVH Canada');
|
||||||
|
o.value('ovh-us', 'OVH US');
|
||||||
|
o.default = 'ovh-eu';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'app_key', 'Application Key');
|
||||||
|
o.password = false;
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'app_secret', 'Application Secret');
|
||||||
|
o.password = true;
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'consumer_key', 'Consumer Key');
|
||||||
|
o.password = true;
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'billing_account', 'Billing Account');
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = '(auto-detected)';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'service_name', 'Service Name (SIP Line)');
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = '(auto-detected)';
|
||||||
|
|
||||||
|
// Manual trunk section
|
||||||
|
s = m.section(form.NamedSection, 'sip_trunk', 'trunk', 'Manual Trunk Configuration');
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', 'Enable Trunk');
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'host', 'SIP Server');
|
||||||
|
o.placeholder = 'sip.provider.com';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'username', 'Username');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'password', 'Password');
|
||||||
|
o.password = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'outbound_proxy', 'Outbound Proxy');
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'codecs', 'Codecs');
|
||||||
|
o.default = 'ulaw,alaw,g729';
|
||||||
|
|
||||||
|
return m.render().then(function(formEl) {
|
||||||
|
view.appendChild(formEl);
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOvhProvision: function() {
|
||||||
|
ui.showModal('Provisioning OVH Trunk', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Connecting to OVH API and configuring SIP trunk...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return api.addOvhTrunk().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message || 'OVH trunk provisioned'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(e) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Provisioning failed: ' + e.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
'use strict';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callStatus = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'status',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callStart = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'start',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callStop = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'stop',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstall = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'install',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUninstall = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'uninstall',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callLogs = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'logs',
|
||||||
|
params: ['lines'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callExtAdd = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'ext_add',
|
||||||
|
params: ['ext', 'name', 'password'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callExtDel = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'ext_del',
|
||||||
|
params: ['ext'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callExtList = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'ext_list',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callOriginate = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'call_originate',
|
||||||
|
params: ['from_ext', 'to_number'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCallsList = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'calls_list',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callTrunkStatus = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'trunk_status',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callTrunkAddOvh = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'trunk_add_ovh',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callConfigureHaproxy = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'configure_haproxy',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callEmancipate = rpc.declare({
|
||||||
|
object: 'luci.voip',
|
||||||
|
method: 'emancipate',
|
||||||
|
params: ['domain'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getStatus: function() {
|
||||||
|
return callStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
start: function() {
|
||||||
|
return callStart();
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: function() {
|
||||||
|
return callStop();
|
||||||
|
},
|
||||||
|
|
||||||
|
install: function() {
|
||||||
|
return callInstall();
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall: function() {
|
||||||
|
return callUninstall();
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogs: function(lines) {
|
||||||
|
return callLogs(lines || 50);
|
||||||
|
},
|
||||||
|
|
||||||
|
addExtension: function(ext, name, password) {
|
||||||
|
return callExtAdd(ext, name, password || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteExtension: function(ext) {
|
||||||
|
return callExtDel(ext);
|
||||||
|
},
|
||||||
|
|
||||||
|
listExtensions: function() {
|
||||||
|
return callExtList();
|
||||||
|
},
|
||||||
|
|
||||||
|
originateCall: function(fromExt, toNumber) {
|
||||||
|
return callOriginate(fromExt, toNumber);
|
||||||
|
},
|
||||||
|
|
||||||
|
listCalls: function() {
|
||||||
|
return callCallsList();
|
||||||
|
},
|
||||||
|
|
||||||
|
getTrunkStatus: function() {
|
||||||
|
return callTrunkStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
addOvhTrunk: function() {
|
||||||
|
return callTrunkAddOvh();
|
||||||
|
},
|
||||||
|
|
||||||
|
configureHaproxy: function() {
|
||||||
|
return callConfigureHaproxy();
|
||||||
|
},
|
||||||
|
|
||||||
|
emancipate: function(domain) {
|
||||||
|
return callEmancipate(domain);
|
||||||
|
}
|
||||||
|
};
|
||||||
477
package/secubox/luci-app-voip/root/usr/libexec/rpcd/luci.voip
Normal file
477
package/secubox/luci-app-voip/root/usr/libexec/rpcd/luci.voip
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# RPCD backend for VoIP LuCI app
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
VOIPCTL="/usr/sbin/voipctl"
|
||||||
|
|
||||||
|
# Helper to get UCI value
|
||||||
|
uci_get() {
|
||||||
|
local section="$1"
|
||||||
|
local option="$2"
|
||||||
|
local default="$3"
|
||||||
|
local val
|
||||||
|
val=$(uci -q get "voip.${section}.${option}")
|
||||||
|
echo "${val:-$default}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get container status
|
||||||
|
get_container_status() {
|
||||||
|
local state="not_installed"
|
||||||
|
local running="false"
|
||||||
|
local lxc_info=""
|
||||||
|
|
||||||
|
if [ -d "/srv/lxc/voip" ]; then
|
||||||
|
state="installed"
|
||||||
|
lxc_info=$(lxc-info -n voip 2>/dev/null)
|
||||||
|
if echo "$lxc_info" | grep -q "State:.*RUNNING"; then
|
||||||
|
running="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$state $running"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: status
|
||||||
|
method_status() {
|
||||||
|
local enabled sip_port
|
||||||
|
local container_state running
|
||||||
|
local info asterisk_up trunk_registered active_calls extensions
|
||||||
|
|
||||||
|
enabled=$(uci_get main enabled 0)
|
||||||
|
sip_port=$(uci_get asterisk sip_port 5060)
|
||||||
|
|
||||||
|
info=$(get_container_status)
|
||||||
|
container_state=$(echo "$info" | awk '{print $1}')
|
||||||
|
running=$(echo "$info" | awk '{print $2}')
|
||||||
|
|
||||||
|
asterisk_up="false"
|
||||||
|
trunk_registered="false"
|
||||||
|
active_calls=0
|
||||||
|
extensions=0
|
||||||
|
|
||||||
|
if [ "$running" = "true" ]; then
|
||||||
|
lxc-attach -n voip -- pgrep asterisk >/dev/null 2>&1 && asterisk_up="true"
|
||||||
|
lxc-attach -n voip -- asterisk -rx "pjsip show registrations" 2>/dev/null | grep -q "Registered" && trunk_registered="true"
|
||||||
|
active_calls=$(lxc-attach -n voip -- asterisk -rx "core show channels" 2>/dev/null | grep -oE "^[0-9]+ active" | cut -d' ' -f1 || echo 0)
|
||||||
|
extensions=$(lxc-attach -n voip -- asterisk -rx "pjsip show endpoints" 2>/dev/null | grep -c "^[0-9]" || echo 0)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local domain haproxy_enabled
|
||||||
|
domain=$(uci_get ssl domain "")
|
||||||
|
haproxy_enabled=$(uci_get ssl enabled "0")
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "enabled" "$enabled"
|
||||||
|
json_add_string "container_state" "$container_state"
|
||||||
|
json_add_string "running" "$running"
|
||||||
|
json_add_string "asterisk" "$asterisk_up"
|
||||||
|
json_add_string "trunk_registered" "$trunk_registered"
|
||||||
|
json_add_int "active_calls" "${active_calls:-0}"
|
||||||
|
json_add_int "extensions" "${extensions:-0}"
|
||||||
|
json_add_string "sip_port" "$sip_port"
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
|
json_add_string "haproxy" "$haproxy_enabled"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: start
|
||||||
|
method_start() {
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL start 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "VoIP started successfully"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: stop
|
||||||
|
method_stop() {
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL stop 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "VoIP stopped successfully"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: install
|
||||||
|
method_install() {
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL install 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "VoIP installed successfully"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: uninstall
|
||||||
|
method_uninstall() {
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL uninstall 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "VoIP uninstalled successfully"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: logs
|
||||||
|
method_logs() {
|
||||||
|
local lines="${1:-50}"
|
||||||
|
local output
|
||||||
|
|
||||||
|
if [ -d "/srv/lxc/voip" ]; then
|
||||||
|
output=$($VOIPCTL logs "$lines" 2>&1 | tail -n "$lines")
|
||||||
|
else
|
||||||
|
output="Container not installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "logs" "$output"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: ext_add
|
||||||
|
method_ext_add() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var ext ext
|
||||||
|
json_get_var name name
|
||||||
|
json_get_var password password
|
||||||
|
|
||||||
|
if [ -z "$ext" ] || [ -z "$name" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Extension and name are required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
if [ -n "$password" ]; then
|
||||||
|
output=$($VOIPCTL ext add "$ext" "$name" "$password" 2>&1)
|
||||||
|
else
|
||||||
|
output=$($VOIPCTL ext add "$ext" "$name" 2>&1)
|
||||||
|
fi
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Extension $ext created"
|
||||||
|
local new_pass=$(echo "$output" | grep -oE 'Password: [^ ]+' | cut -d: -f2 | tr -d ' ')
|
||||||
|
json_add_string "password" "$new_pass"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: ext_del
|
||||||
|
method_ext_del() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var ext ext
|
||||||
|
|
||||||
|
if [ -z "$ext" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Extension is required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL ext del "$ext" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Extension $ext deleted"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: ext_list
|
||||||
|
method_ext_list() {
|
||||||
|
local extensions=""
|
||||||
|
|
||||||
|
if lxc-info -n voip 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
# Get extensions from UCI
|
||||||
|
extensions=$(uci show voip 2>/dev/null | grep "^voip\.ext_" | grep "\.name=" | while read line; do
|
||||||
|
ext=$(echo "$line" | sed "s/voip\.ext_//" | cut -d. -f1)
|
||||||
|
name=$(echo "$line" | cut -d= -f2 | tr -d "'")
|
||||||
|
echo "${ext}:${name}"
|
||||||
|
done | paste -sd,)
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "extensions" "$extensions"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: call_originate
|
||||||
|
method_call_originate() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var from_ext from_ext
|
||||||
|
json_get_var to_number to_number
|
||||||
|
|
||||||
|
if [ -z "$from_ext" ] || [ -z "$to_number" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "From extension and to number are required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL call "$from_ext" "$to_number" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Call initiated"
|
||||||
|
json_add_string "channel" "$output"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: calls_list
|
||||||
|
method_calls_list() {
|
||||||
|
local calls=""
|
||||||
|
|
||||||
|
if lxc-info -n voip 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
calls=$(lxc-attach -n voip -- asterisk -rx "core show channels concise" 2>/dev/null | head -10)
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "calls" "$calls"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: trunk_status
|
||||||
|
method_trunk_status() {
|
||||||
|
local registered="false"
|
||||||
|
local status=""
|
||||||
|
|
||||||
|
if lxc-info -n voip 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
status=$(lxc-attach -n voip -- asterisk -rx "pjsip show registrations" 2>/dev/null)
|
||||||
|
echo "$status" | grep -q "Registered" && registered="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_string "registered" "$registered"
|
||||||
|
json_add_string "status" "$status"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: trunk_add_ovh
|
||||||
|
method_trunk_add_ovh() {
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL trunk add ovh 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "OVH trunk configured"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: configure_haproxy
|
||||||
|
method_configure_haproxy() {
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL configure-haproxy 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "HAProxy configured for VoIP"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: emancipate
|
||||||
|
method_emancipate() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var domain domain
|
||||||
|
|
||||||
|
if [ -z "$domain" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Domain is required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$($VOIPCTL emancipate "$domain" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "VoIP emancipated to $domain"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# List available methods
|
||||||
|
list_methods() {
|
||||||
|
json_init
|
||||||
|
json_add_object "status"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "start"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "stop"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "install"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "uninstall"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "logs"
|
||||||
|
json_add_int "lines" 50
|
||||||
|
json_close_object
|
||||||
|
json_add_object "ext_add"
|
||||||
|
json_add_string "ext" ""
|
||||||
|
json_add_string "name" ""
|
||||||
|
json_add_string "password" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "ext_del"
|
||||||
|
json_add_string "ext" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "ext_list"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "call_originate"
|
||||||
|
json_add_string "from_ext" ""
|
||||||
|
json_add_string "to_number" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "calls_list"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "trunk_status"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "trunk_add_ovh"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "configure_haproxy"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "emancipate"
|
||||||
|
json_add_string "domain" ""
|
||||||
|
json_close_object
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main dispatcher
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
list_methods
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
method_status
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
method_start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
method_stop
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
method_install
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
method_uninstall
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var lines lines
|
||||||
|
method_logs "${lines:-50}"
|
||||||
|
;;
|
||||||
|
ext_add)
|
||||||
|
method_ext_add
|
||||||
|
;;
|
||||||
|
ext_del)
|
||||||
|
method_ext_del
|
||||||
|
;;
|
||||||
|
ext_list)
|
||||||
|
method_ext_list
|
||||||
|
;;
|
||||||
|
call_originate)
|
||||||
|
method_call_originate
|
||||||
|
;;
|
||||||
|
calls_list)
|
||||||
|
method_calls_list
|
||||||
|
;;
|
||||||
|
trunk_status)
|
||||||
|
method_trunk_status
|
||||||
|
;;
|
||||||
|
trunk_add_ovh)
|
||||||
|
method_trunk_add_ovh
|
||||||
|
;;
|
||||||
|
configure_haproxy)
|
||||||
|
method_configure_haproxy
|
||||||
|
;;
|
||||||
|
emancipate)
|
||||||
|
method_emancipate
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Method not found"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Invalid action"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"admin/services/voip": {
|
||||||
|
"title": "VoIP (Asterisk)",
|
||||||
|
"order": 50,
|
||||||
|
"action": {
|
||||||
|
"type": "firstchild"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-voip"],
|
||||||
|
"uci": { "voip": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/voip/overview": {
|
||||||
|
"title": "Overview",
|
||||||
|
"order": 10,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "voip/overview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/voip/extensions": {
|
||||||
|
"title": "Extensions",
|
||||||
|
"order": 20,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "voip/extensions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/voip/trunks": {
|
||||||
|
"title": "Trunks",
|
||||||
|
"order": 30,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "voip/trunks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/voip/click-to-call": {
|
||||||
|
"title": "Click to Call",
|
||||||
|
"order": 40,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "voip/click-to-call"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"luci-app-voip": {
|
||||||
|
"description": "Grant access to VoIP (Asterisk) management",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.voip": [
|
||||||
|
"status",
|
||||||
|
"logs",
|
||||||
|
"ext_list",
|
||||||
|
"calls_list",
|
||||||
|
"trunk_status"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["voip"]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.voip": [
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"install",
|
||||||
|
"uninstall",
|
||||||
|
"ext_add",
|
||||||
|
"ext_del",
|
||||||
|
"call_originate",
|
||||||
|
"trunk_add_ovh",
|
||||||
|
"configure_haproxy",
|
||||||
|
"emancipate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["voip"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,3 +34,21 @@ config jabber 'network'
|
|||||||
config jabber 's2s'
|
config jabber 's2s'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
option require_encryption '1'
|
option require_encryption '1'
|
||||||
|
|
||||||
|
config jingle 'jingle'
|
||||||
|
option enabled '0'
|
||||||
|
option stun_server 'stun.l.google.com:19302'
|
||||||
|
option turn_server ''
|
||||||
|
option turn_user ''
|
||||||
|
option turn_password ''
|
||||||
|
|
||||||
|
config sms_relay 'sms'
|
||||||
|
option enabled '0'
|
||||||
|
option provider 'ovh'
|
||||||
|
option sender 'SecuBox'
|
||||||
|
|
||||||
|
config voicemail_notify 'voicemail'
|
||||||
|
option enabled '0'
|
||||||
|
option ami_host '127.0.0.1'
|
||||||
|
option ami_port '5038'
|
||||||
|
option notify_jid ''
|
||||||
|
|||||||
@ -43,6 +43,14 @@ Exposure:
|
|||||||
configure-haproxy Setup HAProxy vhost for HTTPS/WSS
|
configure-haproxy Setup HAProxy vhost for HTTPS/WSS
|
||||||
emancipate <domain> Full exposure (HAProxy + ACME + DNS + S2S)
|
emancipate <domain> 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 <sender> Configure OVH SMS relay
|
||||||
|
sms send <to> <msg> Send SMS via OVH
|
||||||
|
voicemail-notify Configure Asterisk voicemail notifications
|
||||||
|
|
||||||
Backup:
|
Backup:
|
||||||
backup [path] Backup database and config
|
backup [path] Backup database and config
|
||||||
restore <path> Restore from backup
|
restore <path> Restore from backup
|
||||||
@ -1158,6 +1166,412 @@ cmd_restore() {
|
|||||||
log_info "Restore complete."
|
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 <enable|disable|status>"
|
||||||
|
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
|
||||||
|
-- Jingle VoIP Configuration
|
||||||
|
-- Generated by jabberctl
|
||||||
|
|
||||||
|
modules_enabled = {
|
||||||
|
"external_services";
|
||||||
|
}
|
||||||
|
|
||||||
|
external_services = {
|
||||||
|
JINGLE
|
||||||
|
|
||||||
|
# Add STUN server
|
||||||
|
if [ -n "$stun_server" ]; then
|
||||||
|
local stun_host=$(echo "$stun_server" | cut -d: -f1)
|
||||||
|
local stun_port=$(echo "$stun_server" | cut -d: -f2)
|
||||||
|
[ -z "$stun_port" ] && stun_port="3478"
|
||||||
|
|
||||||
|
cat >> "$jingle_conf" <<STUN
|
||||||
|
{
|
||||||
|
type = "stun",
|
||||||
|
host = "$stun_host",
|
||||||
|
port = $stun_port
|
||||||
|
},
|
||||||
|
STUN
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add TURN server if configured
|
||||||
|
if [ -n "$turn_server" ]; then
|
||||||
|
local turn_host=$(echo "$turn_server" | cut -d: -f1)
|
||||||
|
local turn_port=$(echo "$turn_server" | cut -d: -f2)
|
||||||
|
[ -z "$turn_port" ] && turn_port="3478"
|
||||||
|
|
||||||
|
cat >> "$jingle_conf" <<TURN
|
||||||
|
{
|
||||||
|
type = "turn",
|
||||||
|
host = "$turn_host",
|
||||||
|
port = $turn_port,
|
||||||
|
transport = "udp",
|
||||||
|
username = "$turn_user",
|
||||||
|
secret = "$turn_password"
|
||||||
|
},
|
||||||
|
TURN
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}" >> "$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 <config|send|status>"
|
||||||
|
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" <<SMSCFG
|
||||||
|
-- SMS Gateway Component
|
||||||
|
-- Messages to sms@$hostname will be sent as SMS
|
||||||
|
|
||||||
|
Component "sms.$hostname" "sms_ovh"
|
||||||
|
SMSCFG
|
||||||
|
|
||||||
|
lxc_exec prosodyctl reload
|
||||||
|
|
||||||
|
log_info "SMS relay configured"
|
||||||
|
log_info " Send SMS: message to sms@$hostname"
|
||||||
|
log_info " Format: +33612345678 Your message here"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_sms_send() {
|
||||||
|
local to="$1"
|
||||||
|
local message="$2"
|
||||||
|
|
||||||
|
[ -z "$to" ] || [ -z "$message" ] && {
|
||||||
|
echo "Usage: jabberctl sms send <+33612345678> <message>"
|
||||||
|
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 <<EOF
|
||||||
|
local st = require "util.stanza"
|
||||||
|
local msg = st.message({to="$NOTIFY_JID", type="chat"})
|
||||||
|
:tag("body"):text("Voicemail: $count new message(s) in mailbox $mailbox")
|
||||||
|
module:send(msg)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
connect_ami
|
||||||
|
VMSCRIPT
|
||||||
|
|
||||||
|
chmod +x "$LXC_ROOTFS/usr/local/bin/voicemail-notify.sh"
|
||||||
|
|
||||||
|
# Save config
|
||||||
|
uci set ${CONFIG}.voicemail=voicemail_notify
|
||||||
|
uci set ${CONFIG}.voicemail.enabled='1'
|
||||||
|
uci set ${CONFIG}.voicemail.ami_host="$ami_host"
|
||||||
|
uci set ${CONFIG}.voicemail.ami_port="$ami_port"
|
||||||
|
uci set ${CONFIG}.voicemail.notify_jid="$notify_jid"
|
||||||
|
uci commit "$CONFIG"
|
||||||
|
|
||||||
|
log_info "Voicemail notification configured"
|
||||||
|
log_info " AMI: $ami_host:$ami_port"
|
||||||
|
log_info " Notify JID: $notify_jid"
|
||||||
|
log_info ""
|
||||||
|
log_info "To enable, configure Asterisk AMI user 'jabber':"
|
||||||
|
log_info " /etc/asterisk/manager.conf:"
|
||||||
|
log_info " [jabber]"
|
||||||
|
log_info " secret=your_secret"
|
||||||
|
log_info " permit=127.0.0.1/255.255.255.255"
|
||||||
|
log_info " read=call"
|
||||||
|
log_info " write=originate"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------- service management ----------
|
# ---------- service management ----------
|
||||||
|
|
||||||
cmd_service_run() {
|
cmd_service_run() {
|
||||||
@ -1197,6 +1611,9 @@ case "$1" in
|
|||||||
emancipate) shift; cmd_emancipate "$@" ;;
|
emancipate) shift; cmd_emancipate "$@" ;;
|
||||||
backup) shift; cmd_backup "$@" ;;
|
backup) shift; cmd_backup "$@" ;;
|
||||||
restore) shift; cmd_restore "$@" ;;
|
restore) shift; cmd_restore "$@" ;;
|
||||||
|
jingle) shift; cmd_jingle "$@" ;;
|
||||||
|
sms) shift; cmd_sms "$@" ;;
|
||||||
|
voicemail-notify) cmd_voicemail_notify ;;
|
||||||
service-run) cmd_service_run ;;
|
service-run) cmd_service_run ;;
|
||||||
service-stop) cmd_service_stop ;;
|
service-stop) cmd_service_stop ;;
|
||||||
*) usage; exit 1 ;;
|
*) usage; exit 1 ;;
|
||||||
|
|||||||
45
package/secubox/secubox-app-voip/Makefile
Normal file
45
package/secubox/secubox-app-voip/Makefile
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-voip
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=SecuBox Team
|
||||||
|
PKG_LICENSE:=GPL-3.0
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-voip
|
||||||
|
SECTION:=net
|
||||||
|
CATEGORY:=Network
|
||||||
|
SUBMENU:=Telephony
|
||||||
|
TITLE:=SecuBox VoIP Server (Asterisk PBX)
|
||||||
|
DEPENDS:=+lxc +lxc-common +wget-ssl +tar +jsonfilter +openssl-util +curl
|
||||||
|
PKGARCH:=all
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-voip/description
|
||||||
|
Asterisk PBX in LXC container with OVH SIP trunk auto-provisioning.
|
||||||
|
Features: SIP extensions, OVH telephony integration, click-to-call,
|
||||||
|
voicemail, WebRTC support.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-voip/conffiles
|
||||||
|
/etc/config/voip
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-voip/install
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./files/etc/config/voip $(1)/etc/config/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./files/etc/init.d/voip $(1)/etc/init.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/voipctl $(1)/usr/sbin/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/lib/secubox/voip
|
||||||
|
$(INSTALL_DATA) ./files/usr/lib/secubox/voip/ovh-telephony.sh $(1)/usr/lib/secubox/voip/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-voip))
|
||||||
47
package/secubox/secubox-app-voip/files/etc/config/voip
Normal file
47
package/secubox/secubox-app-voip/files/etc/config/voip
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
config voip 'main'
|
||||||
|
option enabled '0'
|
||||||
|
option data_path '/srv/voip'
|
||||||
|
option memory_limit '512'
|
||||||
|
|
||||||
|
config asterisk 'asterisk'
|
||||||
|
option sip_port '5060'
|
||||||
|
option rtp_start '10000'
|
||||||
|
option rtp_end '20000'
|
||||||
|
option ari_enabled '1'
|
||||||
|
option ari_port '8089'
|
||||||
|
option ari_user 'admin'
|
||||||
|
option ari_password ''
|
||||||
|
option ami_enabled '1'
|
||||||
|
option ami_port '5038'
|
||||||
|
option ami_user 'admin'
|
||||||
|
option ami_secret ''
|
||||||
|
|
||||||
|
config ovh 'ovh_telephony'
|
||||||
|
option enabled '0'
|
||||||
|
option endpoint 'ovh-eu'
|
||||||
|
option app_key ''
|
||||||
|
option app_secret ''
|
||||||
|
option consumer_key ''
|
||||||
|
option billing_account ''
|
||||||
|
option service_name ''
|
||||||
|
|
||||||
|
config trunk 'sip_trunk'
|
||||||
|
option enabled '0'
|
||||||
|
option provider 'ovh'
|
||||||
|
option host 'sip.ovh.net'
|
||||||
|
option username ''
|
||||||
|
option password ''
|
||||||
|
option outbound_proxy ''
|
||||||
|
option codecs 'ulaw,alaw,g729'
|
||||||
|
option dtmf_mode 'rfc4733'
|
||||||
|
option context 'from-trunk'
|
||||||
|
|
||||||
|
config ivr 'ivr'
|
||||||
|
option enabled '0'
|
||||||
|
option welcome_message '/srv/voip/sounds/welcome.wav'
|
||||||
|
option menu_timeout '10'
|
||||||
|
|
||||||
|
config haproxy 'ssl'
|
||||||
|
option enabled '0'
|
||||||
|
option domain ''
|
||||||
|
option webrtc '1'
|
||||||
35
package/secubox/secubox-app-voip/files/etc/init.d/voip
Normal file
35
package/secubox/secubox-app-voip/files/etc/init.d/voip
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=95
|
||||||
|
STOP=10
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
SERVICE_BIN="/usr/sbin/voipctl"
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
config_load voip
|
||||||
|
config_get enabled main enabled 0
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
procd_open_instance voip
|
||||||
|
procd_set_param command "$SERVICE_BIN" service-run
|
||||||
|
procd_set_param respawn 3600 5 5
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
"$SERVICE_BIN" service-stop
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "voip"
|
||||||
|
}
|
||||||
@ -0,0 +1,321 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# OVH Telephony API client for SecuBox VoIP
|
||||||
|
# Handles SIP trunk provisioning and SMS gateway
|
||||||
|
|
||||||
|
# API endpoints
|
||||||
|
OVH_ENDPOINTS="
|
||||||
|
ovh-eu=https://eu.api.ovh.com/1.0
|
||||||
|
ovh-ca=https://ca.api.ovh.com/1.0
|
||||||
|
ovh-us=https://api.us.ovhcloud.com/1.0
|
||||||
|
"
|
||||||
|
|
||||||
|
# Get endpoint URL
|
||||||
|
_ovh_endpoint_url() {
|
||||||
|
local endpoint="$1"
|
||||||
|
echo "$OVH_ENDPOINTS" | grep "^${endpoint}=" | cut -d= -f2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize OVH API
|
||||||
|
ovh_init() {
|
||||||
|
OVH_APP_KEY=$(uci -q get voip.ovh_telephony.app_key)
|
||||||
|
OVH_APP_SECRET=$(uci -q get voip.ovh_telephony.app_secret)
|
||||||
|
OVH_CONSUMER_KEY=$(uci -q get voip.ovh_telephony.consumer_key)
|
||||||
|
OVH_ENDPOINT=$(uci -q get voip.ovh_telephony.endpoint || echo "ovh-eu")
|
||||||
|
OVH_API_URL=$(_ovh_endpoint_url "$OVH_ENDPOINT")
|
||||||
|
|
||||||
|
if [ -z "$OVH_APP_KEY" ] || [ -z "$OVH_APP_SECRET" ] || [ -z "$OVH_CONSUMER_KEY" ]; then
|
||||||
|
echo "OVH API credentials not configured" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$OVH_API_URL" ]; then
|
||||||
|
echo "Invalid OVH endpoint: $OVH_ENDPOINT" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate OVH API signature
|
||||||
|
_ovh_sign() {
|
||||||
|
local method="$1"
|
||||||
|
local url="$2"
|
||||||
|
local body="$3"
|
||||||
|
local timestamp="$4"
|
||||||
|
|
||||||
|
# Signature = SHA1(APP_SECRET + CONSUMER_KEY + METHOD + URL + BODY + TIMESTAMP)
|
||||||
|
local to_sign="${OVH_APP_SECRET}+${OVH_CONSUMER_KEY}+${method}+${url}+${body}+${timestamp}"
|
||||||
|
local signature=$(echo -n "$to_sign" | openssl dgst -sha1 -hex 2>/dev/null | awk '{print $2}')
|
||||||
|
|
||||||
|
echo "\$1\$${signature}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make OVH API request
|
||||||
|
_ovh_request() {
|
||||||
|
local method="$1"
|
||||||
|
local path="$2"
|
||||||
|
local body="$3"
|
||||||
|
|
||||||
|
local url="${OVH_API_URL}${path}"
|
||||||
|
local timestamp=$(curl -s "${OVH_API_URL}/auth/time")
|
||||||
|
local signature=$(_ovh_sign "$method" "$url" "$body" "$timestamp")
|
||||||
|
|
||||||
|
local curl_opts="-s"
|
||||||
|
curl_opts="$curl_opts -H 'X-Ovh-Application: ${OVH_APP_KEY}'"
|
||||||
|
curl_opts="$curl_opts -H 'X-Ovh-Consumer: ${OVH_CONSUMER_KEY}'"
|
||||||
|
curl_opts="$curl_opts -H 'X-Ovh-Timestamp: ${timestamp}'"
|
||||||
|
curl_opts="$curl_opts -H 'X-Ovh-Signature: ${signature}'"
|
||||||
|
curl_opts="$curl_opts -H 'Content-Type: application/json'"
|
||||||
|
|
||||||
|
case "$method" in
|
||||||
|
GET)
|
||||||
|
eval curl $curl_opts "$url"
|
||||||
|
;;
|
||||||
|
POST)
|
||||||
|
eval curl $curl_opts -X POST -d "'$body'" "$url"
|
||||||
|
;;
|
||||||
|
PUT)
|
||||||
|
eval curl $curl_opts -X PUT -d "'$body'" "$url"
|
||||||
|
;;
|
||||||
|
DELETE)
|
||||||
|
eval curl $curl_opts -X DELETE "$url"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- Telephony API ----------
|
||||||
|
|
||||||
|
# Get all billing accounts
|
||||||
|
ovh_get_billing_accounts() {
|
||||||
|
_ovh_request GET "/telephony"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get SIP lines for a billing account
|
||||||
|
ovh_get_lines() {
|
||||||
|
local billing_account="$1"
|
||||||
|
_ovh_request GET "/telephony/${billing_account}/line"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get SIP line details
|
||||||
|
ovh_get_line_info() {
|
||||||
|
local billing_account="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
_ovh_request GET "/telephony/${billing_account}/line/${service_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get SIP accounts for a line
|
||||||
|
ovh_get_sip_accounts() {
|
||||||
|
local billing_account="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
_ovh_request GET "/telephony/${billing_account}/line/${service_name}/sipAccounts"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get SIP account credentials
|
||||||
|
ovh_get_sip_info() {
|
||||||
|
local billing_account="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
local sip_account="$3"
|
||||||
|
|
||||||
|
if [ -z "$sip_account" ]; then
|
||||||
|
# Get first SIP account
|
||||||
|
local accounts=$(ovh_get_sip_accounts "$billing_account" "$service_name")
|
||||||
|
sip_account=$(echo "$accounts" | jsonfilter -e '@[0]' 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
_ovh_request GET "/telephony/${billing_account}/line/${service_name}/sipAccounts/${sip_account}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reset SIP password
|
||||||
|
ovh_reset_sip_password() {
|
||||||
|
local billing_account="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
local sip_account="$3"
|
||||||
|
|
||||||
|
_ovh_request POST "/telephony/${billing_account}/line/${service_name}/sipAccounts/${sip_account}/resetPassword" "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get voicemail settings
|
||||||
|
ovh_get_voicemail() {
|
||||||
|
local billing_account="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
_ovh_request GET "/telephony/${billing_account}/voicemail/${service_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get call history
|
||||||
|
ovh_get_calls() {
|
||||||
|
local billing_account="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
_ovh_request GET "/telephony/${billing_account}/service/${service_name}/eventToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- SMS API ----------
|
||||||
|
|
||||||
|
# Get SMS accounts
|
||||||
|
ovh_get_sms_accounts() {
|
||||||
|
_ovh_request GET "/sms"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get SMS account info
|
||||||
|
ovh_get_sms_info() {
|
||||||
|
local service_name="$1"
|
||||||
|
_ovh_request GET "/sms/${service_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send SMS
|
||||||
|
ovh_send_sms() {
|
||||||
|
local service_name="$1"
|
||||||
|
local sender="$2"
|
||||||
|
local receiver="$3"
|
||||||
|
local message="$4"
|
||||||
|
|
||||||
|
local body=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"charset": "UTF-8",
|
||||||
|
"class": "phoneDisplay",
|
||||||
|
"coding": "7bit",
|
||||||
|
"message": "$message",
|
||||||
|
"noStopClause": false,
|
||||||
|
"priority": "high",
|
||||||
|
"receivers": ["$receiver"],
|
||||||
|
"sender": "$sender",
|
||||||
|
"validityPeriod": 2880
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
_ovh_request POST "/sms/${service_name}/jobs" "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get incoming SMS
|
||||||
|
ovh_get_incoming_sms() {
|
||||||
|
local service_name="$1"
|
||||||
|
_ovh_request GET "/sms/${service_name}/incoming"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get specific incoming SMS
|
||||||
|
ovh_get_sms_message() {
|
||||||
|
local service_name="$1"
|
||||||
|
local sms_id="$2"
|
||||||
|
_ovh_request GET "/sms/${service_name}/incoming/${sms_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- Helper functions ----------
|
||||||
|
|
||||||
|
# Auto-provision OVH trunk to Asterisk
|
||||||
|
ovh_provision_trunk() {
|
||||||
|
ovh_init || return 1
|
||||||
|
|
||||||
|
local billing_account=$(uci -q get voip.ovh_telephony.billing_account)
|
||||||
|
local service_name=$(uci -q get voip.ovh_telephony.service_name)
|
||||||
|
|
||||||
|
# If not configured, get first available
|
||||||
|
if [ -z "$billing_account" ]; then
|
||||||
|
billing_account=$(ovh_get_billing_accounts | jsonfilter -e '@[0]' 2>/dev/null)
|
||||||
|
[ -z "$billing_account" ] && {
|
||||||
|
echo "No OVH billing accounts found" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
uci set voip.ovh_telephony.billing_account="$billing_account"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$service_name" ]; then
|
||||||
|
service_name=$(ovh_get_lines "$billing_account" | jsonfilter -e '@[0]' 2>/dev/null)
|
||||||
|
[ -z "$service_name" ] && {
|
||||||
|
echo "No OVH SIP lines found" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
uci set voip.ovh_telephony.service_name="$service_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get SIP credentials
|
||||||
|
local sip_info=$(ovh_get_sip_info "$billing_account" "$service_name")
|
||||||
|
|
||||||
|
local sip_username=$(echo "$sip_info" | jsonfilter -e '@.username' 2>/dev/null)
|
||||||
|
local sip_domain=$(echo "$sip_info" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$sip_username" ] || [ -z "$sip_domain" ]; then
|
||||||
|
echo "Failed to get SIP credentials" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset password to get new one
|
||||||
|
local new_pass_info=$(ovh_reset_sip_password "$billing_account" "$service_name" "$sip_username")
|
||||||
|
local sip_password=$(echo "$new_pass_info" | jsonfilter -e '@.password' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$sip_password" ]; then
|
||||||
|
echo "Failed to get SIP password" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save to UCI
|
||||||
|
uci set voip.sip_trunk.enabled='1'
|
||||||
|
uci set voip.sip_trunk.provider='ovh'
|
||||||
|
uci set voip.sip_trunk.host="$sip_domain"
|
||||||
|
uci set voip.sip_trunk.username="$sip_username"
|
||||||
|
uci set voip.sip_trunk.password="$sip_password"
|
||||||
|
uci commit voip
|
||||||
|
|
||||||
|
# Generate Asterisk trunk config
|
||||||
|
local trunk_conf="/srv/lxc/voip/rootfs/etc/asterisk/pjsip_trunk.conf"
|
||||||
|
cat > "$trunk_conf" <<EOF
|
||||||
|
; OVH SIP Trunk - Auto-provisioned
|
||||||
|
; Billing Account: $billing_account
|
||||||
|
; Service: $service_name
|
||||||
|
|
||||||
|
[ovh-trunk]
|
||||||
|
type=registration
|
||||||
|
outbound_auth=ovh-auth
|
||||||
|
server_uri=sip:${sip_domain}
|
||||||
|
client_uri=sip:${sip_username}@${sip_domain}
|
||||||
|
retry_interval=60
|
||||||
|
expiration=3600
|
||||||
|
|
||||||
|
[ovh-auth]
|
||||||
|
type=auth
|
||||||
|
auth_type=userpass
|
||||||
|
username=${sip_username}
|
||||||
|
password=${sip_password}
|
||||||
|
|
||||||
|
[ovh-endpoint]
|
||||||
|
type=endpoint
|
||||||
|
context=from-trunk
|
||||||
|
disallow=all
|
||||||
|
allow=ulaw,alaw,g729
|
||||||
|
outbound_auth=ovh-auth
|
||||||
|
aors=ovh-aor
|
||||||
|
from_user=${sip_username}
|
||||||
|
direct_media=no
|
||||||
|
rtp_symmetric=yes
|
||||||
|
force_rport=yes
|
||||||
|
rewrite_contact=yes
|
||||||
|
|
||||||
|
[ovh-aor]
|
||||||
|
type=aor
|
||||||
|
contact=sip:${sip_domain}
|
||||||
|
qualify_frequency=60
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Include in main config
|
||||||
|
echo '#include "pjsip_trunk.conf"' >> "/srv/lxc/voip/rootfs/etc/asterisk/pjsip.conf"
|
||||||
|
|
||||||
|
echo "OVH trunk provisioned successfully"
|
||||||
|
echo " Username: $sip_username"
|
||||||
|
echo " Domain: $sip_domain"
|
||||||
|
echo " Password: (saved to UCI)"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test OVH credentials
|
||||||
|
ovh_test_credentials() {
|
||||||
|
ovh_init || return 1
|
||||||
|
|
||||||
|
local result=$(ovh_get_billing_accounts)
|
||||||
|
if echo "$result" | grep -q "^\["; then
|
||||||
|
echo "OVH credentials valid"
|
||||||
|
echo "Billing accounts: $result"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "OVH credentials invalid or error: $result" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
1263
package/secubox/secubox-app-voip/files/usr/sbin/voipctl
Normal file
1263
package/secubox/secubox-app-voip/files/usr/sbin/voipctl
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user