import logging from twilio.base.exceptions import TwilioRestException from twilio.rest import Client from infrasynth.shared.enums import ChannelType from infrasynth.shared.results import Result from .base import BaseChannel logger = logging.getLogger(__name__) class TwilioSMSChannel(BaseChannel): """SMS channel that sends via the Twilio Programmable SMS API. Configuration keys (read from ``ChannelConfig.config``): - ``account_sid`` (required) - ``auth_token`` (required) - ``from_number`` (required, E.164 format) """ channel_type = ChannelType.SMS def __init__(self, account_sid=None, auth_token=None, from_number=None): self.account_sid = account_sid self.auth_token = auth_token self.from_number = from_number @classmethod def from_config(cls, config: dict) -> "TwilioSMSChannel": config = {str(key).lower(): value for key, value in (config or {}).items()} return cls(**config) def send( self, recipient: str, subject: str, body: str, is_html: bool = True, attachments=None, ) -> Result[bool, str]: if not (self.account_sid and self.auth_token and self.from_number): return Result.err("Twilio credentials not configured") try: client = Client(self.account_sid, self.auth_token) message_body = (body or subject or "")[:1600] client.messages.create( to=recipient, from_=self.from_number, body=message_body, ) except TwilioRestException as exc: logger.exception("Twilio send failed for %s", recipient) return Result.err(f"Twilio error {exc.status}: {exc.msg}") except Exception as exc: # noqa: BLE001 logger.exception("Twilio send failed for %s", recipient) return Result.err(str(exc)) return Result.ok(True) def health_check(self) -> bool: return bool(self.account_sid and self.auth_token and self.from_number)