web47core 0.0.8 → 0.0.9
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 +4 -4
- data/Gemfile +0 -14
- data/Gemfile.lock +30 -9
- data/README.md +27 -4
- data/lib/app/models/concerns/app47_logger.rb +175 -0
- data/lib/app/models/concerns/core_account.rb +51 -0
- data/lib/app/models/concerns/standard_model.rb +104 -6
- data/lib/app/models/email_notification.rb +253 -0
- data/lib/app/models/email_template.rb +6 -0
- data/lib/app/models/notification.rb +276 -0
- data/lib/app/models/notification_template.rb +20 -0
- data/lib/app/models/slack_notification.rb +89 -0
- data/lib/app/models/sms_notification.rb +56 -0
- data/lib/app/models/smtp_configuration.rb +148 -0
- data/lib/app/models/template.rb +21 -0
- data/lib/templates/email/notification_failure.liquid +10 -0
- data/lib/templates/email/notification_failure.subject.liquid +1 -0
- data/lib/templates/slack/error_message.liquid +1 -0
- data/lib/web47core.rb +10 -2
- data/test/factories/account_factories.rb +9 -0
- data/test/factories/notification_factories.rb +14 -0
- data/test/models/app47_logger_test.rb +88 -0
- data/test/models/concerns/{formable_test.rb → standard_model_test.rb} +24 -5
- data/test/models/email_notification_test.rb +297 -0
- data/test/models/notification_test.rb +127 -0
- data/test/models/slack_notification_test.rb +50 -0
- data/test/notification_test_helper.rb +146 -0
- data/test/rails_setup.rb +4 -0
- data/test/test_helper.rb +10 -4
- data/test/test_models_helper.rb +14 -0
- data/web47core.gemspec +5 -2
- metadata +87 -14
- data/lib/app/models/concerns/auto_clear_cache.rb +0 -34
- data/lib/app/models/concerns/formable.rb +0 -111
- data/test/models/concerns/auto_clear_cache_test.rb +0 -27
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# EmailNotification is a concrete class of Notification, sending emails to both members
|
5
|
+
# and users, or in the case of eCommerce, customers.
|
6
|
+
#
|
7
|
+
# This object allows you to create an EmailNotification object, set it's property and
|
8
|
+
# tell it send_notification. It'll handle everything from there including
|
9
|
+
# 1. Running in the background thread
|
10
|
+
# 2. Error recovery
|
11
|
+
# 3. Retry on failure
|
12
|
+
#
|
13
|
+
# The Email object also uses the templating engine to allow the specification of a template
|
14
|
+
# and a set of local variables for dynamic content.
|
15
|
+
#
|
16
|
+
# Usage:
|
17
|
+
#
|
18
|
+
# email = EmailNotification.new
|
19
|
+
# email.to = 'user@abc.com'
|
20
|
+
# email.sender = 'me@abc.com'
|
21
|
+
# email.subject = 'Today is the day!'
|
22
|
+
# email.message = 'Day is today!'
|
23
|
+
# email.send_notification
|
24
|
+
#
|
25
|
+
# You are done! Go about your business of creating awesome software!
|
26
|
+
#
|
27
|
+
class EmailNotification < Notification
|
28
|
+
# The From: "Joe Smith <joe@abc.com>"
|
29
|
+
field :from, type: String
|
30
|
+
# The Reply-To: "no-reply@abc.com"
|
31
|
+
field :reply_to, type: String
|
32
|
+
# The sender: "joe@abc.com"
|
33
|
+
field :sender, type: String
|
34
|
+
# The cc: header
|
35
|
+
field :cc, type: String
|
36
|
+
# The subject
|
37
|
+
field :subject, type: String
|
38
|
+
#
|
39
|
+
# Validations
|
40
|
+
#
|
41
|
+
validates :subject, presence: true
|
42
|
+
validates :to, presence: true
|
43
|
+
|
44
|
+
# has_many :attachments, class_name: 'EmailNotificationAttachment', dependent: :destroy
|
45
|
+
|
46
|
+
def from_template(template_name, locals = {})
|
47
|
+
super
|
48
|
+
self.subject = subject_from_template(template_name, locals)
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Add a file as an attachment to this email notification.
|
53
|
+
#
|
54
|
+
# Expecting a path to the file, not the file or data itself.
|
55
|
+
# TODO consider how to handle a file object or data object.
|
56
|
+
#
|
57
|
+
# Once you have called this method, the file is stored via PaperClip and
|
58
|
+
# does not need to stay persistant through email delivery.
|
59
|
+
# You can delete it after telling the EmailNotification to send
|
60
|
+
# the notification.
|
61
|
+
#
|
62
|
+
# def add_file(file)
|
63
|
+
# # Make sure we are saved
|
64
|
+
# save! unless persisted?
|
65
|
+
# attachment = EmailNotificationAttachment.new email_notification: self
|
66
|
+
# attachment.file = open(file)
|
67
|
+
# attachment.save!
|
68
|
+
# end
|
69
|
+
def deliver_message!
|
70
|
+
mail = Mail.new
|
71
|
+
# Set the from line
|
72
|
+
address = from_address
|
73
|
+
mail.from = address
|
74
|
+
self.from = address
|
75
|
+
# Set the sender line
|
76
|
+
address = sender_address
|
77
|
+
mail.sender = address
|
78
|
+
self.sender = address
|
79
|
+
# Set the reply to
|
80
|
+
address = reply_to_address
|
81
|
+
if address.present?
|
82
|
+
self.reply_to = address
|
83
|
+
mail.reply_to = address
|
84
|
+
mail.sender = address
|
85
|
+
end
|
86
|
+
|
87
|
+
# Set the to address
|
88
|
+
mail.to = to
|
89
|
+
|
90
|
+
# Set the cc line
|
91
|
+
mail.cc = cc unless cc.nil?
|
92
|
+
|
93
|
+
# Set the subject line
|
94
|
+
mail.subject = subject
|
95
|
+
|
96
|
+
# set the message body and send
|
97
|
+
html_message = build_message
|
98
|
+
mail.html_part do
|
99
|
+
content_type 'text/html; charset=UTF-8'
|
100
|
+
body html_message
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add the attachments, if there are any. If not, none are added.
|
104
|
+
add_attachments(attachments) if defined?(attachments)
|
105
|
+
|
106
|
+
# Setup the delivery method for this message only.
|
107
|
+
if 'test'.eql?(ENV['RAILS_ENV'])
|
108
|
+
mail.delivery_method :test
|
109
|
+
else
|
110
|
+
mail.delivery_method :smtp, smtp_configuration
|
111
|
+
end
|
112
|
+
|
113
|
+
# Deliver it
|
114
|
+
mail.deliver
|
115
|
+
rescue StandardError => error
|
116
|
+
# We are catching and rethrowing this only to allow the temp directory to be cleaned up in the ensure block
|
117
|
+
raise error
|
118
|
+
ensure
|
119
|
+
FileUtils.remove_entry_secure(tmp_dir) if defined?(tmp_dir) && tmp_dir.present?
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_attachments(attachments = nil)
|
123
|
+
tmp_dir = Dir.mktmpdir
|
124
|
+
attachments.each do |attachment|
|
125
|
+
# Create a temp directory, it'll get cleaned up in the rescue.
|
126
|
+
tmp_file_path = "#{tmp_dir}/#{attachment.file_file_name}"
|
127
|
+
File.open(tmp_file_path, 'w') do |f|
|
128
|
+
f.write(URI.parse(attachment.file_url).open.read.force_encoding('utf-16').encode)
|
129
|
+
end
|
130
|
+
mail.attachments[attachment.file_file_name] = {
|
131
|
+
mime_type: attachment.file_content_type,
|
132
|
+
content: File.open(tmp_file_path).read.force_encoding('utf-16').encode
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# sender address
|
139
|
+
#
|
140
|
+
def sender_address
|
141
|
+
address = SystemConfiguration.smtp_user_name
|
142
|
+
unless account.nil?
|
143
|
+
account_smtp = account.fetch_smtp_configuration
|
144
|
+
address = account_smtp.username if account_smtp.use?
|
145
|
+
end
|
146
|
+
address
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# From address
|
151
|
+
#
|
152
|
+
def from_address
|
153
|
+
return from if from.present?
|
154
|
+
|
155
|
+
address = SystemConfiguration.default_email
|
156
|
+
if account.present?
|
157
|
+
account_smtp = account.fetch_smtp_configuration
|
158
|
+
address = account_smtp.email_address if account_smtp.use?
|
159
|
+
end
|
160
|
+
address
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
# Reply to address
|
165
|
+
#
|
166
|
+
def reply_to_address
|
167
|
+
return reply_to if reply_to.present?
|
168
|
+
|
169
|
+
address = nil
|
170
|
+
unless account.nil?
|
171
|
+
smtp = account.fetch_smtp_configuration
|
172
|
+
address = smtp.reply_to_address if smtp.use? && !smtp.reply_to_address.nil? && !smtp.reply_to_address.empty?
|
173
|
+
end
|
174
|
+
address
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# SMTP Configuration
|
179
|
+
#
|
180
|
+
def smtp_configuration
|
181
|
+
account.get_smtp_configuration.use? ? account_smtp_configuration : default_smtp_configuration
|
182
|
+
rescue StandardError
|
183
|
+
default_smtp_configuration
|
184
|
+
end
|
185
|
+
|
186
|
+
def account_smtp_configuration
|
187
|
+
smtp = account.fetch_smtp_configuration
|
188
|
+
|
189
|
+
config = {
|
190
|
+
address: smtp.server_name,
|
191
|
+
port: smtp.port,
|
192
|
+
authentication: smtp.authentication_method.to_sym,
|
193
|
+
enable_starttls_auto: smtp.ssl.eql?(true)
|
194
|
+
}
|
195
|
+
config[:domain] = smtp.domain unless smtp.domain.nil? || smtp.domain.empty?
|
196
|
+
config[:user_name] = smtp.username unless smtp.username.nil? || smtp.username.empty?
|
197
|
+
config[:password] = smtp.password unless smtp.password.nil? || smtp.password.empty?
|
198
|
+
config
|
199
|
+
end
|
200
|
+
|
201
|
+
def default_smtp_configuration
|
202
|
+
SystemConfiguration.smtp_configuration
|
203
|
+
end
|
204
|
+
|
205
|
+
#
|
206
|
+
# HTMLize messages
|
207
|
+
#
|
208
|
+
def build_message
|
209
|
+
clean_message = message.strip
|
210
|
+
clean_message = "<html><body><pre>#{clean_message}</pre></body></html>" unless clean_message.start_with?('<')
|
211
|
+
add_notification_tracker(clean_message)
|
212
|
+
end
|
213
|
+
|
214
|
+
def add_notification_tracker(html_message)
|
215
|
+
tracker_url = "#{SystemConfiguration.base_url}/notifications/#{id}/img"
|
216
|
+
return html_message if html_message.include?(tracker_url)
|
217
|
+
|
218
|
+
image_tag = "<img style='height:0;width:0;border:none;display:none;' src='#{tracker_url}' alt=''/></body>"
|
219
|
+
html_message.sub(%r{<\/body>}, image_tag)
|
220
|
+
end
|
221
|
+
|
222
|
+
def subject_from_haml_text(haml_text, locals)
|
223
|
+
locals[:base_url] = SystemConfiguration.base_url
|
224
|
+
haml_engine = Haml::Engine.new(haml_text)
|
225
|
+
self.subject = haml_engine.render(Object.new, stringify_all(locals))
|
226
|
+
end
|
227
|
+
|
228
|
+
def subject_from_liquid_text(liquid_text, locals)
|
229
|
+
self.subject = render_liquid_text(liquid_text, locals)
|
230
|
+
end
|
231
|
+
|
232
|
+
def subject_from_template(template_name, locals)
|
233
|
+
subject = account_subject_template(template_name) ||
|
234
|
+
default_subject_template(template_name) ||
|
235
|
+
template_from_file(template_name, prefix: 'subject')
|
236
|
+
return subject_from_liquid_text(subject, locals) if subject.present?
|
237
|
+
|
238
|
+
subject = template_from_file(template_name, format: 'haml', prefix: 'subject')
|
239
|
+
subject_from_haml_text(subject, locals) if subject.present?
|
240
|
+
end
|
241
|
+
|
242
|
+
def account_subject_template(template_name)
|
243
|
+
self.account.templates.emails.find_by(name: template_name.to_s).subject
|
244
|
+
rescue StandardError
|
245
|
+
nil
|
246
|
+
end
|
247
|
+
|
248
|
+
def default_subject_template(template_name)
|
249
|
+
EmailTemplate.where(account: nil, name: template_name.to_s).template
|
250
|
+
rescue StandardError
|
251
|
+
nil
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
#
|
5
|
+
class Notification
|
6
|
+
include StandardModel
|
7
|
+
#
|
8
|
+
# Constants
|
9
|
+
#
|
10
|
+
STATE_INVALID = 'invalid'.freeze unless defined? STATE_INVALID
|
11
|
+
STATE_NEW = 'new'.freeze unless defined? STATE_NEW
|
12
|
+
STATE_PROCESSED = 'processed'.freeze unless defined? STATE_PROCESSED
|
13
|
+
STATE_PROCESSING = 'processing'.freeze unless defined? STATE_PROCESSING
|
14
|
+
STATE_RESUBMITTED = 'resubmitted'.freeze unless defined? STATE_RESUBMITTED
|
15
|
+
STATE_RETRYING = 'retrying'.freeze unless defined? STATE_RETRYING
|
16
|
+
STATE_SUBMITTED = 'submitted'.freeze unless defined? STATE_SUBMITTED
|
17
|
+
STATE_VIEWED = 'viewed'.freeze unless defined? STATE_VIEWED
|
18
|
+
unless defined? ALL_STATES
|
19
|
+
ALL_STATES = [STATE_INVALID,
|
20
|
+
STATE_NEW,
|
21
|
+
STATE_PROCESSED,
|
22
|
+
STATE_PROCESSING,
|
23
|
+
STATE_RESUBMITTED,
|
24
|
+
STATE_RETRYING,
|
25
|
+
STATE_SUBMITTED,
|
26
|
+
STATE_VIEWED].freeze
|
27
|
+
end
|
28
|
+
#
|
29
|
+
# Channels
|
30
|
+
#
|
31
|
+
DELIVERY_EMAIL = 'email'.freeze unless defined? DELIVERY_EMAIL
|
32
|
+
DELIVERY_SLACK = 'slack'.freeze unless defined? DELIVERY_SLACK
|
33
|
+
DELIVERY_SMS = 'sms'.freeze unless defined? DELIVERY_SMS
|
34
|
+
#
|
35
|
+
# Fields
|
36
|
+
#
|
37
|
+
field :retries, type: Integer, default: 0
|
38
|
+
field :state, type: String, default: STATE_NEW
|
39
|
+
field :to, type: String
|
40
|
+
field :message, type: String
|
41
|
+
field :error_message, type: String
|
42
|
+
field :last_viewed_at, type: Time
|
43
|
+
field :viewed_count, type: Integer, default: 0
|
44
|
+
#
|
45
|
+
# Relationships
|
46
|
+
#
|
47
|
+
belongs_to :account, inverse_of: :notifications, optional: true
|
48
|
+
belongs_to :notification_template, inverse_of: :notifications, optional: true
|
49
|
+
#
|
50
|
+
# Validations
|
51
|
+
#
|
52
|
+
validates :state, presence: true, inclusion: { in: ALL_STATES }
|
53
|
+
validates :message, presence: true
|
54
|
+
|
55
|
+
#
|
56
|
+
# Was this notification successful sent
|
57
|
+
#
|
58
|
+
def successful?
|
59
|
+
[STATE_PROCESSED, STATE_VIEWED].include? state
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# If this notification can be deleted, we don't want to delete one that is currently being processed.
|
64
|
+
#
|
65
|
+
def deletable?
|
66
|
+
[STATE_PROCESSED, STATE_INVALID, STATE_NEW, STATE_VIEWED].include? state
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# If this notification can be resent, we don't want to resend one that is currently being processed.
|
71
|
+
#
|
72
|
+
def sendable?
|
73
|
+
[STATE_PROCESSED, STATE_INVALID, STATE_NEW, STATE_VIEWED].include? state
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Mark this as viewed
|
78
|
+
#
|
79
|
+
def viewed
|
80
|
+
set(state: STATE_VIEWED, last_viewed_at: Time.now.utc, viewed_count: viewed_count + 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Start processing the notification
|
85
|
+
#
|
86
|
+
def start_processing
|
87
|
+
set state: STATE_PROCESSING
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Set to retrying
|
92
|
+
#
|
93
|
+
def retry_delivery(message)
|
94
|
+
set error_message: message, retries: retries + 1, state: STATE_RETRYING
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Finish processing the notification successfully
|
99
|
+
#
|
100
|
+
def finish_processing(message = nil)
|
101
|
+
if message.present?
|
102
|
+
set state: STATE_INVALID, error_message: message
|
103
|
+
else
|
104
|
+
set state: STATE_PROCESSED, error_message: ''
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Send the notification
|
110
|
+
#
|
111
|
+
def send_notification
|
112
|
+
if state.eql? STATE_NEW
|
113
|
+
self.state = STATE_SUBMITTED
|
114
|
+
else
|
115
|
+
self.retries = 0
|
116
|
+
self.state = STATE_RESUBMITTED
|
117
|
+
self.error_message = 'Retrying'
|
118
|
+
end
|
119
|
+
|
120
|
+
begin
|
121
|
+
deliver_message if save!
|
122
|
+
rescue StandardError => error
|
123
|
+
finish_processing error.message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def deliver_message!
|
128
|
+
raise 'Incomplete class, concrete implementation should implment #deliver_message!'
|
129
|
+
end
|
130
|
+
|
131
|
+
def deliver_message
|
132
|
+
start_processing
|
133
|
+
deliver_message!
|
134
|
+
finish_processing
|
135
|
+
rescue StandardError => error
|
136
|
+
if retries > 10
|
137
|
+
log_error "Unable to process notification id: #{id}, done retrying", error
|
138
|
+
finish_processing "Failed final attempt: #{error.message}"
|
139
|
+
notify_failure(error)
|
140
|
+
else
|
141
|
+
log_error "Unable to process notification id: #{id}, retrying!!", error
|
142
|
+
retry_delivery("Failed attempt # #{retries}: #{error.message}")
|
143
|
+
delay(run_at: 10.minutes.from_now).deliver
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
handle_asynchronously :deliver_message, priority: 100
|
148
|
+
|
149
|
+
def from_template(template_name, locals = {})
|
150
|
+
locals[:base_url] = SystemConfiguration.base_url
|
151
|
+
self.message = message_from_template(template_name, locals)
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# Retrieve the template from the account
|
156
|
+
#
|
157
|
+
# If the account does exists or the template in the account does exist, we catch the error and return nil
|
158
|
+
#
|
159
|
+
def account_message_template(template_name)
|
160
|
+
account.templates.find_by(name: template_name.to_s, _type: "Account#{delivery_channel.humanize}Template").template
|
161
|
+
rescue StandardError
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Get the default template stored in the database that is not associated with an account
|
167
|
+
#
|
168
|
+
def default_message_template(template_name)
|
169
|
+
Template.find_by(account: nil, name: template_name.to_s).template
|
170
|
+
rescue StandardError
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# Retrieve the template out of the project
|
176
|
+
#
|
177
|
+
def template_from_file(template_name, format: 'liquid', prefix: nil)
|
178
|
+
file_name = [template_name, prefix, format].compact.join('.')
|
179
|
+
if File.exist?(Rails.root.join('lib', 'templates', delivery_channel, file_name))
|
180
|
+
File.open(Rails.root.join('lib', 'templates', delivery_channel, file_name))
|
181
|
+
else
|
182
|
+
File.read(File.join(__dir__, '../../lib', 'templates', delivery_channel, file_name))
|
183
|
+
end.read
|
184
|
+
rescue StandardError
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
|
188
|
+
#
|
189
|
+
# Default delivery channel is email, override for sms, SMTP or other channels
|
190
|
+
#
|
191
|
+
def delivery_channel
|
192
|
+
DELIVERY_EMAIL
|
193
|
+
end
|
194
|
+
|
195
|
+
def message_from_template(template_name, locals)
|
196
|
+
template = account_message_template(template_name) || template_from_file(template_name)
|
197
|
+
return message_from_liquid_text(template, locals) if template.present?
|
198
|
+
|
199
|
+
template = template_from_file(template_name, format: 'haml')
|
200
|
+
message_from_haml_text(template, locals) if template.present?
|
201
|
+
end
|
202
|
+
|
203
|
+
def message_from_haml_text(haml_text, locals)
|
204
|
+
locals[:base_url] = SystemConfiguration.base_url
|
205
|
+
engine = Haml::Engine.new(haml_text)
|
206
|
+
self.message = engine.render(Object.new, stringify_all(locals))
|
207
|
+
end
|
208
|
+
|
209
|
+
def message_from_haml_file(file_name, locals)
|
210
|
+
message_from_haml_text(File.read(file_name), locals)
|
211
|
+
end
|
212
|
+
|
213
|
+
def message_from_liquid_text(liquid_text, locals)
|
214
|
+
self.message = render_liquid_text(liquid_text, locals)
|
215
|
+
end
|
216
|
+
|
217
|
+
#
|
218
|
+
# Render the given liquid text
|
219
|
+
#
|
220
|
+
def render_liquid_text(liquid_text, locals = {})
|
221
|
+
locals[:base_url] = SystemConfiguration.base_url
|
222
|
+
Liquid::Template.parse(liquid_text).render(stringify_all(locals))
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Convert all keys and values to strings for liquid sanity
|
227
|
+
#
|
228
|
+
def stringify_all(obj)
|
229
|
+
case obj
|
230
|
+
when Hash
|
231
|
+
result = {}
|
232
|
+
obj.each { |key, value| result[key.to_s] = stringify_all(value) }
|
233
|
+
when Array
|
234
|
+
result = []
|
235
|
+
obj.each { |value| result << stringify_all(value) }
|
236
|
+
when FalseClass
|
237
|
+
result = false
|
238
|
+
when TrueClass
|
239
|
+
result = true
|
240
|
+
else
|
241
|
+
result = obj.to_s
|
242
|
+
end
|
243
|
+
result
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
#
|
249
|
+
# Only send a message if this message is associated with an account
|
250
|
+
# and that has an active smtp configuration
|
251
|
+
# otherwise send it to support for the environment
|
252
|
+
#
|
253
|
+
def notify_failure(error)
|
254
|
+
if account.nil? || !account.fetch_or_build_smtp_configuration.use?
|
255
|
+
error_email = EmailNotification.new
|
256
|
+
params = { company_name: SystemConfiguration.company_name,
|
257
|
+
error_message: error.message,
|
258
|
+
notification_url: "#{SystemConfiguration.base_url}/stack/notifications/#{id}" }
|
259
|
+
error_email.from_template(AccountEmailTemplate::EMAIL_NOTIFICATION_FAILED, params)
|
260
|
+
error_email.to = SystemConfiguration.support_email
|
261
|
+
error_email.save!
|
262
|
+
error_email.deliver
|
263
|
+
else
|
264
|
+
account.smtp_admins.each do |admin|
|
265
|
+
error_email = EmailNotification.new
|
266
|
+
params = { company_name: SystemConfiguration.company_name,
|
267
|
+
error_message: error.message,
|
268
|
+
notification_url: "#{SystemConfiguration.base_url}/notifications/#{id}" }
|
269
|
+
error_email.from_template(AccountEmailTemplate::EMAIL_NOTIFICATION_FAILED, params)
|
270
|
+
error_email.to = admin.email
|
271
|
+
error_email.save!
|
272
|
+
error_email.deliver
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Base class for notification
|
5
|
+
#
|
6
|
+
class NotificationTemplate < Template
|
7
|
+
include StandardModel
|
8
|
+
#
|
9
|
+
# Fields
|
10
|
+
#
|
11
|
+
field :draft, type: Boolean, default: true
|
12
|
+
#
|
13
|
+
# Relationships
|
14
|
+
#
|
15
|
+
has_many :notifications, dependent: :nullify
|
16
|
+
#
|
17
|
+
# Validations
|
18
|
+
#
|
19
|
+
validates_presence_of :draft
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# SlackNotification is a concrete class of Notification, sending notifications to the slack
|
5
|
+
# system. Initially this will be only used internally, however we could configure this
|
6
|
+
# to work with an account's slack configuration.
|
7
|
+
#
|
8
|
+
# This object allows you to create an EmailNotification object, set it's property and
|
9
|
+
# tell it send_notification. It'll handle everything from there including
|
10
|
+
# 1. Running in the background thread
|
11
|
+
# 2. Error recovery
|
12
|
+
# 3. Retry on failure
|
13
|
+
#
|
14
|
+
# Usage:
|
15
|
+
#
|
16
|
+
# SlackNotification.say 'Error getting data from resource'
|
17
|
+
#
|
18
|
+
# or
|
19
|
+
# To change the channel it's posted to...
|
20
|
+
#
|
21
|
+
# SlackNotification.say 'Error getting data from resource', to: 'another_channel'
|
22
|
+
#
|
23
|
+
# or
|
24
|
+
# To who the message is from, the default is the environment name
|
25
|
+
#
|
26
|
+
# SlackNotification.say 'Error getting data from resource', to: 'another_channel', from: 'someone else'
|
27
|
+
#
|
28
|
+
# You are done! Go about your business of creating awesome software!
|
29
|
+
#
|
30
|
+
class SlackNotification < Notification
|
31
|
+
# The slack username to use
|
32
|
+
field :from, type: String
|
33
|
+
|
34
|
+
def deliver_message!
|
35
|
+
return unless SystemConfiguration.slack_configured?
|
36
|
+
|
37
|
+
start_processing
|
38
|
+
payload = { text: message }
|
39
|
+
payload[:channel] = if to.present?
|
40
|
+
to
|
41
|
+
elsif SystemConfiguration.slack_support_channel.present?
|
42
|
+
SystemConfiguration.slack_support_channel
|
43
|
+
else
|
44
|
+
'support'
|
45
|
+
end
|
46
|
+
# Use the environment as the default, otherwise set it as the from
|
47
|
+
payload[:username] = from.presence || Rails.env
|
48
|
+
# Setup the delivery method for this message only.
|
49
|
+
RestClient.post(SystemConfiguration.slack_api_url, payload.to_json)
|
50
|
+
finish_processing
|
51
|
+
rescue StandardError => error
|
52
|
+
App47Logger.log_debug '!!! Error sending SLACK notification !!!'
|
53
|
+
App47Logger.log_debug error.message
|
54
|
+
App47Logger.log_debug error.backtrace
|
55
|
+
App47Logger.log_debug '!!! Error sending SLACK notification !!!'
|
56
|
+
finish_processing error.message
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Limit the number of times the slack notification is retried
|
61
|
+
#
|
62
|
+
def max_retries
|
63
|
+
1
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Default delivery channel is email, override for sms, SMTP or other channels
|
68
|
+
#
|
69
|
+
def delivery_channel
|
70
|
+
DELIVERY_SLACK
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Convenience method to say something when we see something
|
75
|
+
#
|
76
|
+
def self.say(message, to: nil, from: nil, template: nil)
|
77
|
+
return unless SystemConfiguration.slack_configured?
|
78
|
+
|
79
|
+
notification = SlackNotification.new(to: to, from: from)
|
80
|
+
notification.message = if template.present?
|
81
|
+
notification.message_from_template template, message
|
82
|
+
elsif message.is_a?(Array)
|
83
|
+
message.join("\n")
|
84
|
+
else
|
85
|
+
message
|
86
|
+
end
|
87
|
+
notification.send_notification
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#
|
2
|
+
# SmsNotification is a concrete class of Notification, sending sms to both members
|
3
|
+
# and users, or in the case of eCommerce, customers.
|
4
|
+
#
|
5
|
+
# This object allows you to create an SmsNotification object, set it's property and
|
6
|
+
# tell it send_notification. It'll handle everything from there including
|
7
|
+
# 1. Running in the background thread
|
8
|
+
# 2. Error recovery
|
9
|
+
# 3. Retry on failure
|
10
|
+
#
|
11
|
+
# The SMS object also uses the templating engine to allow the specification of a template
|
12
|
+
# and a set of local variables for dynamic content.
|
13
|
+
#
|
14
|
+
# Usage:
|
15
|
+
#
|
16
|
+
# sms = SmsNotification.new
|
17
|
+
# sms.to = '5713326267'
|
18
|
+
# sms.message = 'Day is today!'
|
19
|
+
# sms.send_notification
|
20
|
+
#
|
21
|
+
# You are done! Go about your business of creating awesome software!
|
22
|
+
#
|
23
|
+
class SmsNotification < Notification
|
24
|
+
#
|
25
|
+
# Fields
|
26
|
+
#
|
27
|
+
field :sid, type: String
|
28
|
+
|
29
|
+
validates_format_of :to, with: %r{\A\+[1-9]{1}[0-9]{3,14}\z}, message: 'Invalid phone number'
|
30
|
+
validates_presence_of :to
|
31
|
+
validates_presence_of :account_id
|
32
|
+
|
33
|
+
def deliver_message!
|
34
|
+
return unless SystemConfiguration.twilio_configured?
|
35
|
+
|
36
|
+
config = SystemConfiguration.configuration
|
37
|
+
account_sid = config.twilio_account_id
|
38
|
+
auth_token = config.twilio_auth_token
|
39
|
+
client = Twilio::REST::Client.new account_sid, auth_token
|
40
|
+
|
41
|
+
message = client.account.messages.create(
|
42
|
+
body: self.message,
|
43
|
+
to: self.to,
|
44
|
+
from: config.twilio_phone_number
|
45
|
+
)
|
46
|
+
# We are saved in the calling class, no reason to save again
|
47
|
+
set sid: message.sid
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# set the delivery channel for the templates
|
52
|
+
#
|
53
|
+
def delivery_channel
|
54
|
+
DELIVERY_SMS
|
55
|
+
end
|
56
|
+
end
|