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

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_sms.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28**Send SMS via supported backends** 

29 

30""" 

31 

32import logging 

33from typing import Any, Dict, Type 

34 

35import requests 

36from twilio.rest import Client 

37 

38from camcops_server.cc_modules.cc_constants import SmsBackendNames 

39 

40 

41_backends = {} 

42log = logging.getLogger(__name__) 

43 

44 

45class MissingBackendException(Exception): 

46 """ 

47 SMS backend not configured. 

48 """ 

49 

50 pass 

51 

52 

53class SmsBackend: 

54 """ 

55 Base class for sending SMS (text) messages. 

56 """ 

57 

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 

65 

66 def send_sms( 

67 self, recipient: str, message: str, sender: str = None 

68 ) -> None: 

69 """ 

70 Send an SMS message. 

71 

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 

81 

82 

83class ConsoleSmsBackend(SmsBackend): 

84 """ 

85 Debugging "backend" -- just prints the message to the server console. 

86 """ 

87 

88 PREFIX = "SMS debugging: would have sent message" 

89 

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}" 

96 

97 def send_sms( 

98 self, recipient: str, message: str, sender: str = None 

99 ) -> None: 

100 log.info(self.make_msg(recipient, message)) 

101 

102 

103class KapowSmsBackend(SmsBackend): 

104 """ 

105 Send SMS messages via Kapow. 

106 """ 

107 

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" 

112 

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()}" 

121 

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) 

132 

133 

134class TwilioSmsBackend(SmsBackend): 

135 """ 

136 Send SMS messages via Twilio SMS. 

137 """ 

138 

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" 

143 

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 ) 

161 

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 ) 

172 

173 

174def register_backend(name: str, backend_class: Type[SmsBackend]) -> None: 

175 """ 

176 Internal function to register an SMS backend by name. 

177 

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 

186 

187 

188register_backend(SmsBackendNames.CONSOLE, ConsoleSmsBackend) 

189register_backend(SmsBackendNames.KAPOW, KapowSmsBackend) 

190register_backend(SmsBackendNames.TWILIO, TwilioSmsBackend) 

191 

192 

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") 

202 

203 return backend_class(config)