-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
An agent for creating events from text messages received from Twilio
don't need securerandom don't assume ENV['DOMAIN'] exists update TwilioReceiveTextAgent to use new receive_web_request method signature
- Loading branch information
Showing
2 changed files
with
199 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
module Agents | ||
class TwilioReceiveTextAgent < Agent | ||
cannot_be_scheduled! | ||
cannot_receive_events! | ||
|
||
gem_dependency_check { defined?(Twilio) } | ||
|
||
description do <<-MD | ||
The Twilio Receive Text Agent receives text messages from Twilio and emits them as events. | ||
#{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?} | ||
In order to create events with this agent, configure Twilio to send POST requests to: | ||
``` | ||
#{post_url} | ||
``` | ||
#{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id} | ||
Options: | ||
* `server_url` must be set to the URL of your | ||
Huginn installation (probably "https://#{ENV['DOMAIN']}"), which must be web-accessible. Be sure to set http/https correctly. | ||
* `account_sid` and `auth_token` are your Twilio account credentials. `auth_token` must be the primary auth token for your Twilio accout. | ||
* If `reply_text` is set, it's contents will be sent back as a confirmation text. | ||
* `expected_receive_period_in_days` - How often you expect to receive events this way. Used to determine if the agent is working. | ||
MD | ||
end | ||
|
||
def default_options | ||
{ | ||
'account_sid' => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', | ||
'auth_token' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', | ||
'server_url' => "https://#{ENV['DOMAIN'].presence || example.com}", | ||
'reply_text' => '', | ||
"expected_receive_period_in_days" => 1 | ||
} | ||
end | ||
|
||
def validate_options | ||
unless options['account_sid'].present? && options['auth_token'].present? && options['server_url'].present? && options['expected_receive_period_in_days'].present? | ||
errors.add(:base, 'account_sid, auth_token, server_url, and expected_receive_period_in_days are all required') | ||
end | ||
end | ||
|
||
def working? | ||
event_created_within?(interpolated['expected_receive_period_in_days']) && !recent_error_logs? | ||
end | ||
|
||
def post_url | ||
if interpolated['server_url'].present? | ||
"#{interpolated['server_url']}/users/#{user.id}/web_requests/#{id || ':id'}/sms-endpoint" | ||
else | ||
"https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || ':id'}/sms-endpoint" | ||
end | ||
end | ||
|
||
def receive_web_request(request) | ||
params = request.params.except(:action, :controller, :agent_id, :user_id, :format) | ||
method = request.method_symbol.to_s | ||
headers = request.headers | ||
|
||
# check the last url param: 'secret' | ||
secret = params.delete('secret') | ||
return ["Not Authorized", 401] unless secret == "sms-endpoint" | ||
|
||
signature = headers['HTTP_X_TWILIO_SIGNATURE'] | ||
|
||
# validate from twilio | ||
@validator ||= Twilio::Util::RequestValidator.new interpolated['auth_token'] | ||
if !@validator.validate(post_url, params, signature) | ||
error("Twilio Signature Failed to Validate\n\n"+ | ||
"URL: #{post_url}\n\n"+ | ||
"POST params: #{params.inspect}\n\n"+ | ||
"Signature: #{signature}" | ||
) | ||
return ["Not authorized", 401] | ||
end | ||
|
||
if create_event(payload: params) | ||
response = Twilio::TwiML::Response.new do |r| | ||
if interpolated['reply_text'].present? | ||
r.Message interpolated['reply_text'] | ||
end | ||
end | ||
return [response.text, 201, "text/xml"] | ||
else | ||
return ["Bad request", 400] | ||
end | ||
end | ||
|
||
# def client | ||
# @client ||= Twilio::REST::Client.new interpolated['account_sid'], interpolated['auth_token'] | ||
# end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
require 'rails_helper' | ||
|
||
# Twilio Params | ||
# https://www.twilio.com/docs/api/twiml/sms/twilio_request | ||
# url: https://b924379f.ngrok.io/users/1/web_requests/7/sms-endpoint | ||
# params: {"ToCountry"=>"US", "ToState"=>"NY", "SmsMessageSid"=>"SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "NumMedia"=>"0", "ToCity"=>"NEW YORK", "FromZip"=>"48342", "SmsSid"=>"SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "FromState"=>"MI", "SmsStatus"=>"received", "FromCity"=>"PONTIAC", "Body"=>"Lol", "FromCountry"=>"US", "To"=>"+1347555555", "ToZip"=>"10016", "NumSegments"=>"1", "MessageSid"=>"SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "AccountSid"=>"ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "From"=>"+12485551111", "ApiVersion"=>"2010-04-01"} | ||
# signature: K29NMD9+v5/QLzbdGZW/DRGyxNU= | ||
|
||
describe Agents::TwilioReceiveTextAgent do | ||
before do | ||
stub.any_instance_of(Twilio::Util::RequestValidator).validate { true } | ||
end | ||
|
||
let(:payload) { | ||
{ | ||
"ToCountry"=>"US", | ||
"ToState"=>"NY", | ||
"SmsMessageSid"=>"SMxxxxxxxxxxxxxxxx", | ||
"NumMedia"=>"0", | ||
"ToCity"=>"NEW YORK", | ||
"FromZip"=>"48342", | ||
"SmsSid"=>"SMxxxxxxxxxxxxxxxx", | ||
"FromState"=>"MI", | ||
"SmsStatus"=>"received", | ||
"FromCity"=>"PONTIAC", | ||
"Body"=>"Hy ", | ||
"FromCountry"=>"US", | ||
"To"=>"+1347555555", | ||
"ToZip"=>"10016", | ||
"NumSegments"=>"1", | ||
"MessageSid"=>"SMxxxxxxxxxxxxxxxx", | ||
"AccountSid"=>"ACxxxxxxxxxxxxxxxx", | ||
"From"=>"+12485551111", | ||
"ApiVersion"=>"2010-04-01"} | ||
} | ||
|
||
describe 'receive_twilio_text_message' do | ||
before do | ||
@agent = Agents::TwilioReceiveTextAgent.new( | ||
:name => 'twilioreceive', | ||
:options => { :account_sid => 'x', | ||
:auth_token => 'x', | ||
:server_url => 'http://example.com', | ||
:expected_receive_period_in_days => 1 | ||
} | ||
) | ||
@agent.user = users(:bob) | ||
@agent.save! | ||
end | ||
|
||
it 'should create event upon receiving request' do | ||
|
||
request = ActionDispatch::Request.new({ | ||
'action_dispatch.request.request_parameters' => payload.merge({"secret" => "sms-endpoint"}), | ||
'REQUEST_METHOD' => "POST", | ||
'HTTP_ACCEPT' => 'application/xml', | ||
'HTTP_X_TWILIO_SIGNATURE' => "HpS7PBa1Agvt4OtO+wZp75IuQa0=" | ||
}) | ||
|
||
out = nil | ||
expect { | ||
out = @agent.receive_web_request(request) | ||
}.to change { Event.count }.by(1) | ||
expect(out).to eq(["<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>", 201, "text/xml"]) | ||
expect(Event.last.payload).to eq(payload) | ||
end | ||
end | ||
|
||
describe 'receive_twilio_text_message and send a response' do | ||
before do | ||
@agent = Agents::TwilioReceiveTextAgent.new( | ||
:name => 'twilioreceive', | ||
:options => { :account_sid => 'x', | ||
:auth_token => 'x', | ||
:server_url => 'http://example.com', | ||
:reply_text => "thanks!", | ||
:expected_receive_period_in_days => 1 | ||
} | ||
) | ||
@agent.user = users(:bob) | ||
@agent.save! | ||
end | ||
|
||
it 'should create event and send back TwiML Message if reply_text is set' do | ||
out = nil | ||
request = ActionDispatch::Request.new({ | ||
'action_dispatch.request.request_parameters' => payload.merge({"secret" => "sms-endpoint"}), | ||
'REQUEST_METHOD' => "POST", | ||
'HTTP_ACCEPT' => 'application/xml', | ||
'HTTP_X_TWILIO_SIGNATURE' => "HpS7PBa1Agvt4OtO+wZp75IuQa0=" | ||
}) | ||
expect { | ||
out = @agent.receive_web_request(request) | ||
}.to change { Event.count }.by(1) | ||
expect(out).to eq(["<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Message>thanks!</Message></Response>", 201, "text/xml"]) | ||
expect(Event.last.payload).to eq(payload) | ||
end | ||
end | ||
end |