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.
- checksums.yaml +7 -0
- data/.gitignore +51 -0
- data/.rubocop.yml +7 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +2 -0
- data/.vscode/settings.json +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +124 -0
- data/README.md +391 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/generators/noticent/noticent.rb +36 -0
- data/lib/generators/noticent/templates/create_opt_ins.rb +15 -0
- data/lib/generators/noticent/templates/noticent_initializer.rb +17 -0
- data/lib/generators/noticent/templates/opt_in.rb +14 -0
- data/lib/noticent/active_record_opt_in_provider.rb +37 -0
- data/lib/noticent/channel.rb +75 -0
- data/lib/noticent/config.rb +209 -0
- data/lib/noticent/definitions/alert.rb +64 -0
- data/lib/noticent/definitions/channel.rb +43 -0
- data/lib/noticent/definitions/hooks.rb +38 -0
- data/lib/noticent/definitions/product.rb +16 -0
- data/lib/noticent/definitions/product_group.rb +44 -0
- data/lib/noticent/definitions/scope.rb +52 -0
- data/lib/noticent/dispatcher.rb +76 -0
- data/lib/noticent/errors.rb +17 -0
- data/lib/noticent/opt_in.rb +18 -0
- data/lib/noticent/proc_map.rb +35 -0
- data/lib/noticent/version.rb +5 -0
- data/lib/noticent/view.rb +82 -0
- data/lib/noticent.rb +9 -0
- data/noticent.gemspec +36 -0
- data/testing/channels/boo.rb +15 -0
- data/testing/channels/email.rb +20 -0
- data/testing/channels/foo.rb +12 -0
- data/testing/channels/slack.rb +10 -0
- data/testing/channels/webhook.rb +10 -0
- data/testing/models/receipient.rb +10 -0
- data/testing/payloads/comment_payload.rb +8 -0
- data/testing/payloads/payload.rb +9 -0
- data/testing/payloads/post_payload.rb +21 -0
- data/testing/views/email/some_event.html.erb +6 -0
- data/testing/views/email/some_event.txt.erb +6 -0
- data/testing/views/layouts/layout.html.erb +5 -0
- 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
|