62 lines
2 KiB
Python
62 lines
2 KiB
Python
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)
|