noticed 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a1534dc87f0c5a0ff1178c1eafc8babae3e4cd49f3f902311a4e7db9672ed77
4
- data.tar.gz: a5e7f177a02369bc82a1ba3c5989df6b86ecfca2274c9f6905c8f14ca2e7537e
3
+ metadata.gz: 83635d722a21df622c90c4e8ded293206242be768b30b2139c41f13bc4d6712c
4
+ data.tar.gz: 707ff4e0e8d09ed4c1fa1b0a7228353de637b070e849e7eb234b58b6fb491e40
5
5
  SHA512:
6
- metadata.gz: b6d5ae8fc80b975db5aaa09d33e9c35b55a0f8b02aee37cfc73c226357d3f80068d109b034006c563a19b581aa3a8b8d8600070e52a2e1eabe5d83ab76beb33e
7
- data.tar.gz: 4f78b2bb15d8bf64f094b60905b5225abd431104bfb25271163748e9702cedac64bfd15640e9b3898f35f7375c57f8619ab48ccedc8d1f0cba6483fd683e9463
6
+ metadata.gz: 85ddb6e11858df2b21b4dc098096c78465f7ac3dcb7a804037f3cf1855735b954cccb1cb0ad3227d1ebd89cb98e2e6e5d0138af27e73e52d2feeb35ac85ccdec
7
+ data.tar.gz: 80d3485a086e8203397f83bf3269e057ec63c9da89b3016d29a8395499447419336d89e7d01daff911a838159b963423dd45f6161b80c0dfab608f2f4e11c662
data/README.md CHANGED
@@ -12,6 +12,7 @@ Currently, we support these notification delivery methods out of the box:
12
12
  * Email
13
13
  * ActionCable channels
14
14
  * Slack
15
+ * Microsoft Teams
15
16
  * Twilio (SMS)
16
17
  * Vonage / Nexmo (SMS)
17
18
 
@@ -78,7 +79,7 @@ To add delivery methods, simply `include` the module for the delivery methods yo
78
79
  class CommentNotification < Noticed::Base
79
80
  deliver_by :database
80
81
  deliver_by :action_cable
81
- deliver_by :email, if: :email_notifications?
82
+ deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
82
83
 
83
84
  # I18n helpers
84
85
  def message
@@ -86,6 +87,7 @@ class CommentNotification < Noticed::Base
86
87
  end
87
88
 
88
89
  # URL helpers are accessible in notifications
90
+ # Don't forget to set your default_url_options so Rails knows how to generate urls
89
91
  def url
90
92
  post_path(params[:post])
91
93
  end
@@ -104,6 +106,7 @@ end
104
106
 
105
107
  * `if: :method_name` - Calls `method_name`and cancels delivery method if `false` is returned
106
108
  * `unless: :method_name` - Calls `method_name`and cancels delivery method if `true` is returned
109
+ * `delay: ActiveSupport::Duration` - Delays the delivery for the given duration of time
107
110
 
108
111
  ##### Helper Methods
109
112
 
@@ -113,6 +116,12 @@ You can define helper methods inside your Notification object to make it easier
113
116
 
114
117
  Rails url helpers are included in notification classes by default so you have full access to them just like you would in your controllers and views.
115
118
 
119
+ Don't forget, you'll need to configure `default_url_options` in order for Rails to know what host and port to use when generating URLs.
120
+
121
+ ```ruby
122
+ Rails.application.routes.default_url_options[:host] = 'localhost:3000'
123
+ ```
124
+
116
125
  **Callbacks**
117
126
 
118
127
  Like ActiveRecord, notifications have several different types of callbacks.
@@ -120,7 +129,7 @@ Like ActiveRecord, notifications have several different types of callbacks.
120
129
  ```ruby
121
130
  class CommentNotification < Noticed::Base
122
131
  deliver_by :database
123
- deliver_by :email
132
+ deliver_by :email, mailer: 'CommentMailer'
124
133
 
125
134
  # Callbacks for the entire delivery
126
135
  before_deliver :whatever
@@ -158,7 +167,7 @@ For example:
158
167
 
159
168
  ```ruby
160
169
  class CommentNotification < Noticed::Base
161
- deliver_by :email, if: :email_notifications?
170
+ deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
162
171
 
163
172
  def email_notifications?
164
173
  recipient.email_notifications?
@@ -178,7 +187,7 @@ Writes notification to the database.
178
187
 
179
188
  `deliver_by :database`
180
189
 
181
- **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.
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.
182
191
 
183
192
  ##### Options
184
193
 
@@ -246,6 +255,42 @@ Sends a Slack notification via webhook.
246
255
 
247
256
  Defaults to `Rails.application.credentials.slack[:notification_url]`
248
257
 
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
+
249
294
  ### Twilio SMS
250
295
 
251
296
  Sends an SMS notification via Twilio.
@@ -256,7 +301,7 @@ Sends an SMS notification via Twilio.
256
301
 
257
302
  * `credentials: :get_twilio_credentials` - *Optional*
258
303
 
259
- Use a custom method to retrieve the credentials for Twilio. Method should return a Hash with `:account_sid`, `:auth_token` and `:phone_ number` keys.
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.
260
305
 
261
306
  Defaults to `Rails.application.credentials.twilio[:account_sid]` and `Rails.application.credentials.twilio[:auth_token]`
262
307
 
@@ -309,24 +354,57 @@ Sends an SMS notification via Vonage / Nexmo.
309
354
  }
310
355
  ```
311
356
 
312
- ### 🚚 Custom Delivery Methods
357
+ ### Fallback Notifications
313
358
 
314
- You can define a custom delivery method easily by adding a `deliver_by` line with a unique name and class option. The class will be instantiated and should inherit from `Noticed::DeliveryMethods::Base`.
359
+ A common pattern is to deliver a notification via the database and then, after some time has passed, email the user if they have not yet read the notification. You can implement this functionality by combining multiple delivery methods, the `delay` option, and the conditional `if` / `unless` option.
315
360
 
316
361
  ```ruby
317
- class MyNotification < Noticed::Base
318
- deliver_by :discord, class: "DiscordNotification"
362
+ class CommentNotification < Noticed::Base
363
+ deliver_by :database
364
+ deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read?
319
365
  end
320
366
  ```
321
367
 
368
+ Here a notification will be created immediately in the database (for display directly in your app). If the notification has not been read after 15 minutes, the email notification will be sent. If the notification has already been read in the app, the email will be skipped.
369
+
370
+ You can also configure multiple fallback options:
371
+
322
372
  ```ruby
323
- class DiscordNotification < Noticed::DeliveryMethods::Base
373
+ class CriticalSystemNotification < Noticed::Base
374
+ deliver_by :slack
375
+ deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, unless: :read?
376
+ deliver_by :twilio, delay: 20.minutes, unless: :read?
377
+ end
378
+ ```
379
+
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.
381
+
382
+ You can mix and match the options and delivery methods to suit your application specific needs.
383
+
384
+ ### 🚚 Custom Delivery Methods
385
+
386
+ To generate a custom delivery method, simply run
387
+
388
+ `rails generate noticed:delivery_method Discord`
389
+
390
+ This will generate a new `DeliveryMethods::Discord` class inside the `app/notifications/delivery_methods` folder, which can be used to deliver notifications to Discord.
391
+
392
+ ```ruby
393
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
324
394
  def deliver
325
395
  # Logic for sending a Discord notification
326
396
  end
327
397
  end
328
398
  ```
329
399
 
400
+ You can use the custom delivery method thus created by adding a `deliver_by` line with a unique name and `class` option in your notification class.
401
+
402
+ ```ruby
403
+ class MyNotification < Noticed::Base
404
+ deliver_by :discord, class: "DeliveryMethods::Discord"
405
+ end
406
+ ```
407
+
330
408
  Delivery methods have access to the following methods and attributes:
331
409
 
332
410
  * `notification` - The instance of the Notification. You can call methods on the notification to let the user easily override formatting and other functionality of the delivery method.
@@ -334,12 +412,51 @@ Delivery methods have access to the following methods and attributes:
334
412
  * `recipient` - The object who should receive the notification. This is typically a User, Account, or other ActiveRecord model.
335
413
  * `params` - The params passed into the notification. This is details about the event that happened. For example, a user commenting on a post would have params of `{ user: User.first }`
336
414
 
415
+ #### Validating options passed to Custom Delivery methods
416
+
417
+ The presence of the delivery method options is automatically validated if using the `option(s)` method.
418
+
419
+ If you want to validate that the passed options contain valid values, or to add any custom validations, override the `self.validate!(delivery_method_options)` method from the `Noticed::DeliveryMethods::Base` class.
420
+
421
+ ```ruby
422
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
423
+ option :username # Requires the username option to be passed
424
+
425
+ def deliver
426
+ # Logic for sending a Discord notification
427
+ end
428
+
429
+ def self.validate!(delivery_method_options)
430
+ super # Don't forget to call super, otherwise option presence won't be validated
431
+
432
+   # Custom validations
433
+ if delivery_method_options[:username].blank?
434
+ raise Noticed::ValidationError, 'the `username` option must be present'
435
+ end
436
+ end
437
+ end
438
+
439
+ class CommentNotification < Noticed::Base
440
+ deliver_by :discord, class: 'DeliveryMethods::Discord'
441
+ end
442
+ ```
443
+
444
+ Now it will raise an error because a required argument is missing.
445
+
446
+ To fix the error, the argument has to be passed correctly. For example:
447
+
448
+ ```ruby
449
+ class CommentNotification < Noticed::Base
450
+ deliver_by :discord, class: 'DeliveryMethods::Discord', username: User.admin.username
451
+ end
452
+ ```
453
+
337
454
  #### Callbacks
338
455
 
339
456
  Callbacks for delivery methods wrap the *actual* delivery of the notification. You can use `before_deliver`, `around_deliver` and `after_deliver` in your custom delivery methods.
340
457
 
341
458
  ```ruby
342
- class DiscordNotification < Noticed::DeliveryMethods::Base
459
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
343
460
  after_deliver do
344
461
  # Do whatever you want
345
462
  end
@@ -351,13 +468,13 @@ end
351
468
  Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The following syntax should work for Rails 6.1+:
352
469
 
353
470
  ```ruby
354
- deliver_by DiscordNotification
471
+ deliver_by DeliveryMethods::Discord
355
472
  ```
356
473
 
357
- For Rails 6.0 and earlier, you must pass strings of the class names in the `deliver_by` options.
474
+ For Rails 6.0, you must pass strings of the class names in the `deliver_by` options.
358
475
 
359
476
  ```ruby
360
- deliver_by :discord, class: "DiscordNotification"
477
+ deliver_by :discord, class: "DeliveryMethods::Discord"
361
478
  ```
362
479
 
363
480
  We recommend the Rails 6.0 compatible options to prevent confusion.
@@ -374,10 +491,19 @@ Sorting notifications by newest first:
374
491
  user.notifications.newest_first
375
492
  ```
376
493
 
377
- Marking all notifications as read:
494
+ Query for read or unread notifications:
495
+
496
+ ```ruby
497
+ user.notifications.read
498
+ user.notifications.unread
499
+ ```
500
+
501
+
502
+ Marking all notifications as read or unread:
378
503
 
379
504
  ```ruby
380
505
  user.notifications.mark_as_read!
506
+ user.notifications.mark_as_unread!
381
507
  ```
382
508
 
383
509
  #### Instance methods
@@ -388,10 +514,11 @@ Convert back into a Noticed notification object:
388
514
  @notification.to_notification
389
515
  ```
390
516
 
391
- Mark notification as read:
517
+ Mark notification as read / unread:
392
518
 
393
519
  ```ruby
394
520
  @notification.mark_as_read!
521
+ @notification.mark_as_unread!
395
522
  ```
396
523
 
397
524
  Check if read / unread:
@@ -401,6 +528,50 @@ Check if read / unread:
401
528
  @notification.unread?
402
529
  ```
403
530
 
531
+ #### Associating Notifications
532
+
533
+ Adding notification associations to your models makes querying and deleting notifications easy and is a pretty critical feature of most applications.
534
+
535
+ For example, in most cases, you'll want to delete notifications for records that are destroyed.
536
+
537
+ We'll need two associations for this:
538
+
539
+ 1. Notifications where the record is the recipient
540
+ 2. Notifications where the record is in the notification params
541
+
542
+ For example, we can query the notifications and delete them on destroy like so:
543
+
544
+ ```ruby
545
+ class Post < ApplicationRecord
546
+ # Standard association for deleting notifications when you're the recipient
547
+ has_many :notifications, as: :recipient, dependent: :destroy
548
+
549
+ # Helper for associating and destroying Notification records where(params: {post: self})
550
+ has_noticed_notifications
551
+
552
+ # 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"
554
+ end
555
+
556
+ # Create a CommentNotification with a post param
557
+ CommentNotification.with(post: @post).deliver(user)
558
+ # Lookup Notifications where params: {post: @post}
559
+ @post.notifications_as_post
560
+
561
+ CommentNotification.with(parent: @post).deliver(user)
562
+ @post.notifications_as_parent
563
+ ```
564
+
565
+ #### Handling Deleted Records
566
+
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`:
568
+
569
+ ```ruby
570
+ class ApplicationJob < ActiveJob::Base
571
+ discard_on ActiveJob::DeserializationError
572
+ end
573
+ ```
574
+
404
575
  ## 🙏 Contributing
405
576
 
406
577
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
data/Rakefile CHANGED
@@ -18,6 +18,10 @@ require "bundler/gem_tasks"
18
18
 
19
19
  require "rake/testtask"
20
20
 
21
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
22
+ load "rails/tasks/engine.rake"
23
+ load "rails/tasks/statistics.rake"
24
+
21
25
  Rake::TestTask.new(:test) do |t|
22
26
  t.libs << "test"
23
27
  t.pattern = "test/**/*_test.rb"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Noticed
6
+ module Generators
7
+ class DeliveryMethodGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ desc "Generates a class for a custom delivery method with the given NAME."
13
+
14
+ def generate_notification
15
+ template "delivery_method.rb", "app/notifications/delivery_methods/#{singular_name}.rb"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -15,13 +15,22 @@ module Noticed
15
15
  argument :attributes, type: :array, default: [], banner: "field:type field:type"
16
16
 
17
17
  def generate_notification
18
- generate :model, name, "recipient:references{polymorphic}", "type", "params:text", "read_at:datetime", *attributes
18
+ generate :model, name, "recipient:references{polymorphic}", "type", params_column, "read_at:datetime:index", *attributes
19
19
  end
20
20
 
21
21
  def add_noticed_model
22
22
  inject_into_class model_path, class_name, " include Noticed::Model\n"
23
23
  end
24
24
 
25
+ def add_not_nullable
26
+ migration_path = Dir.glob(Rails.root.join("db/migrate/*")).max_by { |f| File.mtime(f) }
27
+
28
+ # Force is required because null: false already exists in the file and Thor isn't smart enough to tell the difference
29
+ insert_into_file migration_path, after: "t.string :type", force: true do
30
+ ", null: false"
31
+ end
32
+ end
33
+
25
34
  def done
26
35
  readme "README" if behavior == :invoke
27
36
  end
@@ -31,6 +40,16 @@ module Noticed
31
40
  def model_path
32
41
  @model_path ||= File.join("app", "models", "#{file_path}.rb")
33
42
  end
43
+
44
+ def params_column
45
+ case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
46
+ when "postgresql"
47
+ "params:jsonb"
48
+ else
49
+ # MySQL and SQLite both support json
50
+ "params:json"
51
+ end
52
+ end
34
53
  end
35
54
  end
36
55
  end
@@ -12,7 +12,7 @@ module Noticed
12
12
  desc "Generates a notification with the given NAME."
13
13
 
14
14
  def generate_notification
15
- template "notification.rb", "app/notifications/#{singular_name}.rb"
15
+ template "notification.rb", "app/notifications/#{file_path}.rb"
16
16
  end
17
17
  end
18
18
  end
@@ -0,0 +1,12 @@
1
+ class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethods::Base
2
+ def deliver
3
+ # Logic for sending the notification
4
+ end
5
+
6
+ # You may override this method to validate options for the delivery method
7
+ # Invalid options should raise a ValidationError
8
+ #
9
+ # def self.validate!(options)
10
+ # raise ValidationError, "required_option missing" unless options[:required_option]
11
+ # end
12
+ end
data/lib/noticed/base.rb CHANGED
@@ -12,6 +12,8 @@ module Noticed
12
12
  # Gives notifications access to the record and recipient when formatting for delivery
13
13
  attr_accessor :record, :recipient
14
14
 
15
+ delegate :read?, :unread?, to: :record
16
+
15
17
  class << self
16
18
  def deliver_by(name, options = {})
17
19
  delivery_methods.push(name: name, options: options)
@@ -29,9 +31,10 @@ module Noticed
29
31
  new(params)
30
32
  end
31
33
 
32
- def param(name)
33
- param_names.push(name)
34
+ def params(*names)
35
+ param_names.concat Array.wrap(names)
34
36
  end
37
+ alias_method :param, :params
35
38
  end
36
39
 
37
40
  def initialize(params = {})
@@ -68,6 +71,9 @@ module Noticed
68
71
  def run_delivery(recipient, enqueue: true)
69
72
  delivery_methods = self.class.delivery_methods.dup
70
73
 
74
+ # Set recipient to instance var so it is available to Notification class
75
+ @recipient = recipient
76
+
71
77
  # Run database delivery inline first if it exists so other methods have access to the record
72
78
  if (index = delivery_methods.find_index { |m| m[:name] == :database })
73
79
  delivery_method = delivery_methods.delete_at(index)
@@ -81,9 +87,6 @@ module Noticed
81
87
 
82
88
  # Actually runs an individual delivery
83
89
  def run_delivery_method(delivery_method, recipient:, enqueue:)
84
- return if (delivery_method_name = delivery_method.dig(:options, :if)) && !send(delivery_method_name)
85
- return if (delivery_method_name = delivery_method.dig(:options, :unless)) && send(delivery_method_name)
86
-
87
90
  args = {
88
91
  notification_class: self.class.name,
89
92
  options: delivery_method[:options],
@@ -93,27 +96,46 @@ module Noticed
93
96
  }
94
97
 
95
98
  run_callbacks delivery_method[:name] do
96
- klass = get_class(delivery_method[:name], delivery_method[:options])
97
- enqueue ? klass.perform_later(args) : klass.perform_now(args)
99
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
100
+
101
+ # Always perfrom later if a delay is present
102
+ if (delay = delivery_method.dig(:options, :delay))
103
+ method.set(wait: delay).perform_later(args)
104
+ elsif enqueue
105
+ method.perform_later(args)
106
+ else
107
+ method.perform_now(args)
108
+ end
98
109
  end
99
110
  end
100
111
 
101
- # Retrieves the correct class for a delivery method
102
- def get_class(name, options)
112
+ def delivery_method_for(name, options)
103
113
  if options[:class]
104
114
  options[:class].constantize
105
115
  else
106
- "Noticed::DeliveryMethods::#{name.to_s.classify}".constantize
116
+ "Noticed::DeliveryMethods::#{name.to_s.camelize}".constantize
107
117
  end
108
118
  end
109
119
 
110
- # Validates that all params are present
111
120
  def validate!
121
+ validate_params_present!
122
+ validate_options_of_delivery_methods!
123
+ end
124
+
125
+ # Validates that all params are present
126
+ def validate_params_present!
112
127
  self.class.param_names.each do |param_name|
113
128
  if params[param_name].nil?
114
129
  raise ValidationError, "#{param_name} is missing."
115
130
  end
116
131
  end
117
132
  end
133
+
134
+ def validate_options_of_delivery_methods!
135
+ delivery_methods.each do |delivery_method|
136
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
137
+ method.validate!(delivery_method[:options])
138
+ end
139
+ end
118
140
  end
119
141
  end
data/lib/noticed/coder.rb CHANGED
@@ -2,12 +2,12 @@ module Noticed
2
2
  class Coder
3
3
  def self.load(data)
4
4
  return if data.nil?
5
- ActiveJob::Arguments.send(:deserialize_argument, JSON.parse(data))
5
+ ActiveJob::Arguments.send(:deserialize_argument, data)
6
6
  end
7
7
 
8
8
  def self.dump(data)
9
9
  return if data.nil?
10
- ActiveJob::Arguments.send(:serialize_argument, data).to_json
10
+ ActiveJob::Arguments.send(:serialize_argument, data)
11
11
  end
12
12
  end
13
13
  end
@@ -16,10 +16,18 @@ module Noticed
16
16
  end
17
17
 
18
18
  def channel
19
- if (method = options[:channel])
20
- notification.send(method)
21
- else
22
- Noticed::NotificationChannel
19
+ @channel ||= begin
20
+ value = options[:channel]
21
+ case value
22
+ when String
23
+ value.constantize
24
+ when Symbol
25
+ notification.send(value)
26
+ when Class
27
+ value
28
+ else
29
+ Noticed::NotificationChannel
30
+ end
23
31
  end
24
32
  end
25
33
  end
@@ -4,17 +4,44 @@ module Noticed
4
4
  extend ActiveModel::Callbacks
5
5
  define_model_callbacks :deliver
6
6
 
7
- attr_reader :notification, :options, :recipient, :record
7
+ class_attribute :option_names, instance_writer: false, default: []
8
8
 
9
- def perform(notification_class:, options:, params:, recipient:, record:)
10
- @notification = notification_class.constantize.new(params)
11
- @options = options
12
- @recipient = recipient
13
- @record = record
9
+ attr_reader :notification, :options, :params, :recipient, :record
10
+
11
+ class << self
12
+ # Copy option names from parent
13
+ def inherited(base) #:nodoc:
14
+ base.option_names = option_names.dup
15
+ super
16
+ end
17
+
18
+ def options(*names)
19
+ option_names.concat Array.wrap(names)
20
+ end
21
+ alias_method :option, :options
22
+
23
+ def validate!(delivery_method_options)
24
+ option_names.each do |option_name|
25
+ unless delivery_method_options.key? option_name
26
+ raise ValidationError, "option `#{option_name}` must be set for #{name}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def perform(args)
33
+ @notification = args[:notification_class].constantize.new(args[:params])
34
+ @options = args[:options]
35
+ @params = args[:params]
36
+ @recipient = args[:recipient]
37
+ @record = args[:record]
14
38
 
15
39
  # Make notification aware of database record and recipient during delivery
16
- @notification.record = record
17
- @notification.recipient = recipient
40
+ @notification.record = args[:record]
41
+ @notification.recipient = args[:recipient]
42
+
43
+ return if (condition = @options[:if]) && !@notification.send(condition)
44
+ return if (condition = @options[:unless]) && @notification.send(condition)
18
45
 
19
46
  run_callbacks :deliver do
20
47
  deliver
@@ -24,6 +51,36 @@ module Noticed
24
51
  def deliver
25
52
  raise NotImplementedError, "Delivery methods must implement a deliver method"
26
53
  end
54
+
55
+ private
56
+
57
+ # Helper method for making POST requests from delivery methods
58
+ #
59
+ # Usage:
60
+ # post("http://example.com", basic_auth: {user:, pass:}, json: {}, form: {})
61
+ #
62
+ def post(url, args = {})
63
+ basic_auth = args.delete(:basic_auth)
64
+
65
+ request = if basic_auth
66
+ HTTP.basic_auth(user: basic_auth[:user], pass: basic_auth[:pass])
67
+ else
68
+ HTTP
69
+ end
70
+
71
+ response = request.post(url, args)
72
+
73
+ if options[:debug]
74
+ Rails.logger.debug("POST #{url}")
75
+ Rails.logger.debug("Response: #{response.code}: #{response}")
76
+ end
77
+
78
+ if !options[:ignore_failure] && !response.status.success?
79
+ raise ResponseUnsuccessful.new(response)
80
+ end
81
+
82
+ response
83
+ end
27
84
  end
28
85
  end
29
86
  end
@@ -6,6 +6,13 @@ module Noticed
6
6
  recipient.send(association_name).create!(attributes)
7
7
  end
8
8
 
9
+ def self.validate!(options)
10
+ super
11
+
12
+ # Must be executed right away so the other deliveries can access the db record
13
+ raise ArgumentError, "database delivery cannot be delayed" if options.key?(:delay)
14
+ end
15
+
9
16
  private
10
17
 
11
18
  def association_name
@@ -1,8 +1,10 @@
1
1
  module Noticed
2
2
  module DeliveryMethods
3
3
  class Email < Base
4
+ option :mailer
5
+
4
6
  def deliver
5
- mailer.with(format).send(method.to_sym).deliver_later
7
+ mailer.with(format).send(method.to_sym).deliver_now
6
8
  end
7
9
 
8
10
  private
@@ -16,14 +18,12 @@ module Noticed
16
18
  end
17
19
 
18
20
  def format
19
- if (method = options[:format])
21
+ params = if (method = options[:format])
20
22
  notification.send(method)
21
23
  else
22
- notification.params.merge(
23
- recipient: recipient,
24
- record: record
25
- )
24
+ notification.params
26
25
  end
26
+ params.merge(recipient: recipient, record: record)
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,32 @@
1
+ module Noticed
2
+ module DeliveryMethods
3
+ class MicrosoftTeams < Base
4
+ def deliver
5
+ post(url, json: format)
6
+ end
7
+
8
+ private
9
+
10
+ def format
11
+ if (method = options[:format])
12
+ notification.send(method)
13
+ else
14
+ {
15
+ title: notification.params[:title],
16
+ text: notification.params[:text],
17
+ sections: notification.params[:sections],
18
+ potentialAction: notification.params[:notification_action]
19
+ }
20
+ end
21
+ end
22
+
23
+ def url
24
+ if (method = options[:url])
25
+ notification.send(method)
26
+ else
27
+ Rails.application.credentials.microsoft_teams[:notification_url]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Slack < Base
4
4
  def deliver
5
- HTTP.post(url, json: format)
5
+ post(url, json: format)
6
6
  end
7
7
 
8
8
  private
@@ -11,7 +11,7 @@ module Noticed
11
11
  if (method = options[:format])
12
12
  notification.send(method)
13
13
  else
14
- params
14
+ notification.params
15
15
  end
16
16
  end
17
17
 
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Twilio < Base
4
4
  def deliver
5
- HTTP.basic_auth(user: account_sid, pass: auth_token).post(url, json: format)
5
+ post(url, basic_auth: {user: account_sid, pass: auth_token}, form: format)
6
6
  end
7
7
 
8
8
  private
@@ -2,7 +2,13 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Vonage < Base
4
4
  def deliver
5
- HTTP.post("https://rest.nexmo.com/sms/json", json: format)
5
+ response = post("https://rest.nexmo.com/sms/json", json: format)
6
+ status = response.parse.dig("messages", 0, "status")
7
+ if !options[:ignore_failure] && status != "0"
8
+ raise ResponseUnsuccessful.new(response)
9
+ end
10
+
11
+ response
6
12
  end
7
13
 
8
14
  private
@@ -1,4 +1,9 @@
1
1
  module Noticed
2
2
  class Engine < ::Rails::Engine
3
+ initializer "noticed.has_notifications" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ include Noticed::HasNotifications
6
+ end
7
+ end
3
8
  end
4
9
  end
@@ -0,0 +1,32 @@
1
+ module Noticed
2
+ module HasNotifications
3
+ # Defines a method for the association and a before_destory callback to remove notifications
4
+ # where this record is a param
5
+ #
6
+ # class User < ApplicationRecord
7
+ # has_noticed_notifications
8
+ # has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
9
+ # end
10
+ #
11
+ # @user.notifications_as_user
12
+ # @user.notifications_as_owner
13
+
14
+ extend ActiveSupport::Concern
15
+
16
+ class_methods do
17
+ def has_noticed_notifications(param_name: model_name.singular, **options)
18
+ model = options.fetch(:model_name, "Notification").constantize
19
+
20
+ define_method "notifications_as_#{param_name}" do
21
+ model.where(params: {param_name.to_sym => self})
22
+ end
23
+
24
+ if options.fetch(:destroy, true)
25
+ before_destroy do
26
+ send("notifications_as_#{param_name}").destroy_all
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/noticed/model.rb CHANGED
@@ -5,32 +5,51 @@ module Noticed
5
5
  included do
6
6
  self.inheritance_column = nil
7
7
 
8
- serialize :params, Noticed::Coder
8
+ serialize :params, noticed_coder
9
9
 
10
10
  belongs_to :recipient, polymorphic: true
11
11
 
12
12
  scope :newest_first, -> { order(created_at: :desc) }
13
+ scope :unread, -> { where(read_at: nil) }
14
+ scope :read, -> { where.not(read_at: nil) }
13
15
  end
14
16
 
15
- module ClassMethods
17
+ class_methods do
16
18
  def mark_as_read!
17
19
  update_all(read_at: Time.current, updated_at: Time.current)
18
20
  end
21
+
22
+ def mark_as_unread!
23
+ update_all(read_at: nil, updated_at: Time.current)
24
+ end
25
+
26
+ def noticed_coder
27
+ case attribute_types["params"].type
28
+ when :json, :jsonb
29
+ Noticed::Coder
30
+ else
31
+ Noticed::TextCoder
32
+ end
33
+ end
19
34
  end
20
35
 
21
36
  # Rehydrate the database notification into the Notification object for rendering
22
37
  def to_notification
23
38
  @_notification ||= begin
24
- instance = type.constantize.with(params)
25
- instance.record = self
26
- instance
27
- end
39
+ instance = type.constantize.with(params)
40
+ instance.record = self
41
+ instance
42
+ end
28
43
  end
29
44
 
30
45
  def mark_as_read!
31
46
  update(read_at: Time.current)
32
47
  end
33
48
 
49
+ def mark_as_unread!
50
+ update(read_at: nil)
51
+ end
52
+
34
53
  def unread?
35
54
  !read?
36
55
  end
@@ -0,0 +1,16 @@
1
+ module Noticed
2
+ class TextCoder
3
+ def self.load(data)
4
+ return if data.nil?
5
+
6
+ # Text columns need JSON parsing
7
+ data = JSON.parse(data)
8
+ ActiveJob::Arguments.send(:deserialize_argument, data)
9
+ end
10
+
11
+ def self.dump(data)
12
+ return if data.nil?
13
+ ActiveJob::Arguments.send(:serialize_argument, data).to_json
14
+ end
15
+ end
16
+ end
@@ -1,21 +1,23 @@
1
- module Translation
2
- extend ActiveSupport::Concern
1
+ module Noticed
2
+ module Translation
3
+ extend ActiveSupport::Concern
3
4
 
4
- # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
5
- def i18n_scope
6
- :notifications
7
- end
5
+ # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
6
+ def i18n_scope
7
+ :notifications
8
+ end
8
9
 
9
- def translate(key, **options)
10
- I18n.translate(scope_translation_key(key), **options)
11
- end
12
- alias t translate
10
+ def translate(key, **options)
11
+ I18n.translate(scope_translation_key(key), **options)
12
+ end
13
+ alias_method :t, :translate
13
14
 
14
- def scope_translation_key(key)
15
- if key.to_s.start_with?(".")
16
- "notifications.#{self.class.name.underscore}#{key}"
17
- else
18
- key
15
+ def scope_translation_key(key)
16
+ if key.to_s.start_with?(".")
17
+ "#{i18n_scope}.#{self.class.name.underscore}#{key}"
18
+ else
19
+ key
20
+ end
19
21
  end
20
22
  end
21
23
  end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.2.5"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/noticed.rb CHANGED
@@ -5,8 +5,11 @@ require "noticed/engine"
5
5
  module Noticed
6
6
  autoload :Base, "noticed/base"
7
7
  autoload :Coder, "noticed/coder"
8
+ autoload :HasNotifications, "noticed/has_notifications"
8
9
  autoload :Model, "noticed/model"
10
+ autoload :TextCoder, "noticed/text_coder"
9
11
  autoload :Translation, "noticed/translation"
12
+ autoload :NotificationChannel, "noticed/notification_channel"
10
13
 
11
14
  module DeliveryMethods
12
15
  autoload :Base, "noticed/delivery_methods/base"
@@ -14,6 +17,7 @@ module Noticed
14
17
  autoload :Database, "noticed/delivery_methods/database"
15
18
  autoload :Email, "noticed/delivery_methods/email"
16
19
  autoload :Slack, "noticed/delivery_methods/slack"
20
+ autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams"
17
21
  autoload :Test, "noticed/delivery_methods/test"
18
22
  autoload :Twilio, "noticed/delivery_methods/twilio"
19
23
  autoload :Vonage, "noticed/delivery_methods/vonage"
@@ -33,4 +37,12 @@ module Noticed
33
37
 
34
38
  class ValidationError < StandardError
35
39
  end
40
+
41
+ class ResponseUnsuccessful < StandardError
42
+ attr_reader :response
43
+
44
+ def initialize(response)
45
+ @response = response
46
+ end
47
+ end
36
48
  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.2.5
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-06 00:00:00.000000000 Z
11
+ date: 2021-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -67,7 +67,35 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: mocha
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mysql2
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
71
99
  requirement: !ruby/object:Gem::Requirement
72
100
  requirements:
73
101
  - - ">="
@@ -91,10 +119,11 @@ files:
91
119
  - MIT-LICENSE
92
120
  - README.md
93
121
  - Rakefile
94
- - app/channels/noticed/notification_channel.rb
122
+ - lib/generators/noticed/delivery_method_generator.rb
95
123
  - lib/generators/noticed/model_generator.rb
96
124
  - lib/generators/noticed/notification_generator.rb
97
125
  - lib/generators/noticed/templates/README
126
+ - lib/generators/noticed/templates/delivery_method.rb.tt
98
127
  - lib/generators/noticed/templates/notification.rb.tt
99
128
  - lib/noticed.rb
100
129
  - lib/noticed/base.rb
@@ -103,12 +132,16 @@ files:
103
132
  - lib/noticed/delivery_methods/base.rb
104
133
  - lib/noticed/delivery_methods/database.rb
105
134
  - lib/noticed/delivery_methods/email.rb
135
+ - lib/noticed/delivery_methods/microsoft_teams.rb
106
136
  - lib/noticed/delivery_methods/slack.rb
107
137
  - lib/noticed/delivery_methods/test.rb
108
138
  - lib/noticed/delivery_methods/twilio.rb
109
139
  - lib/noticed/delivery_methods/vonage.rb
110
140
  - lib/noticed/engine.rb
141
+ - lib/noticed/has_notifications.rb
111
142
  - lib/noticed/model.rb
143
+ - lib/noticed/notification_channel.rb
144
+ - lib/noticed/text_coder.rb
112
145
  - lib/noticed/translation.rb
113
146
  - lib/noticed/version.rb
114
147
  - lib/tasks/noticed_tasks.rake
@@ -116,7 +149,7 @@ homepage: https://github.com/excid3/noticed
116
149
  licenses:
117
150
  - MIT
118
151
  metadata: {}
119
- post_install_message:
152
+ post_install_message:
120
153
  rdoc_options: []
121
154
  require_paths:
122
155
  - lib
@@ -131,8 +164,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
164
  - !ruby/object:Gem::Version
132
165
  version: '0'
133
166
  requirements: []
134
- rubygems_version: 3.1.2
135
- signing_key:
167
+ rubygems_version: 3.2.3
168
+ signing_key:
136
169
  specification_version: 4
137
170
  summary: Notifications for Ruby on Rails applications
138
171
  test_files: []