I recently did some Twilio / Indigo integration but pushed a lot of the work onto a Node.js server (read: handling Twilio callback endpoints). I utilized Indigo's REST API in order to communicate from Real World (e.g. phone call / SMS) ==> Twilio ==(node.js server)==> Indigo .... and Indigo's Actions > Server Actions > Script and File Actions > Run Shell Script to execute scripts in order to communicate from Indigo ==(node.js server)==> Twilio ==> Real World.
It's pretty slick! I can text "arm" / "disarm" / etc. to a particular phone number and my Indigo server will react. If certain events occur, I receive SMS notification.
But the implementation seems pretty clunky seeing as Indigo already has its own webserver and uses the Python env (versus going out to JS). So, I started this plugin but have hit a few snags and have some questions on how we might make this plugin a reality. Please join in if you're at all interested!
Ideally, here's the minimum that I believe this plugin should support (feedback on this greatly appreciated!):
Config
Events
- An SMS is received
... I can't come up with a good reason to support triggering on an incoming phone call (versus triggering on an received SMS), since a phone call contains all the same info as an SMS less message data (which could/can be used for conditionals ... more in a second on this).
Actions
- Send SMS
- Make Call
Current Status
The plugin is almost ready to support the "Send SMS" Action, but now we hit the questions / issues ...
Questions / Issues
1. How does one include a dependency in an Indigo module?
The plugin needs to use some functions from the official Twilio Python library. While we could just tell the user to fire up terminal and use pip or easy_install to get the twilio-python module, it would be great if either:
A) the supporting Python scripts were included w/the plugin (I don't know how to do this correctly), or
B) they would auto-install. Sending SMS is ready if we can accommodate this one line of code:
- Code: Select all
from twilio.rest import TwilioRestClient
2. Receiving POSTed data from Twilio ... here's where things get tricky.
Every Twilio phone number has a voiceUrl and a messageUrl which must be defined (example: http://70.180.40.70:8176/voice). When your Twilio number receives an incoming call or text message, Twilio POSTs call/SMS data to the appropriate endpoint -- your endpoint must then use the data to (mostly for voice calls) render Twilio-flavored XML ("TwiML") that tells Twilio what to do next, example:
- Code: Select all
<Response>
<Say>Holy moly</Say>
</Response>
Challenge 1: Updating the voice and message URLs
If the Indigo server's IP address changes (e.g. 70.180.40.70 ==> 70.180.40.75), then the voiceUrl and messageUrl will no longer point at your server.
Solution 1(?)
In my node.js-based project, I handled this with a periodic routine that gets the server's external IP address and, if it changed since the last check, updates (via the Twilio API) the voice/message URLs, but I don't know how to create a comparable function in Python nor do I know where to put it in the Indigo web server (... and I think this level of modification would move beyond the scope of a normal plugin, yes?)
Challenge 2: Creating Twilio endpoints in the Indigo webserver
Again, apart from the scope of modifications likely falling outside of a normal plugin, I don't know how to navigate the Indigo webserver to create new endpoints.
Proposed SMS Solution
Establish an /sms endpoint that:
- Updates incomingSmsFrom and incomingSmsMessage Indigo variables
- Triggers the An SMS is received Event
Together, these two elements would allow the user to create Triggers that would fire on the reception of an SMS and could use conditional statement(s) on
a) who sent the SMS and/or
b) what the SMS message body contains
Proposed Calling (Voice) Solution
Outgoing calls require a callback URL which will return "TwiML" (again, which tells Twilio what to do next) if/when the dialed person answers ... (Say: "Holy Moly").
If we want to be able to say different things for different Events (yes!), then that means we must be able to pass a whatToSay value out to the XML that is rendered when the dialed person answers ... and that might be 10 seconds after the call is initially placed, at which point other Events might have triggered (!).
Because of this Trigger ===> Answer delay (essentially a delay in rendering the XML with an appropriate whatToSay value), we really can't reference a single, static Indigo variable. If we did, then another Event might trigger and overwrite whatToSay with a completely new value.
One solution would be to (upon a "Make Call" Action):
1. Generate a random Indigo variable name into which to save that Action's whatToSay value
2. Place the call with the random variable in the passed callback URL
3. Establish a /voice/:whatToSay endpoint that:
- i. strips the randomized variable name off the URL parameter
- ii. gets the correct Indigo variable's value (this is whatToSay for that particular call)
- iii. deletes the Indigo variable (in order to prevent buildup of old, used, randomly-named variables)
Here's some code snippets:
Code for the 'Make Call' action
- Code: Select all
def makecall(self, action):
self.debugLog("makecall")
if action is not None:
twilioClient = TwilioRestClient(self.pluginPrefs["accountSid"], self.pluginPrefs["authToken"])
toNumber = str(action.props.get("toNumber"))
if action.props['fromNumber'] != "":
fromNumber = str(self.pluginPrefs[action.props["fromNumber"]])
else:
fromNumber = str(self.pluginPrefs["phone1"])
whatToSay = generate_random()
updateVar(whatToSay, value=str(action.props.get("callMessage")), folder=self.pluginPrefs["folderId"])
twilioClient.calls.create(
to=toNumber,
from_=fromNumber,
url=BASE_URL +'/voice/'+ whatToSay)
return True
Rough code for indigoreqhandler.py
- Code: Select all
def voice(self, *args, **kwargs):
"""
Return Type: xml response
"""
whatToSay = cherrypy.server.indigoDb.GetVariable(cherrypy.server.indigoConn, cherrypy.request.path_info.split('/')[2])
cherrypy.server.indigoDb.DeleteVariable(cherrypy.server.indigoConn, cherrypy.request.path_info.split('/')[2])
cherrypy.response.headers['Content-Type'] = 'text/xml'
tmpl = self._GetAndLockGlobalTemplate('voice.xml')
try:
self._FillDefaultTemplateAttrs(tmpl)
tmpl.whatToSay = whatToSay
return tmpl.RenderTemplate()
finally:
tmpl.ReleaseLock()
voice.exposed = True
Thoughts? Feedback?