Merge pull request #1418 from albertsun/feature/twilio-receive-texts

Twilio receive text message agent

Andrew Cantino 8 years ago
parent
commit
eae83df68c

+ 96 - 0
app/models/agents/twilio_receive_text_agent.rb

@@ -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

+ 99 - 0
spec/models/agents/twilio_receive_text_agent_spec.rb

@@ -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