noticent 0.0.1.pre.pre

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +51 -0
  3. data/.rubocop.yml +7 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +2 -0
  6. data/.vscode/settings.json +5 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +124 -0
  9. data/README.md +391 -0
  10. data/Rakefile +2 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/generators/noticent/noticent.rb +36 -0
  14. data/lib/generators/noticent/templates/create_opt_ins.rb +15 -0
  15. data/lib/generators/noticent/templates/noticent_initializer.rb +17 -0
  16. data/lib/generators/noticent/templates/opt_in.rb +14 -0
  17. data/lib/noticent/active_record_opt_in_provider.rb +37 -0
  18. data/lib/noticent/channel.rb +75 -0
  19. data/lib/noticent/config.rb +209 -0
  20. data/lib/noticent/definitions/alert.rb +64 -0
  21. data/lib/noticent/definitions/channel.rb +43 -0
  22. data/lib/noticent/definitions/hooks.rb +38 -0
  23. data/lib/noticent/definitions/product.rb +16 -0
  24. data/lib/noticent/definitions/product_group.rb +44 -0
  25. data/lib/noticent/definitions/scope.rb +52 -0
  26. data/lib/noticent/dispatcher.rb +76 -0
  27. data/lib/noticent/errors.rb +17 -0
  28. data/lib/noticent/opt_in.rb +18 -0
  29. data/lib/noticent/proc_map.rb +35 -0
  30. data/lib/noticent/version.rb +5 -0
  31. data/lib/noticent/view.rb +82 -0
  32. data/lib/noticent.rb +9 -0
  33. data/noticent.gemspec +36 -0
  34. data/testing/channels/boo.rb +15 -0
  35. data/testing/channels/email.rb +20 -0
  36. data/testing/channels/foo.rb +12 -0
  37. data/testing/channels/slack.rb +10 -0
  38. data/testing/channels/webhook.rb +10 -0
  39. data/testing/models/receipient.rb +10 -0
  40. data/testing/payloads/comment_payload.rb +8 -0
  41. data/testing/payloads/payload.rb +9 -0
  42. data/testing/payloads/post_payload.rb +21 -0
  43. data/testing/views/email/some_event.html.erb +6 -0
  44. data/testing/views/email/some_event.txt.erb +6 -0
  45. data/testing/views/layouts/layout.html.erb +5 -0
  46. metadata +241 -0
@@ -0,0 +1,17 @@
1
+ Noticent.configure do |config|
2
+ config.base_dir = File.join(Rails.root, 'app', 'models', 'noticent')
3
+ config.base_module_name = 'Noticent'
4
+ config.logger = Rails.logger
5
+ config.halt_on_error = !Rails.env.production?
6
+
7
+ # scope :post do
8
+ # channel :email
9
+ # channel :slack, group: :internal
10
+ #
11
+ # alert :new_user do
12
+ # notify :user
13
+ # notify(:staff).on(:internal)
14
+ # end
15
+ # end
16
+
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OptIn < ActiveRecord::Base
4
+ # scope: is the type of domain object that opt-in applies to. For example
5
+ # it could be post or comment
6
+ # entity_id: is the ID of the scope (post id or comment id)
7
+ # channel_name: is the name of the channel opted into. email or slack are examples
8
+ # alert_name: is the name of the alert: new_user or comment_posted
9
+ # user_id: is the name of the user who's opted into this
10
+
11
+ self.table_name = :opt_ins
12
+
13
+ validates_presence_of :scope, :entity_id, :channel_name, :alert_name, :recipient_id
14
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ # should be used only for testing
5
+ class ActiveRecordOptInProvider
6
+ def opt_in(recipient_id:, scope:, entity_id:, alert_name:, channel_name:)
7
+ Noticent::OptIn.create!(recipient_id: recipient_id, scope: scope, entity_id: entity_id, alert_name: alert_name, channel_name: channel_name)
8
+ end
9
+
10
+ def opt_out(recipient_id:, scope:, entity_id:, alert_name:, channel_name:)
11
+ Noticent::OptIn.where(recipient_id: recipient_id, scope: scope, entity_id: entity_id, alert_name: alert_name, channel_name: channel_name).delete
12
+ end
13
+
14
+ def opted_in?(recipient_id:, scope:, entity_id:, alert_name:, channel_name:)
15
+ Noticent::OptIn.where(recipient_id: recipient_id, scope: scope, entity_id: entity_id, alert_name: alert_name, channel_name: channel_name).count != 0
16
+ end
17
+
18
+ def add_alert(scope:, alert_name:, recipient_ids:, channel:)
19
+ ActiveRecord::Base.transaction do
20
+ now = Time.now.utc.to_s(:db)
21
+ # fetch all permutations of recipient and entity id
22
+ permutations = Noticent::OptIn.distinct
23
+ .where('recipient_id IN (?)', recipient_ids)
24
+ .pluck(:entity_id, :recipient_id)
25
+
26
+ return if permutations.empty?
27
+
28
+ values = permutations.map { |e, r| "('#{scope}','#{alert_name}', #{e}, #{r}, '#{channel}', '#{now}', '#{now}')" }.join(',')
29
+ ActiveRecord::Base.connection.execute("INSERT INTO opt_ins (scope, alert_name, entity_id, recipient_id, channel_name, created_at, updated_at) VALUES #{values}")
30
+ end
31
+ end
32
+
33
+ def remove_alert(scope:, alert_name:)
34
+ Noticent::OptIn.where('scope = ? AND alert_name = ?', scope, alert_name).destroy_all
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ class Channel
5
+ @@default_ext = :erb
6
+ @@default_format = :html
7
+
8
+ def initialize(config, recipients, payload, context)
9
+ @config = config
10
+ @recipients = recipients
11
+ @payload = payload
12
+ @context = context
13
+ @current_user = payload.current_user if payload.respond_to? :current_user
14
+ end
15
+
16
+ def render_within_context(template, content)
17
+ rendered_content = ERB.new(content).result(get_binding)
18
+ template.nil? ? rendered_content : ERB.new(template).result(get_binding { rendered_content })
19
+ end
20
+
21
+ protected
22
+
23
+ attr_reader :payload
24
+ attr_reader :recipients
25
+ attr_reader :context
26
+
27
+ class << self
28
+ def default_format(format)
29
+ @@default_format = format
30
+ end
31
+
32
+ def default_ext(ext)
33
+ @@default_ext = ext
34
+ end
35
+ end
36
+
37
+ def get_binding
38
+ binding
39
+ end
40
+
41
+ def current_user
42
+ raise Noticent::NoCurrentUser if @current_user.nil?
43
+
44
+ @current_user
45
+ end
46
+
47
+ def render(format: @@default_format, ext: @@default_ext, layout: '')
48
+ alert_name = caller[0][/`.*'/][1..-2]
49
+ channel_name = self.class.name.split('::').last.underscore
50
+ view_filename, layout_filename = filenames(channel: channel_name, alert: alert_name, format: format, ext: ext, layout: layout)
51
+
52
+ raise Noticent::ViewNotFound, "view #{view_filename} not found" unless File.exist?(view_filename)
53
+
54
+ view = View.new(view_filename, template_filename: layout_filename, channel: self)
55
+ view.process
56
+
57
+ [view.data, view.content]
58
+ end
59
+
60
+ private
61
+
62
+ def view_file(channel:, alert:, format:, ext:)
63
+ File.join(@config.view_dir, channel, "#{alert}.#{format}.#{ext}")
64
+ end
65
+
66
+ def filenames(channel:, alert:, format:, ext:, layout:)
67
+ view_filename = view_file(channel: channel, alert: alert, format: format, ext: ext)
68
+ layout_filename = ''
69
+ layout_filename = File.join(@config.view_dir, 'layouts', "#{layout}.#{format}.#{ext}") unless layout == ''
70
+
71
+ return view_filename, layout_filename
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ def self.configure(options = {}, &block)
5
+ if ENV['NOTICENT_RSPEC'] == '1'
6
+ options = options.merge(
7
+ base_module_name: 'Noticent::Testing',
8
+ base_dir: File.expand_path("#{File.dirname(__FILE__)}/../../testing"),
9
+ halt_on_error: true
10
+ )
11
+ end
12
+
13
+ @config = Noticent::Config::Builder.new(options, &block).build
14
+ @config.validate!
15
+
16
+ @config
17
+ end
18
+
19
+ def self.configuration
20
+ @config || (raise Noticent::MissingConfiguration)
21
+ end
22
+
23
+ def self.notify(alert_name, payload)
24
+ engine = Noticent::Dispatcher.new(@config, alert_name, payload)
25
+
26
+ return if engine.notifiers.nil?
27
+
28
+ engine.dispatch
29
+ end
30
+
31
+ class Config
32
+ attr_reader :hooks
33
+ attr_reader :channels
34
+ attr_reader :scopes
35
+ attr_reader :alerts
36
+ attr_reader :products
37
+
38
+ def initialize(options = {})
39
+ @options = options
40
+ @products = {}
41
+ end
42
+
43
+ def channels_by_group(group)
44
+ return [] if @channels.nil?
45
+
46
+ @channels.values.select { |x| x.group == group }
47
+ end
48
+
49
+ def channel_groups
50
+ return [] if @channels.nil?
51
+
52
+ @channels.values.collect(&:group).uniq
53
+ end
54
+
55
+ def alert_channels(alert_name)
56
+ alert = @alerts[alert_name]
57
+ raise ArgumentError, "no alert #{alert_name} found" if alert.nil?
58
+ return [] if alert.notifiers.nil?
59
+
60
+ alert.notifiers.values.collect { |notifier| channels_by_group(notifier.channel_group).uniq }.uniq.flatten
61
+ end
62
+
63
+ def products_by_alert(alert_name)
64
+ alert = @alerts[alert_name]
65
+ raise ArgumentError "no alert #{alert_name} found" if alert.nil?
66
+
67
+ alert.products
68
+ end
69
+
70
+ def alerts_by_scope(scope)
71
+ return [] if @alerts.nil?
72
+
73
+ @alerts.values.select { |x| x.scope.name == scope }
74
+ end
75
+
76
+ def base_dir
77
+ @options[:base_dir]
78
+ end
79
+
80
+ def base_module_name
81
+ @options[:base_module_name]
82
+ end
83
+
84
+ def opt_in_provider
85
+ @options[:opt_in_provider] || Noticent::ActiveRecordOptInProvider.new
86
+ end
87
+
88
+ def logger
89
+ @options[:logger] || Logger.new(STDOUT)
90
+ end
91
+
92
+ def halt_on_error
93
+ @options[:halt_on_error].nil? || false
94
+ end
95
+
96
+ def payload_dir
97
+ File.join(base_dir, 'payloads')
98
+ end
99
+
100
+ def scope_dir
101
+ File.join(base_dir, 'scopes')
102
+ end
103
+
104
+ def channel_dir
105
+ File.join(base_dir, 'channels')
106
+ end
107
+
108
+ def view_dir
109
+ File.join(base_dir, 'views')
110
+ end
111
+
112
+ def validate!
113
+ # check all scopes
114
+ scopes&.values&.each(&:validate!)
115
+ alerts&.values&.each(&:validate!)
116
+ end
117
+
118
+ class Builder
119
+ def initialize(options = {}, &block)
120
+ @options = options
121
+ @config = Noticent::Config.new(options)
122
+ raise BadConfiguration, 'no OptInProvider configured' if @config.opt_in_provider.nil?
123
+
124
+ instance_eval(&block) if block_given?
125
+
126
+ @config.instance_variable_set(:@options, @options)
127
+ end
128
+
129
+ def build
130
+ @config
131
+ end
132
+
133
+ def base_dir=(value)
134
+ @options[:base_dir] = value
135
+ end
136
+
137
+ def base_module_name=(value)
138
+ @options[:base_module_name] = value
139
+ end
140
+
141
+ def opt_in_provider=(value)
142
+ @options[:opt_in_provider] = value
143
+ end
144
+
145
+ def logger=(value)
146
+ @options[:logger] = value
147
+ end
148
+
149
+ def halt_on_error=(value)
150
+ @options[:halt_on_error] = value
151
+ end
152
+
153
+ def hooks
154
+ if @config.hooks.nil?
155
+ @config.instance_variable_set(:@hooks, Noticent::Definitions::Hooks.new)
156
+ else
157
+ @config.hooks
158
+ end
159
+ end
160
+
161
+ def product(name, &block)
162
+ products = @config.instance_variable_get(:@products) || {}
163
+
164
+ raise BadConfiguration, "product #{name} already defined" if products[name]
165
+
166
+ product = Noticent::Definitions::Product.new(@config, name)
167
+ hooks.run(:pre_product_registration, product)
168
+ product.instance_eval(&block) if block_given?
169
+ hooks.run(:post_product_registration, product)
170
+
171
+ products[name] = product
172
+
173
+ @config.instance_variable_set(:@products, products)
174
+
175
+ product
176
+ end
177
+
178
+ def channel(name, group: :default, klass: nil, &block)
179
+ channels = @config.instance_variable_get(:@channels) || {}
180
+
181
+ raise BadConfiguration, "channel '#{name}' already defined" if channels.include? name
182
+
183
+ channel = Noticent::Definitions::Channel.new(@config, name, group: group, klass: klass)
184
+ hooks.run(:pre_channel_registration, channel)
185
+ channel.instance_eval(&block) if block_given?
186
+ hooks.run(:post_channel_registration, channel)
187
+
188
+ channels[name] = channel
189
+
190
+ @config.instance_variable_set(:@channels, channels)
191
+ channel
192
+ end
193
+
194
+ def scope(name, payload_class: nil, &block)
195
+ scopes = @config.instance_variable_get(:@scopes) || {}
196
+
197
+ raise BadConfiguration, "scope '#{name}' already defined" if scopes.include? name
198
+
199
+ scope = Noticent::Definitions::Scope.new(@config, name, payload_class: payload_class)
200
+ scope.instance_eval(&block)
201
+
202
+ scopes[name] = scope
203
+
204
+ @config.instance_variable_set(:@scopes, scopes)
205
+ scope
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ module Definitions
5
+ class Alert
6
+ attr_reader :name
7
+ attr_reader :scope
8
+ attr_reader :notifiers
9
+ attr_reader :config
10
+ attr_reader :products
11
+
12
+ def initialize(config, name:, scope:)
13
+ @config = config
14
+ @name = name
15
+ @scope = scope
16
+ @products = Noticent::Definitions::ProductGroup.new(@config)
17
+ end
18
+
19
+ def notify(recipient, template: '')
20
+ notifiers = @notifiers || {}
21
+ raise BadConfiguration, "a notify is already defined for '#{recipient}'" unless notifiers[recipient].nil?
22
+
23
+ alert_notifier = Noticent::Definitions::Alert::Notifier.new(self, recipient, template: template)
24
+ notifiers[recipient] = alert_notifier
25
+ @notifiers = notifiers
26
+
27
+ alert_notifier
28
+ end
29
+
30
+ def applies
31
+ @products
32
+ end
33
+
34
+ def validate!
35
+ channels = @config.alert_channels(@name)
36
+ channels.each do |channel|
37
+ raise BadConfiguration, "channel #{channel.name} (#{channel.klass}) has no method called #{@name}" unless channel.klass.method_defined? @name
38
+ end
39
+ end
40
+
41
+ # holds a list of recipient + channel
42
+ class Notifier
43
+ attr_reader :recipient
44
+ attr_reader :channel_group
45
+ attr_reader :template
46
+
47
+ def initialize(alert, recipient, template: '')
48
+ @recipient = recipient
49
+ @alert = alert
50
+ @config = alert.config
51
+ @template = template
52
+ @channel_group = :default
53
+ end
54
+
55
+ def on(channel_group)
56
+ # validate the group name
57
+ raise ArgumentError, "no channel group found named '#{channel_group}'" if @config.channels_by_group(channel_group).empty?
58
+
59
+ @channel_group = channel_group
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ module Definitions
5
+ class Channel
6
+ attr_reader :name
7
+ attr_reader :group
8
+ attr_reader :klass
9
+ attr_reader :options
10
+
11
+ def initialize(config, name, group: :default, klass: nil)
12
+ @name = name
13
+ @group = group
14
+ @config = config
15
+
16
+ suggested_class_name = @config.base_module_name + '::' + name.to_s.camelize
17
+ @klass = klass.nil? ? suggested_class_name.camelize.constantize : klass
18
+ rescue NameError
19
+ raise Noticent::BadConfiguration, "no class found for #{suggested_class_name}"
20
+ end
21
+
22
+ def using(options = {})
23
+ @options = options
24
+ end
25
+
26
+ def instance(config, recipients, payload, context)
27
+ inst = @klass.new(config, recipients, payload, context)
28
+ return inst if @options.nil? || @options.empty?
29
+
30
+ @options.each do |k, v|
31
+ inst.send("#{k}=", v)
32
+ rescue NoMethodError
33
+ raise Noticent::BadConfiguration, "no method #{k}= found on #{@klass} as it is defined with the `using` clause"
34
+ end
35
+
36
+ inst
37
+ rescue ArgumentError
38
+ raise Noticent::BadConfiguration, "channel #{@klass} initializer arguments are mismatching."
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ module Definitions
5
+ class Hooks
6
+ VALID_STEPS = %i[pre_alert_registration post_alert_registration pre_channel_registration post_channel_registration pre_product_registration post_product_registration].freeze
7
+
8
+ def add(step, klass)
9
+ raise BadConfiguration, "invalid step. valid values are #{VALID_STEPS}" unless VALID_STEPS.include? step
10
+ raise BadConfiguration, "hook #{klass} doesn't have a #{step} method" unless klass.respond_to? step
11
+
12
+ storage[step] = [] if storage[step].nil?
13
+ storage[step] << klass
14
+ end
15
+
16
+ def fetch(step)
17
+ raise ::ArgumentError, "invalid step. valid values are #{VALID_STEPS}" unless VALID_STEPS.include? step
18
+
19
+ storage[step]
20
+ end
21
+
22
+ def run(step, chan)
23
+ chain = fetch(step)
24
+ return if chain.nil?
25
+
26
+ chain.each do |to_run|
27
+ to_run.send(step, chan) if to_run.respond_to? step
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def storage
34
+ @storage ||= {}
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ module Definitions
5
+ class Product
6
+ attr_reader :name
7
+
8
+ def initialize(config, name)
9
+ raise BadConfiguration, 'product name should be a symbol' unless name.is_a? Symbol
10
+
11
+ @config = config
12
+ @name = name
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ module Definitions
5
+ class ProductGroup
6
+
7
+ attr_reader :products
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @products = {}
12
+ end
13
+
14
+ def to(name)
15
+ raise BadConfiguration, 'product name should be a symbol' unless name.is_a? Symbol
16
+ raise BadConfiguration, "product #{name} is already in the list" if @products[name]
17
+ raise BadConfiguration, "product #{name} is not defined. Use products to define it first" unless @config.products[name]
18
+
19
+ @products[name] = @config.products[name]
20
+ end
21
+
22
+ def not_to(name)
23
+ raise BadConfiguration, 'product name should be a symbol' unless name.is_a? Symbol
24
+ raise BadConfiguration, "product #{name} is not defined. Use products to define it first" unless @config.products[name]
25
+
26
+ # include all products, except the one named
27
+ @config.products.each { |k, v| @products[k] = v unless k == name }
28
+ end
29
+
30
+ def count
31
+ @products.count
32
+ end
33
+
34
+ def keys
35
+ @products.keys
36
+ end
37
+
38
+ def values
39
+ @products.values
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ module Definitions
5
+ class Scope
6
+ attr_reader :name
7
+ attr_reader :payload_class
8
+
9
+ def initialize(config, name, payload_class: nil)
10
+ @config = config
11
+ @name = name
12
+ suggeste_name = config.base_module_name + "::#{name.capitalize}Payload"
13
+ @payload_class = payload_class.nil? ? suggeste_name.constantize : payload_class
14
+ rescue NameError
15
+ raise BadConfiguration, "scope #{suggeste_name} class not found"
16
+ end
17
+
18
+ def alert(name, &block)
19
+ alerts = @config.instance_variable_get(:@alerts) || {}
20
+
21
+ raise BadConfiguration, "alert '#{name}' already defined" if alerts.include? name
22
+
23
+ alert = Noticent::Definitions::Alert.new(@config, name: name, scope: self)
24
+ @config.hooks&.run(:pre_alert_registration, alert)
25
+ alert.instance_eval(&block) if block_given?
26
+ @config.hooks&.run(:post_alert_registration, alert)
27
+
28
+ alerts[name] = alert
29
+
30
+ @config.instance_variable_set(:@alerts, alerts)
31
+ alert
32
+ end
33
+
34
+ def validate!
35
+ # klass is valid already as it's used in the initializer
36
+ # does it have the right attributes?
37
+ # fetch all alerts for this scope
38
+ return if @payload_class.nil?
39
+
40
+ @config.alerts_by_scope(name).each do |alert|
41
+ next if alert.notifiers.nil?
42
+
43
+ alert.notifiers.keys.each do |recipient|
44
+ raise BadConfiguration, "payload class #{@payload_class} doesn't have a method or attribute called #{recipient}" unless @payload_class.method_defined? recipient
45
+ end
46
+ end
47
+
48
+ raise BadConfiguration, "payload class #{@payload_class} does have an attribute or method called #{name}_id" if !@payload_class.nil? && !@payload_class.method_defined?("#{name}_id")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noticent
4
+ class Dispatcher
5
+ def initialize(config, alert_name, payload, context = {})
6
+ @config = config
7
+ @alert_name = alert_name
8
+ @payload = payload
9
+ @context = context
10
+
11
+ validate!
12
+
13
+ @entity_id = @payload.send("#{scope.name}_id")
14
+ end
15
+
16
+ def alert
17
+ @config.alerts[@alert_name]
18
+ end
19
+
20
+ def scope
21
+ alert.scope
22
+ end
23
+
24
+ def notifiers
25
+ alert.notifiers
26
+ end
27
+
28
+ # returns all recipients of a certain notifier unfiltered regardless of "opt-in" and duplicates
29
+ def recipients(notifier_name)
30
+ raise Noticent::InvalidScope, "payload #{@payload.klass} doesn't have a #{notifier_name} method" unless @payload.respond_to? notifier_name
31
+
32
+ @payload.send(notifier_name)
33
+ end
34
+
35
+ # only returns recipients that have opted-in for this channel
36
+ def filter_recipients(recipients, channel)
37
+ raise ArgumentError, 'channel should be a string or symbol' unless channel.is_a?(String) || channel.is_a?(Symbol)
38
+ raise ArgumentError, 'recipients is nil' if recipients.nil?
39
+
40
+ recipients.select { |recipient| @config.opt_in_provider.opted_in?(recipient_id: recipient.id, scope: scope.name, entity_id: @entity_id, alert_name: alert.name, channel_name: channel) }
41
+ end
42
+
43
+ def dispatch
44
+ notifiers.values.each do |notifier|
45
+ recs = recipients(notifier.recipient)
46
+ @config.channels_by_group(notifier.channel_group).each do |channel|
47
+ to_send = filter_recipients(recs, channel.name)
48
+ channel_instance = channel.instance(@config, to_send, @payload, @context)
49
+ begin
50
+ raise Noticent::BadConfiguration, "channel #{channel.name} (#{channel.klass}) doesn't have a method called #{alert.name}" unless channel_instance.respond_to? alert.name
51
+
52
+ channel_instance.send(alert.name)
53
+ rescue StandardError => e
54
+ # log and move on
55
+ raise if @config.halt_on_error
56
+
57
+ Noticent.logger.error e
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def validate!
66
+ raise Noticent::BadConfiguration, 'no base_dir defined' if @config.base_dir.nil?
67
+ raise Noticent::MissingConfiguration if @config.nil?
68
+ raise Noticent::BadConfiguration if @config.alerts.nil?
69
+ raise Noticent::InvalidAlert, "no alert #{@alert_name} found" if @config.alerts[@alert_name].nil?
70
+ raise ::ArgumentError, 'payload is nil' if @payload.nil?
71
+ raise ::ArgumentError, 'alert is not a symbol' unless @alert_name.is_a?(Symbol)
72
+ raise Noticent::BadConfiguration, "payload (#{@payload.class}) doesn't belong to this scope (#{scope.name}) as it requires #{scope.payload_class}" unless !scope.payload_class.nil? && @payload.is_a?(scope.payload_class)
73
+ raise Noticent::BadConfiguration, "payload doesn't have a #{scope.name}_id method" unless @payload.respond_to?("#{scope.name}_id")
74
+ end
75
+ end
76
+ end