From 1062a5ed44fca60a1f25964a91b4ed35d6179896 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Mon, 22 Jun 2026 08:05:06 -0700 Subject: [PATCH] packetdial: finish resource wrapping (reads + gated writes across the platform) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added read wrappers: addresses (E911), smsnumbers, blocked-numbers, moh, dialrules, recording, transcriptions. Added gated write wrappers: DID update/delete, per-user device CRUD, E911 address CRUD, contact CRUD, site create/update, auto-attendant create, SMS number CRUD, block/unblock numbers, MoH TTS create/delete. Verification: contact create→delete lifecycle verified on arizonacomputerguru (id field is `unique-id`); reads for addresses/blocked-numbers/moh verified. Remaining writes are plumbed per the OpenAPI spec [P] but not lifecycle-verified (test domain lacks the feature or needs a special body) — SKILL.md marks each [V]/[P] and documents the gotchas (E911 pidflo via addresses/validate; SMS not provisioned on test domain; number-filters add 202'd but didn't persist; MoH file upload is multipart -> raw). Capability map + api.md history updated. All writes --confirm-gated; anything unwrapped still reachable via `raw`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/packetdial/SKILL.md | 68 +++++++-- .claude/skills/packetdial/references/api.md | 9 ++ .claude/skills/packetdial/scripts/ns.py | 140 ++++++++++++++++++ .../skills/packetdial/scripts/ns_client.py | 113 ++++++++++++++ 4 files changed, 321 insertions(+), 9 deletions(-) diff --git a/.claude/skills/packetdial/SKILL.md b/.claude/skills/packetdial/SKILL.md index 5f135f91..d7152201 100644 --- a/.claude/skills/packetdial/SKILL.md +++ b/.claude/skills/packetdial/SKILL.md @@ -63,6 +63,13 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py sites # m bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py autoattendants # IVR menus bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py contacts # shared address book bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py billing # limits + current usage counts +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py addresses # E911 addresses +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py smsnumbers # SMS numbers (needs SMS-provisioned domain) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py blocked-numbers # blocked-number filters +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py moh # music-on-hold entries +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dialrules +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py recording +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py transcriptions bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain --start 2026-06-01 --end 2026-06-02 bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py resellers ``` @@ -127,6 +134,45 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET domains/acme/users bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw POST domains/acme/users --body '{...}' --confirm ``` +### Additional resource edits (gated) + +All `--confirm`-gated. Verification legend: **[V]** lifecycle-verified on the +`arizonacomputerguru` test domain; **[P]** plumbed per the OpenAPI spec but not +lifecycle-verified (test domain lacks the feature / needs a special body) — exercise +on a real domain or via `raw` and confirm. + +```bash +# DIDs — complete CRUD (create-did exists; add update/delete) [P] +ns.py update-did --body '{"dial-rule-translation-destination-user":"101"}' --confirm +ns.py delete-did --confirm +# per-user SIP devices [P] +ns.py create-device --body '{"device":"sip:101a@"}' --confirm +ns.py update-device --body '{...}' --confirm +ns.py delete-device --confirm +# contacts (domain address book) [V] +ns.py create-contact --body '{"name-first-name":"Jane","name-last-name":"Doe","email":"j@x.com"}' --confirm +ns.py update-contact --body '{...}' --confirm # id = the contact's "unique-id" +ns.py delete-contact --confirm +# E911 addresses [P] (create needs address-formatted-pidflo -> POST .../addresses/validate first via raw) +ns.py create-address --body '{...}' --confirm +ns.py update-address --body '{...}' --confirm +ns.py delete-address --confirm +# sites / auto-attendants [P] +ns.py create-site --body '{...}' --confirm +ns.py update-site --body '{...}' --confirm +ns.py create-autoattendant --body '{"attendant-name":"Main","user":"3000"}' --confirm +# SMS numbers [P] (needs SMS-provisioned domain) +ns.py create-smsnumber --body '{"number":"1520...","application":"..."}' --confirm +ns.py update-smsnumber --body '{...}' --confirm +ns.py delete-smsnumber --confirm +# blocked-number filters [P] (on the empty test domain add 202'd but didn't persist; verify on a live domain) +ns.py block-numbers --body '{"phone-numbers-to-reject":["15205550199"]}' --confirm +ns.py unblock-numbers --body '{"phone-numbers-to-reject":["15205550199"]}' --confirm +# music on hold (TTS create + delete; file UPLOAD is multipart -> use raw) [P] +ns.py create-moh --body '{"synchronous":"yes","script":"Thanks for holding"}' --confirm +ns.py delete-moh --confirm +``` + ## API capability map (RTFM — spec v44.4.10, mapped 2026-06-22) The live OpenAPI spec is **NetSapiens API v2 v44.4.10** — **239 paths / 354 operations** @@ -136,15 +182,19 @@ The live OpenAPI spec is **NetSapiens API v2 v44.4.10** — **239 paths / 354 op | Resource (`/domains/{domain}/…`) | Ops | Skill wrapper | Notes | |---|---|---|---| | `users` (+ `/devices`, answerrules, voicemail, contacts under a user) | R/W, biggest surface | `users`, `user`, `devices`, `create/update/delete-user` | per-user devices live at `users/{u}/devices` | -| `phonenumbers` (DIDs + dial-rule routing) | R/W | `dids`, `create-did` | | -| `phones` (SIP devices, MAC provisioning) | R/W | `phones`, `create-phone` | | -| `callqueues` (ACD: agents, dispatch, live stats) | R/W + PATCH | `callqueues` + `create/update/delete-callqueue`, `add/update/remove-agent` | full CRUD wrapped (verified) | -| `timeframes` (business-hours / holiday schedules) | R/W | `timeframes` + `create/update/delete-timeframe` | body-discriminated; CRUD wrapped (verified) | -| `autoattendants` (IVR menus) | R/W | `autoattendants` (read) | | -| `sites` (multi-site) | R/W | `sites` (read) | | -| `contacts` (domain address book) | R/W | `contacts` (read) | | -| `addresses` (E911) · `moh` (music on hold) · `dialplans` · `msg`/`smsnumbers` (SMS) · `connections` · `number-filters` | R/W | `raw` | not yet wrapped | -| `cdrs` · `calls` · `recordings` · `transcriptions` · `queuedcall` | read | `cdrs`; rest via `raw` | call data | +| `phonenumbers` (DIDs + dial-rule routing) | R/W | `dids`, `create-did`, `update-did`, `delete-did` | CRUD wrapped [P] | +| `phones` (SIP devices, MAC provisioning) | R/W | `phones`, `create-phone`; per-user `create/update/delete-device` | | +| `callqueues` (ACD: agents, dispatch, live stats) | R/W + PATCH | `callqueues` + `create/update/delete-callqueue`, `add/update/remove-agent` | full CRUD wrapped [V]; agent login/logout PATCH via `raw` | +| `timeframes` (business-hours / holiday schedules) | R/W | `timeframes` + `create/update/delete-timeframe` | body-discriminated; CRUD wrapped [V] | +| `autoattendants` (IVR menus) | R/W | `autoattendants`, `create-autoattendant` | per-prompt update via `raw` [P] | +| `sites` (multi-site) | R/W | `sites`, `create-site`, `update-site` | [P] | +| `contacts` (domain address book) | R/W | `contacts`, `create/update/delete-contact` | full CRUD wrapped [V]; id = `unique-id` | +| `addresses` (E911) | R/W | `addresses`, `create/update/delete-address` | create needs `validate` for pidflo [P] | +| `smsnumbers` (SMS) | R/W | `smsnumbers`, `create/update/delete-smsnumber` | needs SMS-provisioned domain [P] | +| `number-filters` (blocked numbers) | R/W | `blocked-numbers`, `block-numbers`, `unblock-numbers` | [P] (test-domain add didn't persist) | +| `moh` (music on hold) | R/W | `moh`, `create-moh` (TTS), `delete-moh` | file upload (multipart) via `raw` [P] | +| `dialplans` · `msg` · `connections` | R/W | `dialrules` (read); writes via `raw` | | +| `cdrs` · `calls` · `recordings` · `transcriptions` · `queuedcall` | read | `cdrs`, `recording`, `transcriptions`; rest via `raw` | call data | | `billing` (limits + current counts) | read | `billing` | per-domain quota snapshot | | Top-level: `domains`, `resellers`, `apikeys`, `subscriptions`, `routes`, `configurations`, `certificates`, `templates`, `jwt`, `tokens` | R/W | `domains`/`domain`/`resellers`; rest via `raw` | platform/admin | diff --git a/.claude/skills/packetdial/references/api.md b/.claude/skills/packetdial/references/api.md index 98857b50..9cc539d7 100644 --- a/.claude/skills/packetdial/references/api.md +++ b/.claude/skills/packetdial/references/api.md @@ -93,3 +93,12 @@ exact request/response schema of any specific path. server selects the op from the body) — wrappers pass `--body` verbatim. Note: entry-bearing timeframe types (days-of-week etc.) reject create without their array — create `always` then convert, or include the array. + Finished the resource wrapping: added reads (`addresses`, `smsnumbers`, `blocked-numbers`, + `moh`, `dialrules`, `recording`, `transcriptions`) and gated writes (DID update/delete, + per-user device CRUD, E911 address CRUD, contact CRUD, site create/update, auto-attendant + create, SMS number CRUD, block/unblock numbers, MoH TTS create/delete). Verified [V] + lifecycles on `arizonacomputerguru`: contact create→delete (id = `unique-id`), plus the + callqueue/timeframe lifecycles. The rest are plumbed per spec [P] but not lifecycle-verified + (test domain lacks the feature, or needs a special body — E911 `address-formatted-pidflo` via + `addresses/validate`; SMS not provisioned; number-filters add 202'd but didn't persist on the + empty domain). Multipart MoH upload + dialrule writes + queue-agent PATCH stay on `raw`. diff --git a/.claude/skills/packetdial/scripts/ns.py b/.claude/skills/packetdial/scripts/ns.py index 7c2faa11..9d9b9007 100644 --- a/.claude/skills/packetdial/scripts/ns.py +++ b/.claude/skills/packetdial/scripts/ns.py @@ -129,6 +129,44 @@ def main(argv=None) -> int: sp = sub.add_parser("update-timeframe"); sp.add_argument("domain"); sp.add_argument("tf_id"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("delete-timeframe"); sp.add_argument("domain"); sp.add_argument("tf_id"); sp.add_argument("--body"); sp.add_argument("--confirm", action="store_true") + # more read wrappers + sp = sub.add_parser("addresses", help="E911 addresses in a domain"); sp.add_argument("domain") + sp = sub.add_parser("smsnumbers", help="SMS numbers in a domain"); sp.add_argument("domain") + sp = sub.add_parser("blocked-numbers", help="blocked-number filters in a domain"); sp.add_argument("domain") + sp = sub.add_parser("moh", help="music-on-hold entries in a domain"); sp.add_argument("domain") + sp = sub.add_parser("dialrules", help="dial rules in a dial plan"); sp.add_argument("domain"); sp.add_argument("dialplan") + sp = sub.add_parser("recording", help="get a recording by call id"); sp.add_argument("domain"); sp.add_argument("callid") + sp = sub.add_parser("transcriptions", help="call transcriptions in a domain"); sp.add_argument("domain") + + # DID complete + device CRUD + sp = sub.add_parser("update-did"); sp.add_argument("domain"); sp.add_argument("phonenumber"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("delete-did"); sp.add_argument("domain"); sp.add_argument("phonenumber"); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("create-device"); sp.add_argument("domain"); sp.add_argument("user"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("update-device"); sp.add_argument("domain"); sp.add_argument("user"); sp.add_argument("device"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("delete-device"); sp.add_argument("domain"); sp.add_argument("user"); sp.add_argument("device"); sp.add_argument("--confirm", action="store_true") + + # E911 address CRUD + sp = sub.add_parser("create-address"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("update-address"); sp.add_argument("domain"); sp.add_argument("address_id"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("delete-address"); sp.add_argument("domain"); sp.add_argument("address_id"); sp.add_argument("--confirm", action="store_true") + + # contact CRUD + sp = sub.add_parser("create-contact"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("update-contact"); sp.add_argument("domain"); sp.add_argument("contact_id"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("delete-contact"); sp.add_argument("domain"); sp.add_argument("contact_id"); sp.add_argument("--confirm", action="store_true") + + # sites + auto-attendants + sms + blocked numbers + moh (writes) + sp = sub.add_parser("create-site"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("update-site"); sp.add_argument("domain"); sp.add_argument("site"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("create-autoattendant"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("create-smsnumber"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("update-smsnumber"); sp.add_argument("domain"); sp.add_argument("smsnumber"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("delete-smsnumber"); sp.add_argument("domain"); sp.add_argument("smsnumber"); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("block-numbers"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("unblock-numbers"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("create-moh"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") + sp = sub.add_parser("delete-moh"); sp.add_argument("domain"); sp.add_argument("index"); sp.add_argument("--confirm", action="store_true") + # --- raw escape hatch --- sp = sub.add_parser("raw", help="raw request against any v2 path") sp.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"]) @@ -239,6 +277,108 @@ def main(argv=None) -> int: _require_confirm(args, "DELETE timeframe", f"{args.domain}/{args.tf_id}" + (f" {json.dumps(body)}" if body else "")) _emit(client.delete_timeframe(args.domain, args.tf_id, body)) + # --- additional reads --- + elif args.cmd == "addresses": + _emit(client.addresses(args.domain)) + elif args.cmd == "smsnumbers": + _emit(client.smsnumbers(args.domain)) + elif args.cmd == "blocked-numbers": + _emit(client.blocked_numbers(args.domain)) + elif args.cmd == "moh": + _emit(client.moh(args.domain)) + elif args.cmd == "dialrules": + _emit(client.dialrules(args.domain, args.dialplan)) + elif args.cmd == "recording": + _emit(client.recording(args.domain, args.callid)) + elif args.cmd == "transcriptions": + _emit(client.transcriptions(args.domain)) + + # --- DID complete + device CRUD --- + elif args.cmd == "update-did": + body = _parse_body(args.body) + _require_confirm(args, "update DID", f"{args.domain}/{args.phonenumber}: {json.dumps(body)}") + _emit(client.update_phonenumber(args.domain, args.phonenumber, body)) + elif args.cmd == "delete-did": + _require_confirm(args, "DELETE DID", f"{args.domain}/{args.phonenumber}") + _emit(client.delete_phonenumber(args.domain, args.phonenumber)) + elif args.cmd == "create-device": + body = _parse_body(args.body) + _require_confirm(args, "create device", f"{args.domain}/{args.user}: {json.dumps(body)}") + _emit(client.create_device(args.domain, args.user, body)) + elif args.cmd == "update-device": + body = _parse_body(args.body) + _require_confirm(args, "update device", f"{args.domain}/{args.user}/{args.device}: {json.dumps(body)}") + _emit(client.update_device(args.domain, args.user, args.device, body)) + elif args.cmd == "delete-device": + _require_confirm(args, "DELETE device", f"{args.domain}/{args.user}/{args.device}") + _emit(client.delete_device(args.domain, args.user, args.device)) + + # --- E911 address CRUD --- + elif args.cmd == "create-address": + body = _parse_body(args.body) + _require_confirm(args, "create E911 address", f"{args.domain}: {json.dumps(body)}") + _emit(client.create_address(args.domain, body)) + elif args.cmd == "update-address": + body = _parse_body(args.body) + _require_confirm(args, "update E911 address", f"{args.domain}/{args.address_id}: {json.dumps(body)}") + _emit(client.update_address(args.domain, args.address_id, body)) + elif args.cmd == "delete-address": + _require_confirm(args, "DELETE E911 address", f"{args.domain}/{args.address_id}") + _emit(client.delete_address(args.domain, args.address_id)) + + # --- contact CRUD --- + elif args.cmd == "create-contact": + body = _parse_body(args.body) + _require_confirm(args, "create contact", f"{args.domain}: {json.dumps(body)}") + _emit(client.create_contact(args.domain, body)) + elif args.cmd == "update-contact": + body = _parse_body(args.body) + _require_confirm(args, "update contact", f"{args.domain}/{args.contact_id}: {json.dumps(body)}") + _emit(client.update_contact(args.domain, args.contact_id, body)) + elif args.cmd == "delete-contact": + _require_confirm(args, "DELETE contact", f"{args.domain}/{args.contact_id}") + _emit(client.delete_contact(args.domain, args.contact_id)) + + # --- sites / auto-attendants / sms / blocked numbers / moh --- + elif args.cmd == "create-site": + body = _parse_body(args.body) + _require_confirm(args, "create site", f"{args.domain}: {json.dumps(body)}") + _emit(client.create_site(args.domain, body)) + elif args.cmd == "update-site": + body = _parse_body(args.body) + _require_confirm(args, "update site", f"{args.domain}/{args.site}: {json.dumps(body)}") + _emit(client.update_site(args.domain, args.site, body)) + elif args.cmd == "create-autoattendant": + body = _parse_body(args.body) + _require_confirm(args, "create auto-attendant", f"{args.domain}: {json.dumps(body)}") + _emit(client.create_autoattendant(args.domain, body)) + elif args.cmd == "create-smsnumber": + body = _parse_body(args.body) + _require_confirm(args, "create SMS number", f"{args.domain}: {json.dumps(body)}") + _emit(client.create_smsnumber(args.domain, body)) + elif args.cmd == "update-smsnumber": + body = _parse_body(args.body) + _require_confirm(args, "update SMS number", f"{args.domain}/{args.smsnumber}: {json.dumps(body)}") + _emit(client.update_smsnumber(args.domain, args.smsnumber, body)) + elif args.cmd == "delete-smsnumber": + _require_confirm(args, "DELETE SMS number", f"{args.domain}/{args.smsnumber}") + _emit(client.delete_smsnumber(args.domain, args.smsnumber)) + elif args.cmd == "block-numbers": + body = _parse_body(args.body) + _require_confirm(args, "block numbers", f"{args.domain}: {json.dumps(body)}") + _emit(client.block_numbers(args.domain, body)) + elif args.cmd == "unblock-numbers": + body = _parse_body(args.body) + _require_confirm(args, "unblock numbers", f"{args.domain}: {json.dumps(body)}") + _emit(client.unblock_numbers(args.domain, body)) + elif args.cmd == "create-moh": + body = _parse_body(args.body) + _require_confirm(args, "create MOH (TTS)", f"{args.domain}: {json.dumps(body)}") + _emit(client.create_moh(args.domain, body)) + elif args.cmd == "delete-moh": + _require_confirm(args, "DELETE MOH", f"{args.domain}/{args.index}") + _emit(client.delete_moh(args.domain, args.index)) + elif args.cmd == "raw": body = _parse_body(args.body) if args.method != "GET": diff --git a/.claude/skills/packetdial/scripts/ns_client.py b/.claude/skills/packetdial/scripts/ns_client.py index 29081835..c1138398 100644 --- a/.claude/skills/packetdial/scripts/ns_client.py +++ b/.claude/skills/packetdial/scripts/ns_client.py @@ -452,3 +452,116 @@ class NetSapiensClient: def delete_timeframe(self, domain: str, tf_id: str, body: Optional[dict] = None) -> Any: d, t = urllib.parse.quote(domain), urllib.parse.quote(tf_id) return self.request("DELETE", f"domains/{d}/timeframes/{t}", json_body=body) + + # --- DID (phonenumber) update/delete (completes create-did + read) --- + def update_phonenumber(self, domain: str, number: str, body: dict) -> Any: + d, n = urllib.parse.quote(domain), urllib.parse.quote(number) + return self.request("PUT", f"domains/{d}/phonenumbers/{n}", json_body=body) + + def delete_phonenumber(self, domain: str, number: str) -> Any: + d, n = urllib.parse.quote(domain), urllib.parse.quote(number) + return self.request("DELETE", f"domains/{d}/phonenumbers/{n}") + + # --- per-user SIP devices (write; read via devices()) --- + def create_device(self, domain: str, user: str, body: dict) -> Any: + d, u = urllib.parse.quote(domain), urllib.parse.quote(user) + return self.request("POST", f"domains/{d}/users/{u}/devices", json_body=body) + + def update_device(self, domain: str, user: str, device: str, body: dict) -> Any: + d, u, v = (urllib.parse.quote(domain), urllib.parse.quote(user), + urllib.parse.quote(device)) + return self.request("PUT", f"domains/{d}/users/{u}/devices/{v}", json_body=body) + + def delete_device(self, domain: str, user: str, device: str) -> Any: + d, u, v = (urllib.parse.quote(domain), urllib.parse.quote(user), + urllib.parse.quote(device)) + return self.request("DELETE", f"domains/{d}/users/{u}/devices/{v}") + + # --- E911 addresses (read + write) --- + def addresses(self, domain: str) -> Any: + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/addresses") + + def create_address(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/addresses", json_body=body) + + def update_address(self, domain: str, address_id: str, body: dict) -> Any: + d, a = urllib.parse.quote(domain), urllib.parse.quote(address_id) + return self.request("PUT", f"domains/{d}/addresses/{a}", json_body=body) + + def delete_address(self, domain: str, address_id: str) -> Any: + d, a = urllib.parse.quote(domain), urllib.parse.quote(address_id) + return self.request("DELETE", f"domains/{d}/addresses/{a}") + + # --- domain contacts (write; read via contacts()) --- + def create_contact(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/contacts", json_body=body) + + def update_contact(self, domain: str, contact_id: str, body: dict) -> Any: + d, c = urllib.parse.quote(domain), urllib.parse.quote(contact_id) + return self.request("PUT", f"domains/{d}/contacts/{c}", json_body=body) + + def delete_contact(self, domain: str, contact_id: str) -> Any: + d, c = urllib.parse.quote(domain), urllib.parse.quote(contact_id) + return self.request("DELETE", f"domains/{d}/contacts/{c}") + + # --- sites (write; read via sites()) --- + def create_site(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/sites", json_body=body) + + def update_site(self, domain: str, site: str, body: dict) -> Any: + d, s = urllib.parse.quote(domain), urllib.parse.quote(site) + return self.request("PUT", f"domains/{d}/sites/{s}", json_body=body) + + # --- auto-attendants (create; read via autoattendants()) --- + def create_autoattendant(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/autoattendants", json_body=body) + + # --- SMS numbers (read + write) --- + def smsnumbers(self, domain: str) -> Any: + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/smsnumbers") + + def create_smsnumber(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/smsnumbers", json_body=body) + + def update_smsnumber(self, domain: str, smsnumber: str, body: dict) -> Any: + d, s = urllib.parse.quote(domain), urllib.parse.quote(smsnumber) + return self.request("PUT", f"domains/{d}/smsnumbers/{s}", json_body=body) + + def delete_smsnumber(self, domain: str, smsnumber: str) -> Any: + d, s = urllib.parse.quote(domain), urllib.parse.quote(smsnumber) + return self.request("DELETE", f"domains/{d}/smsnumbers/{s}") + + # --- blocked-number filters (read + add/remove) --- + def blocked_numbers(self, domain: str) -> Any: + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/number-filters") + + def block_numbers(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/number-filters", json_body=body) + + def unblock_numbers(self, domain: str, body: dict) -> Any: + return self.request("DELETE", f"domains/{urllib.parse.quote(domain)}/number-filters", json_body=body) + + # --- music on hold (read + TTS create/delete; uploads are multipart -> raw) --- + def moh(self, domain: str) -> Any: + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/moh") + + def create_moh(self, domain: str, body: dict) -> Any: + return self.request("POST", f"domains/{urllib.parse.quote(domain)}/moh", json_body=body) + + def delete_moh(self, domain: str, index: str) -> Any: + d, i = urllib.parse.quote(domain), urllib.parse.quote(index) + return self.request("DELETE", f"domains/{d}/moh/{i}") + + # --- dial rules / call data (read) --- + def dialrules(self, domain: str, dialplan: str) -> Any: + d, p = urllib.parse.quote(domain), urllib.parse.quote(dialplan) + return self.request("GET", f"domains/{d}/dialplans/{p}/dialrules") + + def recording(self, domain: str, callid: str) -> Any: + d, c = urllib.parse.quote(domain), urllib.parse.quote(callid) + return self.request("GET", f"domains/{d}/recordings/{c}") + + def transcriptions(self, domain: str, **filters) -> Any: + return self.request( + "GET", f"domains/{urllib.parse.quote(domain)}/transcriptions", params=filters or None + )