Coverage for cc_modules/cc_sms.py: 71%

1"""

2 camcops_server/cc_modules/cc_sms.py

3

4===============================================================================

5

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

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

8

9 This file is part of CamCOPS.

10

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.

15

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.

20

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

23

24===============================================================================

25

26**Send SMS via supported backends**

27

28"""

29

30 import logging

31 from typing import Any, Dict, Type

32

33 import requests

34 from twilio.rest import Client

35

36 from camcops_server.cc_modules.cc_constants import SmsBackendNames

37

38

39 _backends = {}

40 log = logging.getLogger(__name__)

41

42

43 class MissingBackendException(Exception):

44 """

45 SMS backend not configured.

46 """

47

48 pass

49

50

51 class SmsBackend:

52 """

53 Base class for sending SMS (text) messages.

54 """

55

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

63

64 def send_sms(

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

66 ) -> None:

67 """

68 Send an SMS message.

69

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

79

80

81 class ConsoleSmsBackend(SmsBackend):

82 """

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

84 """

85

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

87

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

94

95 def send_sms(

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

97 ) -> None:

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

99

100

101 class KapowSmsBackend(SmsBackend):

102 """

103 Send SMS messages via Kapow.

104 """

105

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"

110

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

119

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)

130

131

132 class TwilioSmsBackend(SmsBackend):

133 """

134 Send SMS messages via Twilio SMS.

135 """

136

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"

141

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 )

159

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 )

170

171

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

173 """

174 Internal function to register an SMS backend by name.

175

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

184

185

186 register_backend(SmsBackendNames.CONSOLE, ConsoleSmsBackend)

187 register_backend(SmsBackendNames.KAPOW, KapowSmsBackend)

188 register_backend(SmsBackendNames.TWILIO, TwilioSmsBackend)

189

190

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

200

201 return backend_class(config)