4info 1.3.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +10 -10
- data/VERSION +1 -1
- data/init.rb +1 -1
- data/lib/{templates → 4info_templates}/confirm.haml +0 -0
- data/lib/{templates → 4info_templates}/deliver.haml +0 -0
- data/lib/{templates → 4info_templates}/unblock.haml +0 -0
- data/lib/configuration.rb +3 -1
- data/lib/contactable.rb +45 -60
- data/lib/controller.rb +3 -3
- data/lib/gateway.rb +45 -0
- data/lib/gateway_4info.rb +114 -0
- data/lib/gateway_twilio.rb +45 -0
- data/lib/{four_info.rb → txter.rb} +6 -5
- data/test/gateway_4info_test.rb +75 -0
- data/test/gateway_twilio_test.rb +15 -0
- data/test/test_helper.rb +6 -5
- data/test/{four_info_contactable_test.rb → txter_contactable_test.rb} +51 -73
- data/test/{four_info_controller_test.rb → txter_controller_test.rb} +8 -8
- data/test/txter_module_test.rb +56 -0
- data/{4info.gemspec → txter.gemspec} +7 -7
- metadata +20 -16
- data/lib/4info.rb +0 -3
- data/lib/request.rb +0 -75
- data/lib/response.rb +0 -27
- data/test/four_info_module_test.rb +0 -56
data/README.markdown
CHANGED
@@ -8,16 +8,16 @@ If you're using 4info.com as your SMS gateway this gem will give you a painless
|
|
8
8
|
Setting Up Your Model
|
9
9
|
=====
|
10
10
|
|
11
|
-
Include
|
11
|
+
Include Txter::Contactable into your User class or whatever you're using to represent an entity with a phone number.
|
12
12
|
|
13
13
|
class User < ActiveRecord::Base
|
14
|
-
include
|
14
|
+
include Txter::Contactable
|
15
15
|
end
|
16
16
|
|
17
17
|
You can also specify which attributes you'd like to use instead of the defaults
|
18
18
|
|
19
19
|
class User < ActiveRecord::Base
|
20
|
-
include
|
20
|
+
include Txter::Contactable
|
21
21
|
|
22
22
|
sms_phone_number_column :mobile_number
|
23
23
|
sms_blocked_column :is_sms_blocked
|
@@ -31,15 +31,15 @@ You can also specify which attributes you'd like to use instead of the defaults
|
|
31
31
|
Turning the thing on
|
32
32
|
---
|
33
33
|
|
34
|
-
Because it can be expensive to send TXTs accidentally, it's required that you manually configure
|
34
|
+
Because it can be expensive to send TXTs accidentally, it's required that you manually configure Txter in your app. Put this line in config/environments/production.rb or anything that loads _only_ in your production environment:
|
35
35
|
|
36
|
-
|
36
|
+
Txter.mode = :live
|
37
37
|
|
38
38
|
Skipping this step (or adding any other value) will prevent TXTs from actually being sent.
|
39
39
|
|
40
40
|
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:
|
41
41
|
|
42
|
-
|
42
|
+
Txter.configure do |config|
|
43
43
|
# these two are required:
|
44
44
|
# (replace them with your actual account info)
|
45
45
|
config.client_id = 12345
|
@@ -93,19 +93,19 @@ Receiving Messages From 4info.com
|
|
93
93
|
====
|
94
94
|
|
95
95
|
You can also receive data posted to you from 4info.com. This is how you'll receive messages and notices that users have been blocked.
|
96
|
-
All you need is to create a bare controller and include
|
96
|
+
All you need is to create a bare controller and include Txter::Controller into it. Then specify which Ruby class you're using as a contactable user model (likely User)
|
97
97
|
|
98
98
|
|
99
99
|
class SMSController < ApplicationController
|
100
|
-
include
|
100
|
+
include Txter::Controller
|
101
101
|
|
102
|
-
sms_contactable User # or whichever class you included
|
102
|
+
sms_contactable User # or whichever class you included Txter::Contactable into
|
103
103
|
end
|
104
104
|
|
105
105
|
And hook this up in your routes.rb file like so:
|
106
106
|
|
107
107
|
ActionController::Routing::Routes.draw do |map|
|
108
|
-
map.route '4info', :controller => '
|
108
|
+
map.route '4info', :controller => 'txter', :action => :index
|
109
109
|
end
|
110
110
|
|
111
111
|
Now just tell 4info.com to POST messages and block notices to you at:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.0
|
data/init.rb
CHANGED
File without changes
|
File without changes
|
File without changes
|
data/lib/configuration.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
module
|
1
|
+
module Txter
|
2
2
|
class << self
|
3
3
|
def mode
|
4
4
|
@@mode ||= :test
|
@@ -26,7 +26,9 @@ module FourInfo
|
|
26
26
|
|
27
27
|
attr_accessor :client_id
|
28
28
|
attr_accessor :client_key
|
29
|
+
attr_accessor :gateway
|
29
30
|
attr_accessor :short_code
|
31
|
+
attr_accessor :default_from_phone_number
|
30
32
|
attr_accessor :proxy_address
|
31
33
|
attr_accessor :proxy_port
|
32
34
|
attr_accessor :proxy_username
|
data/lib/contactable.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
module
|
1
|
+
module Txter
|
2
2
|
module Contactable
|
3
3
|
|
4
4
|
Attributes = [ :sms_phone_number,
|
@@ -27,14 +27,14 @@ module FourInfo
|
|
27
27
|
# provide helper methods to access the right value
|
28
28
|
# no matter which column it's stored in.
|
29
29
|
#
|
30
|
-
# e.g.: @user.
|
30
|
+
# e.g.: @user.txter_sms_confirmation_code
|
31
31
|
# == @user.send(User.sms_confirmation_code_column)
|
32
32
|
model.class_eval "
|
33
|
-
def
|
33
|
+
def txter_#{attribute}
|
34
34
|
send self.class.#{attribute}_column
|
35
35
|
end
|
36
|
-
alias_method :
|
37
|
-
def
|
36
|
+
alias_method :txter_#{attribute}?, :txter_#{attribute}
|
37
|
+
def txter_#{attribute}=(value)
|
38
38
|
send self.class.#{attribute}_column.to_s+'=', value
|
39
39
|
end
|
40
40
|
"
|
@@ -47,7 +47,7 @@ module FourInfo
|
|
47
47
|
model.before_save :normalize_sms_phone_number
|
48
48
|
model.class_eval do
|
49
49
|
def normalize_sms_phone_number
|
50
|
-
self.
|
50
|
+
self.txter_sms_phone_number = Txter.numerize(txter_sms_phone_number)
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
@@ -62,28 +62,44 @@ module FourInfo
|
|
62
62
|
if msg.to_s.size > 160 && !allow_multiple
|
63
63
|
raise ArgumentError, "SMS Message is too long. Either specify that you want multiple messages or shorten the string."
|
64
64
|
end
|
65
|
-
return false if msg.to_s.strip.blank? ||
|
65
|
+
return false if msg.to_s.strip.blank? || txter_sms_blocked?
|
66
66
|
return false unless sms_confirmed?
|
67
67
|
|
68
68
|
# split into pieces that fit as individual messages.
|
69
69
|
msg.to_s.scan(/.{1,160}/m).map do |text|
|
70
|
-
|
70
|
+
if Txter.deliver(text, txter_sms_phone_number).success?
|
71
|
+
text.size
|
72
|
+
else
|
73
|
+
false
|
74
|
+
end
|
71
75
|
end
|
72
76
|
end
|
73
77
|
|
74
|
-
# Sends an SMS validation request
|
75
|
-
# If request succeeds the 4info-generated confirmation code is saved
|
76
|
-
# in the contactable record.
|
78
|
+
# Sends an SMS validation request through the gateway
|
77
79
|
def send_sms_confirmation!
|
78
|
-
return false if
|
80
|
+
return false if txter_sms_blocked?
|
79
81
|
return true if sms_confirmed?
|
80
|
-
return false if
|
82
|
+
return false if txter_sms_phone_number.blank?
|
83
|
+
|
84
|
+
confirmation_code = Txter.generate_confirmation_code
|
85
|
+
|
86
|
+
# Use this class' confirmation_message method if it
|
87
|
+
# exists, otherwise use the generic message
|
88
|
+
message = (self.class.respond_to?(:confirmation_message) ?
|
89
|
+
self.class :
|
90
|
+
Txter).confirmation_message(confirmation_code)
|
91
|
+
|
92
|
+
if message.to_s.size > 160
|
93
|
+
raise ArgumentError, "SMS Confirmation Message is too long. Limit it to 160 characters of unescaped text."
|
94
|
+
end
|
81
95
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
96
|
+
response = Txter.deliver(message, txter_sms_phone_number)
|
97
|
+
|
98
|
+
if response.success?
|
99
|
+
update_txter_sms_confirmation confirmation_code
|
100
|
+
else
|
101
|
+
false
|
102
|
+
end
|
87
103
|
end
|
88
104
|
|
89
105
|
|
@@ -91,11 +107,11 @@ module FourInfo
|
|
91
107
|
# If request succeeds, changes the contactable record's
|
92
108
|
# sms_blocked_column to false.
|
93
109
|
def unblock_sms!
|
94
|
-
return false unless
|
110
|
+
return false unless txter_sms_blocked?
|
95
111
|
|
96
|
-
response =
|
112
|
+
response = Txter.unblock(txter_sms_phone_number)
|
97
113
|
if response.success?
|
98
|
-
self.
|
114
|
+
self.txter_sms_blocked = 'false'
|
99
115
|
save
|
100
116
|
else
|
101
117
|
false
|
@@ -106,9 +122,9 @@ module FourInfo
|
|
106
122
|
# code. If they match then the current phone number is set
|
107
123
|
# as confirmed by the user.
|
108
124
|
def sms_confirm_with(code)
|
109
|
-
if
|
125
|
+
if txter_sms_confirmation_code.to_s.downcase == code.downcase
|
110
126
|
# save the phone number into the 'confirmed phone number' attribute
|
111
|
-
self.
|
127
|
+
self.txter_sms_confirmed_phone_number = txter_sms_phone_number
|
112
128
|
save
|
113
129
|
else
|
114
130
|
false
|
@@ -118,47 +134,16 @@ module FourInfo
|
|
118
134
|
# Returns true if the current phone number has been confirmed by
|
119
135
|
# the user for recieving TXT messages.
|
120
136
|
def sms_confirmed?
|
121
|
-
return false if
|
122
|
-
|
137
|
+
return false if txter_sms_confirmed_phone_number.blank?
|
138
|
+
txter_sms_confirmed_phone_number == txter_sms_phone_number
|
123
139
|
end
|
124
140
|
|
125
141
|
protected
|
126
|
-
def confirm_four_info_sms_with_custom_message
|
127
|
-
confirmation_code = FourInfo.generate_confirmation_code
|
128
|
-
|
129
|
-
# Use this class' confirmation_message method if it
|
130
|
-
# exists, otherwise use the generic message
|
131
|
-
message = (self.class.respond_to?(:confirmation_message) ?
|
132
|
-
self.class :
|
133
|
-
FourInfo).confirmation_message(confirmation_code)
|
134
|
-
|
135
|
-
if message.to_s.size > 160
|
136
|
-
raise ArgumentError, "SMS Confirmation Message is too long. Limit it to 160 characters of unescaped text."
|
137
|
-
end
|
138
|
-
|
139
|
-
response = FourInfo::Request.new.deliver_message(message, four_info_sms_phone_number)
|
140
|
-
|
141
|
-
if response.success?
|
142
|
-
update_four_info_sms_confirmation confirmation_code
|
143
|
-
else
|
144
|
-
false
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def confirm_four_info_sms_with_default_message
|
149
|
-
response = FourInfo::Request.new.confirm(four_info_sms_phone_number)
|
150
|
-
|
151
|
-
if response.success?
|
152
|
-
update_four_info_sms_confirmation response.confirmation_code
|
153
|
-
else
|
154
|
-
false
|
155
|
-
end
|
156
|
-
end
|
157
142
|
|
158
|
-
def
|
159
|
-
self.
|
160
|
-
self.
|
161
|
-
self.
|
143
|
+
def update_txter_sms_confirmation(new_code)
|
144
|
+
self.txter_sms_confirmation_code = new_code
|
145
|
+
self.txter_sms_confirmation_attempted = Time.now.utc
|
146
|
+
self.txter_sms_confirmed_phone_number = nil
|
162
147
|
save
|
163
148
|
end
|
164
149
|
end
|
data/lib/controller.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
module
|
1
|
+
module Txter
|
2
2
|
module Controller
|
3
3
|
|
4
4
|
def self.included(controller)
|
@@ -25,7 +25,7 @@ module FourInfo
|
|
25
25
|
def recieve_xml
|
26
26
|
|
27
27
|
unless defined?(@@contactable_class)
|
28
|
-
raise RuntimeError, "Please define your user class in the
|
28
|
+
raise RuntimeError, "Please define your user class in the Txter controller via the 'sms_contactable' method"
|
29
29
|
end
|
30
30
|
|
31
31
|
request = params[:request]
|
@@ -33,7 +33,7 @@ module FourInfo
|
|
33
33
|
case request['type']
|
34
34
|
when 'BLOCK'
|
35
35
|
@contactable = find_contactable(request[:block][:recipient][:id])
|
36
|
-
@contactable.
|
36
|
+
@contactable.txter_sms_blocked = true
|
37
37
|
@contactable.save!
|
38
38
|
when 'MESSAGE'
|
39
39
|
@contactable = find_contactable(request[:message][:sender][:id])
|
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
|