Coverage for cc_modules/cc_sms.py: 71%
56 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_sms.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Send SMS via supported backends**
30"""
32import logging
33from typing import Any, Dict, Type
35import requests
36from twilio.rest import Client
38from camcops_server.cc_modules.cc_constants import SmsBackendNames
41_backends = {}
42log = logging.getLogger(__name__)
45class MissingBackendException(Exception):
46 """
47 SMS backend not configured.
48 """
50 pass
53class SmsBackend:
54 """
55 Base class for sending SMS (text) messages.
56 """
58 def __init__(self, config: Dict[str, Any]) -> None:
59 """
60 Args:
61 config:
62 Dictionary of parameters specific to the backend in use.
63 """
64 self.config = config
66 def send_sms(
67 self, recipient: str, message: str, sender: str = None
68 ) -> None:
69 """
70 Send an SMS message.
72 Args:
73 recipient:
74 Recipient's phone number, as a string.
75 message:
76 Message contents.
77 sender:
78 Sender's phone number, if applicable.
79 """
80 raise NotImplementedError
83class ConsoleSmsBackend(SmsBackend):
84 """
85 Debugging "backend" -- just prints the message to the server console.
86 """
88 PREFIX = "SMS debugging: would have sent message"
90 @classmethod
91 def make_msg(cls, recipient: str, message: str) -> str:
92 """
93 Returns the message sent to the console.
94 """
95 return f"{cls.PREFIX} {message!r} to {recipient}"
97 def send_sms(
98 self, recipient: str, message: str, sender: str = None
99 ) -> None:
100 log.info(self.make_msg(recipient, message))
103class KapowSmsBackend(SmsBackend):
104 """
105 Send SMS messages via Kapow.
106 """
108 API_URL = "https://www.kapow.co.uk/scripts/sendsms.php"
109 # Parameters must be in lower case; see _read_sms_config().
110 PARAM_USERNAME = "username"
111 PARAM_PASSWORD = "password"
113 def __init__(self, config: Dict[str, Any]) -> None:
114 super().__init__(config)
115 assert (
116 self.PARAM_USERNAME in config
117 ), f"Kapow SMS: missing parameter {self.PARAM_USERNAME.upper()}"
118 assert (
119 self.PARAM_PASSWORD in config
120 ), f"Kapow SMS: missing parameter {self.PARAM_PASSWORD.upper()}"
122 def send_sms(
123 self, recipient: str, message: str, sender: str = None
124 ) -> None:
125 data = {
126 "username": self.config[self.PARAM_USERNAME],
127 "password": self.config[self.PARAM_PASSWORD],
128 "mobile": recipient,
129 "sms": message,
130 }
131 requests.post(self.API_URL, data=data)
134class TwilioSmsBackend(SmsBackend):
135 """
136 Send SMS messages via Twilio SMS.
137 """
139 # Parameters must be in lower case; see _read_sms_config().
140 PARAM_SID = "sid"
141 PARAM_TOKEN = "token"
142 PARAM_FROM_PHONE_NUMBER = "from_phone_number"
144 def __init__(self, config: Dict[str, Any]) -> None:
145 super().__init__(config)
146 assert (
147 self.PARAM_SID in config
148 ), f"Twilio SMS: missing parameter {self.PARAM_SID.upper()}"
149 assert (
150 self.PARAM_TOKEN in config
151 ), f"Twilio SMS: missing parameter {self.PARAM_TOKEN.upper()}"
152 assert self.PARAM_FROM_PHONE_NUMBER in config, (
153 f"Twilio SMS: missing parameter "
154 f"{self.PARAM_FROM_PHONE_NUMBER.upper()}"
155 )
156 self.client = Client(
157 username=self.config[self.PARAM_SID],
158 password=self.config[self.PARAM_TOKEN]
159 # account_sid: defaults to username
160 )
162 def send_sms(
163 self, recipient: str, message: str, sender: str = None
164 ) -> None:
165 # Twilio accounts are associated with a phone number so we ignore
166 # ``sender``
167 self.client.messages.create(
168 to=recipient,
169 body=message,
170 from_=self.config[self.PARAM_FROM_PHONE_NUMBER],
171 )
174def register_backend(name: str, backend_class: Type[SmsBackend]) -> None:
175 """
176 Internal function to register an SMS backend by name.
178 Args:
179 name:
180 Name of backend (e.g. as referred to in the config file).
181 backend_class:
182 Appropriate subclass of :class:`SmsBackend`.
183 """
184 global _backends
185 _backends[name] = backend_class
188register_backend(SmsBackendNames.CONSOLE, ConsoleSmsBackend)
189register_backend(SmsBackendNames.KAPOW, KapowSmsBackend)
190register_backend(SmsBackendNames.TWILIO, TwilioSmsBackend)
193def get_sms_backend(label: str, config: Dict[str, Any]) -> SmsBackend:
194 """
195 Make an instance of an SMS backend by name, passing it appropriate
196 backend-specific config options.
197 """
198 try:
199 backend_class = _backends[label]
200 except KeyError:
201 raise MissingBackendException(f"No backend {label!r} registered")
203 return backend_class(config)