notify_user 0.0.28 → 0.0.29

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -0
  3. data/app/mailers/notify_user/notification_mailer.rb +6 -2
  4. data/app/models/notify_user/apn_connection.rb +41 -0
  5. data/app/models/notify_user/apns.rb +18 -27
  6. data/app/models/notify_user/base_notification.rb +42 -64
  7. data/app/models/notify_user/houston.rb +98 -0
  8. data/app/models/notify_user/urban_airship.rb +33 -0
  9. data/lib/generators/notify_user/install/templates/initializer.rb +13 -0
  10. data/lib/notify_user.rb +16 -0
  11. data/lib/notify_user/channels/apns/apns_channel.rb +15 -12
  12. data/lib/notify_user/version.rb +1 -1
  13. data/spec/dummy/rails-3.2.17/Gemfile +38 -0
  14. data/spec/dummy/rails-3.2.17/README.rdoc +261 -0
  15. data/spec/dummy/rails-3.2.17/Rakefile +7 -0
  16. data/spec/dummy/rails-3.2.17/app/assets/images/rails.png +0 -0
  17. data/spec/dummy/rails-3.2.17/app/assets/javascripts/application.js +15 -0
  18. data/spec/dummy/rails-3.2.17/app/assets/stylesheets/application.css +13 -0
  19. data/spec/dummy/rails-3.2.17/app/controllers/application_controller.rb +3 -0
  20. data/spec/dummy/rails-3.2.17/app/controllers/notify_user/notifications_controller.rb +9 -0
  21. data/spec/dummy/rails-3.2.17/app/helpers/application_helper.rb +2 -0
  22. data/spec/dummy/rails-3.2.17/app/models/user.rb +3 -0
  23. data/spec/dummy/rails-3.2.17/app/notifications/new_post_notification.rb +11 -0
  24. data/spec/dummy/rails-3.2.17/app/views/layouts/application.html.erb +14 -0
  25. data/spec/dummy/rails-3.2.17/app/views/notify_user/layouts/action_mailer.html.erb +39 -0
  26. data/spec/dummy/rails-3.2.17/app/views/notify_user/new_post_notification/action_mailer/notification.html.erb +1 -0
  27. data/spec/dummy/rails-3.2.17/app/views/notify_user/new_post_notification/mobile_sdk/notification.html.erb +1 -0
  28. data/spec/dummy/rails-3.2.17/config.ru +4 -0
  29. data/spec/dummy/rails-3.2.17/config/application.rb +62 -0
  30. data/spec/dummy/rails-3.2.17/config/boot.rb +6 -0
  31. data/spec/dummy/rails-3.2.17/config/database.yml +24 -0
  32. data/spec/dummy/rails-3.2.17/config/environment.rb +5 -0
  33. data/spec/dummy/rails-3.2.17/config/environments/development.rb +37 -0
  34. data/spec/dummy/rails-3.2.17/config/environments/production.rb +67 -0
  35. data/spec/dummy/rails-3.2.17/config/environments/test.rb +37 -0
  36. data/spec/dummy/rails-3.2.17/config/initializers/backtrace_silencers.rb +7 -0
  37. data/spec/dummy/rails-3.2.17/config/initializers/inflections.rb +15 -0
  38. data/spec/dummy/rails-3.2.17/config/initializers/mime_types.rb +5 -0
  39. data/spec/dummy/rails-3.2.17/config/initializers/notify_user.rb +14 -0
  40. data/spec/dummy/rails-3.2.17/config/initializers/secret_token.rb +7 -0
  41. data/spec/dummy/rails-3.2.17/config/initializers/session_store.rb +8 -0
  42. data/spec/dummy/rails-3.2.17/config/initializers/wrap_parameters.rb +14 -0
  43. data/spec/dummy/rails-3.2.17/config/locales/en.yml +5 -0
  44. data/spec/dummy/rails-3.2.17/config/routes.rb +58 -0
  45. data/spec/dummy/rails-3.2.17/db/migrate/20150106040852_create_users.rb +9 -0
  46. data/spec/dummy/rails-3.2.17/db/migrate/20150106040854857857604000_create_notify_user_notifications.rb +13 -0
  47. data/spec/dummy/rails-3.2.17/db/migrate/20150106040854860860323000_create_notify_user_unsubscribes.rb +10 -0
  48. data/spec/dummy/rails-3.2.17/db/migrate/20150106040854862862248000_create_notify_user_user_hashes.rb +12 -0
  49. data/spec/dummy/rails-3.2.17/db/schema.rb +50 -0
  50. data/spec/dummy/rails-3.2.17/db/seeds.rb +7 -0
  51. data/spec/dummy/rails-3.2.17/doc/README_FOR_APP +2 -0
  52. data/spec/dummy/rails-3.2.17/log/test.log +933 -0
  53. data/spec/dummy/rails-3.2.17/public/404.html +26 -0
  54. data/spec/dummy/rails-3.2.17/public/422.html +26 -0
  55. data/spec/dummy/rails-3.2.17/public/500.html +25 -0
  56. data/spec/dummy/rails-3.2.17/public/favicon.ico +0 -0
  57. data/spec/dummy/rails-3.2.17/public/index.html +241 -0
  58. data/spec/dummy/rails-3.2.17/public/robots.txt +5 -0
  59. data/spec/dummy/rails-3.2.17/script/rails +6 -0
  60. data/spec/dummy/rails-3.2.17/test/fixtures/users.yml +7 -0
  61. data/spec/dummy/rails-3.2.17/test/performance/browsing_test.rb +12 -0
  62. data/spec/dummy/rails-3.2.17/test/test_helper.rb +13 -0
  63. data/spec/dummy/rails-3.2.17/test/unit/user_test.rb +7 -0
  64. data/spec/dummy/rails-4.0.4/log/test.log +38719 -0
  65. data/spec/models/notify_user/houston_spec.rb +33 -0
  66. data/spec/models/notify_user/notification_spec.rb +62 -96
  67. metadata +151 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5d4645847e57a969014f6204305bc9200ea3d241
4
- data.tar.gz: 76db4f9e709b85a4b9e9b6d6f0a4abd23565236c
3
+ metadata.gz: 0a577a48ef571d2031ff5df80e68446265b7c4f1
4
+ data.tar.gz: 007bc887cd900949443af637df989638d61b46ae
5
5
  SHA512:
6
- metadata.gz: 0392478ef4fe31df0bfbbaa0b2152145c5cef77d1a27c67e5ee01f781d7a479658b270894647c894e950bf5a96094e35c5afae95a14df9ff3cf5d0d5c1f12062
7
- data.tar.gz: ee37e8bc234d00045eb7b75a49d575280dfd7d8ee8a28cd95bf8fa6e8eb6ffe972529182cac71b2e463eb10d539cfa7c76abb6f4b19f8356ceed75eccee43340
6
+ metadata.gz: 1e8be63dd0b8d2fa14de7ccbfbe7a927b521b3d086ff9ef344116b48170c6e68e37d9bfae0fbe95d01061d9f698c5e88fb06f6d1543ce9e2a0b2ecefb8777058
7
+ data.tar.gz: 0e98eb7957dd725d95d8750429cd06bf38f01a4d558adcdbcab02265768124b8cfe93028b581de64e551e2c13c9dedde4772f1f19bce19c0e04f8ec662fbfd12
data/README.md CHANGED
@@ -22,6 +22,15 @@ Then send:
22
22
  ```
23
23
  NotifyUser.send_notification('new_my_property').to(user).with("listing_address" => "123 Main St").notify
24
24
  ```
25
+ Dynamic email titles use %{tags} that correspond to your params hash (Will throw an exception if key is missing)
26
+ ```
27
+ channel :action_mailer,
28
+ subject: "%{name} sent you a message",
29
+ aggregate: {
30
+ subject: "%{name} sent you %{count} messages"
31
+ }
32
+ ```
33
+
25
34
 
26
35
  To enable APNS add this line to your app/notification/notification_type.rb
27
36
  ```
@@ -8,7 +8,7 @@ module NotifyUser
8
8
  @notification = notification
9
9
 
10
10
  mail to: notification.target.email,
11
- subject: options[:subject],
11
+ subject: subject(notification, options[:subject]),
12
12
  template_name: "notification",
13
13
  template_path: "notify_user/action_mailer",
14
14
  from: NotifyUser.mailer_sender
@@ -21,10 +21,14 @@ module NotifyUser
21
21
  mail to: @notifications.first.target.email,
22
22
  template_name: "aggregate_notification",
23
23
  template_path: ["notify_user/#{notifications.first.class.name.underscore}/action_mailer", "notify_user/action_mailer"],
24
- subject: options[:aggregate][:subject],
24
+ subject: subject(@notification, options[:aggregate][:subject]),
25
25
  from: NotifyUser.mailer_sender
26
26
  end
27
27
 
28
+ def subject(notification, subject)
29
+ subject % notification.params.symbolize_keys
30
+ end
31
+
28
32
  protected
29
33
  end
30
34
  end
@@ -0,0 +1,41 @@
1
+ module NotifyUser
2
+ class APNConnection
3
+
4
+ attr_accessor :connection
5
+
6
+ def initialize
7
+ setup
8
+ end
9
+
10
+ def setup
11
+ @uri, @certificate = if Rails.env.production? || apn_environment == :production
12
+ [
13
+ ::Houston::APPLE_PRODUCTION_GATEWAY_URI,
14
+ File.read("#{Rails.root}/config/keys/production_push.pem")
15
+ ]
16
+ else
17
+ [
18
+ ::Houston::APPLE_DEVELOPMENT_GATEWAY_URI,
19
+ File.read("#{Rails.root}/config/keys/development_push.pem")
20
+ ]
21
+ end
22
+
23
+ @connection = ::Houston::Connection.new(@uri, @certificate, nil)
24
+ @connection.open
25
+ end
26
+
27
+ def write(data)
28
+ raise "Connection is closed" unless @connection.open?
29
+ @connection.write(data)
30
+ end
31
+
32
+ private
33
+
34
+ def apn_environment
35
+ return nil unless ENV['APN_ENVIRONMENT']
36
+
37
+ ENV['APN_ENVIRONMENT'].downcase.to_sym
38
+ end
39
+
40
+ end
41
+ end
@@ -1,37 +1,28 @@
1
1
  module NotifyUser
2
2
  class Apns
3
- SYMBOL_NAMES_SIZE = 10
3
+ SYMBOL_NAMES_SIZE = 10
4
4
  PAYLOAD_LIMIT = 255
5
5
 
6
- #sends push notification
7
- def self.push_notification(notification)
8
- #calculates the bytes already used
9
- used_space = SYMBOL_NAMES_SIZE + notification.id.size + notification.created_at.to_time.to_i.size +
10
- notification.type.size
11
-
12
- used_space += notification.params[:action_id].size if notification.params[:action_id]
6
+ def initialize(notification, options)
7
+ @notification = notification
8
+ @options = options
9
+ end
10
+
11
+ # Sends push notification:
12
+ def push
13
+ raise "Base APNS class should not be used."
14
+ end
15
+
16
+ private
13
17
 
14
- space_allowance = PAYLOAD_LIMIT - used_space
18
+ # Calculates the bytes already used:
19
+ def used_space
20
+ used_space = SYMBOL_NAMES_SIZE + @notification.id.size + @notification.created_at.to_time.to_i.size +
21
+ @notification.type.size
15
22
 
16
- payload = {
17
- :alias => notification.target_id,
18
- :aps => {alert: notification.mobile_message(space_allowance), badge: notification.count_for_target},
19
- :n_data => {
20
- '#' => notification.id,
21
- t: notification.created_at.to_time.to_i,
22
- '?' => notification.type
23
- }
24
- }
25
- payload[:n_data]['!'] = notification.params[:action_id] if notification.params[:action_id]
23
+ used_space += @notification.params[:action_id].size if @notification.params[:action_id]
26
24
 
27
- response = Urbanairship.push(payload)
28
- if response.success?
29
- Rails.logger.info "Push notification sent successfully."
30
- return true
31
- else
32
- Rails.logger.info "Push notification failed."
33
- return false
34
- end
25
+ used_space
35
26
  end
36
27
  end
37
28
  end
@@ -7,6 +7,9 @@ module NotifyUser
7
7
  include ActionView::Helpers::TextHelper
8
8
  include AASM
9
9
 
10
+ after_commit :deliver!, on: :create
11
+ after_commit :deliver, on: :create
12
+
10
13
  if ActiveRecord::VERSION::MAJOR < 4
11
14
  attr_accessible :params, :target, :type, :state
12
15
  end
@@ -32,6 +35,9 @@ module NotifyUser
32
35
  # Created, not sent yet. Possibly waiting for aggregation.
33
36
  state :pending, initial: true
34
37
 
38
+ # Delivers without aggregation
39
+ state :pending_no_aggregation
40
+
35
41
  # Email/SMS/APNS has been sent.
36
42
  state :sent
37
43
 
@@ -40,7 +46,11 @@ module NotifyUser
40
46
 
41
47
  # Record that we have sent message(s) to the user about this notification.
42
48
  event :mark_as_sent do
43
- transitions from: :pending, to: :sent
49
+ transitions from: [:pending, :pending_no_aggregation], to: :sent
50
+ end
51
+
52
+ event :dont_aggregate do
53
+ transitions from: :pending, to: :pending_no_aggregation
44
54
  end
45
55
 
46
56
  # Record that the user has seen this notification, usually on a page or in the app.
@@ -59,6 +69,7 @@ module NotifyUser
59
69
  end
60
70
  end
61
71
 
72
+ # returns the global unread notification count for a user
62
73
  def count_for_target
63
74
  NotifyUser::BaseNotification.for_target(target).where('state IN (?)', ["sent", "pending"]).count
64
75
  end
@@ -92,30 +103,14 @@ module NotifyUser
92
103
  end
93
104
 
94
105
  def notify!
95
- save
96
-
97
106
  # Bang version of 'notify' ignores aggregation
98
- self.deliver!
107
+ dont_aggregate!
99
108
  end
100
109
 
101
110
  # Send any Emails/SMS/APNS
102
111
  def notify
103
-
104
- if save
105
-
106
- #if aggregation is false bypass aggregation completely
107
- self.channels.each do |channel_name, options|
108
- if(options[:aggregate_per] == false)
109
- self.class.delay.deliver_notification_channel(self.id, channel_name)
110
- else
111
- if not aggregation_pending?
112
- self.class.delay_for(options[:aggregate_per] || self.aggregate_per).notify_aggregated_channel(self.id, channel_name)
113
- end
114
- end
115
- end
116
-
117
- end
118
-
112
+ # Sends with aggregation if enabled
113
+ save
119
114
  end
120
115
 
121
116
  def generate_unsubscribe_hash
@@ -172,32 +167,37 @@ module NotifyUser
172
167
  return (self.class.pending_aggregation_with(self).where('id != ?', id).count > 0)
173
168
  end
174
169
 
175
- # def deliver
176
- # unless user_has_unsubscribed?
177
- # self.mark_as_sent
178
- # self.save
170
+ # Aggregates appropriately
171
+ def deliver
172
+ if pending? and not user_has_unsubscribed?
173
+ self.mark_as_sent!
179
174
 
180
- # self.class.delay.deliver_channels(self.id)
181
- # end
182
- # end
175
+ # if aggregation is false bypass aggregation completely
176
+ self.channels.each do |channel_name, options|
177
+ if(options[:aggregate_per] == false)
178
+ self.class.delay.deliver_notification_channel(self.id, channel_name)
179
+ else
180
+ # only notifies channels if no pending aggreagte notifications
181
+ if not aggregation_pending?
182
+ self.class.delay_for(options[:aggregate_per] || self.aggregate_per).notify_aggregated_channel(self.id, channel_name)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
183
188
 
189
+ # Sends immediately and without aggregation
184
190
  def deliver!
185
- unless user_has_unsubscribed?
186
- self.mark_as_sent
187
- self.save
191
+ if pending_no_aggregation? and not user_has_unsubscribed?
192
+ self.mark_as_sent!
188
193
  self.class.deliver_channels(self.id)
189
194
  end
190
195
  end
191
196
 
192
197
  # Deliver a single notification across each channel.
193
198
  def self.deliver_channels(notification_id)
194
- notification = self.where(id: notification_id).first
195
- return unless notification
196
199
  self.channels.each do |channel_name, options|
197
- unless unsubscribed_from_channel?(notification.target, channel_name)
198
- channel = (channel_name.to_s + "_channel").camelize.constantize
199
- channel.deliver(notification, options)
200
- end
200
+ self.deliver_notification_channel(notification_id, channel_name)
201
201
  end
202
202
  end
203
203
 
@@ -215,11 +215,11 @@ module NotifyUser
215
215
 
216
216
  # Deliver a single notification to a specific channel.
217
217
  def self.deliver_notification_channel(notification_id, channel_name)
218
- notification = self.where(id: notification_id).first
219
- return unless notification
220
- channel_options = channels[channel_name.to_sym]
218
+ notification = self.find(notification_id) # Raise an exception if not found.
221
219
 
220
+ channel_options = channels[channel_name.to_sym]
222
221
  channel = (channel_name.to_s + "_channel").camelize.constantize
222
+
223
223
  unless self.unsubscribed_from_channel?(notification.target, channel_name)
224
224
  channel.deliver(notification, channel_options)
225
225
  end
@@ -236,7 +236,7 @@ module NotifyUser
236
236
  end
237
237
  end
238
238
 
239
- #notifies a single channel for aggregation
239
+ # Prepares a single channel for aggregation
240
240
  def self.notify_aggregated_channel(notification_id, channel_name)
241
241
  notification = self.find(notification_id) # Raise an exception if not found.
242
242
 
@@ -247,7 +247,6 @@ module NotifyUser
247
247
  notifications.map(&:save)
248
248
 
249
249
  return if notifications.empty?
250
-
251
250
  if notifications.length == 1
252
251
  # Despite waiting for more to aggregate, we only got one in the end.
253
252
  self.deliver_notification_channel(notifications.first.id, channel_name)
@@ -257,27 +256,8 @@ module NotifyUser
257
256
  end
258
257
  end
259
258
 
260
- def self.notify_aggregated(notification_id)
261
- notification = self.find(notification_id) # Raise an exception if not found.
262
-
263
- # Find any pending notifications with the same type and target, which can all be sent in one message.
264
- notifications = self.pending_aggregation_with(notification)
265
-
266
- notifications.map(&:mark_as_sent)
267
- notifications.map(&:save)
268
-
269
- return if notifications.empty?
270
-
271
- if notifications.length == 1
272
- # Despite waiting for more to aggregate, we only got one in the end.
273
- self.deliver_channels(notifications.first.id)
274
- else
275
- # We got several notifications while waiting, send them aggregated.
276
- self.deliver_channels_aggregated(notifications)
277
- end
278
- end
279
-
280
259
  private
260
+
281
261
  def unsubscribed_validation
282
262
  errors.add(:target, (" has unsubscribed from this type")) if user_has_unsubscribed?
283
263
  end
@@ -291,9 +271,7 @@ module NotifyUser
291
271
 
292
272
  def self.unsubscribed_from_channel?(user, type)
293
273
  #return true if user has unsubscribed
294
- return true unless NotifyUser::Unsubscribe.has_unsubscribed_from(user, type).empty?
295
-
296
- return false
274
+ return !NotifyUser::Unsubscribe.has_unsubscribed_from(user, type).empty?
297
275
  end
298
276
 
299
277
 
@@ -0,0 +1,98 @@
1
+ require_relative 'apn_connection'
2
+ require 'houston'
3
+
4
+ module NotifyUser
5
+ class Houston < Apns
6
+
7
+ NO_ERROR = -42
8
+ INVALID_TOKEN_ERROR = 8
9
+ APN_POOL = ConnectionPool.new(
10
+ size: NotifyUser.connection_pool_size,
11
+ timeout: NotifyUser.connection_pool_timeout) do
12
+ APNConnection.new
13
+ end
14
+
15
+ attr_accessor :push_options
16
+
17
+ def initialize(notification, options)
18
+ super(notification, options)
19
+
20
+ @push_options = setup_options
21
+
22
+ device_method = @options[:device_method] || :devices
23
+ begin
24
+ @devices = @notification.target.send(device_method)
25
+ rescue
26
+ Rails.logger.info "Notification target, #{@notification.target.class}, does not respond to the method, #{device_method}."
27
+ end
28
+ end
29
+
30
+ def push
31
+ send_notifications
32
+ end
33
+
34
+ private
35
+
36
+ def setup_options
37
+ space_allowance = PAYLOAD_LIMIT - used_space
38
+
39
+ push_options = {
40
+ alert: @notification.mobile_message(space_allowance),
41
+ badge: @notification.count_for_target,
42
+ category: @notification.params[:category] || @notification.type,
43
+ custom_data: @notification.params,
44
+ sound: 'default'
45
+ }
46
+
47
+ if @options[:silent]
48
+ push_options.merge!({
49
+ sound: '',
50
+ content_available: true
51
+ })
52
+ end
53
+
54
+ push_options
55
+ end
56
+
57
+ def send_notifications
58
+ APN_POOL.with do |connection|
59
+ if !connection.connection.open?
60
+ connection = APNConnection.new
61
+ end
62
+
63
+ ssl = connection.connection.ssl
64
+ error_index = NO_ERROR
65
+
66
+ @devices.each_with_index do |device, index|
67
+ notification = ::Houston::Notification.new(@push_options.dup.merge({ token: device.token, id: index }))
68
+ connection.write(notification.message)
69
+ end
70
+
71
+ read_socket, write_socket = IO.select([ssl], [], [ssl], 1)
72
+ if (read_socket && read_socket[0])
73
+ if error = connection.connection.read(6)
74
+ command, status, error_index = error.unpack("ccN")
75
+
76
+ # Remove all the devices prior to the error (we assume they were successful), and close the current connection:
77
+ if error_index != NO_ERROR
78
+ device = @devices.at(error_index)
79
+ Rails.logger.info "Error: #{status} with id: #{error_index}. Token: #{device.token}."
80
+
81
+ # If we encounter the Invalid Token error from APNS, just remove the device:
82
+ if status == ERROR_INVALID_TOKEN
83
+ Rails.logger.info "Invalid token encountered, removing device. Token: #{device.token}."
84
+ device.destroy
85
+ end
86
+
87
+ @devices.slice!(0..error_index)
88
+ connection.connection.close
89
+ end
90
+ end
91
+ end
92
+
93
+ # Resend all notifications after the once that produced the error:
94
+ send_notifications if error_index != NO_ERROR
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ module NotifyUser
2
+ class UrbanAirship < Apns
3
+
4
+ def push
5
+ space_allowance = PAYLOAD_LIMIT - used_space
6
+
7
+ payload = {
8
+ :alias => notification.target_id,
9
+ :aps => {
10
+ :alert => notification.mobile_message(space_allowance),
11
+ :badge => notification.count_for_target
12
+ },
13
+ :n_data => {
14
+ '#' => notification.id,
15
+ :t => notification.created_at.to_time.to_i,
16
+ '?' => notification.type
17
+ }
18
+ }
19
+
20
+ payload[:n_data]['!'] = notification.params[:action_id] if notification.params[:action_id]
21
+
22
+ response = Urbanairship.push(payload)
23
+ if response.success?
24
+ Rails.logger.info "Push notification sent successfully."
25
+ return true
26
+ else
27
+ Rails.logger.info "Push notification failed."
28
+ return false
29
+ end
30
+ end
31
+
32
+ end
33
+ end