diff --git a/deployment/asterisk/README.md b/deployment/asterisk/README.md index df84723..aa84709 100644 --- a/deployment/asterisk/README.md +++ b/deployment/asterisk/README.md @@ -70,5 +70,11 @@ Default lab SIP accounts from `pjsip.conf`: - `2001` (operator A) - `2002` (operator B) +Telecom.kz note: +- `87273901485 / 86205414@sip.telecom.kz` should be configured as a normal SIP account with `type=registration`. +- Do not treat this number as an IP trunk unless Telecom provisions a separate IP-trunk product for it. +- Inbound calls from this account are expected in `from-telecom` and should route to `7100`. +- Keep the real Telecom password out of git and set it on the target host before reload. + Track 14 note: - `2001` and `2002` are allowed to keep multiple contacts so browser registration can coexist with `MicroSIP/Zoiper` in QA. diff --git a/deployment/asterisk/extensions.conf b/deployment/asterisk/extensions.conf index e575a50..0b194ae 100644 --- a/deployment/asterisk/extensions.conf +++ b/deployment/asterisk/extensions.conf @@ -117,3 +117,16 @@ exten => h,1,StopMixMonitor() [mvpcc-operators] exten => _X.,1,Dial(PJSIP/${EXTEN},30) same => n,Hangup() + +[from-telecom] +; Incoming calls from Telecom.kz SIP account 86205414 / DID 87273901485. +; Route them directly to the AI-first entrypoint 7100. +exten => 86205414,1,NoOp(Incoming from telecom.kz DID 87273901485 -> AI) + same => n,Set(CALLERID(dnid)=87273901485) + same => n,Goto(from-softphones,7100,1) + +exten => 87273901485,1,NoOp(Incoming from telecom.kz DID 87273901485 -> AI) + same => n,Goto(from-softphones,7100,1) + +exten => _X.,1,NoOp(Incoming from telecom.kz, DID=${EXTEN}) + same => n,Goto(from-softphones,7100,1) diff --git a/deployment/asterisk/pjsip.conf b/deployment/asterisk/pjsip.conf index 6890720..4e415fc 100644 --- a/deployment/asterisk/pjsip.conf +++ b/deployment/asterisk/pjsip.conf @@ -82,3 +82,55 @@ type=aor max_contacts=1 remove_existing=yes support_path=yes + +;======================================================================== +; Telecom.kz SIP account (REGISTER auth, not IP trunk) +; DID: 87273901485 +; SIP account: 86205414@sip.telecom.kz +;======================================================================== +[telecom-kz-aor] +type=aor +max_contacts=1 +remove_existing=yes +qualify_frequency=60 +support_path=yes + +[telecom-kz-auth] +type=auth +auth_type=userpass +username=86205414 +password=CHANGE_ME_TELECOM_KZ_PASSWORD + +[telecom-kz] +type=endpoint +transport=transport-udp +context=from-telecom +disallow=all +allow=alaw,ulaw +aors=telecom-kz-aor +outbound_auth=telecom-kz-auth +from_user=86205414 +from_domain=sip.telecom.kz +direct_media=no +rtp_symmetric=yes +force_rport=yes +rewrite_contact=yes +trust_id_inbound=yes +send_rpid=yes +identify_by=username +language=ru + +[telecom-kz-registration] +type=registration +transport=transport-udp +outbound_auth=telecom-kz-auth +server_uri=sip:sip.telecom.kz:5060 +client_uri=sip:86205414@sip.telecom.kz +contact_user=86205414 +retry_interval=60 +forbidden_retry_interval=300 +expiration=300 +line=yes +endpoint=telecom-kz +support_path=yes +auth_rejection_permanent=no diff --git a/services/asterisk_bridge_service/bridge_processing.py b/services/asterisk_bridge_service/bridge_processing.py index 1fc7229..d380a4d 100644 --- a/services/asterisk_bridge_service/bridge_processing.py +++ b/services/asterisk_bridge_service/bridge_processing.py @@ -497,7 +497,7 @@ def upload_recording( url=f"{bridge._recording_service_url()}/recordings/import-upload", data=data, files={"file": (file_name, handle, mime_type or "application/octet-stream")}, - retry_reset=handle.seek(0), + retry_reset=lambda: handle.seek(0), ) return response.json() except httpx.HTTPStatusError as exc: diff --git a/tests/test_asterisk_bridge_service.py b/tests/test_asterisk_bridge_service.py index 802fbd9..d846b38 100644 --- a/tests/test_asterisk_bridge_service.py +++ b/tests/test_asterisk_bridge_service.py @@ -205,6 +205,74 @@ def test_post_json_bearer_first_falls_back_to_legacy(monkeypatch): assert DummyClient.seen_headers[1]["X-Role"] == "admin" +def test_upload_recording_bearer_first_fallback_rewinds_file(monkeypatch, tmp_path): + recording_path = tmp_path / "call.wav" + recording_bytes = b"recording-payload" + recording_path.write_bytes(recording_bytes) + + class DummyClient: + def __init__(self, **kwargs): + self.kwargs = kwargs + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + class DummyResponse: + def raise_for_status(self): + return None + + def json(self): + return {"recording_id": "rec_uploaded"} + + attempts: list[dict[str, object]] = [] + + def _fake_request_with_bridge_auth(client, *, retry_reset=None, headers=None, files=None, **kwargs): + del client, headers, kwargs + handle = files["file"][1] + attempts.append( + { + "chunk": handle.read(), + "has_retry_reset": callable(retry_reset), + } + ) + assert callable(retry_reset) + retry_reset() + attempts.append( + { + "chunk": handle.read(), + "has_retry_reset": callable(retry_reset), + } + ) + return DummyResponse() + + monkeypatch.setenv("ASTERISK_BRIDGE_AUTH_MODE", "bearer_first") + monkeypatch.setenv("ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY", "1") + monkeypatch.setenv("ASTERISK_BRIDGE_AUTH_USER", "ast-bridge") + monkeypatch.setenv("ASTERISK_BRIDGE_AUTH_ROLE", "admin") + monkeypatch.setenv("APP_TOKEN_SECRET", "track9-test-secret") + monkeypatch.setattr(bridge_module.httpx, "Client", DummyClient) + monkeypatch.setattr(bridge_module, "_request_with_bridge_auth", _fake_request_with_bridge_auth) + + result = bridge_module._upload_recording( + local_path=recording_path, + call_id="call_upload_fallback", + interaction_id="int_upload_fallback", + source_event_id="src_upload_fallback", + file_name="call.wav", + mime_type="audio/wav", + duration_seconds=3, + ) + + assert result == {"recording_id": "rec_uploaded"} + assert attempts == [ + {"chunk": recording_bytes, "has_retry_reset": True}, + {"chunk": recording_bytes, "has_retry_reset": True}, + ] + + def test_assign_interaction_uses_patch(monkeypatch): class DummyResponse: status_code = 200