noticed 1.3.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83635d722a21df622c90c4e8ded293206242be768b30b2139c41f13bc4d6712c
4
- data.tar.gz: 707ff4e0e8d09ed4c1fa1b0a7228353de637b070e849e7eb234b58b6fb491e40
3
+ metadata.gz: b55f07f54d797f712ad06012dd4ab1ce6272d2c006e2f14fe7b78c59e88b7e14
4
+ data.tar.gz: 555dc41b72c2810780153a002f1a5f8365a4bc0e3df85cd0f9e098f0ca6bef75
5
5
  SHA512:
6
- metadata.gz: 85ddb6e11858df2b21b4dc098096c78465f7ac3dcb7a804037f3cf1855735b954cccb1cb0ad3227d1ebd89cb98e2e6e5d0138af27e73e52d2feeb35ac85ccdec
7
- data.tar.gz: 80d3485a086e8203397f83bf3269e057ec63c9da89b3016d29a8395499447419336d89e7d01daff911a838159b963423dd45f6161b80c0dfab608f2f4e11c662
6
+ metadata.gz: 376a54d888387481f8eb387f481c4b5c672ffdaf78dfb6ca9e61d7131afcbd5c5bd1e572f5630fc6d66de09d2c84bc1e0558130c14eb638c14369d4f1fa3508b
7
+ data.tar.gz: c9e917ab696531278bdbbb2cb5c2575b159d601cc4a21a4e4bb88ecda97d27cda680d7686cfe8268ab3b16f87ecd30babab8bda67ec9c7a3ee0ee48bd062b0a4
data/README.md CHANGED
@@ -15,6 +15,7 @@ Currently, we support these notification delivery methods out of the box:
15
15
  * Microsoft Teams
16
16
  * Twilio (SMS)
17
17
  * Vonage / Nexmo (SMS)
18
+ * iOS Apple Push Notifications
18
19
 
19
20
  And you can easily add new notification types for any other delivery methods.
20
21
 
@@ -159,6 +160,10 @@ For example:
159
160
 
160
161
  `t(".message")` looks up `en.notifications.new_comment.message`
161
162
 
163
+ Or when notification class is in module:
164
+
165
+ `t(".message") # in Admin::NewComment` looks up `en.notifications.admin.new_comment.message`
166
+
162
167
  ##### User Preferences
163
168
 
164
169
  You can use the `if:` and `unless: ` options on your delivery methods to check the user's preferences and skip processing if they have disabled that type of notification.
@@ -175,184 +180,31 @@ class CommentNotification < Noticed::Base
175
180
  end
176
181
  ```
177
182
 
178
- ## 🚛 Delivery Methods
179
-
180
- The delivery methods are designed to be modular so you can customize the way each type gets delivered.
181
-
182
- For example, emails will require a subject, body, and email address while an SMS requires a phone number and simple message. You can define the formats for each of these in your Notification and the delivery method will handle the processing of it.
183
-
184
- ### Database
185
-
186
- Writes notification to the database.
187
-
188
- `deliver_by :database`
189
-
190
- **Note:** Database notifications are special in that they will run before the other delivery methods. We do this so you can reference the database record ID in other delivery methods. For that same reason, the delivery can't be delayed (via the `delay` option) or an error will be raised.
191
-
192
- ##### Options
193
-
194
- * `association` - *Optional*
195
-
196
- The name of the database association to use. Defaults to `:notifications`
197
-
198
- * `format: :format_for_database` - *Optional*
199
-
200
- Use a custom method to define the attributes saved to the database
201
-
202
- ### Email
203
-
204
- Sends an email notification. Emails will always be sent with `deliver_later`
205
-
206
- `deliver_by :email, mailer: "UserMailer"`
207
-
208
- ##### Options
209
-
210
- * `mailer` - **Required**
211
-
212
- The mailer that should send the email
213
-
214
- * `method: :invoice_paid` - *Optional*
215
-
216
- Used to customize the method on the mailer that is called
217
-
218
- * `format: :format_for_email` - *Optional*
219
-
220
- Use a custom method to define the params sent to the mailer. `recipient` will be merged into the params.
221
-
222
- ### ActionCable
223
-
224
- Sends a notification to the browser via websockets (ActionCable channel by default).
225
-
226
- `deliver_by :action_cable`
227
-
228
- ##### Options
229
-
230
- * `format: :format_for_action_cable` - *Optional*
231
-
232
- Use a custom method to define the Hash sent through ActionCable
233
-
234
- * `channel` - *Optional*
235
-
236
- Override the ActionCable channel used to send notifications.
237
-
238
- Defaults to `Noticed::NotificationChannel`
239
-
240
- ### Slack
241
-
242
- Sends a Slack notification via webhook.
243
-
244
- `deliver_by :slack`
245
-
246
- ##### Options
247
-
248
- * `format: :format_for_slack` - *Optional*
249
-
250
- Use a custom method to define the payload sent to Slack. Method should return a Hash.
251
-
252
- * `url: :url_for_slack` - *Optional*
183
+ ## 🐞 Debugging
253
184
 
254
- Use a custom method to retrieve the Slack Webhook URL. Method should return a String.
185
+ In order to figure out what's up when you run in to errors, you can set the `debug` parameter to `true` in your notification, which will give you a more detailed error message about what went wrong.
255
186
 
256
- Defaults to `Rails.application.credentials.slack[:notification_url]`
187
+ Example:
257
188
 
258
- ### Microsoft Teams
259
-
260
- Sends a Teams notification via webhook.
261
-
262
- `deliver_by :microsoft_teams`
263
-
264
- #### Options
265
-
266
- * `format: :format_for_teams` - *Optional*
267
-
268
- Use a custom method to define the payload sent to Microsoft Teams. Method should return a Hash.
269
- Documentation for posting via Webhooks available at: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
270
-
271
- ```ruby
272
- {
273
- title: "This is the title for the card",
274
- text: "This is the body text for the card",
275
- sections: [{activityTitle: "Section Title", activityText: "Section Text"}],
276
- "potentialAction": [{
277
- "@type": "OpenUri",
278
- name: "Button Text",
279
- targets: [{
280
- os: "default",
281
- uri: "https://example.com/foo/action"
282
- }]
283
- }]
284
-
285
- }
286
- ```
287
-
288
- * `url: :url_for_teams_channel`: - *Optional*
289
-
290
- Use a custom method to retrieve the MS Teams Webhook URL. Method should return a string.
291
-
292
- Defaults to `Rails.application.credentials.microsoft_teams[:notification_url]`
293
-
294
- ### Twilio SMS
295
-
296
- Sends an SMS notification via Twilio.
297
-
298
- `deliver_by :twilio`
299
-
300
- ##### Options
301
-
302
- * `credentials: :get_twilio_credentials` - *Optional*
303
-
304
- Use a custom method to retrieve the credentials for Twilio. Method should return a Hash with `:account_sid`, `:auth_token` and `:phone_number` keys.
305
-
306
- Defaults to `Rails.application.credentials.twilio[:account_sid]` and `Rails.application.credentials.twilio[:auth_token]`
307
-
308
- * `url: :get_twilio_url` - *Optional*
309
-
310
- Use a custom method to retrieve the Twilio URL. Method should return the Twilio API url as a string.
311
-
312
- Defaults to `"https://api.twilio.com/2010-04-01/Accounts/#{twilio_credentials(recipient)[:account_sid]}/Messages.json"`
313
-
314
- * `format: :format_for_twilio` - *Optional*
315
-
316
- Use a custom method to define the payload sent to Twilio. Method should return a Hash.
317
-
318
- Defaults to:
319
-
320
- ```ruby
321
- {
322
- Body: notification.params[:message],
323
- From: twilio_credentials[:number],
324
- To: recipient.phone_number
325
- }
326
- ```
327
-
328
- ### Vonage SMS
329
-
330
- Sends an SMS notification via Vonage / Nexmo.
331
-
332
- `deliver_by :vonage`
333
-
334
- ##### Options
335
-
336
- * `credentials: :get_credentials` - *Optional*
337
-
338
- Use a custom method for retrieving credentials. Method should return a Hash with `:api_key` and `:api_secret` keys.
189
+ ```ruby
190
+ deliver_by :slack, debug: true
191
+ ```
339
192
 
340
- Defaults to `Rails.application.credentials.vonage[:api_key]` and `Rails.application.credentials.vonage[:api_secret]`
193
+ ## 🚛 Delivery Methods
341
194
 
342
- * `deliver_by :vonage, format: :format_for_vonage` - *Optional*
195
+ The delivery methods are designed to be modular so you can customize the way each type gets delivered.
343
196
 
344
- Use a custom method to generate the params sent to Vonage. Method should return a Hash. Defaults to:
197
+ For example, emails will require a subject, body, and email address while an SMS requires a phone number and simple message. You can define the formats for each of these in your Notification and the delivery method will handle the processing of it.
345
198
 
346
- ```ruby
347
- {
348
- api_key: vonage_credentials[:api_key],
349
- api_secret: vonage_credentials[:api_secret],
350
- from: notification.params[:from],
351
- text: notification.params[:body],
352
- to: notification.params[:to],
353
- type: "unicode"
354
- }
355
- ```
199
+ * [Database](docs/delivery_methods/database.md)
200
+ * [Email](docs/delivery_methods/email.md)
201
+ * [ActionCable](docs/delivery_methods/action_cable.md)
202
+ * [iOS Apple Push Notifications](docs/delivery_methods/ios.md)
203
+ * [Microsoft Teams](docs/delivery_methods/microsoft_teams.md)
204
+ * [Slack](docs/delivery_methods/slack.md)
205
+ * [Test](docs/delivery_methods/test.md)
206
+ * [Twilio](docs/delivery_methods/twilio.md)
207
+ * [Vonage](docs/delivery_methods/vonage.md)
356
208
 
357
209
  ### Fallback Notifications
358
210
 
@@ -371,16 +223,24 @@ You can also configure multiple fallback options:
371
223
 
372
224
  ```ruby
373
225
  class CriticalSystemNotification < Noticed::Base
226
+ deliver_by :database
374
227
  deliver_by :slack
375
- deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, unless: :read?
376
- deliver_by :twilio, delay: 20.minutes, unless: :read?
228
+ deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, if: :unread?
229
+ deliver_by :twilio, delay: 20.minutes, if: :unread?
377
230
  end
378
231
  ```
379
232
 
380
- In this scenario, you can create an escalating notification that starts with a ping in Slack, then emails the team, and then finally sends an SMS to the on-call phone.
233
+ In this scenario, you have created an escalating notification system that
234
+
235
+ - Immediately creates a record in the database (for display directly in the app)
236
+ - Immediately issues a ping in Slack.
237
+ - If the notification remains unread after 10 minutes, it emails the team.
238
+ - If the notification remains unread after 20 minutes, it sends an SMS to the on-call phone.
381
239
 
382
240
  You can mix and match the options and delivery methods to suit your application specific needs.
383
241
 
242
+ Please note that to implement this pattern, it is essential `deliver_by :database` is one among the different delivery methods specified. Without this, a database record of the notification will not be created.
243
+
384
244
  ### 🚚 Custom Delivery Methods
385
245
 
386
246
  To generate a custom delivery method, simply run
@@ -471,13 +331,13 @@ Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The
471
331
  deliver_by DeliveryMethods::Discord
472
332
  ```
473
333
 
474
- For Rails 6.0, you must pass strings of the class names in the `deliver_by` options.
334
+ For Rails 5.2 and 6.0, you must pass strings of the class names in the `deliver_by` options.
475
335
 
476
336
  ```ruby
477
337
  deliver_by :discord, class: "DeliveryMethods::Discord"
478
338
  ```
479
339
 
480
- We recommend the Rails 6.0 compatible options to prevent confusion.
340
+ We recommend using a string in order to prevent confusion.
481
341
 
482
342
  ### 📦 Database Model
483
343
 
@@ -550,7 +410,7 @@ class Post < ApplicationRecord
550
410
  has_noticed_notifications
551
411
 
552
412
  # You can override the param_name, the notification model name, or disable the before_destroy callback
553
- has_noticed_notifications param_name: :parent, destroy: false, model: "Notification"
413
+ has_noticed_notifications param_name: :parent, destroy: false, model_name: "Notification"
554
414
  end
555
415
 
556
416
  # Create a CommentNotification with a post param
@@ -564,7 +424,7 @@ CommentNotification.with(parent: @post).deliver(user)
564
424
 
565
425
  #### Handling Deleted Records
566
426
 
567
- If you create a notification but delete the associated record and forgot `has_noticed_notifications` on the model, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discord the job on these errors by adding the following to `ApplicationJob`:
427
+ If you create a notification but delete the associated record and forgot `has_noticed_notifications` on the model, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discard the job on these errors by adding the following to `ApplicationJob`:
568
428
 
569
429
  ```ruby
570
430
  class ApplicationJob < ActiveJob::Base
@@ -576,5 +436,13 @@ end
576
436
 
577
437
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
578
438
 
439
+ Running tests against multiple databases locally:
440
+
441
+ ```
442
+ DATABASE_URL=sqlite3:noticed_test rails test
443
+ DATABASE_URL=mysql2://root:@127.0.0.1/noticed_test rails test
444
+ DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
445
+ ```
446
+
579
447
  ## 📝 License
580
448
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -42,14 +42,22 @@ module Noticed
42
42
  end
43
43
 
44
44
  def params_column
45
- case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
46
- when "postgresql"
45
+ case current_adapter
46
+ when "postgresql", "postgis"
47
47
  "params:jsonb"
48
48
  else
49
49
  # MySQL and SQLite both support json
50
50
  "params:json"
51
51
  end
52
52
  end
53
+
54
+ def current_adapter
55
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
56
+ ActiveRecord::Base.connection_db_config.adapter
57
+ else
58
+ ActiveRecord::Base.connection_config[:adapter]
59
+ end
60
+ end
53
61
  end
54
62
  end
55
63
  end
data/lib/noticed/base.rb CHANGED
@@ -9,7 +9,7 @@ module Noticed
9
9
  class_attribute :delivery_methods, instance_writer: false, default: []
10
10
  class_attribute :param_names, instance_writer: false, default: []
11
11
 
12
- # Gives notifications access to the record and recipient when formatting for delivery
12
+ # Gives notifications access to the record and recipient during delivery
13
13
  attr_accessor :record, :recipient
14
14
 
15
15
  delegate :read?, :unread?, to: :record
@@ -21,7 +21,7 @@ module Noticed
21
21
  end
22
22
 
23
23
  # Copy delivery methods from parent
24
- def inherited(base) #:nodoc:
24
+ def inherited(base) # :nodoc:
25
25
  base.delivery_methods = delivery_methods.dup
26
26
  base.param_names = param_names.dup
27
27
  super
@@ -31,6 +31,16 @@ module Noticed
31
31
  new(params)
32
32
  end
33
33
 
34
+ # Shortcut for delivering without params
35
+ def deliver(recipients)
36
+ new.deliver(recipients)
37
+ end
38
+
39
+ # Shortcut for delivering later without params
40
+ def deliver_later(recipients)
41
+ new.deliver_later(recipients)
42
+ end
43
+
34
44
  def params(*names)
35
45
  param_names.concat Array.wrap(names)
36
46
  end
@@ -71,22 +81,19 @@ module Noticed
71
81
  def run_delivery(recipient, enqueue: true)
72
82
  delivery_methods = self.class.delivery_methods.dup
73
83
 
74
- # Set recipient to instance var so it is available to Notification class
75
- @recipient = recipient
76
-
77
84
  # Run database delivery inline first if it exists so other methods have access to the record
78
85
  if (index = delivery_methods.find_index { |m| m[:name] == :database })
79
86
  delivery_method = delivery_methods.delete_at(index)
80
- @record = run_delivery_method(delivery_method, recipient: recipient, enqueue: false)
87
+ record = run_delivery_method(delivery_method, recipient: recipient, enqueue: false, record: nil)
81
88
  end
82
89
 
83
90
  delivery_methods.each do |delivery_method|
84
- run_delivery_method(delivery_method, recipient: recipient, enqueue: enqueue)
91
+ run_delivery_method(delivery_method, recipient: recipient, enqueue: enqueue, record: record)
85
92
  end
86
93
  end
87
94
 
88
95
  # Actually runs an individual delivery
89
- def run_delivery_method(delivery_method, recipient:, enqueue:)
96
+ def run_delivery_method(delivery_method, recipient:, enqueue:, record:)
90
97
  args = {
91
98
  notification_class: self.class.name,
92
99
  options: delivery_method[:options],
@@ -98,11 +105,14 @@ module Noticed
98
105
  run_callbacks delivery_method[:name] do
99
106
  method = delivery_method_for(delivery_method[:name], delivery_method[:options])
100
107
 
108
+ # If the queue is `nil`, ActiveJob will use a default queue name.
109
+ queue = delivery_method.dig(:options, :queue)
110
+
101
111
  # Always perfrom later if a delay is present
102
112
  if (delay = delivery_method.dig(:options, :delay))
103
- method.set(wait: delay).perform_later(args)
113
+ method.set(wait: delay, queue: queue).perform_later(args)
104
114
  elsif enqueue
105
- method.perform_later(args)
115
+ method.set(queue: queue).perform_later(args)
106
116
  else
107
117
  method.perform_now(args)
108
118
  end
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class ActionCable < Base
4
4
  def deliver
5
- channel.broadcast_to recipient, format
5
+ channel.broadcast_to stream, format
6
6
  end
7
7
 
8
8
  private
@@ -30,6 +30,18 @@ module Noticed
30
30
  end
31
31
  end
32
32
  end
33
+
34
+ def stream
35
+ value = options[:stream]
36
+ case value
37
+ when String
38
+ value
39
+ when Symbol
40
+ notification.send(value)
41
+ else
42
+ recipient
43
+ end
44
+ end
33
45
  end
34
46
  end
35
47
  end
@@ -10,7 +10,7 @@ module Noticed
10
10
 
11
11
  class << self
12
12
  # Copy option names from parent
13
- def inherited(base) #:nodoc:
13
+ def inherited(base) # :nodoc:
14
14
  base.option_names = option_names.dup
15
15
  super
16
16
  end
@@ -31,7 +31,7 @@ module Noticed
31
31
 
32
32
  def perform(args)
33
33
  @notification = args[:notification_class].constantize.new(args[:params])
34
- @options = args[:options]
34
+ @options = args[:options] || {}
35
35
  @params = args[:params]
36
36
  @recipient = args[:recipient]
37
37
  @record = args[:record]
@@ -4,17 +4,42 @@ module Noticed
4
4
  option :mailer
5
5
 
6
6
  def deliver
7
- mailer.with(format).send(method.to_sym).deliver_now
7
+ if options[:enqueue]
8
+ mailer.with(format).send(method.to_sym).deliver_later
9
+ else
10
+ mailer.with(format).send(method.to_sym).deliver_now
11
+ end
8
12
  end
9
13
 
10
14
  private
11
15
 
16
+ # mailer: "UserMailer"
17
+ # mailer: UserMailer
18
+ # mailer: :my_method - `my_method` should return Class
12
19
  def mailer
13
- options.fetch(:mailer).constantize
20
+ option = options.fetch(:mailer)
21
+ case option
22
+ when String
23
+ option.constantize
24
+ when Symbol
25
+ notification.send(option)
26
+ else
27
+ option
28
+ end
14
29
  end
15
30
 
31
+ # Method should be a symbol
32
+ #
33
+ # If notification responds to symbol, call that method and use return value
34
+ # If notification does not respond to symbol, use the symbol for the mailer method
35
+ # Otherwise, use the underscored notification class name as the mailer method
16
36
  def method
17
- options[:method] || notification.class.name.underscore
37
+ method_name = options[:method]&.to_sym
38
+ if method_name.present?
39
+ notification.respond_to?(method_name) ? notification.send(method_name) : method_name
40
+ else
41
+ notification.class.name.underscore
42
+ end
18
43
  end
19
44
 
20
45
  def format
@@ -0,0 +1,152 @@
1
+ require "apnotic"
2
+
3
+ module Noticed
4
+ module DeliveryMethods
5
+ class Ios < Base
6
+ cattr_accessor :connection_pool
7
+
8
+ def deliver
9
+ raise ArgumentError, "bundle_identifier is missing" if bundle_identifier.blank?
10
+ raise ArgumentError, "key_id is missing" if key_id.blank?
11
+ raise ArgumentError, "team_id is missing" if team_id.blank?
12
+ raise ArgumentError, "Could not find APN cert at '#{cert_path}'" unless File.exist?(cert_path)
13
+
14
+ device_tokens.each do |device_token|
15
+ connection_pool.with do |connection|
16
+ apn = Apnotic::Notification.new(device_token)
17
+ format_notification(apn)
18
+
19
+ response = connection.push(apn)
20
+ raise "Timeout sending iOS push notification" unless response
21
+
22
+ if bad_token?(response)
23
+ # Allow notification to cleanup invalid iOS device tokens
24
+ cleanup_invalid_token(device_token)
25
+ elsif !response.ok?
26
+ raise "Request failed #{response.body}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def format_notification(apn)
35
+ apn.topic = bundle_identifier
36
+
37
+ if (method = options[:format])
38
+ notification.send(method, apn)
39
+ elsif params[:message].present?
40
+ apn.alert = params[:message]
41
+ else
42
+ raise ArgumentError, "No message for iOS delivery. Either include message in params or add the 'format' option in 'deliver_by :ios'."
43
+ end
44
+ end
45
+
46
+ def device_tokens
47
+ if notification.respond_to?(:ios_device_tokens)
48
+ Array.wrap(notification.ios_device_tokens(recipient))
49
+ else
50
+ raise NoMethodError, <<~MESSAGE
51
+ You must implement `ios_device_tokens` to send iOS notifications
52
+
53
+ # This must return an Array of iOS device tokens
54
+ def ios_device_tokens(user)
55
+ user.ios_device_tokens.pluck(:token)
56
+ end
57
+ MESSAGE
58
+ end
59
+ end
60
+
61
+ def bad_token?(response)
62
+ response.status == "410" || (response.status == "400" && response.body["reason"] == "BadDeviceToken")
63
+ end
64
+
65
+ def cleanup_invalid_token(token)
66
+ return unless notification.respond_to?(:cleanup_device_token)
67
+ notification.send(:cleanup_device_token, token: token, platform: "iOS")
68
+ end
69
+
70
+ def connection_pool
71
+ self.class.connection_pool ||= new_connection_pool
72
+ end
73
+
74
+ def new_connection_pool
75
+ handler = proc do |connection|
76
+ connection.on(:error) do |exception|
77
+ Rails.logger.info "Apnotic exception raised: #{exception}"
78
+ end
79
+ end
80
+
81
+ if options[:development]
82
+ Apnotic::ConnectionPool.development(connection_pool_options, pool_options, &handler)
83
+ else
84
+ Apnotic::ConnectionPool.new(connection_pool_options, pool_options, &handler)
85
+ end
86
+ end
87
+
88
+ def connection_pool_options
89
+ {
90
+ auth_method: :token,
91
+ cert_path: cert_path,
92
+ key_id: key_id,
93
+ team_id: team_id
94
+ }
95
+ end
96
+
97
+ def bundle_identifier
98
+ option = options[:bundle_identifier]
99
+ case option
100
+ when String
101
+ option
102
+ when Symbol
103
+ notification.send(option)
104
+ else
105
+ Rails.application.credentials.dig(:ios, :bundle_identifier)
106
+ end
107
+ end
108
+
109
+ def cert_path
110
+ option = options[:cert_path]
111
+ case option
112
+ when String
113
+ option
114
+ when Symbol
115
+ notification.send(option)
116
+ else
117
+ Rails.root.join("config/certs/ios/apns.p8")
118
+ end
119
+ end
120
+
121
+ def key_id
122
+ option = options[:key_id]
123
+ case option
124
+ when String
125
+ option
126
+ when Symbol
127
+ notification.send(option)
128
+ else
129
+ Rails.application.credentials.dig(:ios, :key_id)
130
+ end
131
+ end
132
+
133
+ def team_id
134
+ option = options[:team_id]
135
+ case option
136
+ when String
137
+ option
138
+ when Symbol
139
+ notification.send(option)
140
+ else
141
+ Rails.application.credentials.dig(:ios, :team_id)
142
+ end
143
+ end
144
+
145
+ def pool_options
146
+ {
147
+ size: options.fetch(:pool_size, 5)
148
+ }
149
+ end
150
+ end
151
+ end
152
+ end
@@ -5,5 +5,9 @@ module Noticed
5
5
  include Noticed::HasNotifications
6
6
  end
7
7
  end
8
+
9
+ initializer "noticed.rails_5_2_support" do
10
+ require "rails_6_polyfills/base" if Rails::VERSION::MAJOR < 6
11
+ end
8
12
  end
9
13
  end
@@ -1,6 +1,6 @@
1
1
  module Noticed
2
2
  module HasNotifications
3
- # Defines a method for the association and a before_destory callback to remove notifications
3
+ # Defines a method for the association and a before_destroy callback to remove notifications
4
4
  # where this record is a param
5
5
  #
6
6
  # class User < ApplicationRecord
@@ -15,10 +15,19 @@ module Noticed
15
15
 
16
16
  class_methods do
17
17
  def has_noticed_notifications(param_name: model_name.singular, **options)
18
- model = options.fetch(:model_name, "Notification").constantize
19
-
20
18
  define_method "notifications_as_#{param_name}" do
21
- model.where(params: {param_name.to_sym => self})
19
+ model = options.fetch(:model_name, "Notification").constantize
20
+ case current_adapter
21
+ when "postgresql", "postgis"
22
+ model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
23
+ when "mysql2"
24
+ model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
25
+ when "sqlite3"
26
+ model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
27
+ else
28
+ # This will perform an exact match which isn't ideal
29
+ model.where(params: {param_name.to_sym => self})
30
+ end
22
31
  end
23
32
 
24
33
  if options.fetch(:destroy, true)
@@ -28,5 +37,13 @@ module Noticed
28
37
  end
29
38
  end
30
39
  end
40
+
41
+ def current_adapter
42
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
43
+ ActiveRecord::Base.connection_db_config.adapter
44
+ else
45
+ ActiveRecord::Base.connection_config[:adapter]
46
+ end
47
+ end
31
48
  end
32
49
  end
data/lib/noticed/model.rb CHANGED
@@ -24,12 +24,16 @@ module Noticed
24
24
  end
25
25
 
26
26
  def noticed_coder
27
+ return Noticed::TextCoder unless table_exists?
28
+
27
29
  case attribute_types["params"].type
28
30
  when :json, :jsonb
29
31
  Noticed::Coder
30
32
  else
31
33
  Noticed::TextCoder
32
34
  end
35
+ rescue ActiveRecord::NoDatabaseError
36
+ Noticed::TextCoder
33
37
  end
34
38
  end
35
39
 
@@ -38,6 +42,7 @@ module Noticed
38
42
  @_notification ||= begin
39
43
  instance = type.constantize.with(params)
40
44
  instance.record = self
45
+ instance.recipient = recipient
41
46
  instance
42
47
  end
43
48
  end
@@ -7,6 +7,10 @@ module Noticed
7
7
  :notifications
8
8
  end
9
9
 
10
+ def class_scope
11
+ self.class.name.underscore.tr("/", ".")
12
+ end
13
+
10
14
  def translate(key, **options)
11
15
  I18n.translate(scope_translation_key(key), **options)
12
16
  end
@@ -14,7 +18,7 @@ module Noticed
14
18
 
15
19
  def scope_translation_key(key)
16
20
  if key.to_s.start_with?(".")
17
- "#{i18n_scope}.#{self.class.name.underscore}#{key}"
21
+ "#{i18n_scope}.#{class_scope}#{key}"
18
22
  else
19
23
  key
20
24
  end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.3.0"
2
+ VERSION = "1.5.0"
3
3
  end
data/lib/noticed.rb CHANGED
@@ -12,12 +12,13 @@ module Noticed
12
12
  autoload :NotificationChannel, "noticed/notification_channel"
13
13
 
14
14
  module DeliveryMethods
15
- autoload :Base, "noticed/delivery_methods/base"
16
15
  autoload :ActionCable, "noticed/delivery_methods/action_cable"
16
+ autoload :Base, "noticed/delivery_methods/base"
17
17
  autoload :Database, "noticed/delivery_methods/database"
18
18
  autoload :Email, "noticed/delivery_methods/email"
19
- autoload :Slack, "noticed/delivery_methods/slack"
19
+ autoload :Ios, "noticed/delivery_methods/ios"
20
20
  autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams"
21
+ autoload :Slack, "noticed/delivery_methods/slack"
21
22
  autoload :Test, "noticed/delivery_methods/test"
22
23
  autoload :Twilio, "noticed/delivery_methods/twilio"
23
24
  autoload :Vonage, "noticed/delivery_methods/vonage"
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/subscription_adapter/base"
4
+ require "action_cable/subscription_adapter/subscriber_map"
5
+ require "action_cable/subscription_adapter/async"
6
+
7
+ module ActionCable
8
+ module SubscriptionAdapter
9
+ # == Test adapter for Action Cable
10
+ #
11
+ # The test adapter should be used only in testing. Along with
12
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
13
+ #
14
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
15
+ #
16
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
17
+ # so it could be used in system tests too.
18
+ class Test < Async
19
+ def broadcast(channel, payload)
20
+ broadcasts(channel) << payload
21
+ super
22
+ end
23
+
24
+ def broadcasts(channel)
25
+ channels_data[channel] ||= []
26
+ end
27
+
28
+ def clear_messages(channel)
29
+ channels_data[channel] = []
30
+ end
31
+
32
+ def clear
33
+ @channels_data = nil
34
+ end
35
+
36
+ private
37
+
38
+ def channels_data
39
+ @channels_data ||= {}
40
+ end
41
+ end
42
+ end
43
+
44
+ # Update how broadcast_for determines the channel name so it's consistent with the Rails 6 way
45
+ module Channel
46
+ module Broadcasting
47
+ delegate :broadcast_to, to: :class
48
+ module ClassMethods
49
+ def broadcast_to(model, message)
50
+ ActionCable.server.broadcast(broadcasting_for(model), message)
51
+ end
52
+
53
+ def broadcasting_for(model)
54
+ serialize_broadcasting([channel_name, model])
55
+ end
56
+
57
+ def serialize_broadcasting(object) # :nodoc:
58
+ case # standard:disable Style/EmptyCaseCondition
59
+ when object.is_a?(Array)
60
+ object.map { |m| serialize_broadcasting(m) }.join(":")
61
+ when object.respond_to?(:to_gid_param)
62
+ object.to_gid_param
63
+ else
64
+ object.to_param
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ # Have ActionCable pick its Test SubscriptionAdapter when it's called for in cable.yml
5
+ module Server
6
+ class Configuration
7
+ def pubsub_adapter
8
+ cable["adapter"] == "test" ? ActionCable::SubscriptionAdapter::Test : super
9
+ end
10
+ end
11
+ end
12
+
13
+ # Provides helper methods for testing Action Cable broadcasting
14
+ module TestHelper
15
+ def before_setup # :nodoc:
16
+ server = ActionCable.server
17
+ test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
18
+
19
+ @old_pubsub_adapter = server.pubsub
20
+
21
+ server.instance_variable_set(:@pubsub, test_adapter)
22
+ super
23
+ end
24
+
25
+ def after_teardown # :nodoc:
26
+ super
27
+ ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
28
+ end
29
+
30
+ # Asserts that the number of broadcasted messages to the stream matches the given number.
31
+ #
32
+ # def test_broadcasts
33
+ # assert_broadcasts 'messages', 0
34
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
35
+ # assert_broadcasts 'messages', 1
36
+ # ActionCable.server.broadcast 'messages', { text: 'world' }
37
+ # assert_broadcasts 'messages', 2
38
+ # end
39
+ #
40
+ # If a block is passed, that block should cause the specified number of
41
+ # messages to be broadcasted.
42
+ #
43
+ # def test_broadcasts_again
44
+ # assert_broadcasts('messages', 1) do
45
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
46
+ # end
47
+ #
48
+ # assert_broadcasts('messages', 2) do
49
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
50
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
51
+ # end
52
+ # end
53
+ #
54
+ def assert_broadcasts(stream, number)
55
+ if block_given?
56
+ original_count = broadcasts_size(stream)
57
+ yield
58
+ new_count = broadcasts_size(stream)
59
+ actual_count = new_count - original_count
60
+ else
61
+ actual_count = broadcasts_size(stream)
62
+ end
63
+
64
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
65
+ end
66
+
67
+ # Asserts that no messages have been sent to the stream.
68
+ #
69
+ # def test_no_broadcasts
70
+ # assert_no_broadcasts 'messages'
71
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
72
+ # assert_broadcasts 'messages', 1
73
+ # end
74
+ #
75
+ # If a block is passed, that block should not cause any message to be sent.
76
+ #
77
+ # def test_broadcasts_again
78
+ # assert_no_broadcasts 'messages' do
79
+ # # No job messages should be sent from this block
80
+ # end
81
+ # end
82
+ #
83
+ # Note: This assertion is simply a shortcut for:
84
+ #
85
+ # assert_broadcasts 'messages', 0, &block
86
+ #
87
+ def assert_no_broadcasts(stream, &block)
88
+ assert_broadcasts stream, 0, &block
89
+ end
90
+
91
+ # Asserts that the specified message has been sent to the stream.
92
+ #
93
+ # def test_assert_transmitted_message
94
+ # ActionCable.server.broadcast 'messages', text: 'hello'
95
+ # assert_broadcast_on('messages', text: 'hello')
96
+ # end
97
+ #
98
+ # If a block is passed, that block should cause a message with the specified data to be sent.
99
+ #
100
+ # def test_assert_broadcast_on_again
101
+ # assert_broadcast_on('messages', text: 'hello') do
102
+ # ActionCable.server.broadcast 'messages', text: 'hello'
103
+ # end
104
+ # end
105
+ #
106
+ def assert_broadcast_on(stream, data)
107
+ # Encode to JSON and back–we want to use this value to compare
108
+ # with decoded JSON.
109
+ # Comparing JSON strings doesn't work due to the order of the keys.
110
+ serialized_msg =
111
+ ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
112
+
113
+ new_messages = broadcasts(stream)
114
+ if block_given?
115
+ old_messages = new_messages
116
+ clear_messages(stream)
117
+
118
+ yield
119
+ new_messages = broadcasts(stream)
120
+ clear_messages(stream)
121
+
122
+ # Restore all sent messages
123
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
124
+ end
125
+
126
+ message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
127
+
128
+ assert message, "No messages sent with #{data} to #{stream}"
129
+ end
130
+
131
+ def pubsub_adapter # :nodoc:
132
+ ActionCable.server.pubsub
133
+ end
134
+
135
+ delegate :broadcasts, :clear_messages, to: :pubsub_adapter
136
+
137
+ private
138
+
139
+ def broadcasts_size(channel)
140
+ broadcasts(channel).size
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ # First add Rails 6.0 ActiveJob Serializers support, and then the
4
+ # DurationSerializer and SymbolSerializer.
5
+ module ActiveJob
6
+ module Arguments
7
+ # :nodoc:
8
+ OBJECT_SERIALIZER_KEY = "_aj_serialized"
9
+
10
+ def serialize_argument(argument)
11
+ case argument
12
+ when *TYPE_WHITELIST
13
+ argument
14
+ when GlobalID::Identification
15
+ convert_to_global_id_hash(argument)
16
+ when Array
17
+ argument.map { |arg| serialize_argument(arg) }
18
+ when ActiveSupport::HashWithIndifferentAccess
19
+ serialize_indifferent_hash(argument)
20
+ when Hash
21
+ symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
22
+ result = serialize_hash(argument)
23
+ result[SYMBOL_KEYS_KEY] = symbol_keys
24
+ result
25
+ when ->(arg) { arg.respond_to?(:permitted?) }
26
+ serialize_indifferent_hash(argument.to_h)
27
+ else # Add Rails 6 support for Serializers
28
+ Serializers.serialize(argument)
29
+ end
30
+ end
31
+
32
+ def deserialize_argument(argument)
33
+ case argument
34
+ when String
35
+ argument
36
+ when *TYPE_WHITELIST
37
+ argument
38
+ when Array
39
+ argument.map { |arg| deserialize_argument(arg) }
40
+ when Hash
41
+ if serialized_global_id?(argument)
42
+ deserialize_global_id argument
43
+ elsif custom_serialized?(argument)
44
+ Serializers.deserialize(argument)
45
+ else
46
+ deserialize_hash(argument)
47
+ end
48
+ else
49
+ raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
50
+ end
51
+ end
52
+
53
+ def custom_serialized?(hash)
54
+ hash.key?(OBJECT_SERIALIZER_KEY)
55
+ end
56
+ end
57
+
58
+ # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
59
+ # and to add new ones. It also has helpers to serialize/deserialize objects.
60
+ module Serializers # :nodoc:
61
+ # Base class for serializing and deserializing custom objects.
62
+ #
63
+ # Example:
64
+ #
65
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
66
+ # def serialize(money)
67
+ # super("amount" => money.amount, "currency" => money.currency)
68
+ # end
69
+ #
70
+ # def deserialize(hash)
71
+ # Money.new(hash["amount"], hash["currency"])
72
+ # end
73
+ #
74
+ # private
75
+ #
76
+ # def klass
77
+ # Money
78
+ # end
79
+ # end
80
+ class ObjectSerializer
81
+ include Singleton
82
+
83
+ class << self
84
+ delegate :serialize?, :serialize, :deserialize, to: :instance
85
+ end
86
+
87
+ # Determines if an argument should be serialized by a serializer.
88
+ def serialize?(argument)
89
+ argument.is_a?(klass)
90
+ end
91
+
92
+ # Serializes an argument to a JSON primitive type.
93
+ def serialize(hash)
94
+ {Arguments::OBJECT_SERIALIZER_KEY => self.class.name}.merge!(hash)
95
+ end
96
+
97
+ # Deserializes an argument from a JSON primitive type.
98
+ def deserialize(_argument)
99
+ raise NotImplementedError
100
+ end
101
+
102
+ private
103
+
104
+ # The class of the object that will be serialized.
105
+ def klass
106
+ raise NotImplementedError
107
+ end
108
+ end
109
+
110
+ class DurationSerializer < ObjectSerializer # :nodoc:
111
+ def serialize(duration)
112
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts.each_with_object({}) { |v, s| s[v.first.to_s] = v.last }))
113
+ end
114
+
115
+ def deserialize(hash)
116
+ value = hash["value"]
117
+ parts = Arguments.deserialize(hash["parts"])
118
+
119
+ klass.new(value, parts)
120
+ end
121
+
122
+ private
123
+
124
+ def klass
125
+ ActiveSupport::Duration
126
+ end
127
+ end
128
+
129
+ class SymbolSerializer < ObjectSerializer # :nodoc:
130
+ def serialize(argument)
131
+ super("value" => argument.to_s)
132
+ end
133
+
134
+ def deserialize(argument)
135
+ argument["value"].to_sym
136
+ end
137
+
138
+ private
139
+
140
+ def klass
141
+ Symbol
142
+ end
143
+ end
144
+
145
+ # -----------------------------
146
+
147
+ mattr_accessor :_additional_serializers
148
+ self._additional_serializers = Set.new
149
+
150
+ class << self
151
+ # Returns serialized representative of the passed object.
152
+ # Will look up through all known serializers.
153
+ # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
154
+ def serialize(argument)
155
+ serializer = serializers.detect { |s| s.serialize?(argument) }
156
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
157
+ serializer.serialize(argument)
158
+ end
159
+
160
+ # Returns deserialized object.
161
+ # Will look up through all known serializers.
162
+ # If no serializer found will raise <tt>ArgumentError</tt>.
163
+ def deserialize(argument)
164
+ serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
165
+ raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
166
+
167
+ serializer = serializer_name.safe_constantize
168
+ raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
169
+
170
+ serializer.deserialize(argument)
171
+ end
172
+
173
+ # Returns list of known serializers.
174
+ def serializers
175
+ self._additional_serializers # standard:disable Style/RedundantSelf
176
+ end
177
+
178
+ # Adds new serializers to a list of known serializers.
179
+ def add_serializers(*new_serializers)
180
+ self._additional_serializers += new_serializers.flatten
181
+ end
182
+ end
183
+
184
+ add_serializers DurationSerializer,
185
+ SymbolSerializer
186
+ # The full set of 6 serializers that Rails 6.0 normally adds here -- feel free to include any others if you wish:
187
+ # SymbolSerializer,
188
+ # DurationSerializer, # (The one that we've added above in order to support testing)
189
+ # DateTimeSerializer,
190
+ # DateSerializer,
191
+ # TimeWithZoneSerializer,
192
+ # TimeSerializer
193
+ end
194
+
195
+ # Is the updated version of perform_enqueued_jobs from Rails 6.0 missing from ActionJob's TestHelper?
196
+ unless TestHelper.private_instance_methods.include?(:flush_enqueued_jobs)
197
+ module TestHelper
198
+ def perform_enqueued_jobs(only: nil, except: nil, queue: nil)
199
+ return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given?
200
+
201
+ super
202
+ end
203
+
204
+ private
205
+
206
+ def jobs_with(jobs, only: nil, except: nil, queue: nil)
207
+ validate_option(only: only, except: except)
208
+
209
+ jobs.count do |job|
210
+ job_class = job.fetch(:job)
211
+
212
+ if only
213
+ next false unless filter_as_proc(only).call(job)
214
+ elsif except
215
+ next false if filter_as_proc(except).call(job)
216
+ end
217
+
218
+ if queue
219
+ next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
220
+ end
221
+
222
+ yield job if block_given?
223
+
224
+ true
225
+ end
226
+ end
227
+
228
+ def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
229
+ jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
230
+ end
231
+
232
+ def flush_enqueued_jobs(only: nil, except: nil, queue: nil)
233
+ enqueued_jobs_with(only: only, except: except, queue: queue) do |payload|
234
+ instantiate_job(payload).perform_now
235
+ queue_adapter.performed_jobs << payload
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,18 @@
1
+ # The following implements polyfills for Rails < 6.0
2
+ module ActionCable
3
+ # If the Rails 6.0 ActionCable::TestHelper is missing then allow it to autoload
4
+ unless ActionCable.const_defined? "TestHelper"
5
+ autoload :TestHelper, "rails_6_polyfills/actioncable/test_helper.rb"
6
+ end
7
+ # If the Rails 6.0 test SubscriptionAdapter is missing then allow it to autoload
8
+ unless ActionCable.const_defined? "SubscriptionAdapter::Test"
9
+ module SubscriptionAdapter
10
+ autoload :Test, "rails_6_polyfills/actioncable/test_adapter.rb"
11
+ end
12
+ end
13
+ end
14
+
15
+ # If the Rails 6.0 ActionJob Serializers are missing then load support for them
16
+ unless Object.const_defined?("ActiveJob::Serializers")
17
+ require "rails_6_polyfills/activejob/serializers"
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noticed
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-13 00:00:00.000000000 Z
11
+ date: 2021-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.0
19
+ version: 5.2.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 6.0.0
26
+ version: 5.2.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: http
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +132,7 @@ files:
132
132
  - lib/noticed/delivery_methods/base.rb
133
133
  - lib/noticed/delivery_methods/database.rb
134
134
  - lib/noticed/delivery_methods/email.rb
135
+ - lib/noticed/delivery_methods/ios.rb
135
136
  - lib/noticed/delivery_methods/microsoft_teams.rb
136
137
  - lib/noticed/delivery_methods/slack.rb
137
138
  - lib/noticed/delivery_methods/test.rb
@@ -144,6 +145,10 @@ files:
144
145
  - lib/noticed/text_coder.rb
145
146
  - lib/noticed/translation.rb
146
147
  - lib/noticed/version.rb
148
+ - lib/rails_6_polyfills/actioncable/test_adapter.rb
149
+ - lib/rails_6_polyfills/actioncable/test_helper.rb
150
+ - lib/rails_6_polyfills/activejob/serializers.rb
151
+ - lib/rails_6_polyfills/base.rb
147
152
  - lib/tasks/noticed_tasks.rake
148
153
  homepage: https://github.com/excid3/noticed
149
154
  licenses:
@@ -164,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
169
  - !ruby/object:Gem::Version
165
170
  version: '0'
166
171
  requirements: []
167
- rubygems_version: 3.2.3
172
+ rubygems_version: 3.2.22
168
173
  signing_key:
169
174
  specification_version: 4
170
175
  summary: Notifications for Ruby on Rails applications