noticent 0.0.1.pre.pre

Sign up to get free protection for your applications and to get access to all the features.
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