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