5d06d4d3ce
Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""Бизнес-логика AMI: канал по типу линии, originate, ping."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING
|
|
|
|
from core.models import ConnectorConfig
|
|
|
|
from .ami import AmiClient, AmiError, DEFAULT_PORT
|
|
|
|
if TYPE_CHECKING:
|
|
from apps.employees.models import Employee
|
|
|
|
EXTEN_RE = re.compile(r'^\d{2,10}$')
|
|
CONNECTOR = 'asterisk'
|
|
|
|
DEFAULT_SIP_CHANNEL = 'PJSIP/{exten}'
|
|
DEFAULT_ANALOG_CHANNEL = 'DAHDI/{exten}'
|
|
DEFAULT_CONTEXT = 'from-internal'
|
|
|
|
|
|
class TelephonyError(Exception):
|
|
"""Ошибка звонка с понятным текстом для пользователя."""
|
|
|
|
|
|
def get_ami_config() -> ConnectorConfig | None:
|
|
return ConnectorConfig.get_for(CONNECTOR)
|
|
|
|
|
|
def ami_is_enabled() -> bool:
|
|
return get_ami_config() is not None
|
|
|
|
|
|
def sanitize_exten(raw: str) -> str:
|
|
digits = re.sub(r'\D', '', raw or '')
|
|
if not EXTEN_RE.fullmatch(digits):
|
|
raise TelephonyError('Некорректный внутренний номер.')
|
|
return digits
|
|
|
|
|
|
def channel_for_employee(employee, extra: dict | None = None) -> str:
|
|
extra = extra or {}
|
|
exten = sanitize_exten(employee.internal_number)
|
|
line = getattr(employee, 'internal_line_type', '') or 'analog'
|
|
if line == 'sip':
|
|
template = (extra.get('sip_channel') or DEFAULT_SIP_CHANNEL)
|
|
else:
|
|
template = (extra.get('analog_channel') or DEFAULT_ANALOG_CHANNEL)
|
|
return str(template).replace('{exten}', exten)
|
|
|
|
|
|
def _client_from_config(cfg: ConnectorConfig) -> AmiClient:
|
|
extra = cfg.extra_config or {}
|
|
timeout = extra.get('timeout', 8)
|
|
try:
|
|
timeout = float(timeout)
|
|
except (TypeError, ValueError):
|
|
timeout = 8
|
|
return AmiClient(
|
|
host=cfg.host,
|
|
port=cfg.port or DEFAULT_PORT,
|
|
username=cfg.username,
|
|
secret=cfg.password,
|
|
timeout=timeout,
|
|
use_ssl=bool(cfg.use_ssl),
|
|
)
|
|
|
|
|
|
def ping_ami(cfg: ConnectorConfig | None = None) -> dict:
|
|
cfg = cfg or get_ami_config()
|
|
if cfg is None:
|
|
raise TelephonyError('Коннектор Asterisk AMI выключен.')
|
|
if not cfg.host or not cfg.username or not cfg.password:
|
|
raise TelephonyError('В AMI не заданы хост, логин или секрет.')
|
|
with _client_from_config(cfg) as client:
|
|
return client.ping()
|
|
|
|
|
|
def originate_between(caller: Employee, callee: Employee) -> dict:
|
|
if caller.pk == callee.pk:
|
|
raise TelephonyError('Нельзя позвонить самому себе.')
|
|
if not caller.internal_number:
|
|
raise TelephonyError('У вас не указан внутренний номер.')
|
|
if not callee.internal_number:
|
|
raise TelephonyError('У сотрудника нет внутреннего номера.')
|
|
|
|
cfg = get_ami_config()
|
|
if cfg is None:
|
|
raise TelephonyError('Коннектор Asterisk AMI выключен.')
|
|
|
|
extra = cfg.extra_config or {}
|
|
context = extra.get('originate_context') or DEFAULT_CONTEXT
|
|
channel = channel_for_employee(caller, extra)
|
|
dest = sanitize_exten(callee.internal_number)
|
|
caller_id = f'{caller.short_name} <{sanitize_exten(caller.internal_number)}>'
|
|
|
|
try:
|
|
with _client_from_config(cfg) as client:
|
|
return client.originate(
|
|
channel=channel,
|
|
exten=dest,
|
|
context=context,
|
|
caller_id=caller_id,
|
|
)
|
|
except AmiError as exc:
|
|
raise TelephonyError(str(exc)) from exc
|