noticed 1.0.0 → 1.2.20

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.
@@ -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 NotificationGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ desc "Generates a notification with the given NAME."
13
+
14
+ def generate_notification
15
+ template "notification.rb", "app/notifications/#{file_path}.rb"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+
2
+ 🚚 Your notifications database model has been generated!
3
+
4
+ Next steps:
5
+ 1. Run "rails db:migrate"
6
+ 2. Add "has_many :notifications, as: :recipient" to your User model(s).
7
+ 3. Generate notifications with "rails g noticed:notification"
@@ -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
@@ -0,0 +1,27 @@
1
+ # To deliver this notification:
2
+ #
3
+ # <%= class_name %>.with(post: @post).deliver_later(current_user)
4
+ # <%= class_name %>.with(post: @post).deliver(current_user)
5
+
6
+ class <%= class_name %> < Noticed::Base
7
+ # Add your delivery methods
8
+ #
9
+ # deliver_by :database
10
+ # deliver_by :email, mailer: "UserMailer"
11
+ # deliver_by :slack
12
+ # deliver_by :custom, class: "MyDeliveryMethod"
13
+
14
+ # Add required params
15
+ #
16
+ # param :post
17
+
18
+ # Define helper methods to make rendering easier.
19
+ #
20
+ # def message
21
+ # t(".message")
22
+ # end
23
+ #
24
+ # def url
25
+ # post_path(params[:post])
26
+ # end
27
+ end
data/lib/noticed/base.rb CHANGED
@@ -1,16 +1,21 @@
1
1
  module Noticed
2
2
  class Base
3
+ include Translation
4
+ include Rails.application.routes.url_helpers
5
+
6
+ extend ActiveModel::Callbacks
7
+ define_model_callbacks :deliver
8
+
3
9
  class_attribute :delivery_methods, instance_writer: false, default: []
4
10
  class_attribute :param_names, instance_writer: false, default: []
5
11
 
6
- attr_accessor :record
12
+ # Gives notifications access to the record and recipient when formatting for delivery
13
+ attr_accessor :record, :recipient
7
14
 
8
15
  class << self
9
16
  def deliver_by(name, options = {})
10
- delivery_methods.push(
11
- name: name,
12
- options: options
13
- )
17
+ delivery_methods.push(name: name, options: options)
18
+ define_model_callbacks(name)
14
19
  end
15
20
 
16
21
  # Copy delivery methods from parent
@@ -24,23 +29,34 @@ module Noticed
24
29
  new(params)
25
30
  end
26
31
 
27
- def param(name)
28
- param_names.push(name)
32
+ def params(*names)
33
+ param_names.concat Array.wrap(names)
29
34
  end
35
+ alias_method :param, :params
30
36
  end
31
37
 
32
38
  def initialize(params = {})
33
39
  @params = params
34
40
  end
35
41
 
36
- def deliver(recipient)
42
+ def deliver(recipients)
37
43
  validate!
38
- run_delivery(recipient, enqueue: false)
44
+
45
+ run_callbacks :deliver do
46
+ Array.wrap(recipients).uniq.each do |recipient|
47
+ run_delivery(recipient, enqueue: false)
48
+ end
49
+ end
39
50
  end
40
51
 
41
- def deliver_later(recipient)
52
+ def deliver_later(recipients)
42
53
  validate!
43
- run_delivery(recipient, enqueue: true)
54
+
55
+ run_callbacks :deliver do
56
+ Array.wrap(recipients).uniq.each do |recipient|
57
+ run_delivery(recipient, enqueue: true)
58
+ end
59
+ end
44
60
  end
45
61
 
46
62
  def params
@@ -53,6 +69,9 @@ module Noticed
53
69
  def run_delivery(recipient, enqueue: true)
54
70
  delivery_methods = self.class.delivery_methods.dup
55
71
 
72
+ # Set recipient to instance var so it is available to Notification class
73
+ @recipient = recipient
74
+
56
75
  # Run database delivery inline first if it exists so other methods have access to the record
57
76
  if (index = delivery_methods.find_index { |m| m[:name] == :database })
58
77
  delivery_method = delivery_methods.delete_at(index)
@@ -66,9 +85,6 @@ module Noticed
66
85
 
67
86
  # Actually runs an individual delivery
68
87
  def run_delivery_method(delivery_method, recipient:, enqueue:)
69
- return if (delivery_method_name = delivery_method.dig(:options, :if)) && !send(delivery_method_name)
70
- return if (delivery_method_name = delivery_method.dig(:options, :unless)) && send(delivery_method_name)
71
-
72
88
  args = {
73
89
  notification_class: self.class.name,
74
90
  options: delivery_method[:options],
@@ -77,28 +93,47 @@ module Noticed
77
93
  record: record
78
94
  }
79
95
 
80
- klass = get_class(delivery_method[:name], delivery_method[:options])
81
- enqueue ? klass.perform_later(args) : klass.perform_now(args)
96
+ run_callbacks delivery_method[:name] do
97
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
98
+
99
+ # Always perfrom later if a delay is present
100
+ if (delay = delivery_method.dig(:options, :delay))
101
+ method.set(wait: delay).perform_later(args)
102
+ elsif enqueue
103
+ method.perform_later(args)
104
+ else
105
+ method.perform_now(args)
106
+ end
107
+ end
82
108
  end
83
109
 
84
- # Retrieves the correct class for a delivery method
85
- def get_class(name, options)
86
- if name.is_a? Class
87
- name
88
- elsif options[:class]
110
+ def delivery_method_for(name, options)
111
+ if options[:class]
89
112
  options[:class].constantize
90
113
  else
91
- "Noticed::DeliveryMethods::#{name.to_s.classify}".constantize
114
+ "Noticed::DeliveryMethods::#{name.to_s.camelize}".constantize
92
115
  end
93
116
  end
94
117
 
95
- # Validates that all params are present
96
118
  def validate!
119
+ validate_params_present!
120
+ validate_options_of_delivery_methods!
121
+ end
122
+
123
+ # Validates that all params are present
124
+ def validate_params_present!
97
125
  self.class.param_names.each do |param_name|
98
126
  if params[param_name].nil?
99
127
  raise ValidationError, "#{param_name} is missing."
100
128
  end
101
129
  end
102
130
  end
131
+
132
+ def validate_options_of_delivery_methods!
133
+ delivery_methods.each do |delivery_method|
134
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
135
+ method.validate!(delivery_method[:options])
136
+ end
137
+ end
103
138
  end
104
139
  end
data/lib/noticed/coder.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  module Noticed
2
2
  class Coder
3
3
  def self.load(data)
4
+ return if data.nil?
4
5
  ActiveJob::Arguments.send(:deserialize_argument, data)
5
6
  end
6
7
 
7
8
  def self.dump(data)
9
+ return if data.nil?
8
10
  ActiveJob::Arguments.send(:serialize_argument, data)
9
11
  end
10
12
  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
@@ -1,23 +1,86 @@
1
1
  module Noticed
2
2
  module DeliveryMethods
3
3
  class Base < Noticed.parent_class.constantize
4
- attr_reader :notification, :options, :recipient
5
- delegate :params, to: :notification
4
+ extend ActiveModel::Callbacks
5
+ define_model_callbacks :deliver
6
6
 
7
- def perform(notification_class:, options:, params:, recipient:, record:)
8
- @notification = notification_class.constantize.new(params)
9
- @options = options
10
- @recipient = recipient
7
+ class_attribute :option_names, instance_writer: false, default: []
11
8
 
12
- # Keep track of the database record for rendering
13
- @notification.record = record
9
+ attr_reader :notification, :options, :params, :recipient, :record
14
10
 
15
- deliver
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]
38
+
39
+ # Make notification aware of database record and recipient during delivery
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)
45
+
46
+ run_callbacks :deliver do
47
+ deliver
48
+ end
16
49
  end
17
50
 
18
51
  def deliver
19
52
  raise NotImplementedError, "Delivery methods must implement a deliver method"
20
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
21
84
  end
22
85
  end
23
86
  end
@@ -3,18 +3,29 @@ module Noticed
3
3
  class Database < Base
4
4
  # Must return the database record
5
5
  def deliver
6
- recipient.notifications.create(attributes)
6
+ recipient.send(association_name).create!(attributes)
7
+ end
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)
7
14
  end
8
15
 
9
16
  private
10
17
 
18
+ def association_name
19
+ options[:association] || :notifications
20
+ end
21
+
11
22
  def attributes
12
23
  if (method = options[:format])
13
24
  notification.send(method)
14
25
  else
15
26
  {
16
27
  type: notification.class.name,
17
- params: params
28
+ params: notification.params
18
29
  }
19
30
  end
20
31
  end
@@ -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(notification.params).send(method.to_sym).deliver_later
7
+ mailer.with(format).send(method.to_sym).deliver_now
6
8
  end
7
9
 
8
10
  private
@@ -14,6 +16,17 @@ module Noticed
14
16
  def method
15
17
  options[:method] || notification.class.name.underscore
16
18
  end
19
+
20
+ def format
21
+ if (method = options[:format])
22
+ notification.send(method)
23
+ else
24
+ notification.params.merge(
25
+ recipient: recipient,
26
+ record: record
27
+ )
28
+ end
29
+ end
17
30
  end
18
31
  end
19
32
  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,9 +2,15 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Test < Base
4
4
  class_attribute :delivered, default: []
5
+ class_attribute :callbacks, default: []
6
+
7
+ after_deliver do
8
+ self.class.callbacks << :after
9
+ end
5
10
 
6
11
  def self.clear!
7
12
  delivered.clear
13
+ callbacks.clear
8
14
  end
9
15
 
10
16
  def deliver
@@ -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
@@ -0,0 +1,61 @@
1
+ module Noticed
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ self.inheritance_column = nil
7
+
8
+ serialize :params, noticed_coder
9
+
10
+ belongs_to :recipient, polymorphic: true
11
+
12
+ scope :newest_first, -> { order(created_at: :desc) }
13
+ scope :unread, -> { where(read_at: nil) }
14
+ scope :read, -> { where.not(read_at: nil) }
15
+ end
16
+
17
+ class_methods do
18
+ def mark_as_read!
19
+ update_all(read_at: Time.current, updated_at: Time.current)
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
34
+ end
35
+
36
+ # Rehydrate the database notification into the Notification object for rendering
37
+ def to_notification
38
+ @_notification ||= begin
39
+ instance = type.constantize.with(params)
40
+ instance.record = self
41
+ instance
42
+ end
43
+ end
44
+
45
+ def mark_as_read!
46
+ update(read_at: Time.current)
47
+ end
48
+
49
+ def mark_as_unread!
50
+ update(read_at: nil)
51
+ end
52
+
53
+ def unread?
54
+ !read?
55
+ end
56
+
57
+ def read?
58
+ read_at?
59
+ end
60
+ end
61
+ 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
@@ -0,0 +1,23 @@
1
+ module Noticed
2
+ module Translation
3
+ extend ActiveSupport::Concern
4
+
5
+ # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
6
+ def i18n_scope
7
+ :notifications
8
+ end
9
+
10
+ def translate(key, **options)
11
+ I18n.translate(scope_translation_key(key), **options)
12
+ end
13
+ alias_method :t, :translate
14
+
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
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.0.0"
2
+ VERSION = "1.2.20"
3
3
  end
data/lib/noticed.rb CHANGED
@@ -1,9 +1,14 @@
1
+ require "active_job/arguments"
1
2
  require "http"
2
3
  require "noticed/engine"
3
4
 
4
5
  module Noticed
5
6
  autoload :Base, "noticed/base"
6
7
  autoload :Coder, "noticed/coder"
8
+ autoload :Model, "noticed/model"
9
+ autoload :TextCoder, "noticed/text_coder"
10
+ autoload :Translation, "noticed/translation"
11
+ autoload :NotificationChannel, "noticed/notification_channel"
7
12
 
8
13
  module DeliveryMethods
9
14
  autoload :Base, "noticed/delivery_methods/base"
@@ -11,6 +16,7 @@ module Noticed
11
16
  autoload :Database, "noticed/delivery_methods/database"
12
17
  autoload :Email, "noticed/delivery_methods/email"
13
18
  autoload :Slack, "noticed/delivery_methods/slack"
19
+ autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams"
14
20
  autoload :Test, "noticed/delivery_methods/test"
15
21
  autoload :Twilio, "noticed/delivery_methods/twilio"
16
22
  autoload :Vonage, "noticed/delivery_methods/vonage"
@@ -30,4 +36,12 @@ module Noticed
30
36
 
31
37
  class ValidationError < StandardError
32
38
  end
39
+
40
+ class ResponseUnsuccessful < StandardError
41
+ attr_reader :response
42
+
43
+ def initialize(response)
44
+ @response = response
45
+ end
46
+ end
33
47
  end