mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-25 08:48:13 +02:00
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>
69 lines
2 KiB
Python
69 lines
2 KiB
Python
"""Async connection pool to the Asterisk Realtime Architecture Postgres.
|
|
|
|
Lives separate from Dograh's primary SQLAlchemy engine because the ARA
|
|
Postgres is operationally distinct (Asterisk-owned schema, typically a
|
|
different host, different credentials). DSN comes from the
|
|
``ASTERISK_ARA_DSN`` environment variable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Optional
|
|
|
|
import asyncpg
|
|
from loguru import logger
|
|
|
|
_POOL: Optional[asyncpg.Pool] = None
|
|
_DSN_ENV = "ASTERISK_ARA_DSN"
|
|
|
|
|
|
class AraNotConfiguredError(RuntimeError):
|
|
"""Raised when ASTERISK_ARA_DSN is missing.
|
|
|
|
The 3CX provider can't provision its trunk without an ARA Postgres to
|
|
write to — callers translate this into a user-visible HTTP 400 with a
|
|
pointer to docs/providers/three_cx.md.
|
|
"""
|
|
|
|
|
|
async def get_pool() -> asyncpg.Pool:
|
|
"""Return the lazily-initialised ARA pool. Idempotent across awaits."""
|
|
global _POOL
|
|
if _POOL is not None:
|
|
return _POOL
|
|
|
|
dsn = os.getenv(_DSN_ENV)
|
|
if not dsn:
|
|
raise AraNotConfiguredError(
|
|
f"{_DSN_ENV} not set — 3CX provider needs an Asterisk Realtime "
|
|
f"Postgres DSN to provision the PJSIP trunk. See "
|
|
f"docs/providers/three_cx.md for setup."
|
|
)
|
|
|
|
logger.info(f"[3CX/ARA] opening asyncpg pool to {_dsn_for_log(dsn)}")
|
|
_POOL = await asyncpg.create_pool(
|
|
dsn=dsn,
|
|
min_size=1,
|
|
max_size=4,
|
|
command_timeout=10,
|
|
)
|
|
return _POOL
|
|
|
|
|
|
async def close_pool() -> None:
|
|
"""Close the pool — exposed for test teardown and graceful shutdown."""
|
|
global _POOL
|
|
if _POOL is not None:
|
|
await _POOL.close()
|
|
_POOL = None
|
|
|
|
|
|
def _dsn_for_log(dsn: str) -> str:
|
|
"""Strip the password from a DSN before logging it."""
|
|
if "@" not in dsn or "://" not in dsn:
|
|
return "<dsn>"
|
|
scheme, rest = dsn.split("://", 1)
|
|
creds, host = rest.split("@", 1)
|
|
user = creds.split(":", 1)[0] if ":" in creds else creds
|
|
return f"{scheme}://{user}:***@{host}"
|