feat(agent-call): add contract-driven mock executor
This commit is contained in:
+47
-12
@@ -49,6 +49,21 @@ def endpoint(name, data, env):
|
||||
if not isinstance(registration, bool) or registration and mode != "digest":
|
||||
raise ValueError("registration requires explicit digest authentication")
|
||||
text = f"[{name}]\ntype=endpoint\ntransport=transport-udp\ncontext=deny-inbound\ndisallow=all\nallow=ulaw\ndirect_media=no\nrtp_symmetric=yes\nforce_rport=yes\nrewrite_contact=yes\naors={name}-aor\n"
|
||||
from_user = data.get("from_user")
|
||||
if from_user is not None:
|
||||
if not isinstance(from_user, str) or not re.fullmatch(
|
||||
r"[A-Za-z0-9_.+\-]+", from_user
|
||||
):
|
||||
raise ValueError(f"{name}.from_user must be a plain caller identifier")
|
||||
text += f"from_user={from_user}\n"
|
||||
display_identity = data.get("caller_id")
|
||||
if display_identity is not None:
|
||||
display_identity = scalar(display_identity, name + ".caller_id")
|
||||
if not re.fullmatch(
|
||||
r"[A-Za-z0-9_.+\- ]+(?:<[A-Za-z0-9_.+\- ]+>)?", display_identity
|
||||
):
|
||||
raise ValueError(f"{name}.caller_id contains unsupported characters")
|
||||
text += f"callerid={display_identity}\n"
|
||||
auth = ""
|
||||
if mode == "digest":
|
||||
user = scalar(data.get("username"), name + ".username")
|
||||
@@ -56,7 +71,9 @@ def endpoint(name, data, env):
|
||||
raise ValueError("SIP username must be a plain user identifier")
|
||||
password_key = "SIP_" + name.removeprefix("provider-").upper() + "_PASSWORD"
|
||||
password = scalar(env.get(password_key), password_key, secret=True)
|
||||
text += f"outbound_auth={name}-auth\nfrom_user={user}\n"
|
||||
text += f"outbound_auth={name}-auth\n"
|
||||
if from_user is None:
|
||||
text += f"from_user={user}\n"
|
||||
auth = f"\n[{name}-auth]\ntype=auth\nauth_type=userpass\nusername={user}\npassword={password}\n"
|
||||
if registration:
|
||||
auth += f"\n[{name}-registration]\ntype=registration\ntransport=transport-udp\noutbound_auth={name}-auth\nserver_uri=sip:{host}:{port}\nclient_uri=sip:{user}@{host}:{port}\nretry_interval=60\n"
|
||||
@@ -89,22 +106,40 @@ def render(cfg, env):
|
||||
password = scalar(env.get("ARI_PASSWORD"), "ARI_PASSWORD", secret=True)
|
||||
if len(password) < 32:
|
||||
raise ValueError("ARI_PASSWORD must have at least 32 characters")
|
||||
primary = endpoint("provider-primary", cfg.get("primary", {}), env)
|
||||
backup = endpoint("provider-backup", cfg.get("backup", {}), env)
|
||||
if (cfg["primary"]["host"], cfg["primary"].get("port", 5060)) == (
|
||||
cfg["backup"]["host"],
|
||||
cfg["backup"].get("port", 5060),
|
||||
):
|
||||
raise ValueError(
|
||||
"primary and backup targets must be distinct; shared failure domains still require validation"
|
||||
)
|
||||
primary_data = cfg.get("primary", {})
|
||||
primary = endpoint("provider-primary", primary_data, env)
|
||||
backup_data = cfg.get("backup")
|
||||
if backup_data is not None and not isinstance(backup_data, dict):
|
||||
raise ValueError("backup must be an object when configured")
|
||||
sections = [primary]
|
||||
if backup_data:
|
||||
backup = endpoint("provider-backup", backup_data, env)
|
||||
if (primary_data["host"], primary_data.get("port", 5060)) == (
|
||||
backup_data["host"],
|
||||
backup_data.get("port", 5060),
|
||||
):
|
||||
raise ValueError(
|
||||
"primary and backup targets must be distinct; shared failure domains still require validation"
|
||||
)
|
||||
sections.append(backup)
|
||||
transport = f"[global]\ntype=global\nuser_agent=agent-call\n\n[transport-udp]\ntype=transport\nprotocol=udp\nbind=0.0.0.0:5060\nlocal_net={network}\nexternal_signaling_address={PUBLIC_IP}\nexternal_media_address={PUBLIC_IP}\n\n"
|
||||
caller = primary_data.get("caller_id") or primary_data.get("from_user")
|
||||
extensions = "[deny-inbound]\nexten => s,1,Hangup()\nexten => _.,1,Hangup()\n"
|
||||
if caller:
|
||||
caller = scalar(caller, "primary.caller_id")
|
||||
extensions += (
|
||||
"\n[outbound]\n"
|
||||
"exten => _X.,1,NoOp(agent-call outbound)\n"
|
||||
f" same => n,Set(CALLERID(all)={caller})\n"
|
||||
" same => n,Dial(PJSIP/${EXTEN}@provider-primary,60)\n"
|
||||
" same => n,Hangup()\n"
|
||||
)
|
||||
return {
|
||||
"http.conf": f"[general]\nenabled=yes\nbindaddr={bind}\nbindport=8088\n",
|
||||
"ari.conf": f"[general]\nenabled=yes\npretty=no\n\n[outbound]\ntype=user\nread_only=no\npassword={password}\n",
|
||||
"pjsip.conf": transport + primary + "\n" + backup,
|
||||
"pjsip.conf": transport + "\n".join(sections),
|
||||
"rtp.conf": "[general]\nrtpstart=10000\nrtpend=10800\nstrictrtp=yes\n",
|
||||
"extensions.conf": "[deny-inbound]\nexten => s,1,Hangup()\nexten => _.,1,Hangup()\n",
|
||||
"extensions.conf": extensions,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user