noticed 1.3.1 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +46 -178
- data/lib/generators/noticed/model_generator.rb +10 -2
- data/lib/noticed/base.rb +20 -10
- data/lib/noticed/delivery_methods/action_cable.rb +13 -1
- data/lib/noticed/delivery_methods/base.rb +2 -2
- data/lib/noticed/delivery_methods/email.rb +28 -3
- data/lib/noticed/delivery_methods/ios.rb +152 -0
- data/lib/noticed/engine.rb +4 -0
- data/lib/noticed/has_notifications.rb +21 -4
- data/lib/noticed/model.rb +1 -0
- data/lib/noticed/translation.rb +5 -1
- data/lib/noticed/version.rb +1 -1
- data/lib/noticed.rb +3 -2
- data/lib/rails_6_polyfills/actioncable/test_adapter.rb +70 -0
- data/lib/rails_6_polyfills/actioncable/test_helper.rb +143 -0
- data/lib/rails_6_polyfills/activejob/serializers.rb +240 -0
- data/lib/rails_6_polyfills/base.rb +18 -0
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff995fcfa939d664a292e883fb4b6b40bb1510c75411bb18d9bee4787f3ccb3a
|
4
|
+
data.tar.gz: c4945ef3d031a3c6565a56826e6538a1e11f4133dc4b8aba5c97e52279e69e0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f087a2e98dbe9c746b334721f5a1807e8cff6373cb2e342ce24abecbc1365f90f93a1282adfba37e8c72f432975354dbd68ff58bc099ec1a02579cce6394caf
|
7
|
+
data.tar.gz: d4ae42066070141132d248d9c76dcbb4b84c19b3c19a2c652e0bdcefb59517719ccbba0a120bd439a4b89f0f6425dc8df0a5f5f5cf9ed633ea8cb32b6e2f9975
|
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
|
-
##
|
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
|
-
|
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
|
-
|
187
|
+
Example:
|
257
188
|
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
193
|
+
## 🚛 Delivery Methods
|
341
194
|
|
342
|
-
|
195
|
+
The delivery methods are designed to be modular so you can customize the way each type gets delivered.
|
343
196
|
|
344
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
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,
|
376
|
-
deliver_by :twilio, delay: 20.minutes,
|
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
|
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
|
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,
|
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
|
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
|
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
|
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)
|
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
|
-
|
87
|
+
self.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
|
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)
|
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
|
-
|
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)
|
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]
|
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
|
data/lib/noticed/engine.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Noticed
|
2
2
|
module HasNotifications
|
3
|
-
# Defines a method for the association and a
|
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.
|
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
data/lib/noticed/translation.rb
CHANGED
@@ -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}.#{
|
21
|
+
"#{i18n_scope}.#{class_scope}#{key}"
|
18
22
|
else
|
19
23
|
key
|
20
24
|
end
|
data/lib/noticed/version.rb
CHANGED
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 :
|
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.
|
4
|
+
version: 1.5.1
|
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-
|
11
|
+
date: 2021-11-15 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:
|
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:
|
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.
|
172
|
+
rubygems_version: 3.2.22
|
168
173
|
signing_key:
|
169
174
|
specification_version: 4
|
170
175
|
summary: Notifications for Ruby on Rails applications
|