mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-13 08:15:21 +02:00
feat: add 3CX telephony provider with Asterisk ARA provisioning
Registers a new `three_cx` provider that fronts a 3CX cloud PBX through an intermediate Asterisk bridge. Save-time hook writes the matching PJSIP endpoint/aor/auth/registration and dialplan rows to the Asterisk Realtime Architecture Postgres (via `ASTERISK_ARA_DSN`), so a config change in the Dograh UI is immediately picked up by Asterisk without a `pjsip reload`. Strip prefix is honoured at the dialplan layer. Inbound calls are matched back to a configuration by the dialled extension (`account_id_credential_field="extension"`), allowing one shared Asterisk to serve multiple Dograh orgs without collision. Touches `providers/__init__.py` and `schemas/telephony_config.py` only — per `providers/AGENTS.md`. Provider/transport/strategies are duplicated from `ari/` rather than imported, in line with the cross-provider-import prohibition. See `docs/providers/three_cx.md` for the Asterisk ARA setup runbook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3df5730076
commit
533a873ab7
13 changed files with 1916 additions and 0 deletions
203
docs/providers/three_cx.md
Normal file
203
docs/providers/three_cx.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# 3CX Telephony Provider
|
||||
|
||||
Connect a Dograh AI agent to a **3CX cloud PBX** through an intermediate
|
||||
**Asterisk** bridge. The Asterisk box terminates the SIP/RTP leg toward
|
||||
3CX and exposes a standard ARI + externalMedia surface to Dograh —
|
||||
identical to the [Asterisk ARI provider](../integrations/telephony/asterisk-ari)
|
||||
plus an automated trunk-provisioning step.
|
||||
|
||||
```
|
||||
+-------------------+ SIP/RTP +-------------+ ARI REST + +---------+
|
||||
| 3CX cloud SBC | <------------> | Asterisk | WS audio | -> | Dograh |
|
||||
| 1156.3cx.cloud | | (PJSIP ARA)| | | agent |
|
||||
+-------------------+ +-------------+ + +---------+
|
||||
^
|
||||
| ps_endpoints, ps_aors,
|
||||
| ps_auths, ps_registrations,
|
||||
| extensions (Postgres)
|
||||
|
|
||||
+----+----+
|
||||
| Dograh |
|
||||
| save UI |
|
||||
+---------+
|
||||
```
|
||||
|
||||
When an admin saves a 3CX TelephonyConfiguration in the Dograh UI, the
|
||||
provider's `preprocess_credentials_on_save` hook writes the matching
|
||||
PJSIP endpoint/aor/auth/registration rows and the `+39`-stripping
|
||||
dialplan into the Asterisk Realtime Architecture (ARA) Postgres. Asterisk
|
||||
picks them up dynamically — no `pjsip reload` needed.
|
||||
|
||||
## §1 — Asterisk side prerequisites
|
||||
|
||||
The bridging Asterisk must be ≥ Asterisk 18 with PJSIP and ARA enabled
|
||||
against the same Postgres Dograh writes to. One Asterisk instance can
|
||||
serve many Dograh 3CX configurations (multi-tenant) because each trunk
|
||||
gets a unique endpoint id of the form `dograh_<slug(sip_domain)>_<extension>`.
|
||||
|
||||
### 1.1 Postgres tables
|
||||
|
||||
Run the standard Asterisk realtime DDL on the ARA database — the
|
||||
relevant tables are `ps_auths`, `ps_aors`, `ps_endpoints`,
|
||||
`ps_registrations`, `ps_transports`, `ps_globals`, and `extensions`.
|
||||
The canonical schema ships with Asterisk under `contrib/realtime/postgresql/`.
|
||||
|
||||
### 1.2 `res_config_pgsql.conf`
|
||||
|
||||
```ini
|
||||
[asterisk]
|
||||
type = pgsql
|
||||
hostname = postgres.internal
|
||||
dbname = asterisk_ara
|
||||
user = asterisk_ro
|
||||
password = ********
|
||||
port = 5432
|
||||
requirements = warn
|
||||
```
|
||||
|
||||
### 1.3 `sorcery.conf`
|
||||
|
||||
```ini
|
||||
[res_pjsip]
|
||||
endpoint = realtime,ps_endpoints
|
||||
auth = realtime,ps_auths
|
||||
aor = realtime,ps_aors
|
||||
domain_alias = realtime,ps_domain_aliases
|
||||
contact = realtime,ps_contacts
|
||||
|
||||
[res_pjsip_endpoint_identifier_ip]
|
||||
identify = realtime,ps_endpoint_id_ips
|
||||
|
||||
[res_pjsip_outbound_registration]
|
||||
registration = realtime,ps_registrations
|
||||
```
|
||||
|
||||
### 1.4 `extconfig.conf`
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
ps_endpoints = pgsql,asterisk
|
||||
ps_auths = pgsql,asterisk
|
||||
ps_aors = pgsql,asterisk
|
||||
ps_registrations = pgsql,asterisk
|
||||
extensions = pgsql,asterisk
|
||||
```
|
||||
|
||||
### 1.5 Static PJSIP transport
|
||||
|
||||
Dograh writes endpoints that reference a transport by name (default:
|
||||
`transport-udp`). Define it once in `pjsip.conf`:
|
||||
|
||||
```ini
|
||||
[transport-udp]
|
||||
type = transport
|
||||
protocol = udp
|
||||
bind = 0.0.0.0:5060
|
||||
```
|
||||
|
||||
### 1.6 Stasis app + externalMedia
|
||||
|
||||
```ini
|
||||
; ari.conf
|
||||
[general]
|
||||
enabled = yes
|
||||
[dograh]
|
||||
type = user
|
||||
read_only = no
|
||||
password = <ARI password to paste in the Dograh UI>
|
||||
|
||||
; websocket_client.conf
|
||||
[dograh_staging]
|
||||
type = websocket_client
|
||||
uri = ws://dograh-backend:8000/api/v1/telephony/ws/ari
|
||||
protocols = media
|
||||
connection_type = persistent
|
||||
```
|
||||
|
||||
Start the Stasis app and confirm registration is happening:
|
||||
|
||||
```bash
|
||||
asterisk -rx "module reload res_pjsip.so"
|
||||
asterisk -rx "pjsip show registrations"
|
||||
```
|
||||
|
||||
## §2 — Dograh side prerequisites
|
||||
|
||||
Set the connection string to the ARA Postgres in `api/.env`:
|
||||
|
||||
```bash
|
||||
ASTERISK_ARA_DSN=postgresql://dograh_rw:********@postgres.internal:5432/asterisk_ara
|
||||
```
|
||||
|
||||
The user needs `SELECT, INSERT, UPDATE, DELETE` on `ps_auths`,
|
||||
`ps_aors`, `ps_endpoints`, `ps_registrations`, and `extensions`. No DDL
|
||||
permissions required at runtime.
|
||||
|
||||
Restart the Dograh API process after setting the env var.
|
||||
|
||||
## §3 — Per-trunk flow in the Dograh UI
|
||||
|
||||
For each 3CX tenant + extension Dograh should serve:
|
||||
|
||||
1. Open *Telephony Configurations* → *Add* → select **3CX (Asterisk bridge)**.
|
||||
2. Fill in the form:
|
||||
|
||||
| Field | Value (example) |
|
||||
| --- | --- |
|
||||
| ARI Endpoint | `http://asterisk.internal:8088` |
|
||||
| Stasis App Name | `dograh` |
|
||||
| ARI Password | _(matches `ari.conf` `[dograh]` password)_ |
|
||||
| websocket_client.conf Name | `dograh_staging` |
|
||||
| 3CX SIP Domain | `1156.3cx.cloud` |
|
||||
| 3CX Extension | `12611` |
|
||||
| SIP Password | _(from `~/.claude-phone/.env` or 3CX admin console)_ |
|
||||
| Strip Prefix (regex) | `^\+39` |
|
||||
| From Numbers | `+393331112222` |
|
||||
|
||||
3. Save. On save the `preprocess_credentials_on_save` hook writes the
|
||||
six-table ARA set in a single transaction. A failure aborts the save
|
||||
with `HTTP 502` and a message describing which write failed; nothing
|
||||
persists.
|
||||
|
||||
## §4 — Verification
|
||||
|
||||
Confirm the trunk landed in ARA:
|
||||
|
||||
```bash
|
||||
psql $ASTERISK_ARA_DSN -c \
|
||||
"SELECT id FROM ps_endpoints WHERE id LIKE 'dograh\\_%'"
|
||||
psql $ASTERISK_ARA_DSN -c \
|
||||
"SELECT id, server_uri FROM ps_registrations WHERE id LIKE 'dograh\\_%'"
|
||||
```
|
||||
|
||||
Confirm Asterisk has registered upstream with 3CX:
|
||||
|
||||
```bash
|
||||
asterisk -rx "pjsip show registrations"
|
||||
# Expect: <id> <server> Registered
|
||||
```
|
||||
|
||||
Originate a test outbound call from Dograh and verify the `+39` prefix
|
||||
was stripped on the way out:
|
||||
|
||||
```bash
|
||||
asterisk -rx "core set verbose 4"
|
||||
# In another terminal: trigger an outbound from the Dograh API.
|
||||
# In the Asterisk console you should see:
|
||||
# Dial: PJSIP/3331112222@dograh_1156_3cx_cloud_12611
|
||||
# i.e. without '+39'.
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
* The hook **only** supports the literal `^\+<digits>` regex form for
|
||||
`strip_prefix`. PCRE alternation isn't translated to Asterisk's
|
||||
ad-hoc extension pattern syntax. Adding a `[02-9]` or branching
|
||||
regex needs an extension to `dialplan._prefix_to_pattern`.
|
||||
* Deprovisioning on TelephonyConfiguration deletion is not currently
|
||||
wired. `provisioning._deprovision_3cx_trunk` exists as a callable but
|
||||
no registry hook fires it; admin tooling can call it directly. Filed
|
||||
for follow-up rather than in scope for the initial provider.
|
||||
* `transport_name` is hard-coded to `transport-udp` (overridable per
|
||||
credentials dict). TLS or TCP trunks toward 3CX need the admin to
|
||||
define the transport and pass the name through.
|
||||
Loading…
Add table
Add a link
Reference in a new issue