"""Бизнес-логика 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