Merge pull request #209 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
This commit is contained in:
@@ -416,8 +416,15 @@ generator output. Fixed by quoting every value in that heredoc.
|
||||
`messaging=yes` flag per extension in `pstn-permissions.conf`,
|
||||
independent of the PSTN calling tiers (an extension can be
|
||||
internal-tier for calling and still messaging-enabled, or vice versa),
|
||||
prompted at install time. **Not done**: the actual dialplan wiring that
|
||||
would make Asterisk *enforce* this flag on inbound `MESSAGE` requests.
|
||||
prompted at install time AND now a checkbox right in the Security
|
||||
Dashboard's PSTN Trunk permissions table (alongside tier/approved-
|
||||
numbers) — no need to re-run the CLI installer just to change who can
|
||||
message. Confirmed it correctly survives tier changes and personal-DID
|
||||
assignment/removal on the same extension (this is what surfaced the
|
||||
tier=internal section-wipe bug fixed above). **Not done**: the actual
|
||||
dialplan wiring that would make Asterisk *enforce* this flag on
|
||||
inbound `MESSAGE` requests — this flag currently does nothing at the
|
||||
Asterisk level yet, it's groundwork.
|
||||
Reasoned through but deliberately not shipped: Easy Asterisk dispatches
|
||||
messages through the same `[intercom]` context calls use (no
|
||||
`message_context` override), and whether a hand-written pattern there
|
||||
@@ -475,3 +482,59 @@ generator output. Fixed by quoting every value in that heredoc.
|
||||
`README-pstn-trunk.md` for the full honest breakdown of what's actually
|
||||
hard (the provider's own $0-balance block) vs. estimate-based (this
|
||||
kill-switch).
|
||||
15. ~~Multiple DIDs, one per extension ("personal numbers")~~ Done —
|
||||
additive to the existing shared trunk DID/ring-group, not a
|
||||
replacement. Anveo Direct's DID pricing (~$0.15/mo + $0.25 setup on the
|
||||
Per Minute plan) makes "everyone gets their own number" genuinely cheap
|
||||
at personal-use volumes, and multiple DIDs sharing one trunk/account is
|
||||
exactly what that plan is built for (10 dedicated incoming channels
|
||||
bundled in). New `pstn-personal-dids.conf` (DID -> owner extension,
|
||||
read live by the dialplan for inbound routing) plus a `personal_did=`
|
||||
field per extension in `pstn-permissions.conf` (the outbound Caller-ID
|
||||
override) — both kept in sync automatically by a single write path
|
||||
(CLI prompt at install/update, or the Security Dashboard's "PSTN
|
||||
Trunk" tab), never requiring the admin to hand-edit both files
|
||||
consistently. Inbound: a call to a personal DID routes straight to its
|
||||
owner, checked against the *owner's own* tier/approved-numbers — no
|
||||
ring-group fallback, since a personal DID isn't the shared line.
|
||||
Outbound: `pstn_check_busy` (the one shared exit point for both
|
||||
domestic and international dialing) looks up the calling extension's
|
||||
`personal_did` and uses it as `CALLERID(num)` instead of the shared
|
||||
trunk DID when one is assigned. A personal DID assigned to an
|
||||
internal-tier extension is accepted but silently never rings anyone
|
||||
until that extension is also granted full/restricted tier — both the
|
||||
CLI and the dashboard warn about this at assignment time rather than
|
||||
blocking it, matching this repo's general permissive-with-warnings
|
||||
style. Caught and fixed a real bug while building this: the
|
||||
dashboard's `write_permission()` did `cp.remove_section(ext)` when
|
||||
tier was set to "internal", which silently discarded any
|
||||
`messaging=yes` or `personal_did=` already on that extension — fixed
|
||||
to remove only the tier/allowed_numbers keys, dropping the section
|
||||
only once nothing else is left in it.
|
||||
16. Known-provider quick-pick — Done. A "1) Anveo Direct / 2) VoIP.ms /
|
||||
3) manual" choice at the top of the provider prompts pre-fills known
|
||||
values (Anveo Direct: `sbc.anveo.com`, the 4 published signaling IPs;
|
||||
VoIP.ms: just the provider name) — every value stays editable at each
|
||||
prompt, so this changes defaults only, never behavior. Extensions/
|
||||
users remain fully independent of this service either way — they're
|
||||
created through the base Asterisk install's own device management,
|
||||
and `pstn-trunk.sh` only grants PSTN permissions/personal numbers to
|
||||
extensions that already exist.
|
||||
17. **Open, unresolved**: Anveo Direct's own "Outbound Trunks" configuration
|
||||
page documents outbound dialing as `[PREFIX]PHONENUMBER@sbc.anveo.com`
|
||||
— a per-trunk custom prefix. This directly contradicts the earlier
|
||||
"no dial prefix needed" finding sourced from their FAQ (see the Anveo
|
||||
Direct provider notes above). Not resolved either way: whether the
|
||||
Prefix field can be left blank when creating an outbound Call
|
||||
Termination trunk in their portal. This dialplan dials the bare
|
||||
number with no prefix, matching the FAQ-sourced finding — if the
|
||||
Prefix field turns out to require a non-empty value, outbound calls
|
||||
through that trunk won't match and will fail, and this installer
|
||||
would need a `PREFIX` setting threaded into `${EXTEN}` before
|
||||
`Dial()`. Flagged with a loud warning in the CLI's Anveo Direct
|
||||
quick-pick rather than silently trusting the older finding. Also
|
||||
unresolved: a Trial Anveo Direct account shows its own "$2 / 30 days"
|
||||
spending limit and "$2 minimum account balance" in the portal —
|
||||
unclear whether either changes once the account is verified/funded
|
||||
beyond Trial status; this sits below and independent of anything this
|
||||
repo's own kill-switch enforces.
|
||||
|
||||
+246
-43
@@ -11,9 +11,9 @@
|
||||
# never gated by any of the above, regardless of tier — the trunk is purely
|
||||
# an additional path out to/in from the real phone network.
|
||||
#
|
||||
# Defaults to VoIP.ms (see docs/pstn-calling-voipms-plan.md for the design/
|
||||
# cost background this is built from) but isn't hardcoded to it — any SIP
|
||||
# trunk provider that supports IP authentication works the same way.
|
||||
# Provider-agnostic — any SIP trunk provider that supports IP authentication
|
||||
# works the same way. VoIP.ms and Anveo Direct are both confirmed working;
|
||||
# see docs/pstn-calling-voipms-plan.md for the design/cost background.
|
||||
#
|
||||
# Requires an existing services/asterisk-digital-ocean.sh OR services/asterisk.sh
|
||||
# install — this adds a PSTN trunk on top of one of them and does not stand
|
||||
@@ -23,7 +23,7 @@
|
||||
#
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
|
||||
register_service pstn-trunk homelab "SIP PSTN trunk for asterisk-digital-ocean/asterisk — US-only, per-extension permission tiers, spend/volume alerts (defaults to VoIP.ms)"
|
||||
register_service pstn-trunk homelab "SIP PSTN trunk for asterisk-digital-ocean/asterisk — US-only, per-extension permission tiers, spend/volume alerts (any IP-authenticated provider — VoIP.ms and Anveo Direct both confirmed)"
|
||||
|
||||
# ── Surviving Easy Asterisk's regeneration ──────────────────────────────────
|
||||
# Easy Asterisk (the vendor project asterisk-digital-ocean.sh/asterisk.sh
|
||||
@@ -306,7 +306,8 @@ exten => pstn_check_busy,1,Set(PSTN_MAX_OUT=${AST_CONFIG(pstn-limits.conf,limits
|
||||
same => n,Set(PSTN_MAX_OUT=${IF($["${PSTN_MAX_OUT}" = ""]?10:${PSTN_MAX_OUT})})
|
||||
same => n,GotoIf($[${GROUP_COUNT(pstn-out)} >= ${PSTN_MAX_OUT}]?pstn_busy,1)
|
||||
same => n,Set(GROUP()=pstn-out)
|
||||
same => n,Set(CALLERID(num)=__PSTN_DID__)
|
||||
same => n,Set(PSTN_PERSONAL_CID=${AST_CONFIG(pstn-permissions.conf,${PSTN_CALLER},personal_did)})
|
||||
same => n,Set(CALLERID(num)=${IF($["${PSTN_PERSONAL_CID}" = ""]?__PSTN_DID__:${PSTN_PERSONAL_CID})})
|
||||
same => n,Set(PSTN_START=${EPOCH})
|
||||
same => n,Dial(PJSIP/${EXTEN}@pstn-trunk,60)
|
||||
same => n,Set(PSTN_DUR=$[${EPOCH} - ${PSTN_START}])
|
||||
@@ -340,9 +341,11 @@ EOF
|
||||
cat >> "$FILE" << 'EOF'
|
||||
|
||||
[from-pstn-trunk]
|
||||
exten => _X.,1,NoOp(Inbound PSTN call from ${CALLERID(num)})
|
||||
exten => _X.,1,NoOp(Inbound PSTN call from ${CALLERID(num)} to ${EXTEN})
|
||||
same => n,Set(PSTN_KILLED=${AST_CONFIG(pstn-trunk-killswitch.conf,state,tripped)})
|
||||
same => n,GotoIf($["${PSTN_KILLED}" = "1"]?pstn_in_killed,1)
|
||||
same => n,Set(PSTN_PERSONAL_OWNER=${AST_CONFIG(pstn-personal-dids.conf,${EXTEN},owner)})
|
||||
same => n,GotoIf($["${PSTN_PERSONAL_OWNER}" != ""]?pstn_personal_inbound,1)
|
||||
same => n,Set(PSTN_RING_LIST=)
|
||||
same => n,Set(PSTN_RING_SEP=)
|
||||
EOF
|
||||
@@ -376,6 +379,29 @@ __ALERT_BUSY_IN_LINE__
|
||||
exten => pstn_in_killed,1,NoOp(PSTN trunk - spend-cap kill-switch is tripped, rejecting inbound call)
|
||||
__ALERT_KILLED_IN_LINE__
|
||||
same => n,Hangup()
|
||||
|
||||
; Personal DID inbound routing — rings ONLY the assigned owner, gated by
|
||||
; that owner's own tier/approved-numbers (same check every ring-group
|
||||
; member gets, just for a single specific target instead of a list, and
|
||||
; with no fallback to the shared ring-group if the owner can't take this
|
||||
; call — it's their own number, not the shared line).
|
||||
exten => pstn_personal_inbound,1,Set(PSTN_OWNER_TIER=${AST_CONFIG(pstn-permissions.conf,${PSTN_PERSONAL_OWNER},tier)})
|
||||
same => n,GotoIf($["${PSTN_OWNER_TIER}" = "full"]?pstn_personal_ring,1)
|
||||
same => n,Set(PSTN_OWNER_ALLOWED=${AST_CONFIG(pstn-permissions.conf,${PSTN_PERSONAL_OWNER},allowed_numbers)})
|
||||
same => n,GotoIf($["${PSTN_OWNER_TIER}" = "restricted" & ${REGEX("^(${PSTN_OWNER_ALLOWED})$" ${CALLERID(num)})}=1]?pstn_personal_ring,1)
|
||||
same => n,NoOp(Denied - personal DID ${EXTEN}'s owner ${PSTN_PERSONAL_OWNER} not authorized for this caller)
|
||||
__ALERT_DENY_PERSONAL_LINE__
|
||||
same => n,Hangup()
|
||||
|
||||
exten => pstn_personal_ring,1,Set(PSTN_MAX_IN=${AST_CONFIG(pstn-limits.conf,limits,max_inbound)})
|
||||
same => n,Set(PSTN_MAX_IN=${IF($["${PSTN_MAX_IN}" = ""]?10:${PSTN_MAX_IN})})
|
||||
same => n,GotoIf($[${GROUP_COUNT(pstn-in)} >= ${PSTN_MAX_IN}]?pstn_in_busy,1)
|
||||
same => n,Set(GROUP()=pstn-in)
|
||||
same => n,Set(PSTN_START=${EPOCH})
|
||||
same => n,Dial(PJSIP/${PSTN_PERSONAL_OWNER},20)
|
||||
same => n,Set(PSTN_DUR=$[${EPOCH} - ${PSTN_START}])
|
||||
same => n,System(printf '%s|in|%s|%s|%s\n' "${PSTN_START}" "${CALLERID(num)}" "${EXTEN}" "${PSTN_DUR}" >> /var/log/asterisk/pstn-trunk-calls.log)
|
||||
same => n,Hangup()
|
||||
EOF
|
||||
|
||||
if [[ -n "$NTFY_URL" ]]; then
|
||||
@@ -383,8 +409,9 @@ EOF
|
||||
sed -i "s#__ALERT_DENY_INBOUND_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: inbound call rejected - caller not approved for any ring target.' '${_esc_url2}' >/dev/null 2>\\&1 \\&)#" "$FILE"
|
||||
sed -i "s#__ALERT_BUSY_IN_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: inbound concurrent-call cap reached - a call was rejected.' '${_esc_url2}' >/dev/null 2>\\&1 \\&)#" "$FILE"
|
||||
sed -i "s#__ALERT_KILLED_IN_LINE__# same => n,System(curl -m 5 -s -H 'Priority: urgent' -d 'PSTN trunk: inbound call rejected - spend-cap kill-switch is tripped.' '${_esc_url2}' >/dev/null 2>\\&1 \\&)#" "$FILE"
|
||||
sed -i "s#__ALERT_DENY_PERSONAL_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: inbound call to a personal DID rejected - owner not authorized for this caller.' '${_esc_url2}' >/dev/null 2>\\&1 \\&)#" "$FILE"
|
||||
else
|
||||
sed -i "/__ALERT_DENY_INBOUND_LINE__/d; /__ALERT_BUSY_IN_LINE__/d; /__ALERT_KILLED_IN_LINE__/d" "$FILE"
|
||||
sed -i "/__ALERT_DENY_INBOUND_LINE__/d; /__ALERT_BUSY_IN_LINE__/d; /__ALERT_KILLED_IN_LINE__/d; /__ALERT_DENY_PERSONAL_LINE__/d" "$FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -411,18 +438,30 @@ _pstn_write_limits_file() {
|
||||
# ── Shared: initial permission tiers (fresh install / explicit reset only —
|
||||
# "update in place" never calls this, matching how .env/firewall/Caddy config
|
||||
# are protected elsewhere in this repo; see file-level comment above) ──────
|
||||
# Args: FILE, space-separated FULL_EXTS, space-separated MESSAGING_EXTS, then
|
||||
# "ext" "pipe|separated|numbers" pairs for each restricted extension.
|
||||
# Args: FILE, space-separated FULL_EXTS, space-separated MESSAGING_EXTS,
|
||||
# space-separated "ext=did" PERSONAL_DID_ASSIGNMENTS, then "ext"
|
||||
# "pipe|separated|numbers" pairs for each restricted extension.
|
||||
_pstn_write_permissions_file() {
|
||||
local FILE="$1" FULL_EXTS="$2" MESSAGING_EXTS="$3"
|
||||
shift 3
|
||||
local FILE="$1" FULL_EXTS="$2" MESSAGING_EXTS="$3" PERSONAL_DID_ASSIGNMENTS="$4"
|
||||
shift 4
|
||||
local -A _personal_did_map=()
|
||||
local _pd_token
|
||||
for _pd_token in $PERSONAL_DID_ASSIGNMENTS; do
|
||||
_personal_did_map["${_pd_token%%=*}"]="${_pd_token#*=}"
|
||||
done
|
||||
local _written_exts=""
|
||||
{
|
||||
echo "; PSTN permission tiers — internal / restricted / full — PLUS an independent"
|
||||
echo "; 'messaging' flag for Asterisk's native internal SIP MESSAGE texting (no"
|
||||
echo "; carrier SMS, no PSTN, no cost — a separate axis from PSTN calling, since"
|
||||
echo "; the risk profile is different: an extension can be internal-tier for"
|
||||
echo "; calling and still messaging-enabled, or vice versa)."
|
||||
echo "; PSTN permission tiers — internal / restricted / full — PLUS two independent"
|
||||
echo "; axes per extension:"
|
||||
echo "; - 'messaging' for Asterisk's native internal SIP MESSAGE texting (no carrier"
|
||||
echo "; SMS, no PSTN, no cost — a separate axis from PSTN calling, since the risk"
|
||||
echo "; profile is different: an extension can be internal-tier for calling and"
|
||||
echo "; still messaging-enabled, or vice versa)."
|
||||
echo "; - 'personal_did' assigns this extension its own DID (see"
|
||||
echo "; pstn-personal-dids.conf, which the dialplan reads for inbound routing —"
|
||||
echo "; this key here is only the OUTBOUND Caller-ID override). A personal DID"
|
||||
echo "; only actually rings anyone if its owner is also full or restricted tier —"
|
||||
echo "; internal tier means no PSTN either way, personal DID or not."
|
||||
echo "; Read LIVE by the dialplan on every call (AST_CONFIG()) — no Asterisk"
|
||||
echo "; restart needed when this changes. Edit here directly, via the Security"
|
||||
echo "; Dashboard web UI's \"PSTN Trunk\" tab (if installed), or by re-running"
|
||||
@@ -438,6 +477,7 @@ _pstn_write_permissions_file() {
|
||||
echo "[$_ext]"
|
||||
echo "tier=full"
|
||||
[[ " $MESSAGING_EXTS " == *" $_ext "* ]] && echo "messaging=yes"
|
||||
[[ -n "${_personal_did_map[$_ext]:-}" ]] && echo "personal_did=${_personal_did_map[$_ext]}"
|
||||
echo ""
|
||||
_written_exts="$_written_exts $_ext"
|
||||
done
|
||||
@@ -448,6 +488,7 @@ _pstn_write_permissions_file() {
|
||||
echo "tier=restricted"
|
||||
echo "allowed_numbers=${_nums}"
|
||||
[[ " $MESSAGING_EXTS " == *" $_ext "* ]] && echo "messaging=yes"
|
||||
[[ -n "${_personal_did_map[$_ext]:-}" ]] && echo "personal_did=${_personal_did_map[$_ext]}"
|
||||
echo ""
|
||||
_written_exts="$_written_exts $_ext"
|
||||
done
|
||||
@@ -455,6 +496,15 @@ _pstn_write_permissions_file() {
|
||||
if [[ " $_written_exts " != *" $_ext "* ]]; then
|
||||
echo "[$_ext]"
|
||||
echo "messaging=yes"
|
||||
[[ -n "${_personal_did_map[$_ext]:-}" ]] && echo "personal_did=${_personal_did_map[$_ext]}"
|
||||
echo ""
|
||||
_written_exts="$_written_exts $_ext"
|
||||
fi
|
||||
done
|
||||
for _ext in "${!_personal_did_map[@]}"; do
|
||||
if [[ " $_written_exts " != *" $_ext "* ]]; then
|
||||
echo "[$_ext]"
|
||||
echo "personal_did=${_personal_did_map[$_ext]}"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
@@ -462,6 +512,36 @@ _pstn_write_permissions_file() {
|
||||
chmod 664 "$FILE"
|
||||
}
|
||||
|
||||
# ── Shared: personal DID -> owner-extension mapping (fresh install / explicit
|
||||
# reset only — same "update never touches it" protection as
|
||||
# pstn-permissions.conf). Args: FILE, then "did" "owner" pairs.
|
||||
_pstn_write_personal_dids_file() {
|
||||
local FILE="$1"
|
||||
shift
|
||||
{
|
||||
echo "; Personal DID -> owner-extension mapping. Read LIVE by the dialplan"
|
||||
echo "; (AST_CONFIG()) on every inbound call — no restart needed. An inbound call"
|
||||
echo "; to a DID listed here routes directly to its owner, checked against the"
|
||||
echo "; owner's OWN tier/approved-numbers in pstn-permissions.conf — no ring-group"
|
||||
echo "; fallback, since this is that extension's own number, not the shared line."
|
||||
echo "; The matching outbound Caller-ID override lives in pstn-permissions.conf"
|
||||
echo "; ('personal_did=' per extension) — kept in sync automatically whenever a"
|
||||
echo "; DID is assigned/removed via the CLI installer or the Security Dashboard's"
|
||||
echo "; PSTN Trunk tab, rather than hand-editing both files separately."
|
||||
echo "; The shared trunk DID keeps working as the main/ring-group line regardless"
|
||||
echo "; of anything assigned here."
|
||||
echo ""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
local _did="$1" _owner="$2"
|
||||
shift 2
|
||||
echo "[$_did]"
|
||||
echo "owner=$_owner"
|
||||
echo ""
|
||||
done
|
||||
} > "$FILE"
|
||||
chmod 664 "$FILE"
|
||||
}
|
||||
|
||||
# ── Shared: kill-switch state (fresh install / explicit reset only — same
|
||||
# "update never touches it" protection as pstn-permissions.conf/
|
||||
# pstn-limits.conf) ─────────────────────────────────────────────────────────
|
||||
@@ -1090,16 +1170,20 @@ install_pstn-trunk() {
|
||||
local PERMISSIONS_FILE="$ASTERISK_DIR/pstn-permissions.conf"
|
||||
local LIMITS_FILE="$ASTERISK_DIR/pstn-limits.conf"
|
||||
local KILLSWITCH_FILE="$ASTERISK_DIR/pstn-trunk-killswitch.conf"
|
||||
local PERSONAL_DIDS_FILE="$ASTERISK_DIR/pstn-personal-dids.conf"
|
||||
local SETTINGS_FILE="$EA_DIR/.pstn-trunk.env"
|
||||
local CONTAINER_NAME="easy-asterisk"
|
||||
[[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]] && CONTAINER_NAME="easy-asterisk-do"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would require an existing asterisk-digital-ocean OR asterisk (LAN) install"
|
||||
echo "[DRY-RUN] Would prompt for: SIP provider name (default VoIP.ms), server/POP hostname, DID,"
|
||||
echo "[DRY-RUN] Would prompt for: known-provider quick-pick (Anveo Direct/VoIP.ms pre-fill known"
|
||||
echo "[DRY-RUN] server/signaling-IP values; still editable) or manual entry, SIP provider name, DID,"
|
||||
echo "[DRY-RUN] full-PSTN extensions, restricted-PSTN extensions + their approved numbers,"
|
||||
echo "[DRY-RUN] internal SIP messaging extensions (separate from PSTN calling permission),"
|
||||
echo "[DRY-RUN] max concurrent outbound/inbound calls (default 10/10), inbound ring-group extensions,"
|
||||
echo "[DRY-RUN] optional personal-number assignments (DID -> owner extension, additive to the"
|
||||
echo "[DRY-RUN] shared trunk DID), max concurrent outbound/inbound calls (default 10/10),"
|
||||
echo "[DRY-RUN] inbound ring-group extensions,"
|
||||
echo "[DRY-RUN] ntfy alert topic (optional), international-calling allow-list (CLI-only,"
|
||||
echo "[DRY-RUN] always asked, never on the web dashboard), per-minute rate + monthly/hourly"
|
||||
echo "[DRY-RUN] alert thresholds, and an optional hard monthly spend-cap kill-switch"
|
||||
@@ -1133,15 +1217,17 @@ install_pstn-trunk() {
|
||||
log_warning "A static IP from your ISP avoids that; asterisk-digital-ocean sidesteps it entirely."
|
||||
fi
|
||||
|
||||
log_info "Configuring a SIP PSTN trunk for $ASTERISK_KIND (defaults to VoIP.ms)."
|
||||
log_info "Configuring a SIP PSTN trunk for $ASTERISK_KIND (any IP-authenticated provider —"
|
||||
log_info "VoIP.ms and Anveo Direct are both confirmed working; see docs/pstn-calling-voipms-plan.md)."
|
||||
log_info "US-only outbound (NANP dialplan), a concurrent-call cap, per-extension permission"
|
||||
log_info "tiers, an inbound ring-group, and ntfy alerts on denied/rejected calls plus"
|
||||
log_info "spend/volume checks."
|
||||
echo ""
|
||||
log_warning "Before continuing, on your provider's side you should already have: created an"
|
||||
log_warning "account, funded and set up prepaid billing with auto-recharge OFF (VoIP.ms: Client"
|
||||
log_warning "Area -> Balance Management), ordered a DID with IP authentication pointed at this"
|
||||
log_warning "box's public IP, and picked a server/POP. Also restrict outbound routing to"
|
||||
log_warning "Area -> Balance Management; Anveo Direct: fund the account balance directly),"
|
||||
log_warning "ordered a DID with IP authentication pointed at this box's public IP, and picked"
|
||||
log_warning "a server/POP. Also restrict outbound routing to"
|
||||
log_warning "US/NANP on the provider's own side if it offers that — this dialplan is the second,"
|
||||
log_warning "independent layer, not a substitute for the first."
|
||||
log_warning "See docs/pstn-calling-voipms-plan.md for the full background."
|
||||
@@ -1199,11 +1285,53 @@ install_pstn-trunk() {
|
||||
|
||||
# ── Prompts — provider account details aren't scriptable, set up manually
|
||||
# on the provider's own site first (see warning above) ───────────────────
|
||||
# Known-provider quick-path: only pre-fills the defaults below (still
|
||||
# fully editable at each prompt) — doesn't change any dialplan/auth
|
||||
# behavior, which stays provider-generic either way.
|
||||
echo " Known/tested providers — this only pre-fills the defaults below (server"
|
||||
echo " hostname, signaling IPs, account-setup reminders); every value is still"
|
||||
echo " editable at each prompt."
|
||||
echo " 1) Anveo Direct"
|
||||
echo " 2) VoIP.ms"
|
||||
echo " 3) Something else / manual entry"
|
||||
local _provider_choice=""
|
||||
prompt_text "Choice (1/2/3):" "3" _provider_choice
|
||||
|
||||
local _default_provider_name="" _default_server="" _default_extra_ips=""
|
||||
case "$_provider_choice" in
|
||||
1)
|
||||
_default_provider_name="Anveo Direct"
|
||||
_default_server="sbc.anveo.com"
|
||||
_default_extra_ips="169.48.232.158 204.216.109.55 176.9.39.206 72.9.149.25"
|
||||
echo ""
|
||||
log_info "Anveo Direct known values pre-filled below (hostname, 4 published signaling IPs)."
|
||||
log_warning "Before continuing: create/verify your account at"
|
||||
log_warning " https://www.anveo.com/account.asp?account_type=direct"
|
||||
log_warning "fund it, order a DID, and configure at least one outbound Call"
|
||||
log_warning "Termination trunk in Anveo's own portal (Outbound Trunks page). A Trial"
|
||||
log_warning "account shows its own \$2/30-day spend limit and \$2 minimum balance —"
|
||||
log_warning "separate from anything this installer enforces; confirm with Anveo"
|
||||
log_warning "support whether those change once verified/funded. Their CallerID policy"
|
||||
log_warning "requires a verified/Anveo-owned number as Caller-ID — any DID you order"
|
||||
log_warning "through them satisfies that automatically (see this service's personal-"
|
||||
log_warning "number feature for assigning a specific one per extension)."
|
||||
log_warning "UNVERIFIED: Anveo's own outbound-trunk page documents dialing as"
|
||||
log_warning "[PREFIX]PHONENUMBER@sbc.anveo.com (a per-trunk prefix) — this contradicts"
|
||||
log_warning "an earlier no-prefix-needed finding from their FAQ. This dialplan dials"
|
||||
log_warning "the bare number, no prefix. Check whether your configured trunk's Prefix"
|
||||
log_warning "field can be left blank before relying on this — if it can't, outbound"
|
||||
log_warning "calls through that trunk won't match and will fail."
|
||||
;;
|
||||
2)
|
||||
_default_provider_name="VoIP.ms"
|
||||
;;
|
||||
esac
|
||||
|
||||
local PROVIDER_NAME=""
|
||||
prompt_text "SIP trunk provider name (for your reference/docs only):" "VoIP.ms" PROVIDER_NAME
|
||||
prompt_text "SIP trunk provider name (for your reference/docs only — e.g. VoIP.ms, Anveo Direct):" "$_default_provider_name" PROVIDER_NAME
|
||||
|
||||
local TRUNK_SERVER=""
|
||||
prompt_text "Server/POP hostname (e.g. atlanta2.voip.ms for VoIP.ms — pick the one closest to this box from your provider's server list):" "" TRUNK_SERVER
|
||||
prompt_text "Server/POP hostname (e.g. atlanta2.voip.ms for VoIP.ms, sbc.anveo.com for Anveo Direct — pick the one closest to this box from your provider's server list):" "$_default_server" TRUNK_SERVER
|
||||
if [[ -z "$TRUNK_SERVER" ]]; then
|
||||
log_error "A server hostname is required — aborting."
|
||||
return 1
|
||||
@@ -1228,7 +1356,7 @@ install_pstn-trunk() {
|
||||
# dial out to) — the resolved IP above always gets included, this just
|
||||
# adds any others the provider documents.
|
||||
local EXTRA_IPS=""
|
||||
prompt_text "Any additional known source IPs for inbound calls, space-separated (check your provider's docs — e.g. a firewall/signaling IP list; blank if the resolved IP above is the only one):" "" EXTRA_IPS
|
||||
prompt_text "Any additional known source IPs for inbound calls, space-separated (check your provider's docs — e.g. a firewall/signaling IP list; blank if the resolved IP above is the only one):" "$_default_extra_ips" EXTRA_IPS
|
||||
local _octet='(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
|
||||
local _ip_re="^${_octet}\\.${_octet}\\.${_octet}\\.${_octet}\$"
|
||||
local TRUNK_SERVER_IPS="$TRUNK_SERVER_IP" _ip
|
||||
@@ -1317,6 +1445,48 @@ install_pstn-trunk() {
|
||||
local MESSAGING_EXTS=""
|
||||
prompt_text "Extensions allowed to use internal SIP messaging (space-separated, blank = none):" "" MESSAGING_EXTS
|
||||
|
||||
# ── Personal numbers — optional, additive to the shared trunk DID ──────
|
||||
# Multiple DIDs can share this one trunk/account. Assigning one to a
|
||||
# specific extension makes inbound calls to it ring ONLY that extension
|
||||
# (still gated by that extension's own tier/approved-numbers — a
|
||||
# personal DID doesn't bypass PSTN permission, it just narrows routing
|
||||
# from "the shared ring group" to "this one owner"), and makes that
|
||||
# extension's outbound calls show its own DID as Caller-ID instead of
|
||||
# the shared one. The shared DID/ring-group above is unaffected either
|
||||
# way — this is purely additive.
|
||||
echo ""
|
||||
echo " Personal numbers (optional): assign specific DIDs to specific extensions."
|
||||
echo " Inbound calls to that DID ring only its owner; outbound calls from that"
|
||||
echo " extension show its own DID as Caller-ID. Requires the owner to also be"
|
||||
echo " full or restricted tier to actually receive anything on it."
|
||||
local WANT_PERSONAL_DIDS=""
|
||||
prompt_yn "Assign any personal DIDs now? (y/n):" "n" WANT_PERSONAL_DIDS
|
||||
local PERSONAL_DID_PAIRS=() PERSONAL_DID_ASSIGNMENTS=""
|
||||
if [[ "$WANT_PERSONAL_DIDS" =~ ^[Yy]$ ]]; then
|
||||
local _pd_more="y"
|
||||
while [[ "$_pd_more" =~ ^[Yy]$ ]]; do
|
||||
local _pd_did="" _pd_owner=""
|
||||
prompt_text " DID (10-digit US number, digits only):" "" _pd_did
|
||||
if [[ "$_pd_did" =~ ^[0-9]{10}$ ]]; then
|
||||
prompt_text " Owner extension for $_pd_did:" "" _pd_owner
|
||||
if [[ "$_pd_owner" =~ ^[0-9]+$ ]]; then
|
||||
PERSONAL_DID_PAIRS+=("$_pd_did" "$_pd_owner")
|
||||
PERSONAL_DID_ASSIGNMENTS="${PERSONAL_DID_ASSIGNMENTS} ${_pd_owner}=${_pd_did}"
|
||||
if [[ " $FULL_EXTS $RESTRICTED_EXTS " != *" $_pd_owner "* ]]; then
|
||||
log_warning "Extension $_pd_owner isn't full/restricted tier yet — it won't actually"
|
||||
log_warning "receive calls on $_pd_did until you also grant it one of those tiers."
|
||||
fi
|
||||
log_success "Will assign $_pd_did to extension $_pd_owner."
|
||||
else
|
||||
log_warning "Not a valid extension — skipped."
|
||||
fi
|
||||
else
|
||||
log_warning "Not a valid 10-digit DID — skipped."
|
||||
fi
|
||||
prompt_yn " Assign another? (y/n):" "n" _pd_more
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local WANT_NTFY=""
|
||||
prompt_yn "Send an ntfy alert when a call is denied (permission tier/approved-number check failed) or rejected (concurrency cap hit)? (y/n):" "y" WANT_NTFY
|
||||
@@ -1348,7 +1518,7 @@ install_pstn-trunk() {
|
||||
log_info "Spend/volume alert settings (used only to estimate cost and flag unusual usage —"
|
||||
log_info "not billing-accurate, just a safety net)."
|
||||
local RATE_PER_MIN=""
|
||||
prompt_text " Outbound per-minute rate in USD (VoIP.ms US rate is 0.01):" "0.01" RATE_PER_MIN
|
||||
prompt_text " Outbound per-minute rate in USD (check your provider's published rate — e.g. VoIP.ms US is ~0.01, Anveo Direct US is ~0.001):" "0.01" RATE_PER_MIN
|
||||
local MONTH_THRESHOLD=""
|
||||
prompt_text " Alert once when estimated spend this month reaches (USD):" "10" MONTH_THRESHOLD
|
||||
local BURST_THRESHOLD=""
|
||||
@@ -1383,9 +1553,10 @@ install_pstn-trunk() {
|
||||
"$RING_EXTS" "$NTFY_URL" "$RATE_PER_MIN" \
|
||||
"$MONTH_THRESHOLD" "$BURST_THRESHOLD" "$PROVIDER_NAME" "$MAX_MONTHLY_SPEND" "$CONTAINER_NAME" || return 1
|
||||
|
||||
_pstn_write_permissions_file "$PERMISSIONS_FILE" "$FULL_EXTS" "$MESSAGING_EXTS" "${RESTRICTED_ARGS[@]}"
|
||||
_pstn_write_permissions_file "$PERMISSIONS_FILE" "$FULL_EXTS" "$MESSAGING_EXTS" "$PERSONAL_DID_ASSIGNMENTS" "${RESTRICTED_ARGS[@]}"
|
||||
_pstn_write_limits_file "$LIMITS_FILE" "$MAX_OUTBOUND" "$MAX_INBOUND"
|
||||
_pstn_write_killswitch_file "$KILLSWITCH_FILE"
|
||||
_pstn_write_personal_dids_file "$PERSONAL_DIDS_FILE" "${PERSONAL_DID_PAIRS[@]}"
|
||||
ensure_docker_dir_ownership "$ASTERISK_DIR"
|
||||
|
||||
# No new firewall rules: the base install already opens SIP (5060/5061)
|
||||
@@ -1461,9 +1632,10 @@ a strong safety net, not an absolute guarantee against any overage. See
|
||||
cat > "$DOC_FILE" << MD
|
||||
# SIP PSTN trunk (add-on to $ASTERISK_KIND)
|
||||
|
||||
US-only outbound PSTN calling over a SIP trunk (defaults to VoIP.ms, works
|
||||
with any IP-authenticated provider), per-extension permission tiers, a
|
||||
configurable concurrent-call cap, and an inbound ring-group. See
|
||||
US-only outbound PSTN calling over a SIP trunk (any IP-authenticated
|
||||
provider — VoIP.ms and Anveo Direct both confirmed working), per-extension
|
||||
permission tiers, a configurable concurrent-call cap, and an inbound
|
||||
ring-group. See
|
||||
\`docs/pstn-calling-voipms-plan.md\` in the repo for the full design
|
||||
background, cost estimate, and toll-fraud reasoning.
|
||||
|
||||
@@ -1644,25 +1816,56 @@ Asterisk's native SIP \`MESSAGE\` support (extension-to-extension texting —
|
||||
no carrier SMS, no PSTN, no cost) is gated by a \`messaging=yes\` flag per
|
||||
extension in \`pstn-permissions.conf\`, independent of the PSTN calling
|
||||
tiers above — off by default, same "opt in" posture. Currently enabled for:
|
||||
${MESSAGING_EXTS:-none}.
|
||||
${MESSAGING_EXTS:-none}. Live-editable any time via the Security
|
||||
Dashboard's "PSTN Trunk" tab, in its own always-available "Internal SIP
|
||||
messaging" card — no need to re-run this installer, and no dependency on
|
||||
this trunk (or any PSTN trunk at all) being installed, unlike the
|
||||
calling-permissions table below it in that same tab.
|
||||
|
||||
**Known gap:** this installer writes the permission flag (live-editable,
|
||||
same mechanism as the calling tiers), but the actual SIP \`MESSAGE\` routing
|
||||
dialplan wiring depends on how Easy Asterisk's own generated
|
||||
\`extensions.conf\`/\`pjsip.conf\` route inbound messages, which needs to be
|
||||
verified against a live install before it's safely automated here — shipping
|
||||
a guessed pattern risked either silently not working or interfering with
|
||||
call-routing precedence in the same \`[intercom]\` context. Treat the
|
||||
permission flag as ready for a dashboard/CLI-managed allow-list once that
|
||||
routing is confirmed, not as fully wired yet.
|
||||
**Won't show up in Easy Asterisk's own web admin, by design** — same as
|
||||
the PSTN calling tiers, this is a permission this repo layers on top,
|
||||
not an Easy Asterisk feature, so it's only manageable here or via the
|
||||
Security Dashboard.
|
||||
|
||||
**Known gap:** the flag above is real and live-editable, but the actual
|
||||
SIP \`MESSAGE\` routing dialplan wiring — does Asterisk actually deliver/
|
||||
gate a message using this flag — depends on how Easy Asterisk's own
|
||||
generated \`extensions.conf\`/\`pjsip.conf\` route inbound messages, which
|
||||
needs to be verified against a live install before it's safely automated
|
||||
here. Shipping a guessed pattern risked either silently not working or
|
||||
interfering with call-routing precedence in the same \`[intercom]\`
|
||||
context, so it hasn't been guessed at. If you want this working end to
|
||||
end, the fastest path is checking a few things on a live box (e.g.
|
||||
whether an endpoint has \`message_context\` set, and what happens when you
|
||||
send a test SIP MESSAGE to one) so the dialplan gate can be built against
|
||||
real behavior instead of assumption — ask if you want to walk through
|
||||
that.
|
||||
|
||||
## Personal numbers
|
||||
|
||||
Multiple DIDs can share this one trunk/account — assign one to a specific
|
||||
extension and inbound calls to it route straight to that owner, still
|
||||
gated by the owner's own tier/approved-numbers (no ring-group fallback,
|
||||
since it's that extension's own line, not the shared one), while that
|
||||
extension's outbound calls show its own DID as Caller-ID instead of the
|
||||
shared trunk DID above. Entirely additive: the shared DID/ring-group keeps
|
||||
working for everyone regardless of what's assigned here.
|
||||
|
||||
$([ "${#PERSONAL_DID_PAIRS[@]}" -gt 0 ] && { local _i; for ((_i=0; _i<${#PERSONAL_DID_PAIRS[@]}; _i+=2)); do echo "- \`${PERSONAL_DID_PAIRS[$_i]}\` -> extension ${PERSONAL_DID_PAIRS[$_i+1]}"; done; } || echo "None assigned yet.")
|
||||
|
||||
Stored in \`config/asterisk/pstn-personal-dids.conf\` (DID -> owner, read live
|
||||
by the dialplan for inbound routing) and a \`personal_did=\` field per
|
||||
extension in \`pstn-permissions.conf\` (the outbound Caller-ID override) —
|
||||
both kept in sync automatically by the CLI installer and the Security
|
||||
Dashboard's "PSTN Trunk" tab, live, no restart needed.
|
||||
|
||||
## Managing this from a web UI
|
||||
|
||||
If \`services/security-dashboard.sh\` is installed, its "PSTN Trunk" tab
|
||||
shows both the per-extension permission tiers and the outbound/inbound
|
||||
concurrency caps, all editable live — no restart, no reinstall. Install/
|
||||
update it any time with \`sudo ./setup.sh security-dashboard\`; it
|
||||
auto-detects this install.
|
||||
shows the per-extension permission tiers, the outbound/inbound concurrency
|
||||
caps, and personal-number assignments, all editable live — no restart, no
|
||||
reinstall. Install/update it any time with \`sudo ./setup.sh
|
||||
security-dashboard\`; it auto-detects this install.
|
||||
|
||||
## Manual edits
|
||||
|
||||
|
||||
+372
-52
@@ -208,19 +208,27 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly.
|
||||
- **Unwhitelist + Ban** does that *and* immediately bans (24h) every IP
|
||||
CrowdSec has ever recorded for that ASN, for accidental-whitelist cases
|
||||
where you don't want to wait for it to misbehave again.
|
||||
- **PSTN Trunk** — detects whether \`services/pstn-trunk.sh\`'s dialplan is
|
||||
actually installed (\`pstn-trunk-dialplan.conf\` present) and shows a clear
|
||||
"not installed" message instead of the caps/tiers editor if not, so it
|
||||
never shows real-looking-but-unenforced defaults. When installed: the
|
||||
outbound/inbound concurrent-call caps, and every known extension (parsed
|
||||
from \`pjsip.conf\`) with its current permission tier (internal /
|
||||
restricted / full) and, for restricted, its approved numbers — all
|
||||
editable live, no Asterisk restart, no reinstall. Writes directly to
|
||||
\`pstn-limits.conf\` / \`pstn-permissions.conf\`, which the dialplan reads
|
||||
fresh on every call. The spend-cap kill-switch and international-calling
|
||||
allow-list are deliberately **not** managed here — CLI-only, via
|
||||
\`sudo ./setup.sh pstn-trunk\` — since both are more security-sensitive
|
||||
than what this tab already exposes.
|
||||
- **PSTN Trunk** — an "Internal SIP messaging" card at the top is always
|
||||
available, whether or not a PSTN trunk has ever been installed: a
|
||||
checkbox per known extension (parsed from \`pjsip.conf\`) for
|
||||
Asterisk's native SIP texting, independent of PSTN calling entirely (no
|
||||
cost, no carrier, no DID, no dependency on \`services/pstn-trunk.sh\`
|
||||
having been run — see its "Known gap" note on messaging for what this
|
||||
flag does and doesn't do yet at the Asterisk level). Below that, the
|
||||
rest of the tab detects whether \`services/pstn-trunk.sh\`'s dialplan is
|
||||
actually installed (\`pstn-trunk-dialplan.conf\` present) and shows a
|
||||
clear "not installed" message instead of the calling-permissions editor
|
||||
if not, so it never shows real-looking-but-unenforced defaults. When
|
||||
installed: the outbound/inbound concurrent-call caps, and every known
|
||||
extension's permission tier (internal / restricted / full) and, for
|
||||
restricted, its approved numbers — all editable live, no Asterisk
|
||||
restart, no reinstall. Also manages personal-number assignments (DID ->
|
||||
owner extension), additive to the shared trunk DID. Writes directly to
|
||||
\`pstn-limits.conf\` / \`pstn-permissions.conf\` / \`pstn-personal-dids.conf\`,
|
||||
which the dialplan reads fresh on every call. The spend-cap kill-switch
|
||||
and international-calling allow-list are deliberately **not** managed
|
||||
here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more
|
||||
security-sensitive than what this tab already exposes.
|
||||
- Link to the Asterisk web admin itself (doesn't embed it, just links out).
|
||||
|
||||
## Manage
|
||||
@@ -925,6 +933,51 @@ def list_extensions():
|
||||
return extensions
|
||||
|
||||
|
||||
def _write_ini_cp(path, header, cp):
|
||||
"""Shared temp-write-then-rename for every live-editable PSTN conf file —
|
||||
one copy of the atomic-write/error-handling logic instead of repeating
|
||||
it per file. Returns (ok, error_message_or_None)."""
|
||||
if not path:
|
||||
return False, "No Asterisk install detected on this box"
|
||||
tmp_path = path + ".tmp"
|
||||
try:
|
||||
with open(tmp_path, "w") as f:
|
||||
f.write(header)
|
||||
cp.write(f)
|
||||
os.replace(tmp_path, path)
|
||||
except OSError as e:
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
return False, "Failed writing %s: %s" % (path, e)
|
||||
return True, None
|
||||
|
||||
|
||||
PERMISSIONS_HEADER = (
|
||||
"; PSTN permission tiers - internal / restricted / full - PLUS two\n"
|
||||
"; independent per-extension axes: messaging (internal SIP MESSAGE\n"
|
||||
"; texting) and personal_did (outbound Caller-ID override; inbound\n"
|
||||
"; routing for personal DIDs lives in pstn-personal-dids.conf).\n"
|
||||
"; Read LIVE by the dialplan on every call (AST_CONFIG()) - no\n"
|
||||
"; Asterisk restart needed. Managed here (Security Dashboard); also\n"
|
||||
"; safe to edit by hand. 'sudo ./setup.sh pstn-trunk' update mode\n"
|
||||
"; never touches this file, only a fresh reinstall does.\n"
|
||||
"; Any extension not listed here is internal-only (no PSTN) by default.\n\n"
|
||||
)
|
||||
|
||||
PERSONAL_DIDS_HEADER = (
|
||||
"; Personal DID -> owner-extension mapping. Read LIVE by the dialplan\n"
|
||||
"; (AST_CONFIG()) on every inbound call - no restart needed. Managed here\n"
|
||||
"; (Security Dashboard); also safe to edit by hand. Kept in sync with\n"
|
||||
"; pstn-permissions.conf's personal_did= field automatically by\n"
|
||||
"; write_personal_did()/remove_personal_did() below - editing this file\n"
|
||||
"; by hand also requires updating that field yourself to match.\n"
|
||||
"; 'sudo ./setup.sh pstn-trunk' update mode never touches this file, only\n"
|
||||
"; a fresh reinstall does.\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _permissions_path():
|
||||
return os.path.join(ASTERISK_CONFIG_DIR, "pstn-permissions.conf") if ASTERISK_CONFIG_DIR else None
|
||||
|
||||
@@ -941,12 +994,13 @@ def _read_permissions_cp():
|
||||
|
||||
|
||||
def get_all_permissions():
|
||||
"""{ext: {"tier": ..., "allowed_numbers": "num|num|..."}} for every
|
||||
extension with a non-internal tier on record. Extensions with no section
|
||||
are implicitly "internal" — the dialplan's AST_CONFIG() lookup treats a
|
||||
missing section as empty/denied the same way, so there's nothing to
|
||||
return for them here; the UI fills in "internal" as the default for any
|
||||
known extension (from list_extensions()) not present in this dict."""
|
||||
"""{ext: {"tier": ..., "allowed_numbers": "num|num|...", "messaging":
|
||||
bool}} for every extension with a non-default record. Extensions with
|
||||
no section are implicitly "internal"/messaging-disabled — the
|
||||
dialplan's AST_CONFIG() lookup treats a missing section/key as empty/
|
||||
denied the same way, so there's nothing to return for them here; the
|
||||
UI fills in the defaults for any known extension (from
|
||||
list_extensions()) not present in this dict."""
|
||||
cp = _read_permissions_cp()
|
||||
result = {}
|
||||
for section in cp.sections():
|
||||
@@ -955,12 +1009,17 @@ def get_all_permissions():
|
||||
result[section] = {
|
||||
"tier": cp.get(section, "tier", fallback="internal"),
|
||||
"allowed_numbers": cp.get(section, "allowed_numbers", fallback=""),
|
||||
"messaging": cp.getboolean(section, "messaging", fallback=False),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def write_permission(ext, tier, numbers_raw):
|
||||
"""Saves one extension's tier + (for restricted) approved-number list.
|
||||
def write_permission(ext, tier, numbers_raw, messaging_enabled=False):
|
||||
"""Saves one extension's tier + (for restricted) approved-number list +
|
||||
messaging flag in one action — messaging is an independent axis from
|
||||
the calling tier (see pstn-trunk.sh's file-level comment: an extension
|
||||
can be internal-tier for calling and still messaging-enabled, or vice
|
||||
versa), so it's set/cleared regardless of which tier branch runs below.
|
||||
Numbers are normalized to a pipe-separated list of 11-digit US numbers —
|
||||
pipe, not comma, because the dialplan uses this value directly as a
|
||||
REGEX() alternation pattern (see services/pstn-trunk.sh's file-level
|
||||
@@ -980,8 +1039,17 @@ def write_permission(ext, tier, numbers_raw):
|
||||
|
||||
cp = _read_permissions_cp()
|
||||
if tier == "internal":
|
||||
# Only drop the tier/allowed_numbers keys, NOT the whole section —
|
||||
# an extension can independently have messaging=yes and/or a
|
||||
# personal_did assigned, and those must survive a tier change back
|
||||
# to internal. Confirmed live as a real bug: cp.remove_section(ext)
|
||||
# here used to silently discard both whenever tier was set to
|
||||
# internal.
|
||||
if cp.has_section(ext):
|
||||
cp.remove_section(ext)
|
||||
if cp.has_option(ext, "tier"):
|
||||
cp.remove_option(ext, "tier")
|
||||
if cp.has_option(ext, "allowed_numbers"):
|
||||
cp.remove_option(ext, "allowed_numbers")
|
||||
else:
|
||||
if not cp.has_section(ext):
|
||||
cp.add_section(ext)
|
||||
@@ -991,32 +1059,59 @@ def write_permission(ext, tier, numbers_raw):
|
||||
elif cp.has_option(ext, "allowed_numbers"):
|
||||
cp.remove_option(ext, "allowed_numbers")
|
||||
|
||||
path = _permissions_path()
|
||||
tmp_path = path + ".tmp"
|
||||
try:
|
||||
with open(tmp_path, "w") as f:
|
||||
f.write(
|
||||
"; PSTN permission tiers - internal / restricted / full.\n"
|
||||
"; Read LIVE by the dialplan on every call (AST_CONFIG()) - no\n"
|
||||
"; Asterisk restart needed. Managed here (Security Dashboard); also\n"
|
||||
"; safe to edit by hand. 'sudo ./setup.sh pstn-trunk' update mode\n"
|
||||
"; never touches this file, only a fresh reinstall does.\n"
|
||||
"; Any extension not listed here is internal-only (no PSTN) by default.\n\n"
|
||||
)
|
||||
cp.write(f)
|
||||
os.replace(tmp_path, path)
|
||||
except OSError as e:
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
return False, "Failed writing %s: %s" % (path, e)
|
||||
if messaging_enabled:
|
||||
if not cp.has_section(ext):
|
||||
cp.add_section(ext)
|
||||
cp.set(ext, "messaging", "yes")
|
||||
elif cp.has_section(ext) and cp.has_option(ext, "messaging"):
|
||||
cp.remove_option(ext, "messaging")
|
||||
|
||||
# Drop the section entirely once nothing (tier, numbers, messaging,
|
||||
# personal_did) is left in it — only reached this way when tier is
|
||||
# internal, messaging is off, and no personal_did was ever assigned.
|
||||
if cp.has_section(ext) and not cp.options(ext):
|
||||
cp.remove_section(ext)
|
||||
|
||||
ok, err = _write_ini_cp(_permissions_path(), PERMISSIONS_HEADER, cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
|
||||
if tier == "restricted" and not clean_numbers:
|
||||
return True, "Saved as restricted with an EMPTY approved list — no PSTN number can reach/be reached by it yet."
|
||||
return True, "Saved"
|
||||
|
||||
|
||||
def write_messaging(ext, enabled):
|
||||
"""Sets/clears just the messaging flag for one extension, leaving any
|
||||
tier/allowed_numbers/personal_did untouched. This is the write path for
|
||||
the standalone "Internal SIP messaging" card, which works whether or
|
||||
not a PSTN trunk has ever been installed — messaging has no dependency
|
||||
on one (no cost, no carrier, no DID), unlike the calling-permissions
|
||||
table this dashboard otherwise gates behind pstn_installed(). Creates
|
||||
pstn-permissions.conf from scratch if it doesn't exist yet."""
|
||||
if not ASTERISK_CONFIG_DIR:
|
||||
return False, "No Asterisk install detected on this box"
|
||||
ext = str(ext).strip()
|
||||
if not EXTEN_RE.match(ext):
|
||||
return False, "Invalid extension"
|
||||
|
||||
cp = _read_permissions_cp()
|
||||
if enabled:
|
||||
if not cp.has_section(ext):
|
||||
cp.add_section(ext)
|
||||
cp.set(ext, "messaging", "yes")
|
||||
elif cp.has_section(ext) and cp.has_option(ext, "messaging"):
|
||||
cp.remove_option(ext, "messaging")
|
||||
|
||||
if cp.has_section(ext) and not cp.options(ext):
|
||||
cp.remove_section(ext)
|
||||
|
||||
ok, err = _write_ini_cp(_permissions_path(), PERMISSIONS_HEADER, cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
return True, "Saved"
|
||||
|
||||
|
||||
LIMIT_RE = re.compile(r"^\d+$")
|
||||
|
||||
|
||||
@@ -1083,6 +1178,118 @@ def write_limits(max_outbound, max_inbound):
|
||||
return True, "Saved"
|
||||
|
||||
|
||||
PERSONAL_DID_RE = re.compile(r"^\d{10}$")
|
||||
|
||||
|
||||
def _personal_dids_path():
|
||||
return os.path.join(ASTERISK_CONFIG_DIR, "pstn-personal-dids.conf") if ASTERISK_CONFIG_DIR else None
|
||||
|
||||
|
||||
def _read_personal_dids_cp():
|
||||
cp = configparser.ConfigParser(delimiters=("=",))
|
||||
path = _personal_dids_path()
|
||||
if path and os.path.isfile(path):
|
||||
try:
|
||||
cp.read(path)
|
||||
except configparser.Error:
|
||||
pass
|
||||
return cp
|
||||
|
||||
|
||||
def list_personal_dids():
|
||||
"""[{"did": ..., "owner": ...}] for every currently-assigned personal
|
||||
DID, sorted by DID."""
|
||||
cp = _read_personal_dids_cp()
|
||||
result = []
|
||||
for section in cp.sections():
|
||||
if not PERSONAL_DID_RE.match(section):
|
||||
continue
|
||||
result.append({"did": section, "owner": cp.get(section, "owner", fallback="")})
|
||||
result.sort(key=lambda d: d["did"])
|
||||
return result
|
||||
|
||||
|
||||
def write_personal_did(did, owner):
|
||||
"""Assigns did -> owner, keeping pstn-personal-dids.conf (inbound
|
||||
routing, read by the dialplan) and pstn-permissions.conf's
|
||||
personal_did= (outbound Caller-ID override) in sync. One owner has at
|
||||
most one personal_did (AST_CONFIG() returns a single value per key), so
|
||||
reassigning a DID to a new owner drops the previous owner's claim on
|
||||
it, and giving an extension a new personal DID drops whichever one it
|
||||
had before — this always leaves a clean 1:1 mapping in both files,
|
||||
rather than requiring the caller to clean up the old assignment
|
||||
itself."""
|
||||
if not ASTERISK_CONFIG_DIR:
|
||||
return False, "No Asterisk install detected on this box"
|
||||
did = str(did).strip()
|
||||
owner = str(owner).strip()
|
||||
if not PERSONAL_DID_RE.match(did):
|
||||
return False, "DID must be a 10-digit US number"
|
||||
if not EXTEN_RE.match(owner):
|
||||
return False, "Invalid owner extension"
|
||||
|
||||
dids_cp = _read_personal_dids_cp()
|
||||
perms_cp = _read_permissions_cp()
|
||||
|
||||
for section in perms_cp.sections():
|
||||
if section != owner and perms_cp.get(section, "personal_did", fallback="") == did:
|
||||
perms_cp.remove_option(section, "personal_did")
|
||||
if not perms_cp.options(section):
|
||||
perms_cp.remove_section(section)
|
||||
|
||||
for section in list(dids_cp.sections()):
|
||||
if section != did and dids_cp.get(section, "owner", fallback="") == owner:
|
||||
dids_cp.remove_section(section)
|
||||
|
||||
if not dids_cp.has_section(did):
|
||||
dids_cp.add_section(did)
|
||||
dids_cp.set(did, "owner", owner)
|
||||
|
||||
if not perms_cp.has_section(owner):
|
||||
perms_cp.add_section(owner)
|
||||
perms_cp.set(owner, "personal_did", did)
|
||||
|
||||
ok, err = _write_ini_cp(_personal_dids_path(), PERSONAL_DIDS_HEADER, dids_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
ok, err = _write_ini_cp(_permissions_path(), PERMISSIONS_HEADER, perms_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
|
||||
owner_tier = perms_cp.get(owner, "tier", fallback="internal")
|
||||
if owner_tier not in ("full", "restricted"):
|
||||
return True, "Assigned %s to extension %s - note: %s is internal-tier, so it won't actually receive calls on this DID until you also grant it full or restricted tier." % (did, owner, owner)
|
||||
return True, "Assigned %s to extension %s" % (did, owner)
|
||||
|
||||
|
||||
def remove_personal_did(did):
|
||||
if not ASTERISK_CONFIG_DIR:
|
||||
return False, "No Asterisk install detected on this box"
|
||||
did = str(did).strip()
|
||||
if not PERSONAL_DID_RE.match(did):
|
||||
return False, "Invalid DID"
|
||||
|
||||
dids_cp = _read_personal_dids_cp()
|
||||
perms_cp = _read_permissions_cp()
|
||||
|
||||
if dids_cp.has_section(did):
|
||||
dids_cp.remove_section(did)
|
||||
|
||||
for section in perms_cp.sections():
|
||||
if perms_cp.get(section, "personal_did", fallback="") == did:
|
||||
perms_cp.remove_option(section, "personal_did")
|
||||
if not perms_cp.options(section):
|
||||
perms_cp.remove_section(section)
|
||||
|
||||
ok, err = _write_ini_cp(_personal_dids_path(), PERSONAL_DIDS_HEADER, dids_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
ok, err = _write_ini_cp(_permissions_path(), PERMISSIONS_HEADER, perms_cp)
|
||||
if not ok:
|
||||
return False, err
|
||||
return True, "Removed %s" % did
|
||||
|
||||
|
||||
INDEX_HTML = """<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>Security Dashboard</title>
|
||||
@@ -1155,9 +1362,17 @@ INDEX_HTML = """<!doctype html>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-pstn" style="display:none">
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0">Internal SIP messaging</h3>
|
||||
<p class="muted">
|
||||
Asterisk's native SIP texting between extensions — no carrier SMS, no PSTN, no cost, and no dependency on a PSTN trunk being installed at all. Independent of the calling permissions below. Note: this flag is live-editable here, but whether Asterisk actually delivers/gates messages using it depends on dialplan wiring not yet verified against a live install.
|
||||
</p>
|
||||
<table id="msg-table"><thead><tr><th>Ext</th><th>Name</th><th>Enabled</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div id="msg-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
</div>
|
||||
<div class="card" id="pstn-not-installed" style="display:none">
|
||||
<h3 style="margin-top:0">PSTN trunk not installed</h3>
|
||||
<p class="muted">No PSTN trunk dialplan was found on this box — <code>sudo ./setup.sh pstn-trunk</code> hasn't been run (or its config was removed). Nothing below is enforced yet; install it first, then this tab will manage the real permission tiers and concurrency caps.</p>
|
||||
<p class="muted">No PSTN trunk dialplan was found on this box — <code>sudo ./setup.sh pstn-trunk</code> hasn't been run (or its config was removed). The calling permissions/caps/personal-numbers below aren't enforced yet; install it first to use them. Internal SIP messaging above works independently of this.</p>
|
||||
</div>
|
||||
<div id="pstn-installed-cards" style="display:none">
|
||||
<div class="card">
|
||||
@@ -1178,9 +1393,25 @@ INDEX_HTML = """<!doctype html>
|
||||
<b>full</b> — internal, plus any US number.
|
||||
Changes apply live, on the next call — no Asterisk restart needed.
|
||||
</p>
|
||||
<table id="pstn-table"><thead><tr><th>Ext</th><th>Name</th><th>Tier</th><th>Approved numbers (restricted only)</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<p class="muted">
|
||||
<b>Messaging</b> — Asterisk's native internal SIP texting (no carrier SMS, no PSTN, no cost), independent of the calling tier. Note: this flag is live-editable here, but whether Asterisk actually delivers/gates messages using it depends on dialplan wiring not yet verified against a live install — see this service's README.
|
||||
</p>
|
||||
<table id="pstn-table"><thead><tr><th>Ext</th><th>Name</th><th>Tier</th><th>Approved numbers (restricted only)</th><th>Messaging</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div id="pstn-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0">Personal numbers</h3>
|
||||
<p class="muted">
|
||||
Multiple DIDs can share this one trunk. Assigning a DID to an extension routes inbound calls to that DID straight to its owner (still gated by the owner's own tier/approved-numbers above — no ring-group fallback), and makes that extension's outbound calls show this DID as Caller-ID instead of the shared trunk DID. The shared DID/ring-group keeps working regardless.
|
||||
</p>
|
||||
<div class="row">
|
||||
<input type="text" id="pd-did" placeholder="DID, e.g. 15551234567" style="width:12rem">
|
||||
<select id="pd-owner"></select>
|
||||
<button class="action" id="pd-save">Assign</button>
|
||||
</div>
|
||||
<table id="pd-table" style="margin-top:0.75rem"><thead><tr><th>DID</th><th>Owner</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div id="pd-msg" class="muted" style="margin-top:0.5rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1193,7 +1424,7 @@ document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
TABS.forEach(t => { document.getElementById("tab-" + t).style.display = btn.dataset.tab === t ? "" : "none"; });
|
||||
if (btn.dataset.tab === "pstn") { loadPstnStatus(); }
|
||||
if (btn.dataset.tab === "pstn") { loadPstnStatus(); loadMessaging(); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1346,7 +1577,7 @@ async function loadPstnStatus() {
|
||||
const data = await res.json();
|
||||
document.getElementById("pstn-not-installed").style.display = data.installed ? "none" : "";
|
||||
document.getElementById("pstn-installed-cards").style.display = data.installed ? "" : "none";
|
||||
if (data.installed) { loadPstnLimits(); loadPstnPermissions(); }
|
||||
if (data.installed) { loadPstnLimits(); loadPstnPermissions(); loadPersonalDids(); }
|
||||
}
|
||||
|
||||
async function loadPstnLimits() {
|
||||
@@ -1356,6 +1587,38 @@ async function loadPstnLimits() {
|
||||
document.getElementById("limit-in").value = data.max_inbound;
|
||||
}
|
||||
|
||||
// Independent of pstn_installed() — messaging has no dependency on a PSTN
|
||||
// trunk existing, unlike everything else in this tab, so this loads/saves
|
||||
// regardless of whether services/pstn-trunk.sh has ever been run.
|
||||
async function loadMessaging() {
|
||||
const res = await fetch("/api/pstn-permissions");
|
||||
const data = await res.json();
|
||||
const exts = data.extensions || [];
|
||||
const tbody = document.querySelector("#msg-table tbody");
|
||||
if (!exts.length) {
|
||||
tbody.innerHTML = '<tr><td colspan=4 class=muted>No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = exts.map(e => `<tr data-ext="${esc(e.ext)}">
|
||||
<td>${esc(e.ext)}</td>
|
||||
<td>${esc(e.name)}</td>
|
||||
<td style="text-align:center"><input type="checkbox" class="msg-enabled" ${e.messaging ? "checked" : ""}></td>
|
||||
<td><button class="action" onclick="saveMessagingRow('${esc(e.ext)}')">Save</button></td>
|
||||
</tr>`).join("");
|
||||
}
|
||||
|
||||
async function saveMessagingRow(ext) {
|
||||
const row = document.querySelector(`#msg-table tr[data-ext="${ext}"]`);
|
||||
const enabled = row.querySelector(".msg-enabled").checked;
|
||||
const res = await fetch("/api/pstn-messaging", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ext: ext, enabled: enabled}),
|
||||
});
|
||||
const data = await res.json();
|
||||
document.getElementById("msg-msg").textContent = (data.message || (data.ok ? "Saved" : "Failed")) + " (extension " + ext + ")";
|
||||
loadMessaging();
|
||||
}
|
||||
|
||||
document.getElementById("limits-save").addEventListener("click", async () => {
|
||||
const maxOut = document.getElementById("limit-out").value;
|
||||
const maxIn = document.getElementById("limit-in").value;
|
||||
@@ -1372,9 +1635,14 @@ async function loadPstnPermissions() {
|
||||
const res = await fetch("/api/pstn-permissions");
|
||||
const data = await res.json();
|
||||
const exts = data.extensions || [];
|
||||
|
||||
const ownerSel = document.getElementById("pd-owner");
|
||||
ownerSel.innerHTML = exts.map(e => `<option value="${esc(e.ext)}">${esc(e.ext)} — ${esc(e.name)}</option>`).join("")
|
||||
|| '<option value="">No extensions found</option>';
|
||||
|
||||
const tbody = document.querySelector("#pstn-table tbody");
|
||||
if (!exts.length) {
|
||||
tbody.innerHTML = '<tr><td colspan=5 class=muted>No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan=6 class=muted>No extensions found (no Asterisk install detected, or pjsip.conf has no devices yet).</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = exts.map(e => `<tr data-ext="${esc(e.ext)}">
|
||||
@@ -1388,6 +1656,7 @@ async function loadPstnPermissions() {
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="pstn-numbers" value="${esc(e.allowed_numbers)}" placeholder="15551234567,15559876543" ${e.tier === "restricted" ? "" : "disabled"}></td>
|
||||
<td style="text-align:center"><input type="checkbox" class="pstn-messaging" ${e.messaging ? "checked" : ""}></td>
|
||||
<td><button class="action" onclick="savePstnPermission('${esc(e.ext)}')">Save</button></td>
|
||||
</tr>`).join("");
|
||||
|
||||
@@ -1402,15 +1671,52 @@ async function savePstnPermission(ext) {
|
||||
const row = document.querySelector(`#pstn-table tr[data-ext="${ext}"]`);
|
||||
const tier = row.querySelector(".pstn-tier").value;
|
||||
const numbers = row.querySelector(".pstn-numbers").value;
|
||||
const messaging = row.querySelector(".pstn-messaging").checked;
|
||||
const res = await fetch("/api/pstn-permissions", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ext: ext, tier: tier, allowed_numbers: numbers}),
|
||||
body: JSON.stringify({ext: ext, tier: tier, allowed_numbers: numbers, messaging: messaging}),
|
||||
});
|
||||
const data = await res.json();
|
||||
document.getElementById("pstn-msg").textContent = (data.message || (data.ok ? "Saved" : "Failed")) + " (extension " + ext + ")";
|
||||
loadPstnPermissions();
|
||||
}
|
||||
|
||||
async function loadPersonalDids() {
|
||||
const res = await fetch("/api/pstn-personal-dids");
|
||||
const data = await res.json();
|
||||
const dids = data.dids || [];
|
||||
const tbody = document.querySelector("#pd-table tbody");
|
||||
tbody.innerHTML = dids.map(d => `<tr>
|
||||
<td>${esc(d.did)}</td>
|
||||
<td>${esc(d.owner)}${d.owner_name ? " — " + esc(d.owner_name) : ""}</td>
|
||||
<td><button class="action" onclick="removePersonalDid('${esc(d.did)}')">Remove</button></td>
|
||||
</tr>`).join("") || "<tr><td colspan=3 class=muted>No personal numbers assigned — every extension shares the main trunk DID.</td></tr>";
|
||||
}
|
||||
|
||||
document.getElementById("pd-save").addEventListener("click", async () => {
|
||||
const did = document.getElementById("pd-did").value.trim();
|
||||
const owner = document.getElementById("pd-owner").value;
|
||||
const res = await fetch("/api/pstn-personal-dids", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({did: did, owner: owner}),
|
||||
});
|
||||
const data = await res.json();
|
||||
document.getElementById("pd-msg").textContent = data.message || (data.ok ? "Saved" : "Failed");
|
||||
if (data.ok) document.getElementById("pd-did").value = "";
|
||||
loadPersonalDids();
|
||||
});
|
||||
|
||||
async function removePersonalDid(did) {
|
||||
if (!confirm("Remove personal number " + did + "? Its owner falls back to the shared trunk DID for outbound Caller-ID, and this DID stops routing anywhere until reassigned.")) return;
|
||||
const res = await fetch("/api/pstn-personal-dids/delete", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({did: did}),
|
||||
});
|
||||
const data = await res.json();
|
||||
document.getElementById("pd-msg").textContent = data.message || (data.ok ? "Removed" : "Failed");
|
||||
loadPersonalDids();
|
||||
}
|
||||
|
||||
const adminUrl = "__ASTERISK_ADMIN_URL__";
|
||||
if (adminUrl) {
|
||||
const link = document.getElementById("admin-link");
|
||||
@@ -1463,12 +1769,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
perms = get_all_permissions()
|
||||
extensions = []
|
||||
for e in list_extensions():
|
||||
p = perms.get(e["ext"], {"tier": "internal", "allowed_numbers": ""})
|
||||
extensions.append({"ext": e["ext"], "name": e["name"],
|
||||
"tier": p["tier"], "allowed_numbers": p["allowed_numbers"]})
|
||||
p = perms.get(e["ext"], {"tier": "internal", "allowed_numbers": "", "messaging": False})
|
||||
extensions.append({"ext": e["ext"], "name": e["name"], "tier": p["tier"],
|
||||
"allowed_numbers": p["allowed_numbers"], "messaging": p["messaging"]})
|
||||
self._json({"extensions": extensions})
|
||||
elif self.path == "/api/pstn-limits":
|
||||
self._json(get_limits())
|
||||
elif self.path == "/api/pstn-personal-dids":
|
||||
names = {e["ext"]: e["name"] for e in list_extensions()}
|
||||
dids = [dict(d, owner_name=names.get(d["owner"], "")) for d in list_personal_dids()]
|
||||
self._json({"dids": dids})
|
||||
elif self.path == "/api/pstn-status":
|
||||
self._json({"installed": pstn_installed()})
|
||||
else:
|
||||
@@ -1492,12 +1802,22 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._json(ban_asn(payload.get("asn", "")))
|
||||
elif self.path == "/api/pstn-permissions":
|
||||
ok, message = write_permission(
|
||||
payload.get("ext", ""), payload.get("tier", ""), payload.get("allowed_numbers", "")
|
||||
payload.get("ext", ""), payload.get("tier", ""), payload.get("allowed_numbers", ""),
|
||||
bool(payload.get("messaging", False))
|
||||
)
|
||||
self._json({"ok": ok, "message": message})
|
||||
elif self.path == "/api/pstn-limits":
|
||||
ok, message = write_limits(payload.get("max_outbound", ""), payload.get("max_inbound", ""))
|
||||
self._json({"ok": ok, "message": message})
|
||||
elif self.path == "/api/pstn-personal-dids":
|
||||
ok, message = write_personal_did(payload.get("did", ""), payload.get("owner", ""))
|
||||
self._json({"ok": ok, "message": message})
|
||||
elif self.path == "/api/pstn-personal-dids/delete":
|
||||
ok, message = remove_personal_did(payload.get("did", ""))
|
||||
self._json({"ok": ok, "message": message})
|
||||
elif self.path == "/api/pstn-messaging":
|
||||
ok, message = write_messaging(payload.get("ext", ""), bool(payload.get("enabled", False)))
|
||||
self._json({"ok": ok, "message": message})
|
||||
else:
|
||||
self._json({"error": "not found"}, 404)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user