diff --git a/docs/anveo-direct-setup-guide.md b/docs/anveo-direct-setup-guide.md index 11534d7..b8c063c 100644 --- a/docs/anveo-direct-setup-guide.md +++ b/docs/anveo-direct-setup-guide.md @@ -199,7 +199,7 @@ In the Security Dashboard's PSTN Trunk tab: # container is named easy-asterisk-do instead ``` -## 8. SMS — receiving verification codes +## 8. SMS — receiving texts in Sipnetic Voice and SMS are separate features on an Anveo DID and are configured in different places. This section covers **receiving** only; see "What about @@ -242,41 +242,49 @@ Run the installer and follow what it prints: sudo ./setup.sh sms-inbound ``` -It generates a long random ntfy topic, then offers two ways for Anveo to -reach it: +A small systemd service on the droplet receives Anveo's webhook request, +looks up which extension (or Ring Group) currently owns the destination DID +— the exact same `pstn-personal-dids.conf`/`pstn-groups.conf` data +inbound-voice ring logic already reads — and delivers the text into +Asterisk over its Manager Interface as a SIP `MESSAGE`, landing in Sipnetic +the same way this project's internal extension-to-extension texting already +does. Not a push notification: a real message in the softphone's own thread. -- **Relay (recommended)** — a small systemd service on the droplet receives - Anveo's request and republishes to ntfy properly. Two concrete wins: a - message body containing `&` survives intact (Anveo interpolates the text - into the query string unescaped, so an unencoded `&` otherwise truncates - the message), and your ntfy credentials never get stored in Anveo's portal. -- **Direct** — Anveo calls ntfy itself; nothing runs on the droplet. Simpler, - but the URL you paste into Anveo carries your ntfy token, and the `&` case - loses the tail of the message. +("SMS over SIP" — Anveo delivering the text as a MESSAGE/INVITE directly to +this box's SIP trunk, skipping the HTTP webhook entirely — was tried first +and confirmed **not offered** on this DID: the SMS tab only ever showed the +plain "Forward to URL" option, no SIP-based alternative.) Then in the Anveo portal: **Phone Numbers → the DID → SMS tab**. The tab has exactly one control — a **Forward to URL** checkbox and a text field. Tick the box, paste the string the installer printed into the field, and press **SAVE** (not RETURN, which discards). Keep the `$[message]$` placeholder **last** in -that URL — that's what makes the unescaped-`&` case recoverable. +that URL — Anveo interpolates the message text into the query string +unescaped, so a body containing `&` otherwise truncates; with the placeholder +last, everything after it is recoverable verbatim. -The field has no visible length limit, but the generated URLs are long -(~90 characters in relay mode, ~150+ in direct mode, since that one carries -the ntfy auth parameter). After saving, reopen the tab and confirm the whole -string came back intact rather than truncated — if it didn't, relay mode is -the shorter of the two. - -Send a text to the number from another phone; the notification should arrive -within seconds. `journalctl -u sms-inbound -f` shows sender, recipient and -message length (never the body — these are one-time passcodes and the journal -has a wider audience than the notification does). +Send a text to the number from another phone; it should land in Sipnetic +within seconds. `journalctl -u sms-inbound -f` shows sender, recipient, +message length, and the full AMI exchange — never the message body itself, +since the journal has a wider audience than the message thread does. ### What about sending? -Not covered, on purpose. Outbound SMS isn't available on Anveo Direct — Anveo -support directs users to an Anveo **Retail** account for it, which is a -second account to fund and manage. Any of the free texting apps covers -sending without involving this box. +Two things confirmed this way, neither of them a working path yet: + +- **Outbound over SIP** — sending a `MESSAGE` toward Anveo's trunk (the + mirror image of the inbound webhook) was tried directly: Asterisk placed + the request fine, but Anveo's SBC came back `501 Not Implemented`. A real, + unambiguous rejection of the method itself — not a config or auth problem, + and not something any amount of retrying the request format will get past. +- **Outbound via Anveo's HTTP API** (`api/v1.asp?action=sms`) — real and + documented, but gated behind Anveo support activating SMS-API access for + this account; still pending as of this writing (limited support tickets on + this account, so this has been a slow back-and-forth rather than a quick + toggle). + +Until that activation lands, there's no outbound path from this box. Any of +the free texting apps covers sending in the meantime without involving it. ### MMS and group texts diff --git a/services/sms-inbound.sh b/services/sms-inbound.sh index 5bcccef..716d05a 100644 --- a/services/sms-inbound.sh +++ b/services/sms-inbound.sh @@ -60,11 +60,8 @@ _sms_detect_container_name() { # ── AMI: a scoped manager.conf user, MessageSend only ─────────────────────── # Localhost-only (bindaddr + permit below) since the relay runs on this same # host, not over the network — no firewall port to open for this. "message" -# is the AMI permission class MessageSend needs; this hasn't been confirmed -# against a live MessageSend call yet (see the relay's own comment on -# ami_deliver) — if the very first real delivery attempt gets an AMI -# permission error in the journal, widen read/write here first before -# looking anywhere else. +# as the AMI permission class is confirmed live: MessageSend succeeds with +# just read/write = message, no wider class needed. # # Idempotent: creates manager.conf fresh if absent, otherwise ensures # enabled=yes and the [smsrelay] section exist without disturbing anything @@ -333,21 +330,25 @@ def _ami_read_response(sock_file): def ami_deliver(from_number, to_exts, body): """Logs into AMI once and sends one MessageSend action per recipient. - "message" as the AMI permission class is confirmed live (login succeeds). - Destination (not To) is what actually resolves an outgoing message's - endpoint/technology -- confirmed against this box's own - `manager show command MessageSend`: To alone is documented as a - backward-compatible fallback for the destination, but live testing - (bare "pjsip:212", then "pjsip:212@domain", both for To with no - Destination) produced zero SIP wire traffic in either case -- `pjsip - set logger on` during a real attempt showed Asterisk never even tried - reaching the target's registered contact, so that fallback path isn't - actually wired up on this Asterisk version regardless of what the docs - promise. Destination's documented "endpoint" form -- bare "pjsip:", - no domain -- resolves via the endpoint's own default aor/contact, which - is exactly the live, registered contact `pjsip show contacts` already - confirmed exists. Every AMI response is still logged in full so the - next attempt is self-diagnosing if this isn't the whole fix either. + Confirmed working end-to-end live (2026-07-27): a real inbound text + delivered through this exact path landed in Sipnetic with a SIP 200 OK. + Two things worth keeping straight if this ever needs touching again: + + - "message" as the AMI permission class is enough on its own (login + + MessageSend both succeed with just read/write = message). + - Use Destination, not To, to resolve the endpoint. `manager show + command MessageSend` documents To-alone as a backward-compatible + fallback for the destination when Destination is omitted, but live + testing that fallback (bare "pjsip:212", then "pjsip:212@domain", + both as To with no Destination) produced zero SIP wire traffic + either way -- Asterisk never even attempted to reach the target's + registered contact. Destination's documented "endpoint" form (bare + "pjsip:", no domain) is what actually resolves via the + endpoint's default aor/contact and is what's used below. + + Every AMI response is still logged in full, since a wrong recipient, + an unregistered contact, etc. all still need to be diagnosable from + the journal alone. Returns (delivered_count, total_count).""" if not AMI_SECRET: