Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# (c) Stefan Countryman, 2017-2019 

2 

3""" 

4FileHandlers that send SMS messages for high-priority human-in-the-loop alerts. 

5 

6You will need to configure certain environmental variables to let LLAMA know 

7how to send messages to the outside world. 

8""" 

9 

10import logging 

11import datetime 

12from abc import abstractproperty 

13import textwrap 

14import json 

15import shutil 

16import tempfile 

17from twilio.rest import Client 

18from llama.classes import optional_env_var 

19from llama.filehandler import GenerationError 

20from llama.com.utils import UploadReceipt 

21from llama.files.advok import Advok 

22from llama.files.skymap_info import SkymapInfo 

23 

24LOGGER = logging.getLogger(__name__) 

25 

26NO_PHONEBOOK_MESSAGE = """You must specify phone numbers in a JSON format in 

27the environmental variable LLAMA_PHONE_NUMBERS in order to send SMS 

28receipts. Your phonebook should be defined in your .bashrc or somewhere similar 

29like: 

30 

31export LLAMA_PHONE_NUMBERS='[ 

32 { 

33 "phone_number": "+395555555555", 

34 "name": "John Smith" 

35 }, 

36 { 

37 "phone_number": "+12345678900", 

38 "name": "Jane Doe" 

39 } 

40]' 

41""" 

42NOT_CONFIGURED_MESSAGE = """Twilio API credentials are not configured; 

43you will not be able to submit text message notifications until this 

44is fixed. 

45 

46Please fill in your Twilio API credentials in e.g. your .bashrc, which should 

47look something like the following (where ``PLACEHOLDER`` has obviously been 

48replaced with your actual authentication information): 

49 

50export TWILIO_ACCOUNT_SID=PLACEHOLDER 

51export TWILIO_AUTH_TOKEN=PLACEHOLDER 

52export TWILIO_NUMBER=2407432233 

53""" 

54__doc__ += f""" 

55If you have not specified your twilio credentials, you will see the following 

56message in your warnings (which are usually supressed for optional features 

57like this), and if you try to send out SMS uploads, you'll see the following as 

58the error message explaining why file generation failed: 

59 

60.. code:: 

61 

62{textwrap.indent(NOT_CONFIGURED_MESSAGE, 3*' ')} 

63 

64If you have not specified phone numbers with which to contact team members, you 

65will see the following message in your warnings and errors: 

66 

67.. code:: 

68 

69{textwrap.indent(NO_PHONEBOOK_MESSAGE, 3*' ')} 

70 

71You will need to specify environmental variables with the missing configuration 

72data as described above in order to use SMS receipts. 

73""" 

74 

75 

76LLAMA_PHONE_NUMBERS_JSON = optional_env_var(['LLAMA_PHONE_NUMBERS'], 

77 NO_PHONEBOOK_MESSAGE)[0] 

78if LLAMA_PHONE_NUMBERS_JSON is None: 

79 LLAMA_PHONE_NUMBERS = None 

80else: 

81 LLAMA_PHONE_NUMBERS = json.loads(LLAMA_PHONE_NUMBERS_JSON) 

82TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_NUMBER = optional_env_var( 

83 ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_NUMBER'], 

84 NOT_CONFIGURED_MESSAGE 

85) 

86 

87 

88class MessageClient(object): 

89 """ 

90 A client for actually sending messages using Twilio's api, modified from 

91 their example app. 

92 """ 

93 def __init__(self): 

94 if None in [TWILIO_NUMBER, TWILIO_AUTH_TOKEN, TWILIO_AUTH_TOKEN]: 

95 raise GenerationError(NOT_CONFIGURED_MESSAGE) 

96 self.twilio_number = TWILIO_NUMBER 

97 self.twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) 

98 

99 def send_message(self, body, recipient): 

100 """Send a text message. 

101 

102 Parameters 

103 ---------- 

104 body : str 

105 The body of the text message. 

106 recipient : str 

107 The cell phone number of the recipient. 

108 """ 

109 self.twilio_client.messages.create( 

110 body=body, 

111 to=recipient, 

112 from_=self.twilio_number 

113 # media_url=['https://demo.twilio.com/owl.png']) 

114 ) 

115 

116 

117# pylint: disable=duplicate-bases 

118class SMSReceipt(UploadReceipt): 

119 """A log file created when a text message is sent out to team members.""" 

120 

121 FILENAME_FMT = 'rct_sms_{}.log' 

122 

123 @classmethod 

124 def send_message_to_team(cls, body): 

125 """Send a text message to all team members.""" 

126 if LLAMA_PHONEBOOK is None: 

127 raise GenerationError(NO_PHONEBOOK_MESSAGE) 

128 for team_member in LLAMA_PHONEBOOK: 

129 MessageClient().send_message(body, team_member['phone_number']) 

130 

131 @classmethod 

132 def set_class_attributes(cls, subclass): 

133 """See ``UploadReceipt.set_class_attributes``; this method additionally 

134 adds ``Advok`` to the ``DEPENDENCIES`` list for ``subclass`` (if it is 

135 not already there).""" 

136 super().set_class_attributes(subclass) 

137 if Advok not in subclass.DEPENDENCIES: 

138 subclass.DEPENDENCIES = tuple(subclass.DEPENDENCIES) + (Advok,) 

139 return subclass 

140 

141 @abstractproperty 

142 def message_for_team(self): 

143 """Must specify a formula for generating the message that the team 

144 receives.""" 

145 

146 def _generate(self): # pylint: disable=arguments-differ 

147 # perform and log the upload to a tempfile, then rename when finished 

148 # so that logfile existence is synonymous with upload success 

149 if LLAMA_PHONEBOOK is None: 

150 raise GenerationError(NO_PHONEBOOK_MESSAGE) 

151 with tempfile.NamedTemporaryFile(mode='w', dir=self.eventdir, 

152 delete=False) as outf: 

153 # make sure this is a real event. we don't want to waste money 

154 # and spam ourselves on test events. 

155 outf.write('Sending notifications to:\n') 

156 outf.write('{}\n'.format(LLAMA_PHONEBOOK)) 

157 outf.write('Message to be sent:\n') 

158 outf.write('{}\n'.format(self.message_for_team)) 

159 # timestamp 

160 outf.write('Sending at {}\n'.format(datetime.datetime.now())) 

161 self.send_message_to_team(self.message_for_team) 

162 outf.write('Succeeded at {}\n'.format(datetime.datetime.now())) 

163 fname = outf.name 

164 shutil.move(fname, self.fullpath) 

165 

166 

167# pylint: disable=duplicate-bases 

168@SMSReceipt.set_class_attributes 

169class RctSMSAdvokJSON(SMSReceipt): 

170 """ 

171 A log file created when alerting LLAMA team members of a new 

172 event that requires immediate attention. 

173 """ 

174 

175 @property 

176 def message_for_team(self): 

177 return f"ALERT FROM LLAMA\nNew event: {SkymapInfo(self).graceid}" 

178 

179 UPLOAD = SkymapInfo