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
3"""
4FileHandlers that send SMS messages for high-priority human-in-the-loop alerts.
6You will need to configure certain environmental variables to let LLAMA know
7how to send messages to the outside world.
8"""
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
24LOGGER = logging.getLogger(__name__)
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:
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.
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):
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:
60.. code::
62{textwrap.indent(NOT_CONFIGURED_MESSAGE, 3*' ')}
64If you have not specified phone numbers with which to contact team members, you
65will see the following message in your warnings and errors:
67.. code::
69{textwrap.indent(NO_PHONEBOOK_MESSAGE, 3*' ')}
71You will need to specify environmental variables with the missing configuration
72data as described above in order to use SMS receipts.
73"""
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)
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)
99 def send_message(self, body, recipient):
100 """Send a text message.
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 )
117# pylint: disable=duplicate-bases
118class SMSReceipt(UploadReceipt):
119 """A log file created when a text message is sent out to team members."""
121 FILENAME_FMT = 'rct_sms_{}.log'
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'])
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
141 @abstractproperty
142 def message_for_team(self):
143 """Must specify a formula for generating the message that the team
144 receives."""
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)
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 """
175 @property
176 def message_for_team(self):
177 return f"ALERT FROM LLAMA\nNew event: {SkymapInfo(self).graceid}"
179 UPLOAD = SkymapInfo