blythedunham-sms_on_rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/Manifest +101 -0
- data/README +163 -0
- data/README.rdoc +211 -0
- data/Rakefile +76 -0
- data/app/controllers/admin/sms_on_rails/base_controller.rb +11 -0
- data/app/controllers/admin/sms_on_rails/drafts_controller.rb +75 -0
- data/app/controllers/admin/sms_on_rails/outbounds_controller.rb +117 -0
- data/app/controllers/admin/sms_on_rails/phone_carriers_controller.rb +85 -0
- data/app/controllers/admin/sms_on_rails/phone_numbers_controller.rb +101 -0
- data/app/controllers/sms_on_rails/creation_support.rb +99 -0
- data/app/controllers/sms_on_rails_controller.rb +14 -0
- data/app/helpers/admin/sms_on_rails/drafts_helper.rb +2 -0
- data/app/helpers/admin/sms_on_rails/phone_carriers_helper.rb +2 -0
- data/app/helpers/sms_on_rails/phone_numbers_helper.rb +9 -0
- data/app/helpers/sms_on_rails/sms_helper.rb +44 -0
- data/app/models/sms_on_rails/draft.rb +9 -0
- data/app/models/sms_on_rails/outbound.rb +17 -0
- data/app/models/sms_on_rails/phone_carrier.rb +14 -0
- data/app/models/sms_on_rails/phone_number.rb +8 -0
- data/app/views/admin/sms_on_rails/base/index.html.erb +5 -0
- data/app/views/admin/sms_on_rails/drafts/_show.html.erb +34 -0
- data/app/views/admin/sms_on_rails/drafts/edit.html.erb +36 -0
- data/app/views/admin/sms_on_rails/drafts/index.html.erb +32 -0
- data/app/views/admin/sms_on_rails/drafts/new.html.erb +34 -0
- data/app/views/admin/sms_on_rails/drafts/send_sms.html.erb +3 -0
- data/app/views/admin/sms_on_rails/drafts/show.html.erb +4 -0
- data/app/views/admin/sms_on_rails/outbounds/edit.html.erb +68 -0
- data/app/views/admin/sms_on_rails/outbounds/index.html.erb +37 -0
- data/app/views/admin/sms_on_rails/outbounds/new.html.erb +54 -0
- data/app/views/admin/sms_on_rails/outbounds/show.html.erb +69 -0
- data/app/views/admin/sms_on_rails/phone_carriers/edit.html.erb +24 -0
- data/app/views/admin/sms_on_rails/phone_carriers/index.html.erb +24 -0
- data/app/views/admin/sms_on_rails/phone_carriers/new.html.erb +22 -0
- data/app/views/admin/sms_on_rails/phone_carriers/show.html.erb +24 -0
- data/app/views/admin/sms_on_rails/phone_numbers/edit.html.erb +33 -0
- data/app/views/admin/sms_on_rails/phone_numbers/index.html.erb +28 -0
- data/app/views/admin/sms_on_rails/phone_numbers/new.html.erb +31 -0
- data/app/views/admin/sms_on_rails/phone_numbers/show.html.erb +32 -0
- data/app/views/layouts/sms_on_rails/basic.html.erb +26 -0
- data/app/views/sms_on_rails/_phone_carrier_form_item.html.erb +6 -0
- data/app/views/sms_on_rails/_send_sms.html.erb +33 -0
- data/app/views/sms_on_rails/index.html.erb +8 -0
- data/app/views/sms_on_rails/send_sms.html.erb +3 -0
- data/app/views/sms_on_rails/show.html.erb +29 -0
- data/config/routes.rb +19 -0
- data/db/data/fixtures/sms_phone_carriers.yml +110 -0
- data/db/migrate/sms_on_rails_carrier_tables.rb +9 -0
- data/db/migrate/sms_on_rails_model_tables.rb +48 -0
- data/db/migrate/sms_on_rails_phone_number_tables.rb +11 -0
- data/db/seed_data.rb +16 -0
- data/generators/sms_on_rails/USAGE +31 -0
- data/generators/sms_on_rails/commands/inserts.rb +63 -0
- data/generators/sms_on_rails/commands/timestamps.rb +33 -0
- data/generators/sms_on_rails/runners/add_all_models.rb +6 -0
- data/generators/sms_on_rails/runners/dependencies.rb +1 -0
- data/generators/sms_on_rails/runners/remove_all_models.rb +5 -0
- data/generators/sms_on_rails/runners/sms_on_rails_routes.rb +14 -0
- data/generators/sms_on_rails/sms_on_rails_generator.rb +255 -0
- data/generators/sms_on_rails/templates/configuration/clickatell.rb +6 -0
- data/generators/sms_on_rails/templates/configuration/email_gateway.rb +7 -0
- data/generators/sms_on_rails/templates/migrate/schema_migration.rb +15 -0
- data/generators/sms_on_rails/templates/migrate/sms_on_rails_update_phone_numbers.rb +40 -0
- data/generators/sms_on_rails/templates/phone_number_collision.rb +2 -0
- data/init.rb +3 -0
- data/install.rb +1 -0
- data/lib/sms_on_rails.rb +8 -0
- data/lib/sms_on_rails/activerecord_extensions/acts_as_deliverable.rb +92 -0
- data/lib/sms_on_rails/activerecord_extensions/acts_as_substitutable.rb +80 -0
- data/lib/sms_on_rails/activerecord_extensions/has_a_sms_service_provider.rb +101 -0
- data/lib/sms_on_rails/activerecord_extensions/lockable_record.rb +186 -0
- data/lib/sms_on_rails/all_models.rb +3 -0
- data/lib/sms_on_rails/model_support/draft.rb +178 -0
- data/lib/sms_on_rails/model_support/outbound.rb +136 -0
- data/lib/sms_on_rails/model_support/phone_carrier.rb +77 -0
- data/lib/sms_on_rails/model_support/phone_number.rb +248 -0
- data/lib/sms_on_rails/model_support/phone_number_associations.rb +13 -0
- data/lib/sms_on_rails/schema_helper.rb +51 -0
- data/lib/sms_on_rails/service_providers/base.rb +222 -0
- data/lib/sms_on_rails/service_providers/clickatell.rb +52 -0
- data/lib/sms_on_rails/service_providers/dummy.rb +19 -0
- data/lib/sms_on_rails/service_providers/email_gateway.rb +68 -0
- data/lib/sms_on_rails/service_providers/email_gateway_support/errors.rb +20 -0
- data/lib/sms_on_rails/service_providers/email_gateway_support/sms_mailer.rb +21 -0
- data/lib/sms_on_rails/service_providers/email_gateway_support/sms_mailer/sms_through_gateway.erb +6 -0
- data/lib/sms_on_rails/util/sms_error.rb +12 -0
- data/lib/smsonrails.rb +1 -0
- data/public/images/sms_on_rails/railsYoDawg.jpg +0 -0
- data/public/stylesheets/sms_on_rails.css +137 -0
- data/sms_on_rails.gemspec +32 -0
- data/tasks/sms_on_rails_tasks.rake +67 -0
- data/test/active_record_extensions/delivery_and_locking_test.rb +84 -0
- data/test/models/draft_test.rb +72 -0
- data/test/models/outbound_test.rb +89 -0
- data/test/models/phone_number_test.rb +131 -0
- data/test/run.rb +18 -0
- data/test/service_providers/abstract_test_support.rb +104 -0
- data/test/service_providers/clickatell_test.rb +39 -0
- data/test/service_providers/email_gateway_test.rb +30 -0
- data/test/test_helper.rb +24 -0
- data/uninstall.rb +1 -0
- metadata +187 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module Draft
|
4
|
+
|
5
|
+
def self.included(base)#:nodoc
|
6
|
+
base.send :accepts_nested_attributes_for, :outbounds
|
7
|
+
#base.acts_as_deliverable :fatal_exception => SmsOnRails::FatalSmsError
|
8
|
+
|
9
|
+
base.validates_presence_of :message
|
10
|
+
base.validate :validates_message_length
|
11
|
+
|
12
|
+
base.class_inheritable_accessor :default_header
|
13
|
+
base.class_inheritable_accessor :default_footer
|
14
|
+
base.class_inheritable_accessor :max_message_length
|
15
|
+
|
16
|
+
base.max_message_length = SmsOnRails::ServiceProviders::Base.max_characters
|
17
|
+
|
18
|
+
base.validates_associated :outbounds
|
19
|
+
|
20
|
+
base.send :include, InstanceMethods
|
21
|
+
base.send :extend, ClassMethods
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
# Create a new sms draft
|
27
|
+
# +message+ - can either be a string, an attribute hash (nested ok) or and ActiveRecord
|
28
|
+
# +phone_numbers+ - array or single text phone number or phone number . If +message+ is
|
29
|
+
# a nested attributes (Rails 2.3), +phone_numbers+ and +options+ should be blank
|
30
|
+
#
|
31
|
+
# ===Options
|
32
|
+
# <tt>:send_immediately</tt> - Send all outbounds now
|
33
|
+
# <tt>:draft</tt> - a hash of attributes for the draft object
|
34
|
+
# <tt>:phone_number</tt> - a hash of attributes applied to all +phone_numbers+
|
35
|
+
# <tt>:outbound</tt> - a hash of attributes applied to all generated +Outbound+ instances
|
36
|
+
# <tt>:keep_failed_outbounds</tt> - typically outbound messages are destroyed if they fail during delivery
|
37
|
+
# <tt>:deliver</tt> - options to pass to the deliver method if <tt>:send_immediately</tt> is used
|
38
|
+
#
|
39
|
+
# ===Example
|
40
|
+
# SmsOnRails::Draft.create_sms('my_message', '9995556667')
|
41
|
+
# SmsOnRails::Draft.create_sms('my_message', ['2065556666', '9995556667'])
|
42
|
+
#
|
43
|
+
# SmsOnRails::Draft.create_sms(params[:draft]) # assume nested with :outbound_attributes
|
44
|
+
# SmsOnRails::Draft.create_sms(params[:draft], params[:phone_numbers], :send_immediately => true
|
45
|
+
def create_sms(message, phone_numbers=nil, options={})
|
46
|
+
|
47
|
+
draft = create_draft(message)
|
48
|
+
draft.attributes = options[:draft] if options[:draft]
|
49
|
+
|
50
|
+
# Update draft with any existing phone numbers
|
51
|
+
draft.outbounds.each {|o| o.assign_existing_phone } if draft.outbounds
|
52
|
+
|
53
|
+
# Generate new phone_numbers
|
54
|
+
draft.create_outbounds_for_phone_numbers(phone_numbers, options) if phone_numbers
|
55
|
+
|
56
|
+
if draft.send(options[:bang] ? :save! : :save) && options[:send_immediately]
|
57
|
+
if !draft.send(options[:bang] ? :deliver! : :deliver, options[:deliver])
|
58
|
+
# this is really crappy but when we are sending multiple messages
|
59
|
+
# locking has to actually create the object.
|
60
|
+
# so if we fail try to delete
|
61
|
+
# this could be terribly slow if there are a lot of outbounds
|
62
|
+
if options[:keep_failed_outbounds]
|
63
|
+
draft.outbounds.each{|o| o.update_attribute(:sms_draft_id, nil ) }
|
64
|
+
else
|
65
|
+
draft.outbounds.each{|o| o.destroy }
|
66
|
+
draft.outbounds = []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
draft
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_sms!(message, phone_numbers=nil, options={})
|
74
|
+
create_sms(message, phone_numbers, options.reverse_merge(:bang => true, :create => :create!))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create the draft object
|
78
|
+
# +options+ - A Draft object, a hash of attributes(can be nested) or a String with the draft message
|
79
|
+
def create_draft(options={})
|
80
|
+
if options.is_a?(ActiveRecord::Base)
|
81
|
+
options.dup
|
82
|
+
elsif options.is_a?(Hash)
|
83
|
+
new(options)
|
84
|
+
elsif options.is_a?(String)
|
85
|
+
new(:message => options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module InstanceMethods
|
91
|
+
|
92
|
+
def initialize(args={})#:nodoc:
|
93
|
+
super args
|
94
|
+
default_headers_and_footers
|
95
|
+
end
|
96
|
+
|
97
|
+
def save_and_deliver(options={})
|
98
|
+
save and deliver(options)
|
99
|
+
end
|
100
|
+
|
101
|
+
def save_and_deliver!(options={})
|
102
|
+
save! and deliver!(options)
|
103
|
+
end
|
104
|
+
|
105
|
+
# The length of the message
|
106
|
+
# This does not take into consideration substituted params
|
107
|
+
# TODO: adjust max length for substituted params
|
108
|
+
def message_length; complete_message.length; end
|
109
|
+
def max_message_length; self.class.max_message_length; end
|
110
|
+
|
111
|
+
# default the headers and footers
|
112
|
+
def default_headers_and_footers
|
113
|
+
self.header ||= self.class.default_header
|
114
|
+
self.footer ||= self.class.default_footer
|
115
|
+
end
|
116
|
+
|
117
|
+
# The complete message with header and footer
|
118
|
+
def complete_message
|
119
|
+
complete_message = ""
|
120
|
+
complete_message << "#{header}\n" unless header.blank?
|
121
|
+
complete_message << message
|
122
|
+
complete_message << "\n#{footer}" unless footer.blank?
|
123
|
+
complete_message
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# Deliver all the unsent outbound messages
|
128
|
+
# +error+ - set this option to add an error message to the draft object
|
129
|
+
#
|
130
|
+
# This is locked and safe for individual messages
|
131
|
+
# but the draft object itself is not locked down,
|
132
|
+
# so it can be processed by multiple threads
|
133
|
+
def deliver(options={})
|
134
|
+
options||={}
|
135
|
+
deliver_method = options.delete(:bang) ? :deliver! : :deliver
|
136
|
+
|
137
|
+
error_messages = outbounds.inject([]) do |error_messages, o|
|
138
|
+
next(error_messages) if o.delivered?
|
139
|
+
unless o.send(deliver_method, options)
|
140
|
+
error_messages << "Message could not be delivered to: #{o.phone_number.human_display}"
|
141
|
+
end
|
142
|
+
error_messages
|
143
|
+
end
|
144
|
+
|
145
|
+
self.update_attribute(:status, error_messages.blank? ? 'PROCESSED' : 'FAILED')
|
146
|
+
error_messages.each { |msg| errors.add_to_base(msg) }
|
147
|
+
error_messages.blank?
|
148
|
+
end
|
149
|
+
|
150
|
+
# Deliver all outbound messages safely using optimisitic locking
|
151
|
+
# Progates any exception thrown
|
152
|
+
def deliver!(options={})
|
153
|
+
deliver((options||{}).merge(:bang => true))
|
154
|
+
end
|
155
|
+
|
156
|
+
# Create Outbound Instances based on +phone_numbers+ and +options+
|
157
|
+
# Refer to SmsOnRails::ModelSupport::Outbound.create_outbounds_from_phone
|
158
|
+
def create_outbounds_for_phone_numbers(phone_numbers, options={})
|
159
|
+
self.outbounds = self.class.reflections[:outbounds].klass.create_outbounds_for_phone_numbers(phone_numbers, options)
|
160
|
+
end
|
161
|
+
|
162
|
+
# if there is only one outbound message, return the actual (substituted)
|
163
|
+
# message. Otherwise, returns the draft message with substituted strings if any
|
164
|
+
def actual_message
|
165
|
+
self.outbounds.length == 1 ? outbounds.first.actual_message : message
|
166
|
+
end
|
167
|
+
|
168
|
+
protected
|
169
|
+
|
170
|
+
# validates the length of the message including the header and footer
|
171
|
+
def validates_message_length
|
172
|
+
errors.add(:message, "must be less than #{max_message_length} characters") unless message_length < max_message_length
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module Outbound
|
4
|
+
|
5
|
+
mattr_accessor :default_options
|
6
|
+
self.default_options = {}
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
|
10
|
+
base.has_a_sms_service_provider
|
11
|
+
|
12
|
+
base.acts_as_deliverable :fatal_exception => SmsOnRails::FatalSmsError,
|
13
|
+
:error => 'Unable to send message.'
|
14
|
+
|
15
|
+
|
16
|
+
base.acts_as_substitutable :draft_message,
|
17
|
+
:phone_number_digits => :phone_number_digits,
|
18
|
+
:phone_number => Proc.new{|record| record.phone_number.human_display },
|
19
|
+
:sender_name => :sender_name
|
20
|
+
|
21
|
+
base.send :alias_method, :full_message, :substituted_draft_message
|
22
|
+
base.send :alias_method, :send_immediately, :deliver
|
23
|
+
base.send :alias_method, :send_immediately!, :deliver!
|
24
|
+
|
25
|
+
base.send :cattr_accessor, :default_options
|
26
|
+
|
27
|
+
base.send :accepts_nested_attributes_for, :phone_number
|
28
|
+
|
29
|
+
base.send :include, InstanceMethods
|
30
|
+
base.send :extend, ClassMethods
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def send_immediately(message, phone_number, options={})
|
35
|
+
create_sms(message, phone_number, options.merge(:send_immediately => true))
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_immediately!(message, phone_number, options={})
|
39
|
+
create_sms!(message, phone_number, options.merge(:send_immediately => true))
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_sms(message, number, options={})
|
43
|
+
draft = reflections[:draft].klass.create_sms(message, number, options.reverse_merge(:keep_failed_outbounds => true))
|
44
|
+
number.is_a?(Array) ? draft.outbounds : draft.outbounds.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_sms!(message, number, options={})
|
48
|
+
draft = reflections[:draft].klass.create_sms!(message, number, options)
|
49
|
+
number.is_a?(Array) ? draft.outbounds : draft.outbounds.first
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_outbounds_for_phone_numbers(phone_numbers, options={})
|
53
|
+
smses = reflections[:phone_number].klass.find_and_create_all_by_numbers(phone_numbers, (options[:find]||{}).reverse_merge(:create => :new)).inject([]) do |smses, phone|
|
54
|
+
phone.attributes = options[:phone_number] if options[:phone_number]
|
55
|
+
phone.carrier = options[:carrier] if options[:carrier]
|
56
|
+
sms = self.new(options[:sms]||{})
|
57
|
+
sms.phone_number = phone
|
58
|
+
sms.service_provider = options[:service_provider] if options[:service_provider]
|
59
|
+
smses << sms
|
60
|
+
smses
|
61
|
+
end
|
62
|
+
smses
|
63
|
+
end
|
64
|
+
|
65
|
+
#Create the object find the existing phone if already stored
|
66
|
+
def create_with_phone(attributes, draft=nil)
|
67
|
+
outbound = new(attributes)
|
68
|
+
transaction {
|
69
|
+
outbound.assign_existing_phone
|
70
|
+
outbound.draft = draft
|
71
|
+
outbound.save
|
72
|
+
}
|
73
|
+
outbound
|
74
|
+
end
|
75
|
+
|
76
|
+
end #ClassMethods
|
77
|
+
|
78
|
+
module InstanceMethods
|
79
|
+
|
80
|
+
def phone_number_digits
|
81
|
+
self['phone_number_digits']||(phone_number ? phone_number.number : nil)
|
82
|
+
end
|
83
|
+
|
84
|
+
def phone_number_digits=(digits)
|
85
|
+
self.phone_number ||= SmsOnRails::PhoneNumber.new
|
86
|
+
self.phone_number.number = digits
|
87
|
+
assign_existing_phone
|
88
|
+
self.phone_number.number
|
89
|
+
end
|
90
|
+
|
91
|
+
def assign_phone_number(phone)
|
92
|
+
self.phone_number = SmsOnRails::PhoneNumber.find_and_create_by_number(phone)
|
93
|
+
end
|
94
|
+
|
95
|
+
def assign_existing_phone; assign_phone_number(self.phone_number); end
|
96
|
+
|
97
|
+
# The actual (not substituted draft message
|
98
|
+
# Substituted message can be obtained with +substituted_draft_message+
|
99
|
+
def draft_message; draft.complete_message if draft; end
|
100
|
+
|
101
|
+
def actual_message
|
102
|
+
read_attribute(:actual_message) || draft_message
|
103
|
+
end
|
104
|
+
|
105
|
+
#only save the actual message if it differs from the draft message
|
106
|
+
def actual_message=(msg)
|
107
|
+
write_attribute(:actual_message, msg) unless substituted_draft_message == draft_message
|
108
|
+
end
|
109
|
+
|
110
|
+
#Todo
|
111
|
+
def sender_name; ''; end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# deliver_message is the actual call to the service provider to send the message
|
116
|
+
# this method is called during deliver in the acts_as_deliverable
|
117
|
+
def deliver_message(options)
|
118
|
+
self.sms_service_provider||= default_service_provider
|
119
|
+
|
120
|
+
# set the actual message if it differs; not a before_save to reduce the
|
121
|
+
# overhead of checking for all commits
|
122
|
+
self.actual_message = substituted_draft_message
|
123
|
+
|
124
|
+
result = (self.sms_service_provider).send_sms(self)
|
125
|
+
self.unique_id = result[:unique_id] if result.is_a?(Hash)
|
126
|
+
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
def log_delivery_error(exc)
|
131
|
+
logger.error "SMS Delivery Error: #{self.phone_number.human_display if self.phone_number}: #{exc}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module PhoneCarrier
|
4
|
+
def self.included(base)
|
5
|
+
base.send :validates_presence_of, :name
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Returns the email address for sms
|
13
|
+
#
|
14
|
+
# * +phone+ - phone number digits or an SmsOnRails::PhoneCarrier
|
15
|
+
# * +carrier+ - the name, instance, or id of a carrier
|
16
|
+
#
|
17
|
+
# SmsOnRails::PhoneCarrier.sms_email_address('12065551111', 1) => '2065551111@att.txt.net'
|
18
|
+
def sms_email_address(phone, carrier)
|
19
|
+
phone_carrier = carrier_by_value(carrier)
|
20
|
+
raise SmsOnRails::SmsError("Invalid carrier: #{carrier}") unless phone_carrier
|
21
|
+
phone_carrier.sms_email_address(phone)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrurns the SmsOnRails::PhoneCarrier object
|
25
|
+
# +carrier+ can be
|
26
|
+
# * symbol name of the object (ex :verizon)
|
27
|
+
# * text name (Ex. 'Verizon')
|
28
|
+
# * SmsOnRails::PhoneCarrier instance returns self
|
29
|
+
# * the id number
|
30
|
+
def carrier_by_value(carrier)
|
31
|
+
phone_carrier = case carrier.class.to_s
|
32
|
+
when 'Symbol', 'String' then find_by_name(carrier)
|
33
|
+
when "#{self.class.to_s}" then carrier
|
34
|
+
when 'Fixnum' then find_by_id(carrier)
|
35
|
+
else nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the number text and carrier obj from an email string
|
40
|
+
# carrier_from_sms_email '12065551234@txt.att.net ' => [12065551234, <SmsOnRails::PhoneCarrier>]
|
41
|
+
def carrier_from_sms_email(address)
|
42
|
+
|
43
|
+
number = address
|
44
|
+
carrier = nil
|
45
|
+
|
46
|
+
if address.match(/^\s*(\d+)@(\S+)\s*$/)
|
47
|
+
number = match[1]
|
48
|
+
carrier_name = match[2]
|
49
|
+
carrier = find_by_email_domain(match[2]) if match[2]
|
50
|
+
end
|
51
|
+
|
52
|
+
[number, carrier]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
module InstanceMethods
|
56
|
+
|
57
|
+
# Returns the email address for sms
|
58
|
+
#
|
59
|
+
# * +phone+ - phone number digits or an SmsOnRails::PhoneNumber
|
60
|
+
# * +options+ - empty space now
|
61
|
+
#
|
62
|
+
# att_carrier.sms_email_address('12065551111') => '2065551111@att.txt.net'
|
63
|
+
#
|
64
|
+
def sms_email_address(phone, options={})
|
65
|
+
email = (phone.is_a?(ActiveRecord::Base) ? phone.digits : phone).dup
|
66
|
+
email.gsub!(/^1/, '')
|
67
|
+
email << '@'
|
68
|
+
email << self.email_domain
|
69
|
+
email
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return the carriers name when stringified
|
73
|
+
def to_s; name; end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module SmsOnRails
|
2
|
+
module ModelSupport
|
3
|
+
module PhoneNumber
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include,InstanceMethods
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :validates_format_of, :phone_number_digits, :with => /^\d{5,30}$/, :message => 'must be a number and have at least 5 digits'
|
8
|
+
base.before_save {|record| record.number = record.digits}
|
9
|
+
base.send :validates_presence_of, :number
|
10
|
+
base.send :attr_reader, :original_number
|
11
|
+
base.class_inheritable_accessor :valid_finder_create_options
|
12
|
+
base.valid_finder_create_options = %w(create keep_duplicates skip_sort)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
# adds a sanitize search for numbers for the options
|
19
|
+
# used for finders
|
20
|
+
# Use with find for other ActiveRecord objects
|
21
|
+
# Outbound.find :all, add_number_search([1,2,3], :conditions => 'outbounds.send_priority == 1')
|
22
|
+
def add_number_search(numbers, options={}, merge_options={})
|
23
|
+
number_digits = [numbers].flatten
|
24
|
+
number_digits.collect!{|n| digits(n) } unless merge_options[:skip_sanitize]
|
25
|
+
number_conditions = ['number in (?)', number_digits.uniq.compact]
|
26
|
+
options[:conditions] = merge_conditions(options[:conditions]||{}, number_conditions)
|
27
|
+
options
|
28
|
+
end
|
29
|
+
|
30
|
+
# Find all numbers (sanitized) that match a list of String +numbers+
|
31
|
+
def find_all_by_numbers(numbers, options={}, merge_options={})
|
32
|
+
return [] unless numbers.dup.delete_if{|x| x.blank? }.any?
|
33
|
+
find(:all, add_number_search(numbers, options, merge_options))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find a single number
|
37
|
+
def find_by_number(number, options={})
|
38
|
+
find_all_by_numbers([number], options).first;
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
# Find all numbers and create if it doesn't already exist
|
44
|
+
# +number_list+ - a list of numbers (String, ActiveRecord or attribute hash)
|
45
|
+
# +options+ - additional create options and normal finder options like <tt>:conditions</tt>
|
46
|
+
# === Additional Options
|
47
|
+
# <tt>:create</tt> - <tt>:new</tt>(Default), <tt>:create</tt>, or <tt>:create!</tt>
|
48
|
+
# If the number does not exist, create it with this method. Using <tt>:new</tt> means none of the objects are saved
|
49
|
+
# <tt>:keep_duplicates<tt> - When set to true, duplicates in the list are returned
|
50
|
+
# <tt>:skip_sort</tt> - Default is to sort the list to be in the same order as +number_list+.
|
51
|
+
# Set to false to skip sorting (perf boost). Instance creation or attribute update does not occur
|
52
|
+
# when <tt>skip_sort</tt> is false.
|
53
|
+
def find_and_create_all_by_numbers(number_list, options={})
|
54
|
+
create_options, finder_options = seperate_find_and_create_options(options)
|
55
|
+
|
56
|
+
# Collect a list of digits and a list of attributes
|
57
|
+
attribute_list = []
|
58
|
+
number_digits = [number_list].flatten.inject([]) do |list, n|
|
59
|
+
attribute_list << (new_attributes = value_to_hash(n))
|
60
|
+
digit = digits(new_attributes[:number]||new_attributes['number'])
|
61
|
+
digit = new_attributes[:number]||new_attributes['number'] if digit.blank?
|
62
|
+
list << digit
|
63
|
+
list
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
found_numbers = find_all_by_numbers(number_digits, finder_options, :skip_sanitize => true)
|
68
|
+
# sort the list based on the order of the original input
|
69
|
+
# not found values have nil
|
70
|
+
if create_options[:skip_sort]
|
71
|
+
found_numbers
|
72
|
+
else
|
73
|
+
sorted_numbers = sort_by_numbers(found_numbers, number_digits, create_options)
|
74
|
+
transaction { update_attributes_on_list(sorted_numbers, attribute_list, create_options) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find a number and create if it does not exist
|
79
|
+
# +options+ include those specified in find_all_and_create_by_number
|
80
|
+
def find_and_create_by_number(number, options={})
|
81
|
+
find_and_create_all_by_numbers(number, options.reverse_merge(:create => :new)).first
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Return the phone number with specified carrier if the phone number is an sms email address
|
86
|
+
def find_by_sms_email_address(address, options={})
|
87
|
+
number, carrier = reflections[:carrier].klass.carrier_from_sms_email(address)
|
88
|
+
|
89
|
+
if number
|
90
|
+
phone = find_by_number(number, options)||new(:number => number)
|
91
|
+
phone.carrier = carrier if carrier
|
92
|
+
phone
|
93
|
+
else
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
# The digits (numbers) only of the phone number
|
100
|
+
# Digits are how phone numbers are stored in the database
|
101
|
+
# The following all return +12065555555+
|
102
|
+
# SmsOnRails::digits(12065555555')
|
103
|
+
# SmsOnRails::digits(206.555.5555')
|
104
|
+
# SmsOnRails::digits(1206-555 5555')
|
105
|
+
def digits(text)
|
106
|
+
return text.digits if text.is_a?(self)
|
107
|
+
number = text.to_s.gsub(/\D/,'')
|
108
|
+
number = "1#{number}" if number.length == 10
|
109
|
+
number
|
110
|
+
end
|
111
|
+
|
112
|
+
# The human display pretty phone number
|
113
|
+
# (206) 555-5555
|
114
|
+
def human_display(number)
|
115
|
+
base_number = digits(number)
|
116
|
+
if base_number.length == 11 && base_number.first == '1'
|
117
|
+
"(#{base_number[1..3]}) #{base_number[4..6]}-#{base_number[7..10]}"
|
118
|
+
elsif base_number.length > 0
|
119
|
+
"+#{base_number}"
|
120
|
+
else
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
# Return create_options hash and finder options hash from all options
|
127
|
+
def seperate_find_and_create_options(options={})#:nodoc:
|
128
|
+
|
129
|
+
seperated_options = (options||{}).inject([{}, {}]) {|map, (k,v)|
|
130
|
+
if valid_finder_create_options.include?(k.to_s)
|
131
|
+
map.first[k.to_sym] = v
|
132
|
+
else
|
133
|
+
map.last[k.to_sym] = v
|
134
|
+
end
|
135
|
+
map
|
136
|
+
}
|
137
|
+
seperated_options.first[:create] = :new if seperated_options.first[:create].is_a?(TrueClass)
|
138
|
+
seperated_options
|
139
|
+
end
|
140
|
+
|
141
|
+
# Convert a PhoneNumber, hash map or string number
|
142
|
+
# into an attribute hash
|
143
|
+
def value_to_hash(digits)#:nodoc:
|
144
|
+
attributes = if digits.is_a?(ActiveRecord::Base)
|
145
|
+
digits.attributes
|
146
|
+
elsif digits.is_a?(Hash)
|
147
|
+
digits.dup
|
148
|
+
elsif digits
|
149
|
+
{:number => digits}
|
150
|
+
end
|
151
|
+
attributes
|
152
|
+
end
|
153
|
+
|
154
|
+
# Return a sorted list of PhoneNumber instances
|
155
|
+
# Will create new records for missing records if attribute_list is specified
|
156
|
+
# +unsorted_list+ - unsorted list of PhoneNumbers
|
157
|
+
# +sorted_digits+ - sorted list of digits (Strings)
|
158
|
+
# +attribute_list+ - optional list of attributes in sorted order
|
159
|
+
# +creation_method+ - :create, :new, or :create!
|
160
|
+
def sort_by_numbers(unsorted_list, sorted_digits, options={})
|
161
|
+
|
162
|
+
unsorted_map = unsorted_list.inject({}) {|map, v| map[v.number] = v; map }
|
163
|
+
|
164
|
+
# First sort the list in the order of the sorted digits
|
165
|
+
# leaving nils for empty spaces
|
166
|
+
sorted_list = if sorted_digits.length > 1
|
167
|
+
sorted = [nil] * sorted_digits.length
|
168
|
+
sorted_digits.each_with_index{|n, idx|
|
169
|
+
sorted[idx] = unsorted_map[n]
|
170
|
+
unsorted_map[n] = :used unless options[:keep_duplicates]
|
171
|
+
}
|
172
|
+
|
173
|
+
sorted.delete_if{|x| x == :used } unless options[:keep_duplicates]
|
174
|
+
sorted
|
175
|
+
else
|
176
|
+
unsorted_list.any? ? unsorted_list.dup : [nil]
|
177
|
+
end
|
178
|
+
sorted_list
|
179
|
+
end
|
180
|
+
|
181
|
+
# create new records for any records not found
|
182
|
+
# update the attributes of the found records
|
183
|
+
# save if :create
|
184
|
+
def update_attributes_on_list(sorted_list, attribute_list, options={})
|
185
|
+
|
186
|
+
unless attribute_list.blank?
|
187
|
+
0.upto(sorted_list.length - 1) {|idx|
|
188
|
+
if sorted_list[idx]
|
189
|
+
if (attr = attribute_list[idx].delete_if{|x, y| x.to_s == 'number'}).any?
|
190
|
+
sorted_list[idx].attributes = attr
|
191
|
+
sorted_list[idx].save if options[:create]
|
192
|
+
sorted_list[idx].save! if options[:create!]
|
193
|
+
end
|
194
|
+
elsif options[:create]
|
195
|
+
sorted_list[idx] = self.send(options[:create], attribute_list[idx])
|
196
|
+
end
|
197
|
+
}
|
198
|
+
end
|
199
|
+
sorted_list
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
module InstanceMethods
|
205
|
+
# The human display pretty phone number
|
206
|
+
# (206) 555-5555
|
207
|
+
def human_display
|
208
|
+
self.class.human_display(self.number)
|
209
|
+
end
|
210
|
+
|
211
|
+
def number=(value)
|
212
|
+
@original_number = value unless value.blank?
|
213
|
+
@digits = self.class.digits(value)
|
214
|
+
write_attribute :number, @digits
|
215
|
+
end
|
216
|
+
|
217
|
+
def number
|
218
|
+
n = read_attribute :number
|
219
|
+
n.blank? ? original_number : n
|
220
|
+
end
|
221
|
+
|
222
|
+
def digits
|
223
|
+
@digits||=self.class.digits(self.number)
|
224
|
+
end
|
225
|
+
|
226
|
+
# return the sms email address from the carrier
|
227
|
+
def sms_email_address
|
228
|
+
carrier.sms_email_address(self) if carrier
|
229
|
+
end
|
230
|
+
|
231
|
+
#Assign the carrier to the phone number if it exists
|
232
|
+
#+carrier+ - can be a ActiveRecord object, a name of the carrier, or carrier id
|
233
|
+
def assign_carrier(carrier)
|
234
|
+
specified_carrier = self.class.reflections[:carrier].klass.carrier_by_value(carrier)
|
235
|
+
self.carrier = specified_carrier if specified_carrier
|
236
|
+
end
|
237
|
+
|
238
|
+
alias_method :phone_number_digits, :digits
|
239
|
+
|
240
|
+
# Returns true if the number is marked as do not send (not blank)
|
241
|
+
# Values could be abuse, bounce, opt-out, etc
|
242
|
+
def do_not_send?
|
243
|
+
!self.do_not_send.blank?
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|