@@ -0,0 +1,96 @@ |
||
| 1 |
+module Agents |
|
| 2 |
+ class TwilioReceiveTextAgent < Agent |
|
| 3 |
+ cannot_be_scheduled! |
|
| 4 |
+ cannot_receive_events! |
|
| 5 |
+ |
|
| 6 |
+ gem_dependency_check { defined?(Twilio) }
|
|
| 7 |
+ |
|
| 8 |
+ description do <<-MD |
|
| 9 |
+ The Twilio Receive Text Agent receives text messages from Twilio and emits them as events. |
|
| 10 |
+ |
|
| 11 |
+ #{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?}
|
|
| 12 |
+ |
|
| 13 |
+ In order to create events with this agent, configure Twilio to send POST requests to: |
|
| 14 |
+ |
|
| 15 |
+ ``` |
|
| 16 |
+ #{post_url}
|
|
| 17 |
+ ``` |
|
| 18 |
+ |
|
| 19 |
+ #{'The placeholder symbols above will be replaced by their values once the agent is saved.' unless id}
|
|
| 20 |
+ |
|
| 21 |
+ Options: |
|
| 22 |
+ |
|
| 23 |
+ * `server_url` must be set to the URL of your |
|
| 24 |
+ Huginn installation (probably "https://#{ENV['DOMAIN']}"), which must be web-accessible. Be sure to set http/https correctly.
|
|
| 25 |
+ |
|
| 26 |
+ * `account_sid` and `auth_token` are your Twilio account credentials. `auth_token` must be the primary auth token for your Twilio accout. |
|
| 27 |
+ |
|
| 28 |
+ * If `reply_text` is set, it's contents will be sent back as a confirmation text. |
|
| 29 |
+ |
|
| 30 |
+ * `expected_receive_period_in_days` - How often you expect to receive events this way. Used to determine if the agent is working. |
|
| 31 |
+ MD |
|
| 32 |
+ end |
|
| 33 |
+ |
|
| 34 |
+ def default_options |
|
| 35 |
+ {
|
|
| 36 |
+ 'account_sid' => 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', |
|
| 37 |
+ 'auth_token' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', |
|
| 38 |
+ 'server_url' => "https://#{ENV['DOMAIN'].presence || example.com}",
|
|
| 39 |
+ 'reply_text' => '', |
|
| 40 |
+ "expected_receive_period_in_days" => 1 |
|
| 41 |
+ } |
|
| 42 |
+ end |
|
| 43 |
+ |
|
| 44 |
+ def validate_options |
|
| 45 |
+ unless options['account_sid'].present? && options['auth_token'].present? && options['server_url'].present? && options['expected_receive_period_in_days'].present? |
|
| 46 |
+ errors.add(:base, 'account_sid, auth_token, server_url, and expected_receive_period_in_days are all required') |
|
| 47 |
+ end |
|
| 48 |
+ end |
|
| 49 |
+ |
|
| 50 |
+ def working? |
|
| 51 |
+ event_created_within?(interpolated['expected_receive_period_in_days']) && !recent_error_logs? |
|
| 52 |
+ end |
|
| 53 |
+ |
|
| 54 |
+ def post_url |
|
| 55 |
+ if interpolated['server_url'].present? |
|
| 56 |
+ "#{interpolated['server_url']}/users/#{user.id}/web_requests/#{id || ':id'}/sms-endpoint"
|
|
| 57 |
+ else |
|
| 58 |
+ "https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || ':id'}/sms-endpoint"
|
|
| 59 |
+ end |
|
| 60 |
+ end |
|
| 61 |
+ |
|
| 62 |
+ def receive_web_request(request) |
|
| 63 |
+ params = request.params.except(:action, :controller, :agent_id, :user_id, :format) |
|
| 64 |
+ method = request.method_symbol.to_s |
|
| 65 |
+ headers = request.headers |
|
| 66 |
+ |
|
| 67 |
+ # check the last url param: 'secret' |
|
| 68 |
+ secret = params.delete('secret')
|
|
| 69 |
+ return ["Not Authorized", 401] unless secret == "sms-endpoint" |
|
| 70 |
+ |
|
| 71 |
+ signature = headers['HTTP_X_TWILIO_SIGNATURE'] |
|
| 72 |
+ |
|
| 73 |
+ # validate from twilio |
|
| 74 |
+ @validator ||= Twilio::Util::RequestValidator.new interpolated['auth_token'] |
|
| 75 |
+ if !@validator.validate(post_url, params, signature) |
|
| 76 |
+ error("Twilio Signature Failed to Validate\n\n"+
|
|
| 77 |
+ "URL: #{post_url}\n\n"+
|
|
| 78 |
+ "POST params: #{params.inspect}\n\n"+
|
|
| 79 |
+ "Signature: #{signature}"
|
|
| 80 |
+ ) |
|
| 81 |
+ return ["Not authorized", 401] |
|
| 82 |
+ end |
|
| 83 |
+ |
|
| 84 |
+ if create_event(payload: params) |
|
| 85 |
+ response = Twilio::TwiML::Response.new do |r| |
|
| 86 |
+ if interpolated['reply_text'].present? |
|
| 87 |
+ r.Message interpolated['reply_text'] |
|
| 88 |
+ end |
|
| 89 |
+ end |
|
| 90 |
+ return [response.text, 201, "text/xml"] |
|
| 91 |
+ else |
|
| 92 |
+ return ["Bad request", 400] |
|
| 93 |
+ end |
|
| 94 |
+ end |
|
| 95 |
+ end |
|
| 96 |
+end |
@@ -0,0 +1,99 @@ |
||
| 1 |
+require 'rails_helper' |
|
| 2 |
+ |
|
| 3 |
+# Twilio Params |
|
| 4 |
+# https://www.twilio.com/docs/api/twiml/sms/twilio_request |
|
| 5 |
+# url: https://b924379f.ngrok.io/users/1/web_requests/7/sms-endpoint |
|
| 6 |
+# 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"}
|
|
| 7 |
+# signature: K29NMD9+v5/QLzbdGZW/DRGyxNU= |
|
| 8 |
+ |
|
| 9 |
+describe Agents::TwilioReceiveTextAgent do |
|
| 10 |
+ before do |
|
| 11 |
+ stub.any_instance_of(Twilio::Util::RequestValidator).validate { true }
|
|
| 12 |
+ end |
|
| 13 |
+ |
|
| 14 |
+ let(:payload) {
|
|
| 15 |
+ {
|
|
| 16 |
+ "ToCountry"=>"US", |
|
| 17 |
+ "ToState"=>"NY", |
|
| 18 |
+ "SmsMessageSid"=>"SMxxxxxxxxxxxxxxxx", |
|
| 19 |
+ "NumMedia"=>"0", |
|
| 20 |
+ "ToCity"=>"NEW YORK", |
|
| 21 |
+ "FromZip"=>"48342", |
|
| 22 |
+ "SmsSid"=>"SMxxxxxxxxxxxxxxxx", |
|
| 23 |
+ "FromState"=>"MI", |
|
| 24 |
+ "SmsStatus"=>"received", |
|
| 25 |
+ "FromCity"=>"PONTIAC", |
|
| 26 |
+ "Body"=>"Hy ", |
|
| 27 |
+ "FromCountry"=>"US", |
|
| 28 |
+ "To"=>"+1347555555", |
|
| 29 |
+ "ToZip"=>"10016", |
|
| 30 |
+ "NumSegments"=>"1", |
|
| 31 |
+ "MessageSid"=>"SMxxxxxxxxxxxxxxxx", |
|
| 32 |
+ "AccountSid"=>"ACxxxxxxxxxxxxxxxx", |
|
| 33 |
+ "From"=>"+12485551111", |
|
| 34 |
+ "ApiVersion"=>"2010-04-01"} |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ describe 'receive_twilio_text_message' do |
|
| 38 |
+ before do |
|
| 39 |
+ @agent = Agents::TwilioReceiveTextAgent.new( |
|
| 40 |
+ :name => 'twilioreceive', |
|
| 41 |
+ :options => { :account_sid => 'x',
|
|
| 42 |
+ :auth_token => 'x', |
|
| 43 |
+ :server_url => 'http://example.com', |
|
| 44 |
+ :expected_receive_period_in_days => 1 |
|
| 45 |
+ } |
|
| 46 |
+ ) |
|
| 47 |
+ @agent.user = users(:bob) |
|
| 48 |
+ @agent.save! |
|
| 49 |
+ end |
|
| 50 |
+ |
|
| 51 |
+ it 'should create event upon receiving request' do |
|
| 52 |
+ |
|
| 53 |
+ request = ActionDispatch::Request.new({
|
|
| 54 |
+ 'action_dispatch.request.request_parameters' => payload.merge({"secret" => "sms-endpoint"}),
|
|
| 55 |
+ 'REQUEST_METHOD' => "POST", |
|
| 56 |
+ 'HTTP_ACCEPT' => 'application/xml', |
|
| 57 |
+ 'HTTP_X_TWILIO_SIGNATURE' => "HpS7PBa1Agvt4OtO+wZp75IuQa0=" |
|
| 58 |
+ }) |
|
| 59 |
+ |
|
| 60 |
+ out = nil |
|
| 61 |
+ expect {
|
|
| 62 |
+ out = @agent.receive_web_request(request) |
|
| 63 |
+ }.to change { Event.count }.by(1)
|
|
| 64 |
+ expect(out).to eq(["<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>", 201, "text/xml"]) |
|
| 65 |
+ expect(Event.last.payload).to eq(payload) |
|
| 66 |
+ end |
|
| 67 |
+ end |
|
| 68 |
+ |
|
| 69 |
+ describe 'receive_twilio_text_message and send a response' do |
|
| 70 |
+ before do |
|
| 71 |
+ @agent = Agents::TwilioReceiveTextAgent.new( |
|
| 72 |
+ :name => 'twilioreceive', |
|
| 73 |
+ :options => { :account_sid => 'x',
|
|
| 74 |
+ :auth_token => 'x', |
|
| 75 |
+ :server_url => 'http://example.com', |
|
| 76 |
+ :reply_text => "thanks!", |
|
| 77 |
+ :expected_receive_period_in_days => 1 |
|
| 78 |
+ } |
|
| 79 |
+ ) |
|
| 80 |
+ @agent.user = users(:bob) |
|
| 81 |
+ @agent.save! |
|
| 82 |
+ end |
|
| 83 |
+ |
|
| 84 |
+ it 'should create event and send back TwiML Message if reply_text is set' do |
|
| 85 |
+ out = nil |
|
| 86 |
+ request = ActionDispatch::Request.new({
|
|
| 87 |
+ 'action_dispatch.request.request_parameters' => payload.merge({"secret" => "sms-endpoint"}),
|
|
| 88 |
+ 'REQUEST_METHOD' => "POST", |
|
| 89 |
+ 'HTTP_ACCEPT' => 'application/xml', |
|
| 90 |
+ 'HTTP_X_TWILIO_SIGNATURE' => "HpS7PBa1Agvt4OtO+wZp75IuQa0=" |
|
| 91 |
+ }) |
|
| 92 |
+ expect {
|
|
| 93 |
+ out = @agent.receive_web_request(request) |
|
| 94 |
+ }.to change { Event.count }.by(1)
|
|
| 95 |
+ expect(out).to eq(["<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Message>thanks!</Message></Response>", 201, "text/xml"]) |
|
| 96 |
+ expect(Event.last.payload).to eq(payload) |
|
| 97 |
+ end |
|
| 98 |
+ end |
|
| 99 |
+end |