twilio_contactable 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,144 @@
1
+ Twilo Contactable
2
+ =====
3
+
4
+ Twilo makes voice and SMS interactions easy. But if you want to be able to seamlessly validate your user's phone numbers for
5
+ both voice and text there's a lot of work you'll have to do in your Rails app. Unless you use this gem.
6
+
7
+ Why bother?
8
+ =====
9
+
10
+ Unless you're programming Ruby like it's PHP you don't enjoy passing strings around and writing all procedural code. This gem lets you
11
+ ask for a phone number from your users, confirm their ownership of it via SMS or Voice or both, and keep track of whether the number is
12
+ still validated when they edit it.
13
+
14
+
15
+ Setting Up Your Model
16
+ =====
17
+
18
+ Include Twilio::Contactable into your User class or whatever you're using to represent an entity with a phone number.
19
+
20
+ class User < ActiveRecord::Base
21
+ twilio_contactable
22
+ end
23
+
24
+ You can also specify which attributes you'd like to use instead of the defaults
25
+
26
+ class User < ActiveRecord::Base
27
+ twilio_contactable do |config|
28
+ config.phone_number_column :mobile_number
29
+ config.formatted_phone_number_column :formatted_mobile_number
30
+ config.sms_blocked_column :should_we_not_txt_this_user
31
+ config.sms_confirmation_code_column :the_sms_confirmation_code
32
+ config.sms_confirmation_attempted_column :when_was_the_sms_confirmation_attempted
33
+ config.sms_confirmed_phone_number_column :the_mobile_number_thats_been_confirmed_for_sms
34
+ config.voice_blocked_column :should_we_not_call_this_user
35
+ config.voice_confirmation_code_column :the_voice_confirmation_code
36
+ config.voice_confirmation_attempted_column :when_was_the_voice_confirmation_attempted
37
+ config.voice_confirmed_phone_number_column :the_mobile_number_thats_been_confirmed_for_voice
38
+
39
+ # Defaults to the name on the left (minus the '_column' at the end)
40
+ # e.g., the sms_blocked_column is 'sms_blocked'
41
+ #
42
+ # You don't need all those columns, omit any that you're sure you won't want.
43
+ end
44
+
45
+ Turning the thing on
46
+ ---
47
+
48
+ Because it can be expensive to send TXTs or make calls accidentally, it's required that you manually configure TwilioContactable in your app. Put this line in config/environments/production.rb or anything that loads _only_ in your production environment:
49
+
50
+ TwilioContactable.mode = :live
51
+
52
+ Skipping this step (or adding any other value) will prevent TXTs from actually being sent.
53
+
54
+ You'll also want to configure your setup with your client_id and client_key. Put this in the same file as above or in a separate initializer if you wish:
55
+
56
+ TwilioContactable.configure do |config|
57
+ # these three are required:
58
+ # (replace them with your actual account info)
59
+ config.client_id = 12345
60
+ config.client_key = 'ABC123'
61
+ config.website_address = 'http://myrubyapp.com' # <- Twilio.com needs to be able to find this
62
+
63
+ # the rest are optional:
64
+ config.short_code = 00001 # if you have a custom short code
65
+ config.proxy_address = 'my.proxy.com'
66
+ config.proxy_port = '80'
67
+ config.proxy_username = 'user'
68
+ config.proxy_password = 'password'
69
+ end
70
+
71
+ Phone number formatting
72
+ ---
73
+
74
+ Whatever is stored in the phone_number_column will be subject to normalized formatting:
75
+
76
+ user = User.create :phone_number => '(206) 555-1234'
77
+ user.phone_number # => (206) 555-1234
78
+ user.formatted_phone_number # => 12065551234 (defaults to US country code)
79
+
80
+ If you want to preserve the format of the number exactly as the user entered it you'll want
81
+ to save that in a different attribute.
82
+
83
+
84
+ Confirming Phone Number And Sending Messages
85
+ ====
86
+
87
+ When your users first hand you their number it will be unconfirmed:
88
+
89
+ @user = User.create(:phone_number => '555-222-3333')
90
+ @user.send_sms_confirmation! # fires off a TXT to the user with a generated confirmation code
91
+ @user.sms_confirmed? # => false, because we've only started the process
92
+
93
+ then ask the user for the confirmation code off their phone and pass it in to sms_confirm_with:
94
+
95
+ @user.sms_confirm_with('123XYZ')
96
+
97
+ If the code is right then the user's current phone number will be automatically marked as confirmed. You can check this at any time with:
98
+
99
+ @user.sms_confirmed? # => true
100
+ @user.send_sms!("Hi! This is a text message.")
101
+
102
+ If the code is wrong then the user's current phone number will stay unconfirmed.
103
+
104
+ @user.sms_confirmed? # => false
105
+ @user.send_sms!("Hi! This is a text message.") # sends nothing
106
+
107
+
108
+ Receiving TXTs and Voice calls
109
+ ====
110
+
111
+ You can also receive data posted to you from Twilio. This is how you'll receive messages, txts and notices that users have been blocked.
112
+ All you need is to create a bare controller and include TwilioContactable::Controller into it. Then specify which Ruby class you're using as a contactable user model (likely User)
113
+
114
+
115
+ class SMSController < ApplicationController
116
+ include TwilioContactable::Controller
117
+
118
+ sms_contactable User # or whichever class you included TwilioContactable::Contactable into
119
+ end
120
+
121
+ And hook this up in your routes.rb file like so:
122
+
123
+ ActionController::Routing::Routes.draw do |map|
124
+ map.route 'twilio', :controller => 'twilio_contactable', :action => :index
125
+ end
126
+
127
+ Now just tell Twilio to POST messages and block notices to you at:
128
+
129
+ http://myrubyapp.com/twilio
130
+
131
+ Now if your users reply to an SMS with 'STOP' or 'BLOCK' your database will be automatically updated to reflect this.
132
+
133
+ Incoming messages from a user will automatically be sent to that user's record:
134
+
135
+ # If "I love you!" is sent to you from a user with
136
+ # the phone number "555-111-9999"
137
+ # then the following will be executed:
138
+ User.find_by_phone_number('5551119999').receive_sms("I love you!")
139
+
140
+ It's up to you to implement the 'receive_sms' method on User.
141
+
142
+ That's it. Patches welcome, forks celebrated.
143
+
144
+ Copyright (c) 2010 [Jack Danger Canty](http://jåck.com/), released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "twilio_contactable"
8
+ gem.summary = %Q{Help authorize the users of your Rails apps to confirm and use their phone numbers}
9
+ gem.description = %Q{Does all the hard work with letting you confirm your user's phone numbers for Voice or TXT over the Twilio API}
10
+ gem.email = "gitcommit@6brand.com"
11
+ gem.homepage = "http://github.com/JackDanger/twilio_contactable"
12
+ gem.authors = ["Jack Danger Canty"]
13
+ gem.add_dependency "twiliolib", ">= 2.0.5"
14
+ gem.add_development_dependency "shoulda", ">= 0"
15
+ gem.add_development_dependency "mocha", ">= 0"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ desc "Test Twilio Contactable"
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/*_test.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.7.1
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # Include hook code here
2
+
3
+ require 'twilio_contactable'
@@ -0,0 +1,41 @@
1
+ module TwilioContactable
2
+ class << self
3
+ def mode
4
+ @@mode ||= :test
5
+ end
6
+
7
+ def mode=(new_mode)
8
+ @@mode = new_mode
9
+ end
10
+
11
+ def configured?
12
+ return false unless configuration
13
+ configuration.client_id && configuration.client_key
14
+ end
15
+
16
+ def configuration
17
+ @@configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure(&block)
21
+ @@configuration = Configuration.new(&block)
22
+ end
23
+ end
24
+
25
+ class Configuration
26
+
27
+ attr_accessor :client_id
28
+ attr_accessor :client_key
29
+ attr_accessor :short_code
30
+ attr_accessor :website_address
31
+ attr_accessor :default_from_phone_number
32
+ attr_accessor :proxy_address
33
+ attr_accessor :proxy_port
34
+ attr_accessor :proxy_username
35
+ attr_accessor :proxy_password
36
+
37
+ def initialize
38
+ yield self
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,204 @@
1
+ module TwilioContactable
2
+ module Contactable
3
+
4
+ Attributes = [
5
+ :phone_number,
6
+ :formatted_phone_number,
7
+ :sms_blocked,
8
+ :sms_confirmation_code,
9
+ :sms_confirmation_attempted,
10
+ :sms_confirmed_phone_number,
11
+ :voice_blocked,
12
+ :voice_confirmation_code,
13
+ :voice_confirmation_attempted,
14
+ :voice_confirmed_phone_number
15
+ ]
16
+
17
+ class Configuration
18
+ Attributes.each do |attr|
19
+ attr_accessor "#{attr}_column"
20
+ end
21
+ # the following is set when a controller includes TwilioContactable
22
+ # and calls twilio_contactable with this model as an argument
23
+ attr_accessor :controller
24
+
25
+ def initialize
26
+
27
+ yield self if block_given?
28
+
29
+ Attributes.each do |attr|
30
+ # set the defaults if the user hasn't specified anything
31
+ if send("#{attr}_column").blank?
32
+ send("#{attr}_column=", attr)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.included(model)
39
+
40
+ # set up the configuration, available within the class object
41
+ # via this same 'twilio_contactable' method
42
+ model.instance_eval do
43
+ def twilio_contactable(&block)
44
+ @@twilio_contactable = Configuration.new(&block) if block
45
+ @@twilio_contactable ||= Configuration.new
46
+ end
47
+ end
48
+
49
+ # normalize the phone number before it's saved in the database
50
+ # (only for model classes using callbacks a la ActiveModel,
51
+ # other folks will have to do this by hand)
52
+ if model.respond_to?(:before_save)
53
+ model.before_save :format_phone_number
54
+ model.class_eval do
55
+ def format_phone_number
56
+ self._TC_formatted_phone_number =
57
+ TwilioContactable.internationalize(_TC_phone_number)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # Set up a bridge to access the data for a specific instance
64
+ # by referring to the column values in the configuration.
65
+ def twilio_contactable
66
+ self.class.twilio_contactable
67
+ end
68
+ Attributes.each do |attr|
69
+ eval %Q{
70
+ def _TC_#{attr}
71
+ read_attribute self.class.twilio_contactable.#{attr}_column
72
+ end
73
+ def _TC_#{attr}=(value)
74
+ write_attribute self.class.twilio_contactable.#{attr}_column, value
75
+ end
76
+ }
77
+ end
78
+
79
+
80
+
81
+ # Sends an SMS validation request through the gateway
82
+ def send_sms_confirmation!
83
+ return false if _TC_sms_blocked
84
+ return true if sms_confirmed?
85
+ return false if _TC_phone_number.blank?
86
+
87
+ confirmation_code = TwilioContactable.generate_confirmation_code
88
+
89
+ # Use this class' confirmation_message method if it
90
+ # exists, otherwise use the generic message
91
+ message = (self.class.respond_to?(:confirmation_message) ?
92
+ self.class :
93
+ TwilioContactable).confirmation_message(confirmation_code)
94
+
95
+ if message.to_s.size > 160
96
+ raise ArgumentError, "SMS Confirmation Message is too long. Limit it to 160 characters of unescaped text."
97
+ end
98
+
99
+ response = TwilioContactable::Gateway.deliver(message, _TC_phone_number)
100
+
101
+ if response.success?
102
+ update_twilio_contactable_sms_confirmation confirmation_code
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ # Begins a phone call to the user where they'll need to type
109
+ # their confirmation code
110
+ def send_voice_confirmation!
111
+ return false if _TC_voice_blocked
112
+ return true if voice_confirmed?
113
+ return false if _TC_phone_number.blank?
114
+
115
+ confirmation_code = TwilioContactable.generate_confirmation_code
116
+
117
+ response = TwilioContactable::Gateway.initiate_voice_call(self, _TC_phone_number)
118
+
119
+ if response.success?
120
+ update_twilio_contactable_voice_confirmation confirmation_code
121
+ else
122
+ false
123
+ end
124
+ end
125
+
126
+ # Compares user-provided code with the stored confirmation
127
+ # code. If they match then the current phone number is set
128
+ # as confirmed by the user.
129
+ def sms_confirm_with(code)
130
+ if _TC_sms_confirmation_code.to_s.downcase == code.downcase
131
+ # save the phone number into the 'confirmed phone number' attribute
132
+ self._TC_sms_confirmed_phone_number = _TC_phone_number
133
+ save
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ # Returns true if the current phone number has been confirmed by
140
+ # the user for recieving TXT messages.
141
+ def sms_confirmed?
142
+ return false if _TC_sms_confirmed_phone_number.blank?
143
+ self._TC_sms_confirmed_phone_number == _TC_phone_number
144
+ end
145
+
146
+ # Compares user-provided code with the stored confirmation
147
+ # code. If they match then the current phone number is set
148
+ # as confirmed by the user.
149
+ def voice_confirm_with(code)
150
+ if _TC_voice_confirmation_code.to_s.downcase == code.downcase
151
+ # save the phone number into the 'confirmed phone number' attribute
152
+ self._TC_voice_confirmed_phone_number = _TC_phone_number
153
+ save
154
+ else
155
+ false
156
+ end
157
+ end
158
+
159
+ # Returns true if the current phone number has been confirmed by
160
+ # the user by receiving a phone call
161
+ def voice_confirmed?
162
+ return false if _TC_voice_confirmed_phone_number.blank?
163
+ self._TC_voice_confirmed_phone_number == _TC_phone_number
164
+ end
165
+
166
+ # Sends one or more TXT messages to the contactable record's
167
+ # mobile number (if the number has been confirmed).
168
+ # Any messages longer than 160 characters will need to be accompanied
169
+ # by a second argument <tt>true</tt> to clarify that sending
170
+ # multiple messages is intentional.
171
+ def send_sms!(msg, allow_multiple = false)
172
+ if msg.to_s.size > 160 && !allow_multiple
173
+ raise ArgumentError, "SMS Message is too long. Either specify that you want multiple messages or shorten the string."
174
+ end
175
+ return false if msg.to_s.strip.blank? || _TC_sms_blocked
176
+ return false unless sms_confirmed?
177
+
178
+ # split into pieces that fit as individual messages.
179
+ msg.to_s.scan(/.{1,160}/m).map do |text|
180
+ if TwilioContactable::Gateway.deliver_sms(text, _TC_phone_number).success?
181
+ text.size
182
+ else
183
+ false
184
+ end
185
+ end
186
+ end
187
+
188
+ protected
189
+
190
+ def update_twilio_contactable_sms_confirmation(new_code)
191
+ self._TC_sms_confirmation_code = new_code
192
+ self._TC_sms_confirmation_attempted = Time.now.utc
193
+ self._TC_sms_confirmed_phone_number = nil
194
+ save
195
+ end
196
+
197
+ def update_twilio_contactable_voice_confirmation(new_code)
198
+ self._TC_voice_confirmation_code = new_code
199
+ self._TC_voice_confirmation_attempted = Time.now.utc
200
+ self._TC_voice_confirmed_phone_number = nil
201
+ save
202
+ end
203
+ end
204
+ end