notify_user 0.0.28 → 0.0.29

Sign up to get free protection for your applications and to get access to all the features.
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