pinecall-runtime 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pinecall/__init__.py +0 -0
- pinecall/channels/__init__.py +0 -0
- pinecall/channels/_chooser.py +45 -0
- pinecall/channels/offers.py +219 -0
- pinecall/channels/rooms.py +176 -0
- pinecall/channels/routes.py +275 -0
- pinecall/channels/telephony/__init__.py +0 -0
- pinecall/channels/telephony/_twilio.py +349 -0
- pinecall/channels/telephony/carrier.py +229 -0
- pinecall/channels/telephony/carrier_catalog.py +92 -0
- pinecall/channels/telephony/carriers.csv +41 -0
- pinecall/channels/telephony/dialing.py +419 -0
- pinecall/channels/telephony/firewall.py +58 -0
- pinecall/channels/telephony/hand_over.py +78 -0
- pinecall/channels/telephony/number_path.py +176 -0
- pinecall/channels/telephony/numbers.py +530 -0
- pinecall/channels/telephony/origination.py +77 -0
- pinecall/channels/telephony/sip.py +448 -0
- pinecall/channels/whatsapp.py +413 -0
- pinecall/cli/__init__.py +0 -0
- pinecall/cli/_archive.py +85 -0
- pinecall/cli/_drift.py +38 -0
- pinecall/cli/_facts.py +67 -0
- pinecall/cli/_load.py +433 -0
- pinecall/cli/_operator.py +626 -0
- pinecall/cli/_sessions.py +164 -0
- pinecall/cli/_traceback.py +95 -0
- pinecall/cli/_usage.py +34 -0
- pinecall/cli/main.py +521 -0
- pinecall/domain/__init__.py +0 -0
- pinecall/domain/agent.py +446 -0
- pinecall/domain/call.py +152 -0
- pinecall/domain/country_codes.py +32 -0
- pinecall/domain/errors.py +151 -0
- pinecall/domain/names.py +115 -0
- pinecall/domain/org.py +120 -0
- pinecall/domain/person.py +208 -0
- pinecall/domain/scope.py +21 -0
- pinecall/evals/__init__.py +0 -0
- pinecall/evals/_evidence.py +158 -0
- pinecall/evals/calibration.py +112 -0
- pinecall/evals/callers.py +160 -0
- pinecall/evals/case.py +327 -0
- pinecall/evals/checks.py +437 -0
- pinecall/evals/compliance.py +197 -0
- pinecall/evals/dataset.py +451 -0
- pinecall/evals/goldens.py +331 -0
- pinecall/evals/judges.py +525 -0
- pinecall/evals/runs.py +252 -0
- pinecall/evals/spoken.py +356 -0
- pinecall/fleet/__init__.py +0 -0
- pinecall/fleet/client.py +569 -0
- pinecall/fleet/demand.py +51 -0
- pinecall/fleet/heartbeat.py +194 -0
- pinecall/fleet/measures.py +157 -0
- pinecall/fleet/roster.py +232 -0
- pinecall/fleet/worlds.py +44 -0
- pinecall/gateway/__init__.py +0 -0
- pinecall/gateway/_call_setup.py +53 -0
- pinecall/gateway/_deps.py +556 -0
- pinecall/gateway/_gateway.py +75 -0
- pinecall/gateway/_served.py +396 -0
- pinecall/gateway/_sockets.py +444 -0
- pinecall/gateway/_streams.py +83 -0
- pinecall/gateway/_text_calls.py +182 -0
- pinecall/gateway/api/__init__.py +0 -0
- pinecall/gateway/api/accounts.py +324 -0
- pinecall/gateway/api/agents.py +150 -0
- pinecall/gateway/api/apps.py +478 -0
- pinecall/gateway/api/box.py +658 -0
- pinecall/gateway/api/calibration.py +58 -0
- pinecall/gateway/api/callbacks.py +105 -0
- pinecall/gateway/api/calls.py +655 -0
- pinecall/gateway/api/chat.py +223 -0
- pinecall/gateway/api/dataset.py +163 -0
- pinecall/gateway/api/desk.py +113 -0
- pinecall/gateway/api/entries.py +77 -0
- pinecall/gateway/api/evals.py +647 -0
- pinecall/gateway/api/fleet.py +73 -0
- pinecall/gateway/api/hosting.py +241 -0
- pinecall/gateway/api/judges.py +97 -0
- pinecall/gateway/api/keys.py +110 -0
- pinecall/gateway/api/line.py +142 -0
- pinecall/gateway/api/members.py +197 -0
- pinecall/gateway/api/metrics.py +226 -0
- pinecall/gateway/api/numbers.py +476 -0
- pinecall/gateway/api/ops.py +428 -0
- pinecall/gateway/api/org.py +283 -0
- pinecall/gateway/api/personas.py +157 -0
- pinecall/gateway/api/pipeline.py +206 -0
- pinecall/gateway/api/prompts.py +50 -0
- pinecall/gateway/api/providers.py +240 -0
- pinecall/gateway/api/recordings.py +89 -0
- pinecall/gateway/api/relay.py +169 -0
- pinecall/gateway/api/retrieval.py +403 -0
- pinecall/gateway/api/runner.py +109 -0
- pinecall/gateway/api/sessions.py +102 -0
- pinecall/gateway/api/settings.py +463 -0
- pinecall/gateway/api/signup.py +164 -0
- pinecall/gateway/api/sso_login.py +168 -0
- pinecall/gateway/api/threads.py +187 -0
- pinecall/gateway/api/usage.py +303 -0
- pinecall/gateway/api/visitors.py +222 -0
- pinecall/gateway/api/whatsapp.py +48 -0
- pinecall/gateway/api/widget.py +49 -0
- pinecall/gateway/app.py +504 -0
- pinecall/gateway/calls/__init__.py +0 -0
- pinecall/gateway/calls/binding.py +63 -0
- pinecall/gateway/calls/commands.py +18 -0
- pinecall/gateway/calls/inbox.py +100 -0
- pinecall/gateway/calls/known.py +23 -0
- pinecall/gateway/calls/owners.py +87 -0
- pinecall/gateway/calls/pump.py +33 -0
- pinecall/gateway/calls/serving.py +118 -0
- pinecall/gateway/calls/threads.py +367 -0
- pinecall/gateway/dispatching/__init__.py +0 -0
- pinecall/gateway/dispatching/arrivals.py +58 -0
- pinecall/gateway/dispatching/sweep.py +43 -0
- pinecall/gateway/ending/__init__.py +0 -0
- pinecall/gateway/ending/reaper.py +160 -0
- pinecall/gateway/ending/seal.py +312 -0
- pinecall/gateway/ending/stranded.py +117 -0
- pinecall/log/__init__.py +0 -0
- pinecall/log/_histogram.py +74 -0
- pinecall/log/_relay.py +259 -0
- pinecall/log/_writer.py +454 -0
- pinecall/log/days.py +154 -0
- pinecall/log/drift.py +550 -0
- pinecall/log/facts.py +358 -0
- pinecall/log/inbox.py +169 -0
- pinecall/log/lists.py +155 -0
- pinecall/log/logs.py +581 -0
- pinecall/log/openings.py +71 -0
- pinecall/log/private.py +135 -0
- pinecall/log/queries.py +229 -0
- pinecall/log/readers.py +207 -0
- pinecall/log/reduce.py +659 -0
- pinecall/log/refold.py +217 -0
- pinecall/log/store.py +370 -0
- pinecall/log/usage.py +242 -0
- pinecall/postgres/__init__.py +0 -0
- pinecall/postgres/migrate.py +202 -0
- pinecall/postgres/migrations/0001_schema.sql +591 -0
- pinecall/postgres/migrations/0002_worlds.sql +15 -0
- pinecall/postgres/migrations/0003_lent.sql +4 -0
- pinecall/postgres/migrations/0004_carriers.sql +23 -0
- pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
- pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
- pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
- pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
- pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
- pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
- pinecall/postgres/migrations/0011_erasure.sql +32 -0
- pinecall/postgres/migrations/0012_org_policy.sql +10 -0
- pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
- pinecall/postgres/migrations/0014_consents.sql +24 -0
- pinecall/postgres/migrations/0015_disclosure.sql +7 -0
- pinecall/postgres/migrations/0016_call_records.sql +21 -0
- pinecall/postgres/migrations/0017_reads.sql +17 -0
- pinecall/postgres/migrations/0018_hosting.sql +41 -0
- pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
- pinecall/postgres/migrations/0020_written_once.sql +7 -0
- pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
- pinecall/postgres/migrations/0022_sealed.sql +39 -0
- pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
- pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
- pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
- pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
- pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
- pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
- pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
- pinecall/postgres/migrations/0060_drift.sql +62 -0
- pinecall/postgres/migrations/0061_canaries.sql +24 -0
- pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
- pinecall/postgres/migrations/0063_prompts.sql +16 -0
- pinecall/postgres/migrations/0075_call_private.sql +14 -0
- pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
- pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
- pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
- pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
- pinecall/postgres/migrations/0080_call_openings.sql +20 -0
- pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
- pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
- pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
- pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
- pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
- pinecall/postgres/migrations/0086_route_origin.sql +17 -0
- pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
- pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
- pinecall/postgres/migrations/0089_offers.sql +21 -0
- pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
- pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
- pinecall/postgres/migrations/0092_key_parent.sql +6 -0
- pinecall/postgres/migrations/0093_code_tries.sql +10 -0
- pinecall/postgres/migrations/0094_route_approval.sql +12 -0
- pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
- pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
- pinecall/postgres/migrations/0097_case_status.sql +19 -0
- pinecall/postgres/pool.py +190 -0
- pinecall/process/__init__.py +0 -0
- pinecall/process/_objects.py +97 -0
- pinecall/process/box_settings.py +23 -0
- pinecall/process/connections.py +124 -0
- pinecall/process/metrics.py +127 -0
- pinecall/process/recordings.py +323 -0
- pinecall/process/resolver.py +41 -0
- pinecall/process/sealed_audio.py +173 -0
- pinecall/process/settings.py +534 -0
- pinecall/process/shared.py +158 -0
- pinecall/process/signal.py +416 -0
- pinecall/providers/__init__.py +0 -0
- pinecall/providers/build.py +570 -0
- pinecall/providers/catalog.py +266 -0
- pinecall/providers/credentials.py +230 -0
- pinecall/providers/declared.py +69 -0
- pinecall/providers/prices.py +240 -0
- pinecall/providers/tried.py +104 -0
- pinecall/providers/voices.py +174 -0
- pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
- pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
- pinecall/public/console/favicon.png +0 -0
- pinecall/public/console/index.html +20 -0
- pinecall/public/console/logo-mark.png +0 -0
- pinecall/public/console/pinecall-logo-dark.png +0 -0
- pinecall/public/console/pinecall-logo.png +0 -0
- pinecall/public/console/pinecall-mark.png +0 -0
- pinecall/public/console/sw.js +37 -0
- pinecall/public/widget/pinecall-widget.js +853 -0
- pinecall/py.typed +0 -0
- pinecall/retrieval/__init__.py +0 -0
- pinecall/retrieval/_search.py +186 -0
- pinecall/retrieval/_sections.py +70 -0
- pinecall/retrieval/embed.py +340 -0
- pinecall/retrieval/extraction.py +403 -0
- pinecall/retrieval/knowledge.py +659 -0
- pinecall/retrieval/lookups.py +192 -0
- pinecall/retrieval/memory.py +603 -0
- pinecall/runner/__init__.py +0 -0
- pinecall/runner/_kube.py +459 -0
- pinecall/runner/_plan.py +157 -0
- pinecall/runner/main.py +306 -0
- pinecall/session/__init__.py +0 -0
- pinecall/session/_agent.py +141 -0
- pinecall/session/_hearing.py +101 -0
- pinecall/session/_livekit.py +195 -0
- pinecall/session/_prompt.py +147 -0
- pinecall/session/_recording.py +237 -0
- pinecall/session/call.py +367 -0
- pinecall/session/clock.py +40 -0
- pinecall/session/hold.py +239 -0
- pinecall/session/room.py +626 -0
- pinecall/session/session.py +690 -0
- pinecall/session/text.py +144 -0
- pinecall/session/tools.py +483 -0
- pinecall/session/voice.py +118 -0
- pinecall/session/widget.py +201 -0
- pinecall/tenancy/__init__.py +0 -0
- pinecall/tenancy/admission.py +204 -0
- pinecall/tenancy/agents.py +164 -0
- pinecall/tenancy/canary.py +126 -0
- pinecall/tenancy/carrier_networks.py +156 -0
- pinecall/tenancy/carriers.py +265 -0
- pinecall/tenancy/codes.py +196 -0
- pinecall/tenancy/consents.py +167 -0
- pinecall/tenancy/dial_policy.py +405 -0
- pinecall/tenancy/disclosure.py +41 -0
- pinecall/tenancy/erasure.py +204 -0
- pinecall/tenancy/export.py +167 -0
- pinecall/tenancy/hosted_running.py +166 -0
- pinecall/tenancy/hosting.py +475 -0
- pinecall/tenancy/judges.py +126 -0
- pinecall/tenancy/keys.py +467 -0
- pinecall/tenancy/knocks.py +40 -0
- pinecall/tenancy/letters.py +378 -0
- pinecall/tenancy/mail.py +458 -0
- pinecall/tenancy/org_secrets.py +123 -0
- pinecall/tenancy/orgs.py +78 -0
- pinecall/tenancy/people.py +543 -0
- pinecall/tenancy/personas.py +145 -0
- pinecall/tenancy/policy.py +65 -0
- pinecall/tenancy/prompts.py +48 -0
- pinecall/tenancy/reads.py +73 -0
- pinecall/tenancy/recording_keys.py +41 -0
- pinecall/tenancy/remembered.py +148 -0
- pinecall/tenancy/retention.py +86 -0
- pinecall/tenancy/scopes.py +406 -0
- pinecall/tenancy/signin.py +622 -0
- pinecall/tenancy/spend.py +69 -0
- pinecall/tenancy/sso.py +431 -0
- pinecall/tenancy/sso_domains.py +132 -0
- pinecall/tenancy/throttle.py +88 -0
- pinecall/tenancy/tokens.py +400 -0
- pinecall/tenancy/traceback.py +70 -0
- pinecall/tenancy/usage.py +136 -0
- pinecall/tenancy/vault.py +204 -0
- pinecall/tenancy/words.py +144 -0
- pinecall/wire/__init__.py +0 -0
- pinecall/wire/commands.py +350 -0
- pinecall/wire/events.py +655 -0
- pinecall/wire/frames.py +65 -0
- pinecall/wire/metrics.py +287 -0
- pinecall/wire/parts.py +320 -0
- pinecall/wire/rest/__init__.py +0 -0
- pinecall/wire/rest/accounts.py +512 -0
- pinecall/wire/rest/agents.py +172 -0
- pinecall/wire/rest/calls.py +397 -0
- pinecall/wire/rest/evals.py +453 -0
- pinecall/wire/rest/fleet.py +69 -0
- pinecall/wire/rest/hosting.py +174 -0
- pinecall/wire/rest/numbers.py +280 -0
- pinecall/wire/rest/ops.py +342 -0
- pinecall/wire/rest/providers.py +83 -0
- pinecall/wire/rest/retrieval.py +301 -0
- pinecall/wire/rest/settings.py +184 -0
- pinecall/wire/rest/usage.py +245 -0
- pinecall/wire/room.py +61 -0
- pinecall/wire/scores.py +49 -0
- pinecall/wire/state.py +243 -0
- pinecall/worker/__init__.py +0 -0
- pinecall/worker/_job.py +655 -0
- pinecall/worker/_recorder.py +82 -0
- pinecall/worker/_traces.py +48 -0
- pinecall/worker/main.py +307 -0
- pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
- pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
- pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
- pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
- pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""What an account is to telephony: who may call the box with it, where it is dialled, its API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from pinecall.channels import routes, whatsapp
|
|
8
|
+
from pinecall.channels.telephony._twilio import Twilio, twilio_of, verify
|
|
9
|
+
from pinecall.channels.telephony.carrier_catalog import BOX_CARRIER, known_carrier
|
|
10
|
+
from pinecall.domain.errors import (
|
|
11
|
+
Conflict,
|
|
12
|
+
DeclarationRefused,
|
|
13
|
+
NotAvailable,
|
|
14
|
+
NotFound,
|
|
15
|
+
UpstreamFailed,
|
|
16
|
+
)
|
|
17
|
+
from pinecall.process.connections import Connections
|
|
18
|
+
from pinecall.tenancy import carrier_networks
|
|
19
|
+
from pinecall.tenancy.carriers import (
|
|
20
|
+
Account,
|
|
21
|
+
Carrier,
|
|
22
|
+
SipPeer,
|
|
23
|
+
Transport,
|
|
24
|
+
TwilioAccount,
|
|
25
|
+
WhatsappAccount,
|
|
26
|
+
box_twilio,
|
|
27
|
+
carrier_named,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Twilio's signalling edges, from the catalog: the fence of every Twilio number on the SFU, and the
|
|
31
|
+
# set the cloud firewall opens 5060 to. A test holds them equal.
|
|
32
|
+
TWILIO_SIGNALLING: tuple[str, ...] = known_carrier(BOX_CARRIER).networks
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
NOT_PROVISIONED = (
|
|
36
|
+
"the org's account {account} cannot place a call yet: POST /v1/carrier/outbound provisions it"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
NO_OUTBOUND_HOST = (
|
|
41
|
+
"SIP peer {account} declared no outbound_host: the networks a peer calls FROM are not an "
|
|
42
|
+
"address it takes calls AT"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
PLACES_NO_CALL = "{account} is a WhatsApp number: it places no call"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
ON_NO_TRUNK = "{account} is a WhatsApp number: it is on no SIP trunk"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
META_REFUSED = "Meta does not open WhatsApp number {number} with this token: {why}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Fence:
|
|
57
|
+
"""Who the SFU admits a number's INVITE from: one trunk per fence."""
|
|
58
|
+
|
|
59
|
+
trunk: str
|
|
60
|
+
networks: tuple[str, ...]
|
|
61
|
+
username: str = ""
|
|
62
|
+
password: str = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Dialled:
|
|
67
|
+
"""Where a call through the account is sent: the host, its transport and the pair."""
|
|
68
|
+
|
|
69
|
+
hostname: str
|
|
70
|
+
transport: Transport
|
|
71
|
+
username: str
|
|
72
|
+
password: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# A trunk that lists no address admits every source: a fence with none approved is no trunk at all.
|
|
76
|
+
# `via` is the catalog carrier a number with no account comes through; `own` the approved networks
|
|
77
|
+
# of the peer, or of a number hooked with networks of its own (None: it named none).
|
|
78
|
+
def fence_of(
|
|
79
|
+
org: str,
|
|
80
|
+
number: str,
|
|
81
|
+
carrier: Carrier | None,
|
|
82
|
+
via: str | None,
|
|
83
|
+
own: tuple[str, ...] | None,
|
|
84
|
+
) -> Fence | None:
|
|
85
|
+
"""Who the SFU admits the number from, or None until the operator approves a network of it."""
|
|
86
|
+
if carrier is None:
|
|
87
|
+
if own is not None:
|
|
88
|
+
return Fence(trunk=f"{org}:{number}", networks=own) if own else None
|
|
89
|
+
kind = via or BOX_CARRIER
|
|
90
|
+
trunk = org if kind == BOX_CARRIER else f"{org}:{kind}"
|
|
91
|
+
return Fence(trunk=trunk, networks=known_carrier(kind).networks)
|
|
92
|
+
match carrier.account:
|
|
93
|
+
case TwilioAccount():
|
|
94
|
+
return Fence(trunk=org, networks=TWILIO_SIGNALLING)
|
|
95
|
+
case SipPeer():
|
|
96
|
+
peer = carrier.account
|
|
97
|
+
if not own:
|
|
98
|
+
return None
|
|
99
|
+
return Fence(
|
|
100
|
+
trunk=f"{org}:{peer.username}",
|
|
101
|
+
networks=own,
|
|
102
|
+
username=peer.username,
|
|
103
|
+
password=peer.password,
|
|
104
|
+
)
|
|
105
|
+
case WhatsappAccount():
|
|
106
|
+
raise DeclarationRefused(ON_NO_TRUNK.format(account=carrier.id))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def declared_networks(carrier: Carrier) -> tuple[str, ...] | None:
|
|
110
|
+
"""The networks a peer says it calls from, for the operator to approve; None for the others."""
|
|
111
|
+
match carrier.account:
|
|
112
|
+
case SipPeer():
|
|
113
|
+
return tuple(carrier.account.addresses)
|
|
114
|
+
case TwilioAccount() | WhatsappAccount():
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def own_networks(
|
|
119
|
+
declared: tuple[str, ...] | None, approved: tuple[str, ...]
|
|
120
|
+
) -> tuple[tuple[str, ...] | None, tuple[str, ...]]:
|
|
121
|
+
"""Of the networks named for a fence, the approved ones (None: it named none) and the rest."""
|
|
122
|
+
if declared is None:
|
|
123
|
+
return None, ()
|
|
124
|
+
named = tuple(carrier_networks.written(network) for network in declared)
|
|
125
|
+
return (
|
|
126
|
+
tuple(network for network in named if network in approved),
|
|
127
|
+
tuple(network for network in named if network not in approved),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def missing_to_dial(carrier: Carrier) -> list[str]:
|
|
132
|
+
"""What the account lacks before a call can go out through it, one sentence each."""
|
|
133
|
+
match carrier.account:
|
|
134
|
+
case TwilioAccount():
|
|
135
|
+
return [] if carrier.outbound else [NOT_PROVISIONED.format(account=carrier.id)]
|
|
136
|
+
case SipPeer():
|
|
137
|
+
peer = carrier.account
|
|
138
|
+
return [] if peer.outbound_host else [NO_OUTBOUND_HOST.format(account=carrier.id)]
|
|
139
|
+
case WhatsappAccount():
|
|
140
|
+
return [PLACES_NO_CALL.format(account=carrier.id)]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def dialled_of(carrier: Carrier) -> Dialled:
|
|
144
|
+
"""Where a call through the account goes; Conflict naming what is missing."""
|
|
145
|
+
missing = missing_to_dial(carrier)
|
|
146
|
+
if missing:
|
|
147
|
+
raise Conflict(missing[0])
|
|
148
|
+
match carrier.account:
|
|
149
|
+
case TwilioAccount():
|
|
150
|
+
termination = carrier.outbound
|
|
151
|
+
if termination is None:
|
|
152
|
+
raise Conflict(NOT_PROVISIONED.format(account=carrier.id))
|
|
153
|
+
return Dialled(
|
|
154
|
+
hostname=termination.host,
|
|
155
|
+
transport="auto",
|
|
156
|
+
username=termination.username,
|
|
157
|
+
password=termination.password,
|
|
158
|
+
)
|
|
159
|
+
case SipPeer():
|
|
160
|
+
peer = carrier.account
|
|
161
|
+
return Dialled(
|
|
162
|
+
hostname=str(peer.outbound_host),
|
|
163
|
+
transport=peer.outbound_transport,
|
|
164
|
+
username=peer.outbound_username or peer.username,
|
|
165
|
+
password=peer.outbound_password or peer.password,
|
|
166
|
+
)
|
|
167
|
+
case WhatsappAccount():
|
|
168
|
+
raise Conflict(PLACES_NO_CALL.format(account=carrier.id))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Twilio is the one carrier with an API the box drives; a peer is SIP terms and nothing more.
|
|
172
|
+
def control_of(http: httpx.AsyncClient, carrier: Carrier) -> Twilio | None:
|
|
173
|
+
"""The account's API the box can list, point and provision numbers through, if it has one."""
|
|
174
|
+
match carrier.account:
|
|
175
|
+
case TwilioAccount():
|
|
176
|
+
return twilio_of(http, carrier.account)
|
|
177
|
+
case SipPeer() | WhatsappAccount():
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def meta_of(carrier: Carrier) -> WhatsappAccount | None:
|
|
182
|
+
"""The WhatsApp number at Meta the account is, if it is one."""
|
|
183
|
+
match carrier.account:
|
|
184
|
+
case WhatsappAccount():
|
|
185
|
+
return carrier.account
|
|
186
|
+
case TwilioAccount() | SipPeer():
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def verify_account(http: httpx.AsyncClient, account: Account) -> None:
|
|
191
|
+
"""Refuse an account its carrier does not open with what was given; a peer is not asked."""
|
|
192
|
+
match account:
|
|
193
|
+
case TwilioAccount():
|
|
194
|
+
await verify(http, account)
|
|
195
|
+
case WhatsappAccount():
|
|
196
|
+
await _whatsapp_opens(http, account)
|
|
197
|
+
case SipPeer():
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Twilio's networks are every Twilio customer's, so the firewall says only that a call came from
|
|
202
|
+
# Twilio; the account Twilio stamps on it says whose. A number the box bought is the box's account,
|
|
203
|
+
# one imported is the org's; a number hooked or typed has no account known here, and goes unasked.
|
|
204
|
+
async def twilio_account_of(connections: Connections, org: str, number: str) -> str | None:
|
|
205
|
+
"""The Twilio account a call to the org's number must come from; None where none is known."""
|
|
206
|
+
pool, vault = connections.pool, connections.vault
|
|
207
|
+
record = await routes.record_of(pool, org, number)
|
|
208
|
+
if record is None:
|
|
209
|
+
return None
|
|
210
|
+
try:
|
|
211
|
+
if record.route.managed:
|
|
212
|
+
return (await box_twilio(pool, vault)).account_sid
|
|
213
|
+
if record.account is None:
|
|
214
|
+
return None
|
|
215
|
+
found = await carrier_named(pool, vault, org, record.account)
|
|
216
|
+
except (NotAvailable, NotFound):
|
|
217
|
+
return None
|
|
218
|
+
return found.account.account_sid if isinstance(found.account, TwilioAccount) else None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Meta is asked for the number the id names, as the inbox asks it: a token that does not open it
|
|
222
|
+
# is refused here, in Meta's words, and not on the first message.
|
|
223
|
+
async def _whatsapp_opens(http: httpx.AsyncClient, account: WhatsappAccount) -> None:
|
|
224
|
+
try:
|
|
225
|
+
await whatsapp.display_number(http, account.access_token, account.phone_number_id)
|
|
226
|
+
except UpstreamFailed as refused:
|
|
227
|
+
raise DeclarationRefused(
|
|
228
|
+
META_REFUSED.format(number=account.phone_number_id, why=refused)
|
|
229
|
+
) from refused
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""The carriers a box knows (carriers.csv) and which of them its operator admits."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import date
|
|
6
|
+
from functools import cache
|
|
7
|
+
from importlib import resources
|
|
8
|
+
|
|
9
|
+
from pinecall.domain.errors import Conflict, NotFound
|
|
10
|
+
from pinecall.postgres.pool import Pool
|
|
11
|
+
from pinecall.process import box_settings
|
|
12
|
+
|
|
13
|
+
# Each carrier's networks are read from its own page, with the page and the day beside them.
|
|
14
|
+
FILE = "carriers.csv"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ADMITTED = "carriers/admitted"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# The box's own carrier: always admitted, its networks are the cloud firewall's own set.
|
|
21
|
+
BOX_CARRIER = "twilio"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
NO_SUCH_CARRIER = "this box knows no carrier {kind}: one of {known}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
ALWAYS_ADMITTED = "{name} is the box's own carrier: it is always admitted"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class KnownCarrier:
|
|
32
|
+
"""A carrier of the catalog: its published signalling networks, and whether it has an API."""
|
|
33
|
+
|
|
34
|
+
kind: str
|
|
35
|
+
name: str
|
|
36
|
+
# The box has a client for its API (lists, points, buys numbers); the rest is SIP terms.
|
|
37
|
+
control: bool
|
|
38
|
+
networks: tuple[str, ...]
|
|
39
|
+
source: str
|
|
40
|
+
read_on: date
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cache
|
|
44
|
+
def known() -> dict[str, KnownCarrier]:
|
|
45
|
+
"""Every carrier of the shipped catalog, by kind, in the file's order."""
|
|
46
|
+
text = resources.files(__package__).joinpath(FILE).read_text(encoding="utf-8")
|
|
47
|
+
rows = list(csv.DictReader(text.splitlines()))
|
|
48
|
+
kinds = dict.fromkeys(row["kind"] for row in rows)
|
|
49
|
+
return {
|
|
50
|
+
kind: KnownCarrier(
|
|
51
|
+
kind=kind,
|
|
52
|
+
name=first["name"],
|
|
53
|
+
control=first["control"] == "yes",
|
|
54
|
+
networks=tuple(row["network"] for row in rows if row["kind"] == kind),
|
|
55
|
+
source=first["source"],
|
|
56
|
+
read_on=date.fromisoformat(first["read_on"]),
|
|
57
|
+
)
|
|
58
|
+
for kind in kinds
|
|
59
|
+
for first in [next(row for row in rows if row["kind"] == kind)]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def known_carrier(kind: str) -> KnownCarrier:
|
|
64
|
+
"""The catalog's carrier of that kind; NotFound naming the ones there are."""
|
|
65
|
+
found = known().get(kind)
|
|
66
|
+
if found is None:
|
|
67
|
+
raise NotFound(NO_SUCH_CARRIER.format(kind=kind, known=", ".join(known())))
|
|
68
|
+
return found
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def admitted(pool: Pool) -> frozenset[str]:
|
|
72
|
+
"""The kinds the operator admits, the box's own carrier always among them."""
|
|
73
|
+
async with pool.connection() as connection:
|
|
74
|
+
value = await box_settings.read(connection, ADMITTED)
|
|
75
|
+
kinds = value.get("kinds") if value is not None else None
|
|
76
|
+
chosen = {BOX_CARRIER}
|
|
77
|
+
for kind in kinds if isinstance(kinds, list) else []:
|
|
78
|
+
if isinstance(kind, str) and kind in known():
|
|
79
|
+
chosen.add(kind)
|
|
80
|
+
return frozenset(chosen)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def admit(pool: Pool, kind: str, *, on: bool) -> frozenset[str]:
|
|
84
|
+
"""Admit the carrier or stop admitting it; the kinds admitted after."""
|
|
85
|
+
carrier = known_carrier(kind)
|
|
86
|
+
if kind == BOX_CARRIER and not on:
|
|
87
|
+
raise Conflict(ALWAYS_ADMITTED.format(name=carrier.name))
|
|
88
|
+
kinds = await admitted(pool)
|
|
89
|
+
after = sorted(kinds | {kind} if on else kinds - {kind})
|
|
90
|
+
async with pool.connection() as connection:
|
|
91
|
+
await box_settings.write(connection, ADMITTED, {"kinds": list(after)})
|
|
92
|
+
return frozenset(after)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
kind,name,control,network,source,read_on
|
|
2
|
+
twilio,Twilio,yes,54.172.60.0/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
3
|
+
twilio,Twilio,yes,54.244.51.0/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
4
|
+
twilio,Twilio,yes,54.171.127.192/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
5
|
+
twilio,Twilio,yes,35.156.191.128/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
6
|
+
twilio,Twilio,yes,54.65.63.192/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
7
|
+
twilio,Twilio,yes,54.169.127.128/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
8
|
+
twilio,Twilio,yes,54.252.254.64/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
9
|
+
twilio,Twilio,yes,177.71.206.192/30,https://www.twilio.com/docs/sip-trunking/ip-addresses,2026-10-03
|
|
10
|
+
telnyx,Telnyx,no,192.76.120.10/32,https://sip.telnyx.com/,2026-10-03
|
|
11
|
+
telnyx,Telnyx,no,64.16.250.10/32,https://sip.telnyx.com/,2026-10-03
|
|
12
|
+
telnyx,Telnyx,no,185.246.41.140/32,https://sip.telnyx.com/,2026-10-03
|
|
13
|
+
telnyx,Telnyx,no,185.246.41.141/32,https://sip.telnyx.com/,2026-10-03
|
|
14
|
+
telnyx,Telnyx,no,103.115.244.145/32,https://sip.telnyx.com/,2026-10-03
|
|
15
|
+
telnyx,Telnyx,no,103.115.244.146/32,https://sip.telnyx.com/,2026-10-03
|
|
16
|
+
telnyx,Telnyx,no,192.76.120.31/32,https://sip.telnyx.com/,2026-10-03
|
|
17
|
+
telnyx,Telnyx,no,64.16.250.13/32,https://sip.telnyx.com/,2026-10-03
|
|
18
|
+
telnyx,Telnyx,no,185.246.42.128/32,https://sip.telnyx.com/,2026-10-03
|
|
19
|
+
telnyx,Telnyx,no,185.246.42.129/32,https://sip.telnyx.com/,2026-10-03
|
|
20
|
+
telnyx,Telnyx,no,103.115.244.158/32,https://sip.telnyx.com/,2026-10-03
|
|
21
|
+
telnyx,Telnyx,no,103.115.244.159/32,https://sip.telnyx.com/,2026-10-03
|
|
22
|
+
vonage,Vonage,no,216.147.0.1/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
23
|
+
vonage,Vonage,no,216.147.0.2/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
24
|
+
vonage,Vonage,no,216.147.1.1/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
25
|
+
vonage,Vonage,no,216.147.1.2/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
26
|
+
vonage,Vonage,no,216.147.2.1/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
27
|
+
vonage,Vonage,no,216.147.2.2/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
28
|
+
vonage,Vonage,no,216.147.3.1/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
29
|
+
vonage,Vonage,no,216.147.3.2/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
30
|
+
vonage,Vonage,no,216.147.4.1/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
31
|
+
vonage,Vonage,no,216.147.4.2/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
32
|
+
vonage,Vonage,no,216.147.5.1/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
33
|
+
vonage,Vonage,no,216.147.5.2/32,https://api.support.vonage.com/hc/en-us/articles/360035471331,2026-10-03
|
|
34
|
+
plivo,Plivo,no,13.52.9.0/25,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
35
|
+
plivo,Plivo,no,216.120.187.128/26,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
36
|
+
plivo,Plivo,no,18.214.109.128/25,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
37
|
+
plivo,Plivo,no,18.215.142.0/26,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
38
|
+
plivo,Plivo,no,204.89.148.128/26,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
39
|
+
plivo,Plivo,no,3.120.121.128/26,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
40
|
+
plivo,Plivo,no,18.228.70.64/26,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|
|
41
|
+
plivo,Plivo,no,13.238.202.192/26,https://www.plivo.com/docs/sip-trunking/concepts/technical-specifications,2026-10-03
|