diff --git a/app/models/agents/twilio_receive_text_agent.rb b/app/models/agents/twilio_receive_text_agent.rb new file mode 100644 index 00000000..b3c7d92c --- /dev/null +++ b/app/models/agents/twilio_receive_text_agent.rb @@ -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 diff --git a/spec/models/agents/twilio_receive_text_agent_spec.rb b/spec/models/agents/twilio_receive_text_agent_spec.rb new file mode 100644 index 00000000..5622e7ad --- /dev/null +++ b/spec/models/agents/twilio_receive_text_agent_spec.rb @@ -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(["", 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(["thanks!", 201, "text/xml"]) + expect(Event.last.payload).to eq(payload) + end + end +end