Source code for camcops_server.cc_modules.cc_sms

#!/usr/bin/env python

"""
camcops_server/cc_modules/cc_sms.py

===============================================================================

    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

===============================================================================

**Send SMS via supported backends**

"""

import logging
from typing import Any, Dict, Type

import requests
from twilio.rest import Client

from camcops_server.cc_modules.cc_constants import SmsBackendNames


_backends = {}
log = logging.getLogger(__name__)


[docs]class MissingBackendException(Exception): """ SMS backend not configured. """ pass
[docs]class SmsBackend: """ Base class for sending SMS (text) messages. """
[docs] def __init__(self, config: Dict[str, Any]) -> None: """ Args: config: Dictionary of parameters specific to the backend in use. """ self.config = config
[docs] def send_sms( self, recipient: str, message: str, sender: str = None ) -> None: """ Send an SMS message. Args: recipient: Recipient's phone number, as a string. message: Message contents. sender: Sender's phone number, if applicable. """ raise NotImplementedError
[docs]class ConsoleSmsBackend(SmsBackend): """ Debugging "backend" -- just prints the message to the server console. """ PREFIX = "SMS debugging: would have sent message"
[docs] @classmethod def make_msg(cls, recipient: str, message: str) -> str: """ Returns the message sent to the console. """ return f"{cls.PREFIX} {message!r} to {recipient}"
[docs] def send_sms( self, recipient: str, message: str, sender: str = None ) -> None: log.info(self.make_msg(recipient, message))
[docs]class KapowSmsBackend(SmsBackend): """ Send SMS messages via Kapow. """ API_URL = "https://www.kapow.co.uk/scripts/sendsms.php" # Parameters must be in lower case; see _read_sms_config(). PARAM_USERNAME = "username" PARAM_PASSWORD = "password"
[docs] def __init__(self, config: Dict[str, Any]) -> None: super().__init__(config) assert ( self.PARAM_USERNAME in config ), f"Kapow SMS: missing parameter {self.PARAM_USERNAME.upper()}" assert ( self.PARAM_PASSWORD in config ), f"Kapow SMS: missing parameter {self.PARAM_PASSWORD.upper()}"
[docs] def send_sms( self, recipient: str, message: str, sender: str = None ) -> None: data = { "username": self.config[self.PARAM_USERNAME], "password": self.config[self.PARAM_PASSWORD], "mobile": recipient, "sms": message, } requests.post(self.API_URL, data=data)
[docs]class TwilioSmsBackend(SmsBackend): """ Send SMS messages via Twilio SMS. """ # Parameters must be in lower case; see _read_sms_config(). PARAM_SID = "sid" PARAM_TOKEN = "token" PARAM_FROM_PHONE_NUMBER = "from_phone_number"
[docs] def __init__(self, config: Dict[str, Any]) -> None: super().__init__(config) assert ( self.PARAM_SID in config ), f"Twilio SMS: missing parameter {self.PARAM_SID.upper()}" assert ( self.PARAM_TOKEN in config ), f"Twilio SMS: missing parameter {self.PARAM_TOKEN.upper()}" assert self.PARAM_FROM_PHONE_NUMBER in config, ( f"Twilio SMS: missing parameter " f"{self.PARAM_FROM_PHONE_NUMBER.upper()}" ) self.client = Client( username=self.config[self.PARAM_SID], password=self.config[self.PARAM_TOKEN] # account_sid: defaults to username )
[docs] def send_sms( self, recipient: str, message: str, sender: str = None ) -> None: # Twilio accounts are associated with a phone number so we ignore # ``sender`` self.client.messages.create( to=recipient, body=message, from_=self.config[self.PARAM_FROM_PHONE_NUMBER], )
[docs]def register_backend(name: str, backend_class: Type[SmsBackend]) -> None: """ Internal function to register an SMS backend by name. Args: name: Name of backend (e.g. as referred to in the config file). backend_class: Appropriate subclass of :class:`SmsBackend`. """ global _backends _backends[name] = backend_class
register_backend(SmsBackendNames.CONSOLE, ConsoleSmsBackend) register_backend(SmsBackendNames.KAPOW, KapowSmsBackend) register_backend(SmsBackendNames.TWILIO, TwilioSmsBackend)
[docs]def get_sms_backend(label: str, config: Dict[str, Any]) -> SmsBackend: """ Make an instance of an SMS backend by name, passing it appropriate backend-specific config options. """ try: backend_class = _backends[label] except KeyError: raise MissingBackendException(f"No backend {label!r} registered") return backend_class(config)