tolliver 1.0.0 → 2.1.2

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 +4 -4
  2. data/README.md +71 -13
  3. data/app/models/tolliver/notification.rb +0 -1
  4. data/app/models/tolliver/notification_attachment.rb +16 -0
  5. data/app/models/tolliver/notification_delivery.rb +1 -3
  6. data/app/models/tolliver/notification_receiver.rb +0 -2
  7. data/app/models/tolliver/notification_template.rb +0 -1
  8. data/app/views/tolliver/notification_mailer/notify.html.erb +1 -0
  9. data/app/views/tolliver/notification_mailer/notify.text.erb +1 -0
  10. data/config/locales/en.yml +1 -1
  11. data/db/migrate/20160121093817_create_notifications.rb +1 -12
  12. data/db/migrate/20160121094838_create_notification_receivers.rb +6 -4
  13. data/db/migrate/20160509144238_create_notification_templates.rb +4 -4
  14. data/db/migrate/20170630190600_create_notification_deliveries.rb +10 -3
  15. data/db/migrate/20201027150000_create_notification_attachments.rb +17 -0
  16. data/db/migrate/20210415120000_add_notification_attachments_url.rb +6 -0
  17. data/lib/tolliver.rb +66 -54
  18. data/lib/tolliver/errors/bad_request.rb +17 -0
  19. data/lib/tolliver/errors/not_found.rb +17 -0
  20. data/lib/tolliver/errors/standard_error.rb +17 -0
  21. data/lib/tolliver/jobs/batch_policy_job.rb +19 -0
  22. data/lib/tolliver/jobs/delivery_job.rb +18 -0
  23. data/lib/tolliver/mailers/notification_mailer.rb +10 -13
  24. data/lib/tolliver/models/notification.rb +11 -64
  25. data/lib/tolliver/models/notification_attachment.rb +47 -0
  26. data/lib/tolliver/models/notification_delivery.rb +32 -40
  27. data/lib/tolliver/models/notification_receiver.rb +3 -25
  28. data/lib/tolliver/models/notification_template.rb +6 -10
  29. data/lib/tolliver/services/delivery_service.rb +63 -0
  30. data/lib/tolliver/services/methods/email.rb +55 -0
  31. data/lib/tolliver/services/methods/email/mailgun.rb +55 -0
  32. data/lib/tolliver/services/{delivery.rb → methods/email/smtp.rb} +11 -13
  33. data/lib/tolliver/services/methods/sms.rb +54 -0
  34. data/lib/tolliver/services/methods/sms/plivo.rb +50 -0
  35. data/lib/tolliver/services/notification_service.rb +179 -0
  36. data/lib/tolliver/services/policies/batch.rb +82 -0
  37. data/lib/tolliver/services/policies/instantly.rb +50 -0
  38. metadata +27 -16
  39. data/app/views/notification_mailer/notify.html.erb +0 -1
  40. data/app/views/notification_mailer/notify.text.erb +0 -1
  41. data/lib/tolliver/models/notification_delivery/batch.rb +0 -77
  42. data/lib/tolliver/models/notification_delivery/instantly.rb +0 -55
  43. data/lib/tolliver/models/notification_receiver/email.rb +0 -46
  44. data/lib/tolliver/models/notification_receiver/sms.rb +0 -45
  45. data/lib/tolliver/services/notification.rb +0 -234
  46. data/lib/tolliver/services/sms/plivo.rb +0 -49
@@ -20,19 +20,14 @@ module Tolliver
20
20
  # Structure
21
21
  # *********************************************************************
22
22
 
23
- has_many :notification_templates, class_name: Tolliver.notification_template_model.to_s, dependent: :nullify
23
+ has_many :notifications, class_name: Tolliver.notification_model.to_s, dependent: :nullify
24
24
 
25
25
  # *********************************************************************
26
26
  # Validators
27
27
  # *********************************************************************
28
28
 
29
- validates_presence_of :ref
30
-
31
- # *********************************************************************
32
- # Ref
33
- # *********************************************************************
34
-
35
- enum_column :ref, Tolliver.template_refs
29
+ validates_presence_of :ref, :subject
30
+ validates :ref, uniqueness: true
36
31
 
37
32
  end
38
33
 
@@ -40,10 +35,11 @@ module Tolliver
40
35
 
41
36
  def permitted_columns
42
37
  [
38
+ :ref,
43
39
  :subject,
44
40
  :message,
45
- :disabled,
46
- :dry
41
+ :is_disabled,
42
+ :is_dry
47
43
  ]
48
44
  end
49
45
 
@@ -0,0 +1,63 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Common delivery service
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 19. 4. 2017
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ require 'singleton'
13
+
14
+ module Tolliver
15
+ module Services
16
+ class DeliveryService
17
+ include Singleton
18
+
19
+ def enqueue_for_delivery(notification)
20
+ return nil if notification.nil?
21
+ Tolliver::Jobs::DeliveryJob.perform_later(notification.id)
22
+ end
23
+
24
+ # Deliver notification to receivers by all configured methods
25
+ def deliver(notification)
26
+ return nil if notification.nil?
27
+
28
+ # Load balancing
29
+ unless Tolliver.load_balance.blank?
30
+ notifications_sent_in_protected_time_window = Tolliver.notification_model
31
+ .joins(:notification_deliveries)
32
+ .where.not(id: notification.id)
33
+ .where("notification_deliveries.sent_at > ?", Time.current - Tolliver.load_balance.minutes).distinct
34
+ if notifications_sent_in_protected_time_window.count > 0
35
+
36
+ # Sleep for given amount of time
37
+ sleep(Tolliver.load_balance * 60) # seconds
38
+
39
+ # Then try again
40
+ enqueue_for_delivery(notification)
41
+
42
+ return false
43
+ end
44
+ end
45
+
46
+ # Process all notification deliveries
47
+ notification.notification_deliveries.each do |notification_delivery|
48
+ notification_delivery.policy_service.deliver(notification_delivery)
49
+ end
50
+
51
+ true
52
+ end
53
+
54
+ def reset_delivery(notification)
55
+ return nil if notification.nil?
56
+ notification.notification_deliveries.update_all(sent_count: 0, sent_at: nil)
57
+ notification.notification_receivers.update_all(sent_at: nil, received_at: nil, status: :created, error_message: nil)
58
+ true
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Notification method - E-mail
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 21. 1. 2016
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Tolliver
13
+ module Services
14
+ module Methods
15
+ class Email
16
+
17
+ def deliver(notification_receiver)
18
+ return false if provider.nil?
19
+
20
+ # Prepare notification
21
+ notification = notification_receiver.notification_delivery.notification
22
+
23
+ # Send email
24
+ begin
25
+ provider.deliver(notification, notification_receiver)
26
+ notification_receiver.status = 'sent'
27
+ #rescue Net::SMTPFatalError, Net::SMTPSyntaxError
28
+ rescue StandardError => e
29
+ notification_receiver.status = 'error'
30
+ notification_receiver.error_message = e.message
31
+ end
32
+
33
+ # Mark as sent
34
+ notification_receiver.sent_at = Time.current
35
+
36
+ # Save
37
+ notification_receiver.save
38
+
39
+ true
40
+ end
41
+
42
+ protected
43
+
44
+ def provider
45
+ if @provider.nil? && Tolliver.email_provider
46
+ provider_class_name = "Tolliver::Services::Methods::Email::#{Tolliver.email_provider.to_s.camelize}"
47
+ @provider = provider_class_name.constantize.new(Tolliver.email_provider_params)
48
+ end
49
+ @provider
50
+ end
51
+
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Mailgun e-mail provider
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 1. 12. 2017
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Tolliver
13
+ module Services
14
+ module Methods
15
+ class Email
16
+ class Mailgun
17
+
18
+ def initialize(params = {})
19
+ require 'mailgun-ruby'
20
+ if params[:api_key].blank? || params[:domain].blank?
21
+ raise Tolliver::Errors::StandardError.new('Please provide API key and domain in e-mail provider params.')
22
+ end
23
+ @client = ::Mailgun::Client.new(params[:api_key])
24
+ @domain = params[:domain]
25
+ end
26
+
27
+ def deliver(notification, notification_receiver)
28
+
29
+ # Sender
30
+ raise Tolliver::Errors::StandardError.new("Please specify e-mail sender.") if Tolliver.email_sender.nil?
31
+
32
+ # Message builder
33
+ message = ::Mailgun::MessageBuilder.new
34
+ message.from(Tolliver.email_sender, {'full_name' => Tolliver.email_sender_name})
35
+ message.add_recipient(:to, notification_receiver.receiver_contact.to_s)
36
+ message.reply_to(notification_receiver.notification_delivery.sender_contact.to_s) unless notification_receiver.notification_delivery.sender_contact.blank?
37
+ message.subject(notification.subject)
38
+ message.body_text(ActionController::Base.helpers.strip_tags(notification.message.to_s))
39
+ message.body_html(notification.message)
40
+ notification.notification_attachments.each do |notification_attachment|
41
+ message.add_attachment(StringIO.new(notification_attachment.read), notification_attachment.name) if notification_attachment.read
42
+ end
43
+ response = @client.send_message(@domain, message)
44
+ if response.code != 200
45
+ raise Tolliver::Errors::StandardError.new(response.body)
46
+ end
47
+
48
+ true
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -2,29 +2,27 @@
2
2
  # * Copyright (c) 2019 Matěj Outlý
3
3
  # *****************************************************************************
4
4
  # *
5
- # * Common delivery service
5
+ # * SMTP e-mail provider
6
6
  # *
7
7
  # * Author: Matěj Outlý
8
- # * Date : 19. 4. 2017
8
+ # * Date : 1. 12. 2017
9
9
  # *
10
10
  # *****************************************************************************
11
11
 
12
12
  module Tolliver
13
13
  module Services
14
- module Delivery
15
- extend ActiveSupport::Concern
14
+ module Methods
15
+ class Email
16
+ class Smtp
16
17
 
17
- module ClassMethods
18
+ def initialize(params = {}) end
18
19
 
19
- #
20
- # Deliver notification (defined by ID) to receivers by all configured methods
21
- #
22
- def deliver(notification_id)
23
- Tolliver.notification_model.deliver(notification_id)
24
- end
20
+ def deliver(notification, notification_receiver)
21
+ Tolliver::NotificationMailer.notify(notification, notification_receiver).deliver_now
22
+ end
25
23
 
24
+ end
26
25
  end
27
-
28
26
  end
29
27
  end
30
- end
28
+ end
@@ -0,0 +1,54 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Notification receiver
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 21. 1. 2016
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Tolliver
13
+ module Services
14
+ module Methods
15
+ class Sms
16
+
17
+ def deliver(notification_receiver)
18
+ return false if provider.nil?
19
+
20
+ # Prepare notification
21
+ notification = notification_receiver.notification_delivery.notification
22
+
23
+ # Send SMS
24
+ begin
25
+ provider.deliver(notification, notification_receiver)
26
+ notification_receiver.status = 'sent'
27
+ rescue StandardError => e
28
+ notification_receiver.status = 'error'
29
+ notification_receiver.error_message = e.message
30
+ end
31
+
32
+ # Mark as sent
33
+ notification_receiver.sent_at = Time.current
34
+
35
+ # Save
36
+ notification_receiver.save
37
+
38
+ true
39
+ end
40
+
41
+ protected
42
+
43
+ def provider
44
+ if @provider.nil? && Tolliver.sms_provider
45
+ provider_class_name = "Tolliver::Services::Methods::Sms::#{Tolliver.sms_provider.to_s.camelize}"
46
+ @provider = provider_class_name.constantize.new(Tolliver.sms_provider_params)
47
+ end
48
+ @provider
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,50 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Plivo SMS provider
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 1. 12. 2017
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Tolliver
13
+ module Services
14
+ module Methods
15
+ class Sms
16
+ class Plivo
17
+
18
+ def initialize(params = {})
19
+ require 'plivo'
20
+ if params[:auth_id].blank? || params[:auth_token].blank?
21
+ raise Tolliver::Errors::StandardError.new('Please provide Auth ID and Auth Token in SMS provider params.')
22
+ end
23
+ @auth_id = params[:auth_id]
24
+ @auth_token = params[:auth_token]
25
+ @api = ::Plivo::RestAPI.new(@auth_id, @auth_token)
26
+ end
27
+
28
+ def deliver(notification, notification_receiver)
29
+
30
+ # Check message length.
31
+ if message.bytesize > 200
32
+ raise 'Message too long.'
33
+ end
34
+
35
+ # Request API
36
+ response = @api.send_message({
37
+ 'src' => Tolliver.sms_sender, # TODO: This should be improved to take sender from number pool and remember number / message mapping
38
+ 'dst' => notification_receiver.receiver_contact.to_s,
39
+ 'text' => ActionController::Base.helpers.strip_tags(notification.message.to_s),
40
+ 'method' => 'POST'
41
+ })
42
+
43
+ true
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,179 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Notification service
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 19. 4. 2017
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ require 'singleton'
13
+
14
+ module Tolliver
15
+ module Services
16
+ class NotificationService
17
+ include Singleton
18
+
19
+ def notify(options)
20
+ options = options.deep_symbolize_keys
21
+
22
+ # Object
23
+ notification = Tolliver.notification_model.new
24
+
25
+ Tolliver.notification_model.transaction do
26
+
27
+ # Get notification template object
28
+ template = options[:template]
29
+ raise Tolliver::Errors::BadRequest.new('Missing template.') if template.blank?
30
+ notification_template = Tolliver.notification_template_model.where(ref: template).first
31
+ notification_template = Tolliver.notification_template_model.where(id: template).first if notification_template.nil?
32
+ raise Tolliver::Errors::NotFound.new("Template #{template.to_s} not found.") if notification_template.nil?
33
+
34
+ if !notification_template.is_disabled && !notification_template.message.blank?
35
+
36
+ # Interpret params and store it in DB
37
+ params = map_params(options[:params] || {})
38
+ notification.message = eval_expressions(interpret_named_params(notification_template.message, params))
39
+ notification.subject = eval_expressions(interpret_named_params(notification_template.subject, params))
40
+
41
+ # Notification template
42
+ notification.notification_template = notification_template
43
+
44
+ # Signature
45
+ unless options[:signature].blank?
46
+ notification.message += options[:signature].to_s
47
+ end
48
+
49
+ # Save to DB
50
+ notification.save
51
+
52
+ # Attachments
53
+ unless options[:attachments].blank?
54
+ attachments = options[:attachments]
55
+ attachments = [attachments] unless attachments.is_a?(Array)
56
+ attachments.each do |attachment|
57
+ raise Tolliver::Errors::BadRequest.new('Missing attachment name.') if attachment[:name].blank?
58
+ if attachment[:attachment].blank? && attachment[:url].blank?
59
+ raise Tolliver::Errors::BadRequest.new('Missing attachment data or URL.')
60
+ end
61
+ notification.notification_attachments.create(name: attachment[:name], attachment: attachment[:attachment], url: attachment[:url])
62
+ end
63
+ end
64
+
65
+ unless notification_template.is_dry
66
+
67
+ # Delivery methods
68
+ if options[:methods].blank?
69
+ methods = Tolliver.delivery_methods
70
+ else
71
+ # Select only wanted delivery methods, but valid according to module config
72
+ methods = options[:methods]
73
+ methods = [methods] unless methods.is_a?(Array)
74
+ methods = methods.delete_if { |method| !Tolliver.delivery_methods.include?(method.to_sym) }
75
+ end
76
+
77
+ methods.each do |method|
78
+
79
+ # Delivery object
80
+ notification_delivery = notification.notification_deliveries.create(method: method)
81
+
82
+ # Is postponed
83
+ if options[:is_postponed]
84
+ notification_delivery.is_postponed = options[:is_postponed]
85
+ end
86
+
87
+ # Sender
88
+ unless options[:sender].blank?
89
+ raise Tolliver::Errors::BadRequest.new('Missing sender ref.') if options[:sender][:ref].blank?
90
+ raise Tolliver::Errors::BadRequest.new('Missing sender contact.') if options[:sender][:contact].blank?
91
+ notification_delivery.sender_ref = options[:sender][:ref]
92
+ notification_delivery.sender_contact = options[:sender][:contact]
93
+ end
94
+
95
+ # Receivers
96
+ receivers = options[:receivers] || []
97
+ raise Tolliver::Errors::BadRequest.new('Missing receivers.') if receivers.blank? || receivers.empty?
98
+ receivers = [receivers] unless receivers.is_a?(Array)
99
+ filtered_receivers = receivers.dup # TODO contact validation based on method and filter
100
+
101
+ # Save to DB
102
+ filtered_receivers.each do |receiver|
103
+ raise Tolliver::Errors::BadRequest.new('Missing receiver ref.') if receiver[:ref].blank?
104
+ raise Tolliver::Errors::BadRequest.new('Missing receiver contact.') if receiver[:contact].blank?
105
+ notification_delivery.notification_receivers.create(receiver_ref: receiver[:ref], receiver_contact: receiver[:contact])
106
+ end
107
+ notification_delivery.sent_count = 0
108
+ notification_delivery.receivers_count = filtered_receivers.size
109
+ notification_delivery.save
110
+
111
+ end
112
+
113
+ end
114
+
115
+ else
116
+ notification = nil # Do not create notification with empty message
117
+ end
118
+
119
+ end
120
+
121
+ # Enqueue for delivery
122
+ if !notification.nil? && !options[:is_postponed]
123
+ notification.enqueue_for_delivery
124
+ end
125
+
126
+ notification
127
+ end
128
+
129
+ protected
130
+
131
+ def map_params(params)
132
+ result = {}
133
+ params.each do |param|
134
+ raise Tolliver::Errors::BadRequest.new('Missing param key.') if param[:key].blank?
135
+ raise Tolliver::Errors::BadRequest.new('Missing param value.') if param[:value].nil?
136
+ result[param[:key].to_s] = param[:value]
137
+ end
138
+ result
139
+ end
140
+
141
+ def interpret_named_params(text, params)
142
+ text.gsub(/%{[^{}]+}/) do |match|
143
+ key = match[2..-2].to_s.strip
144
+ if params.has_key?(key)
145
+ params[key] # return param value if key found
146
+ else
147
+ match # return entire match if key not found
148
+ end
149
+ end
150
+ end
151
+
152
+ def eval_expressions(text)
153
+ text.gsub(/%{[^{}]+}/) do |match|
154
+ template_to_eval = match[2..-2].to_s.strip
155
+ begin
156
+ evaluated_match = eval(template_to_eval) # evaluate match
157
+ rescue
158
+ evaluated_match = ""
159
+ end
160
+ evaluated_match
161
+ end
162
+ end
163
+
164
+ def interpret_positional_params_and_eval_expressions(text, params)
165
+ text.gsub(/%{[^{}]+}/) do |match|
166
+ template_to_eval = match[2..-2].gsub(/%([0-9]+)/, "params[\\1]") # substitute all %1, %2, %3, ... to a form which can be evaluated
167
+ begin
168
+ evaluated_match = eval(template_to_eval) # evaluate match against params
169
+ rescue
170
+ evaluated_match = ""
171
+ end
172
+ evaluated_match
173
+ end
174
+ end
175
+
176
+ end
177
+
178
+ end
179
+ end