Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

106 lines
3.6 KiB
Python

"""Twilio SMS delivery helper — posts investigation findings via Twilio SMS.
This module is independent of the WhatsApp integration: WhatsApp delivery
lives in :mod:`integrations.whatsapp.delivery` and the two share no code.
"""
from __future__ import annotations
import logging
from typing import Any
from platform.common.truncation import truncate
from platform.notifications.delivery_errors import extract_http_error
from platform.notifications.delivery_transport import post_form
from platform.notifications.redaction import redact_token
logger = logging.getLogger(__name__)
_SMS_LIMIT = 1600
_TWILIO_BASE_URL = "https://api.twilio.com/2010-04-01/Accounts"
def post_twilio_sms(
to: str,
text: str,
account_sid: str,
auth_token: str,
from_number: str = "",
messaging_service_sid: str = "",
status_callback: str = "",
) -> tuple[bool, str, str]:
"""Send an SMS via the Twilio Messaging API.
Returns ``(success, error, message_sid)``. Either ``from_number`` or
``messaging_service_sid`` must be set; if both are provided,
``messaging_service_sid`` wins (Twilio's documented precedence).
"""
if not (from_number or messaging_service_sid):
return False, "Missing from_number or messaging_service_sid.", ""
logger.debug("[twilio-sms] post message to %s", to)
url = f"{_TWILIO_BASE_URL}/{account_sid}/Messages.json"
payload: dict[str, str] = {
"To": to.strip(),
"Body": text,
}
if messaging_service_sid:
payload["MessagingServiceSid"] = messaging_service_sid
elif from_number:
payload["From"] = from_number.strip()
if status_callback:
payload["StatusCallback"] = status_callback
response = post_form(
url,
payload,
auth=(account_sid, auth_token),
timeout=15.0,
)
if not response.ok:
error = redact_token(response.error, auth_token)
logger.warning("[twilio-sms] post exception: %s", error)
return False, error, ""
if response.status_code not in (200, 201):
error_message = extract_http_error(response.data, response.status_code, response.text)
error_message = redact_token(error_message, auth_token)
logger.warning("[twilio-sms] post failed: %s", error_message)
return False, error_message, ""
return True, "", str(response.data.get("sid") or "")
def send_twilio_sms_report(
report: str,
sms_ctx: dict[str, Any],
) -> tuple[bool, str, str]:
"""Send a truncated report as SMS via Twilio.
Returns ``(success, error, message_sid)``. ``sms_ctx`` must include
``account_sid``, ``auth_token``, ``to``, and either ``from_number`` or
``messaging_service_sid``. ``status_callback`` is optional.
"""
account_sid = str(sms_ctx.get("account_sid") or "")
auth_token = str(sms_ctx.get("auth_token") or "")
to = str(sms_ctx.get("to") or "")
from_number = str(sms_ctx.get("from_number") or "")
messaging_service_sid = str(sms_ctx.get("messaging_service_sid") or "")
status_callback = str(sms_ctx.get("status_callback") or "")
if not account_sid or not auth_token or not to:
return False, "Missing account_sid, auth_token, or to", ""
if not (from_number or messaging_service_sid):
return False, "Missing from_number or messaging_service_sid", ""
text = truncate(report, _SMS_LIMIT, suffix="…")
return post_twilio_sms(
to=to,
text=text,
account_sid=account_sid,
auth_token=auth_token,
from_number=from_number,
messaging_service_sid=messaging_service_sid,
status_callback=status_callback,
)