txter 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/gateway.rb ADDED
@@ -0,0 +1,45 @@
1
+ module Txter
2
+ class Gateway
3
+ class Request
4
+ end
5
+
6
+ class Response
7
+ def initialize(*args)
8
+ @options = args.last
9
+ end
10
+
11
+ def success?
12
+ :success == @options[:status]
13
+ end
14
+ end
15
+ Success = Txter::Gateway::Response.new(:status => :success)
16
+ Error = Txter::Gateway::Response.new(:status => :error)
17
+
18
+ def self.deliver(*args)
19
+ # subclasses should actually do something here
20
+ Success
21
+ end
22
+
23
+ def self.unblock(*args)
24
+ # subclasses should actually do something here
25
+ Success
26
+ end
27
+
28
+ def self.current
29
+ case Txter.configuration.gateway
30
+ when 'twilio'
31
+ gem 'twiliolib'
32
+ require 'twiliolib'
33
+ GatewayTwilio
34
+ when '4info'
35
+ Gateway4info
36
+ when 'test'
37
+ Txter::Gateway
38
+ else
39
+ raise "You need to specify your Txter gateway!"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ require File.join File.dirname(__FILE__), 'gateway_4info'
45
+ require File.join File.dirname(__FILE__), 'gateway_twilio'
@@ -0,0 +1,114 @@
1
+ module Txter
2
+ class Gateway4info < Txter::Gateway
3
+
4
+ API = 'http://gateway.4info.net/msg'
5
+
6
+ def self.deliver(message, number)
7
+ Gateway4info.perform Request.new.deliver_message(message, number)
8
+ end
9
+
10
+ def self.perform(body)
11
+ if :live == Txter.mode
12
+ require 'net/http'
13
+ uri = URI.parse API
14
+ body = start do |http|
15
+ http.post(
16
+ uri.path,
17
+ body,
18
+ {'Content-Type' => 'text/xml'}
19
+ ).read_body
20
+ end
21
+ Response.new(body)
22
+ else
23
+ Txter.log "Would have sent to 4info.net: #{body}"
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def self.start
30
+ c = Txter.configuration
31
+ net = c.proxy_address ?
32
+ Net::HTTP::Proxy(
33
+ c.proxy_address,
34
+ c.proxy_port,
35
+ c.proxy_username,
36
+ c.proxy_password) :
37
+ Net::HTTP
38
+ uri = URI.parse API
39
+ net.start(uri.host, uri.port) do |http|
40
+ yield http
41
+ end
42
+ end
43
+
44
+ class Response < Txter::Gateway::Response
45
+ def initialize(xml)
46
+ gem 'hpricot'
47
+ require 'hpricot'
48
+ @xml = xml
49
+ @body = Hpricot.parse(xml)
50
+ end
51
+
52
+ def inspect
53
+ @xml.to_s
54
+ end
55
+
56
+ def [](name)
57
+ nodes = (@body/name)
58
+ 1 == nodes.size ? nodes.first : nodes
59
+ end
60
+
61
+ def success?
62
+ 'Success' == self['message'].inner_text
63
+ end
64
+
65
+ def confirmation_code
66
+ self[:confcode].inner_text
67
+ end
68
+ end
69
+
70
+ class Request < Txter::Gateway::Request
71
+
72
+ attr_accessor :number
73
+ attr_accessor :message
74
+ attr_accessor :config
75
+
76
+ def initialize
77
+ unless Txter.configured?
78
+ raise "You need to configure Txter before using it!"
79
+ end
80
+ self.config = Txter.configuration
81
+ end
82
+
83
+ def deliver_message(message, number)
84
+ self.number = Txter.internationalize(number)
85
+ self.message = message
86
+
87
+ template(:deliver).render(self)
88
+ end
89
+
90
+ def confirm(number)
91
+ self.number = Txter.internationalize(number)
92
+
93
+ template(:confirm).render(self)
94
+ end
95
+
96
+ def unblock(number)
97
+ self.number = Txter.internationalize(number)
98
+
99
+ template(:unblock).render(self)
100
+ end
101
+
102
+ protected
103
+
104
+ def template(name)
105
+ # Haml templates for XML
106
+ require 'cgi'
107
+ templates = Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), '4info_templates', '*.haml')))
108
+ file = templates.detect {|t| File.basename(t).chomp('.haml').to_sym == name.to_sym }
109
+ raise ArgumentError, "Missing 4Info template: #{name}" unless file
110
+ Haml::Engine.new(File.read(file))
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,45 @@
1
+ module Txter
2
+ class GatewayTwilio < Txter::Gateway
3
+
4
+ API_VERSION = '2008-08-01'
5
+
6
+ class << self
7
+
8
+ def deliver(message, to, from = nil)
9
+
10
+ from ||= Txter.configuration.default_from_phone_number
11
+ raise "'From' number required for Twilio" unless from
12
+
13
+ response = post 'To' => to,
14
+ 'From' => from,
15
+ 'Body' => message
16
+
17
+ Net::HTTPCreated == response.code_type ?
18
+ Txter::Gateway::Success :
19
+ Txter::Gateway::Error
20
+ end
21
+
22
+ def account
23
+ @account ||= begin
24
+ if Txter.configuration.client_id.blank? ||
25
+ Txter.configuration.client_key.blank?
26
+ raise "Add your Twilio account id (as client_id) and token (as client_key) to the Txter.configure block"
27
+ end
28
+
29
+ Twilio::RestAccount.new(
30
+ Txter.configuration.client_id,
31
+ Txter.configuration.client_key
32
+ )
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def post(data = {})
39
+ account.request "/#{API_VERSION}/Accounts/#{Txter.configuration.client_id}/SMS/Messages",
40
+ "POST",
41
+ data
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/txter.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Txter
2
+ class << self
3
+ def gateway
4
+ Txter::Gateway.current
5
+ end
6
+
7
+ delegate :deliver, :to => :gateway
8
+ delegate :unblock, :to => :gateway
9
+
10
+ def log(msg)
11
+ if defined?(Rails)
12
+ Rails.logger.info msg
13
+ else
14
+ STDOUT.puts msg
15
+ end
16
+ end
17
+
18
+ def numerize(numberish)
19
+ numberish.to_s.scan(/\d+/).join
20
+ end
21
+
22
+ def internationalize(given_number)
23
+ number = numerize(given_number)
24
+ case number.size
25
+ when 10
26
+ "+1#{number}"
27
+ when 11
28
+ "+#{number}"
29
+ when 12
30
+ number =~ /\+\d(11)/ ? number : nil
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def confirmation_message(confirmation_code)
37
+ "4INFO alert confirm. code: #{confirmation_code} Enter code on web to verify phone. Msg&data rates may apply. Freq set by u. T&C & support at www.4info.com. Txt HELP for help"
38
+ end
39
+
40
+ def generate_confirmation_code
41
+ chars = (0..9).to_a + ('A'..'Z').to_a
42
+ (0...6).collect { chars[Kernel.rand(chars.length)] }.join
43
+ end
44
+ end
45
+ end
46
+
47
+ require File.join(File.dirname(__FILE__), 'configuration')
48
+ require File.join(File.dirname(__FILE__), 'gateway')
49
+ require File.join(File.dirname(__FILE__), 'contactable')
50
+ require File.join(File.dirname(__FILE__), 'controller')
data/test/.gitignore ADDED
@@ -0,0 +1 @@
1
+ debug.log
data/test/database.yml ADDED
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :database: plugin.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :database: ":memory:"
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: plugin_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: rails
17
+ :password:
18
+ :database: plugin_test
@@ -0,0 +1,75 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Gateway4infoTest < ActiveSupport::TestCase
4
+
5
+ Error = Txter::Gateway4info::Response.new('<?xml version="1.0" encoding="UTF-8"?>
6
+ <response>
7
+ <status>
8
+ <id>4</id>
9
+ <message>Error</message>
10
+ </status>
11
+ </response>')
12
+ Success = Txter::Gateway4info::Response.new('<?xml version="1.0" ?>
13
+ <response>
14
+ <requestId>F81D4FAE-7DEC-11D0-A765-00A0C91E6BF6</requestId>
15
+ <status>
16
+ <id>1</id>
17
+ <message>Success</message>
18
+ </status>
19
+ </response>')
20
+
21
+ context "with 4info gateway" do
22
+ setup {
23
+ Txter.configure do |config|
24
+ config.client_id = 12345
25
+ config.client_key = 'ABC123'
26
+ config.gateway = '4info'
27
+ end
28
+ }
29
+ context "with stubbed success" do
30
+ setup {
31
+ Txter::Gateway4info.stubs(:perform).returns(Success)
32
+ }
33
+ should "generate a success response object" do
34
+ assert Txter.deliver("msg", "1-555-867-5309").success?
35
+ end
36
+ end
37
+ context "with stubbed error" do
38
+ setup {
39
+ Txter::Gateway4info.stubs(:perform).returns(Error)
40
+ }
41
+ should "generate a success response object" do
42
+ assert !Txter.deliver("msg", "1-555-867-5309").success?
43
+ end
44
+ end
45
+ should "create proper xml for delivery" do
46
+ expected = <<-EOXML
47
+ <?xml version='1.0' encoding='utf-8' ?>
48
+ <request clientId='12345' clientKey='ABC123' type='MESSAGE'>
49
+ <message>
50
+ <recipient>
51
+ <type>5</type>
52
+ <id>+15558675309</id>
53
+ </recipient>
54
+ <text>msg</text>
55
+ </message>
56
+ </request>
57
+ EOXML
58
+ assert_equal expected, Txter::Gateway4info::Request.new.deliver_message("msg", "1-555-867-5309")
59
+ end
60
+ should "create proper xml for unblock" do
61
+ expected = <<-EOXML
62
+ <?xml version='1.0' encoding='utf-8' ?>
63
+ <request clientId='12345' clientKey='ABC123' type='UNBLOCK'>
64
+ <unblock>
65
+ <recipient>
66
+ <type>5</type>
67
+ <id>+15558675309</id>
68
+ </recipient>
69
+ </unblock>
70
+ </request>
71
+ EOXML
72
+ assert_equal expected, Txter::Gateway4info::Request.new.unblock("1-555-867-5309")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Gateway4infoTest < ActiveSupport::TestCase
4
+
5
+ context "with twilio gateway" do
6
+ setup {
7
+ Txter.configure do |config|
8
+ config.client_id = 12345
9
+ config.client_key = 'ABC123'
10
+ config.gateway = 'twilio'
11
+ end
12
+ }
13
+ # TODO: figure out what's left to test
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ gem 'test-unit'
3
+ require 'test/unit'
4
+ require 'mocha'
5
+ require 'active_support'
6
+ require 'active_record'
7
+ require 'active_support/test_case'
8
+ require 'shoulda'
9
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'txter')
10
+
11
+ # default test configuration
12
+ Txter.configure do |config|
13
+ config.client_id = '1'
14
+ config.client_key = 'ABCDEF'
15
+ config.gateway = 'test'
16
+ end
17
+
18
+ Txter.mode = :test
19
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
20
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
21
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
22
+
23
+ ActiveRecord::Schema.define(:version => 1) do
24
+ create_table :users do |t|
25
+ t.column :sms_phone_number, :string
26
+ t.column :sms_confirmed_phone_number, :string
27
+ t.column :sms_blocked, :boolean
28
+ t.column :sms_confirmation_attempted, :datetime
29
+ t.column :sms_confirmation_code, :string
30
+
31
+ t.column :type, :string
32
+ end
33
+ end
34
+
35
+ class User < ActiveRecord::Base
36
+ include Txter::Contactable
37
+ end
38
+
39
+ # kill all network access
40
+ module Txter
41
+ class Request
42
+ def start
43
+ raise "You forgot to stub out your net requests!"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,270 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class TxterContactableTest < ActiveSupport::TestCase
4
+
5
+ Success = Txter::Gateway::Response.new(:status => :success)
6
+ Error = Txter::Gateway::Response.new(:status => :error)
7
+
8
+ context "contactable class" do
9
+ setup {
10
+ @klass = Class.new
11
+ @klass.send :include, Txter::Contactable
12
+ Txter.configure do |config|
13
+ config.client_id = 12345
14
+ config.client_key = 'ABC123'
15
+ config.gateway = 'test'
16
+ end
17
+ }
18
+ Txter::Contactable::Attributes.each do |attribute|
19
+ should "begin with appropriate default for #{attribute}_column" do
20
+ assert_equal attribute, @klass.send("#{attribute}_column")
21
+ end
22
+ should "allow setting #{attribute}_column" do
23
+ new_column_name = :custom_column
24
+ @klass.send "#{attribute}_column", new_column_name
25
+ assert_equal new_column_name, @klass.send("#{attribute}_column")
26
+ end
27
+ end
28
+ end
29
+
30
+ context "contactable instance" do
31
+ setup {
32
+ User.delete_all
33
+ @user = User.create! User.sms_phone_number_column => '(555) 123-4567'
34
+ }
35
+
36
+ should "normalize phone number" do
37
+ assert_equal '5551234567', @user.txter_sms_phone_number
38
+ end
39
+ context "when phone number is blank" do
40
+ setup { @user.txter_sms_phone_number = nil}
41
+ context "confirming phone number" do
42
+ setup { @user.send_sms_confirmation! }
43
+ should_not_change "any attributes" do
44
+ @user.attributes.inspect
45
+ end
46
+ end
47
+ context "sending message" do
48
+ setup {
49
+ Txter.gateway.stubs(:perform).returns(Success)
50
+ @worked = @user.send_sms!('message')
51
+ }
52
+ should "not work" do assert !@worked end
53
+ should_not_change "any attributes" do
54
+ @user.attributes.inspect
55
+ end
56
+ end
57
+ end
58
+
59
+ context "when phone number exists" do
60
+ setup { @user.txter_sms_phone_number = "206-555-5555"}
61
+ context "confirming phone number" do
62
+ setup {
63
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
64
+ @worked = @user.send_sms_confirmation!
65
+ }
66
+ should "work" do assert @worked end
67
+ should "save confirmation number in proper attribute" do
68
+ assert @user.txter_sms_confirmation_code
69
+ end
70
+ should "set confirmation attempted time" do
71
+ assert @user.txter_sms_confirmation_attempted > 3.minutes.ago
72
+ end
73
+ should_change "stored code" do
74
+ @user.txter_sms_confirmation_code
75
+ end
76
+ should "not have number confirmed yet" do
77
+ assert !@user.sms_confirmed?
78
+ end
79
+ context "calling sms_confirm_with(right_code)" do
80
+ setup { @user.sms_confirm_with(@user.txter_sms_confirmation_code) }
81
+ should "work" do
82
+ assert @worked
83
+ end
84
+ should "save the phone number into the confirmed attribute" do
85
+ assert_equal @user.txter_sms_confirmed_phone_number,
86
+ @user.txter_sms_phone_number
87
+ end
88
+ should_change "confirmed phone number attribute" do
89
+ @user.txter_sms_confirmed_phone_number
90
+ end
91
+ context "and then attempting to confirm another number" do
92
+ setup {
93
+ @user.txter_sms_phone_number = "206-555-5555"
94
+ Txter.stubs(:deliver).returns(Success).once
95
+ @user.send_sms_confirmation!
96
+ }
97
+ should "eliminate the previous confirmed phone number" do
98
+ assert @user.txter_sms_confirmed_phone_number.blank?
99
+ end
100
+ should "un-confirm the record" do
101
+ assert !@user.sms_confirmed?
102
+ end
103
+ end
104
+ end
105
+ context "calling sms_confirm_with(right code, wrong case)" do
106
+ setup {
107
+ @downcased_code = @user.txter_sms_confirmation_code.downcase
108
+ @worked = @user.sms_confirm_with(@downcased_code)
109
+ }
110
+ should "have good test data" do
111
+ assert_not_equal @downcased_code,
112
+ @user.txter_sms_confirmation_code
113
+ end
114
+ should "work" do
115
+ assert @worked
116
+ end
117
+ should "save the phone number into the confirmed attribute" do
118
+ assert_equal @user.txter_sms_confirmed_phone_number,
119
+ @user.txter_sms_phone_number
120
+ end
121
+ end
122
+ context "calling sms_confirm_with(wrong_code)" do
123
+ setup { @worked = @user.sms_confirm_with('wrong_code') }
124
+ should "not work" do
125
+ assert !@worked
126
+ end
127
+ should "not save the phone number into the confirmed attribute" do
128
+ assert_not_equal @user.txter_sms_confirmed_phone_number,
129
+ @user.txter_sms_phone_number
130
+ end
131
+ should_not_change "confirmed phone number attribute" do
132
+ @user.reload.txter_sms_confirmed_phone_number
133
+ end
134
+ end
135
+ end
136
+ context "confirming phone number with a custom short code" do
137
+ context "with expectations" do
138
+ setup {
139
+ Txter.configure do |config|
140
+ config.short_code = '0005'
141
+ config.gateway = 'test'
142
+ config.client_id = 1
143
+ config.client_key = 'ABC123'
144
+ end
145
+ message = "long message blah blah MYCODE blah"
146
+ Txter.expects(:generate_confirmation_code).returns('MYCODE').once
147
+ Txter.expects(:confirmation_message).returns(message).once
148
+ Txter::Request.any_instance.expects(:deliver_message).with(message, @user.txter_sms_phone_number).once
149
+ @user.send_sms_confirmation!
150
+ }
151
+ end
152
+ context "(normal)" do
153
+ setup {
154
+ Txter.configure do |config|
155
+ config.short_code = '0005'
156
+ config.gateway = 'test'
157
+ config.client_id = 1
158
+ config.client_key = 'ABC123'
159
+ end
160
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
161
+ @worked = @user.send_sms_confirmation!
162
+ }
163
+ should "work" do
164
+ assert @worked
165
+ end
166
+ end
167
+ end
168
+ context "confirming phone number when the confirmation fails for some reason" do
169
+ setup {
170
+ Txter.stubs(:deliver).returns(Error)
171
+ @worked = @user.send_sms_confirmation!
172
+ }
173
+ should "not work" do assert !@worked end
174
+ should "not save confirmation number" do
175
+ assert @user.txter_sms_confirmation_code.blank?
176
+ end
177
+ should_not_change "stored code" do
178
+ @user.txter_sms_confirmation_code
179
+ end
180
+ end
181
+ end
182
+
183
+ context "when the number is not confirmed" do
184
+ context "sending a message" do
185
+ setup {
186
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
187
+ @result = @user.send_sms!('message')
188
+ }
189
+ should "send send no messages" do
190
+ assert_equal false, @result
191
+ end
192
+ end
193
+ end
194
+ context "when the number is blocked" do
195
+ setup {
196
+ @user.txter_sms_blocked = true
197
+ @user.save!
198
+ }
199
+ context "sending a message" do
200
+ setup { @result = @user.send_sms!('message') }
201
+ should "send nothing" do
202
+ assert_equal false, @result
203
+ end
204
+ end
205
+ end
206
+ context "when the number is confirmed" do
207
+ setup {
208
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
209
+ @user.stubs(:sms_confirmed?).returns(true)
210
+ }
211
+ context "sending a message" do
212
+ setup { @result = @user.send_sms!('message') }
213
+ should "send send exactly one message" do
214
+ assert_equal [7], @result
215
+ end
216
+ end
217
+ context "sending a blank message" do
218
+ setup { @result = @user.send_sms!('') }
219
+ should "send send zero messages" do
220
+ assert_equal false, @result
221
+ end
222
+ end
223
+ context "sending a huge message" do
224
+ context "without the allow_multiple flag" do
225
+ should "raise an error" do
226
+ assert_raises ArgumentError do
227
+ @user.send_sms!("A"*200)
228
+ end
229
+ end
230
+ end
231
+ context "with the allow_multiple flag" do
232
+ setup { @result = @user.send_sms!("A"*200, true) }
233
+ should "send multiple messages" do
234
+ assert_equal [160, 40], @result
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ context "when the number is not blocked" do
241
+ setup {
242
+ Txter::Request.any_instance.expects(:perform).never
243
+ }
244
+ context "unblocking" do
245
+ setup { @worked = @user.unblock_sms! }
246
+ should "not do anything" do
247
+ assert !@worked
248
+ end
249
+ should_not_change "any attributes" do
250
+ @user.attributes.inspect
251
+ end
252
+ end
253
+ end
254
+ context "when the number is blocked" do
255
+ setup {
256
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
257
+ @user.update_attributes!(:sms_blocked => true)
258
+ }
259
+ context "unblocking" do
260
+ setup { @worked = @user.unblock_sms! }
261
+ should "work" do
262
+ assert @worked
263
+ end
264
+ should_change "blocked attribute" do
265
+ @user.reload.sms_blocked
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end