nuntius 0.1.2 → 1.0.27

Sign up to get free protection for your applications and to get access to all the features.
Files changed (169) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +199 -63
  4. data/Rakefile +62 -1
  5. data/app/controllers/nuntius/admin/campaigns_controller.rb +56 -0
  6. data/app/controllers/nuntius/admin/layouts/attachments_controller.rb +32 -0
  7. data/app/controllers/nuntius/admin/layouts_controller.rb +50 -0
  8. data/app/controllers/nuntius/admin/lists/subscribers_controller.rb +50 -0
  9. data/app/controllers/nuntius/admin/lists_controller.rb +46 -0
  10. data/app/controllers/nuntius/admin/locales_controller.rb +46 -0
  11. data/app/controllers/nuntius/admin/messages_controller.rb +24 -0
  12. data/app/controllers/nuntius/admin/templates_controller.rb +56 -0
  13. data/app/controllers/nuntius/api/events_controller.rb +16 -0
  14. data/app/controllers/nuntius/application_admin_controller.rb +11 -0
  15. data/app/controllers/nuntius/application_controller.rb +12 -0
  16. data/app/controllers/nuntius/callbacks_controller.rb +21 -0
  17. data/app/controllers/nuntius/dashboard_controller.rb +12 -0
  18. data/app/controllers/nuntius/feedback_controller.rb +17 -0
  19. data/app/controllers/nuntius/inbound_messages/twilio_inbound_smses_controller.rb +56 -0
  20. data/app/controllers/nuntius/messages_controller.rb +13 -0
  21. data/app/drops/nuntius/application_drop.rb +17 -0
  22. data/app/drops/nuntius/campaign_drop.rb +7 -0
  23. data/app/drops/nuntius/layout_drop.rb +11 -0
  24. data/app/drops/nuntius/message_drop.rb +11 -0
  25. data/app/drops/nuntius/template_drop.rb +7 -0
  26. data/app/exceptions/nuntius/base_exception.rb +4 -0
  27. data/app/exceptions/nuntius/missing_messenger_exception.rb +4 -0
  28. data/app/helpers/nuntius/application_helper.rb +78 -0
  29. data/app/jobs/nuntius/application_job.rb +9 -0
  30. data/app/jobs/nuntius/deliver_inbound_message_job.rb +10 -0
  31. data/app/jobs/nuntius/messenger_job.rb +17 -0
  32. data/app/jobs/nuntius/process_inbound_message_job.rb +10 -0
  33. data/app/jobs/nuntius/purge_message_job.rb +13 -0
  34. data/app/jobs/nuntius/retrieve_mail_job.rb +18 -0
  35. data/app/jobs/nuntius/transport_delivery_job.rb +32 -0
  36. data/app/jobs/nuntius/transport_refresh_job.rb +22 -0
  37. data/app/message_boxes/nuntius/base_message_box.rb +75 -0
  38. data/app/messengers/nuntius/base_messenger.rb +193 -0
  39. data/app/messengers/nuntius/custom_messenger.rb +6 -0
  40. data/app/models/nuntius/application_record.rb +11 -0
  41. data/app/models/nuntius/attachment.rb +13 -0
  42. data/app/models/nuntius/campaign.rb +75 -0
  43. data/app/models/nuntius/concerns/metadata_scoped.rb +31 -0
  44. data/app/models/nuntius/concerns/yamlify.rb +23 -0
  45. data/app/models/nuntius/inbound_message.rb +14 -0
  46. data/app/models/nuntius/layout.rb +13 -0
  47. data/app/models/nuntius/list.rb +10 -0
  48. data/app/models/nuntius/locale.rb +11 -0
  49. data/app/models/nuntius/message.rb +148 -0
  50. data/app/models/nuntius/subscriber.rb +12 -0
  51. data/app/models/nuntius/template.rb +102 -0
  52. data/app/presenters/application_presenter.rb +28 -0
  53. data/app/presenters/template_presenter.rb +17 -0
  54. data/app/providers/nuntius/apnotic_push_provider.rb +44 -0
  55. data/app/providers/nuntius/base_provider.rb +72 -0
  56. data/app/providers/nuntius/firebase_push_provider.rb +26 -0
  57. data/app/providers/nuntius/houston_push_provider.rb +40 -0
  58. data/app/providers/nuntius/message_bird_sms_provider.rb +39 -0
  59. data/app/providers/nuntius/slack_slack_provider.rb +36 -0
  60. data/app/providers/nuntius/smtp_mail_provider.rb +80 -0
  61. data/app/providers/nuntius/twilio_sms_provider.rb +37 -0
  62. data/app/providers/nuntius/twilio_voice_provider.rb +70 -0
  63. data/app/reportlets/nuntius/application_reportlet.rb +35 -0
  64. data/app/reportlets/nuntius/daily_messages_per_template_reportlet.rb +50 -0
  65. data/app/runners/nuntius/application_runner.rb +6 -0
  66. data/app/runners/nuntius/basic_application_runner.rb +15 -0
  67. data/app/runners/nuntius/timebased_events_runner.rb +15 -0
  68. data/app/services/nuntius/application_service.rb +7 -0
  69. data/app/services/nuntius/aws_sns_processor_service.rb +81 -0
  70. data/app/services/nuntius/deliver_inbound_message_service.rb +14 -0
  71. data/app/services/nuntius/retrieve_inbound_mail_service.rb +36 -0
  72. data/app/tables/nuntius_campaigns_table.rb +21 -0
  73. data/app/tables/nuntius_layouts_table.rb +24 -0
  74. data/app/tables/nuntius_lists_table.rb +19 -0
  75. data/app/tables/nuntius_locales_table.rb +18 -0
  76. data/app/tables/nuntius_messages_table.rb +36 -0
  77. data/app/tables/nuntius_subscribers_table.rb +19 -0
  78. data/app/tables/nuntius_templates_table.rb +30 -0
  79. data/app/transports/nuntius/base_transport.rb +36 -0
  80. data/app/transports/nuntius/mail_transport.rb +35 -0
  81. data/app/transports/nuntius/push_transport.rb +15 -0
  82. data/app/transports/nuntius/slack_transport.rb +6 -0
  83. data/app/transports/nuntius/sms_transport.rb +6 -0
  84. data/app/transports/nuntius/voice_transport.rb +6 -0
  85. data/app/validators/liquid_validator.rb +11 -0
  86. data/app/views/nuntius/admin/campaigns/edit.html.slim +49 -0
  87. data/app/views/nuntius/admin/campaigns/index.html.slim +2 -0
  88. data/app/views/nuntius/admin/layouts/attachments/_attachments.html.slim +28 -0
  89. data/app/views/nuntius/admin/layouts/attachments/_index.html.slim +2 -0
  90. data/app/views/nuntius/admin/layouts/attachments/create.json.jbuilder +5 -0
  91. data/app/views/nuntius/admin/layouts/edit.html.slim +24 -0
  92. data/app/views/nuntius/admin/layouts/index.html.slim +2 -0
  93. data/app/views/nuntius/admin/lists/edit.html.slim +14 -0
  94. data/app/views/nuntius/admin/lists/index.html.slim +2 -0
  95. data/app/views/nuntius/admin/lists/subscribers/edit.html.slim +14 -0
  96. data/app/views/nuntius/admin/locales/edit.html.slim +14 -0
  97. data/app/views/nuntius/admin/locales/index.html.slim +2 -0
  98. data/app/views/nuntius/admin/messages/index.html.slim +2 -0
  99. data/app/views/nuntius/admin/messages/show.html.slim +56 -0
  100. data/app/views/nuntius/admin/templates/edit.html.slim +81 -0
  101. data/app/views/nuntius/admin/templates/index.html.slim +2 -0
  102. data/app/views/nuntius/dashboard/show.html.slim +7 -0
  103. data/app/views/nuntius/messages/show.html.slim +7 -0
  104. data/config/locales/en.yml +22 -0
  105. data/config/locales/nl.yml +22 -0
  106. data/config/routes.rb +38 -0
  107. data/config/webpack/development.js +5 -0
  108. data/config/webpack/environment.js +4 -0
  109. data/config/webpack/production.js +5 -0
  110. data/config/webpack/test.js +5 -0
  111. data/config/webpacker.yml +119 -0
  112. data/db/migrate/20190301201541_create_nuntius_templates.rb +27 -0
  113. data/db/migrate/20190301202436_create_nuntius_messages.rb +30 -0
  114. data/db/migrate/20190322112815_create_nuntius_lists.rb +12 -0
  115. data/db/migrate/20190322114340_create_nuntius_campaigns.rb +18 -0
  116. data/db/migrate/20190322121338_create_nuntius_subscribers.rb +17 -0
  117. data/db/migrate/20190327123535_add_metadata_to_models.rb +8 -0
  118. data/db/migrate/20190327124407_create_nuntius_layouts.rb +24 -0
  119. data/db/migrate/20190327143921_add_campaign_to_message.rb +7 -0
  120. data/db/migrate/20190327155112_add_state_to_campaign.rb +7 -0
  121. data/db/migrate/20190412103335_add_enabled_to_templates.rb +7 -0
  122. data/db/migrate/20190417125153_change_message_status_default.rb +9 -0
  123. data/db/migrate/20190417144554_add_nuntiable_to_subscriber.rb +7 -0
  124. data/db/migrate/20190521135011_add_payload_to_nuntius_messages.rb +7 -0
  125. data/db/migrate/20190522122657_add_metadata_to_nuntius_message.rb +7 -0
  126. data/db/migrate/20190825080757_update_nuntius_message_sending_to_sent.rb +11 -0
  127. data/db/migrate/20200220154927_change_nuntius_templates_payload_type.rb +7 -0
  128. data/db/migrate/20200224132337_create_nuntius_locales.rb +13 -0
  129. data/db/migrate/20200318095339_create_nuntius_attachments.rb +12 -0
  130. data/db/migrate/20200407050646_add_interval_to_nuntius_template.rb +5 -0
  131. data/db/migrate/20200430131219_make_html_processing_optional.rb +6 -0
  132. data/db/migrate/20200430154032_make_html_processing_optional_revert.rb +6 -0
  133. data/db/migrate/20201121185718_create_nuntius_inbound_messages.rb +21 -0
  134. data/db/migrate/20220412114148_add_last_sent_to_message.rb +5 -0
  135. data/lib/nuntius/active_record_helpers.rb +28 -0
  136. data/lib/nuntius/active_storage_helpers.rb +8 -0
  137. data/lib/nuntius/configuration.rb +97 -0
  138. data/lib/nuntius/deprecator.rb +8 -0
  139. data/lib/nuntius/devise.rb +19 -0
  140. data/lib/nuntius/engine.rb +46 -0
  141. data/lib/nuntius/i18n_store.rb +100 -0
  142. data/lib/nuntius/liquid/tags/attach_tag.rb +44 -0
  143. data/lib/nuntius/mail_allow_list.rb +23 -0
  144. data/lib/nuntius/nuntiable.rb +24 -0
  145. data/lib/nuntius/state_machine.rb +19 -0
  146. data/lib/nuntius/transactio.rb +30 -0
  147. data/lib/nuntius/version.rb +3 -1
  148. data/lib/nuntius.rb +74 -8
  149. data/lib/preamble.rb +87 -0
  150. data/lib/tasks/nuntius_tasks.rake +27 -0
  151. metadata +532 -67
  152. data/.gitignore +0 -4
  153. data/Gemfile +0 -4
  154. data/lib/nuntius/encodings/url_safe_base64.rb +0 -26
  155. data/lib/nuntius/encodings.rb +0 -9
  156. data/lib/nuntius/envelope.rb +0 -45
  157. data/lib/nuntius/key.rb +0 -52
  158. data/lib/nuntius/messenger.rb +0 -37
  159. data/nuntius.gemspec +0 -23
  160. data/spec/keys/alice.pem +0 -27
  161. data/spec/keys/alice.pub +0 -9
  162. data/spec/keys/bob.pem +0 -27
  163. data/spec/keys/bob.pub +0 -9
  164. data/spec/nuntius/encodings/url_safe_base64_spec.rb +0 -36
  165. data/spec/nuntius/envelope_spec.rb +0 -6
  166. data/spec/nuntius/key_spec.rb +0 -22
  167. data/spec/nuntius/messenger_spec.rb +0 -26
  168. data/spec/spec_helper.rb +0 -9
  169. data/spec/support/keys.rb +0 -6
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ # Messengers select templates, can manipulate them and
5
+ class BaseMessenger
6
+ include ActiveSupport::Callbacks
7
+
8
+ delegate :liquid_variable_name_for, :class_name_for, :class_names_for, :event_name_for, to: :class
9
+
10
+ define_callbacks :action, terminator: ->(_target, result_lambda) { result_lambda.call == false }
11
+
12
+ attr_reader :templates, :attachments, :event, :object, :params
13
+
14
+ def initialize(object, event, params = {})
15
+ @object = object
16
+ @event = event
17
+ @params = params
18
+ @attachments = params.fetch(:attachments, [])
19
+ end
20
+
21
+ # Calls the event method on the messenger
22
+ def call
23
+ select_templates
24
+ run_callbacks(:action) do
25
+ send(@event.to_sym, @object, @params) if respond_to?(@event.to_sym)
26
+ end
27
+ end
28
+
29
+ # Turns the templates in messages, and dispatches the messages to transports
30
+ def dispatch(filtered_templates)
31
+ filtered_templates.each do |template|
32
+ template.layout = override_layout(template.layout)
33
+ msg = template.new_message(@object, liquid_context, params)
34
+ @attachments.each do |attachment|
35
+ msg.add_attachment(attachment)
36
+ end
37
+
38
+ transport = Nuntius::BaseTransport.class_from_name(template.transport).new
39
+ transport.deliver(msg) if msg.to.present?
40
+ end
41
+ end
42
+
43
+ # Allow messengers to override the selected layout
44
+ def override_layout(selected_layout)
45
+ selected_layout
46
+ end
47
+
48
+ def attach(attachment)
49
+ @attachments << attachment
50
+ end
51
+
52
+ class << self
53
+ #
54
+ # Returns the variable name used in the liquid context
55
+ #
56
+ # @param [Object] obj Any object with a backing drop
57
+ #
58
+ # @return [String] underscored, lowercase string
59
+ #
60
+ def liquid_variable_name_for(obj)
61
+ return obj.keys.first.to_s if obj.is_a?(Hash)
62
+
63
+ plural = obj.is_a?(Array) || obj.is_a?(ActiveRecord::Relation)
64
+ list = plural ? obj : [obj]
65
+ klass = list.first.class
66
+ klass = klass.base_class if klass.respond_to?(:base_class)
67
+ name = klass.name.demodulize
68
+ name = name.pluralize if plural
69
+ name.underscore
70
+ end
71
+
72
+ def class_name_for(obj)
73
+ if obj.is_a?(Array) || obj.is_a?(ActiveRecord::Relation)
74
+ obj.first.class.name.demodulize
75
+ elsif obj.is_a?(Hash)
76
+ 'Custom'
77
+ elsif obj.is_a?(Class)
78
+ obj.name.demodulize
79
+ else
80
+ obj.class.name
81
+ end
82
+ end
83
+
84
+ def class_names_for(obj)
85
+ main_class_name = class_name_for(obj)
86
+
87
+ return [main_class_name] if !obj.class.respond_to?(:base_class?) || obj.class.base_class?
88
+
89
+ list = [main_class_name]
90
+ list << obj.class.base_class.name
91
+ list
92
+ end
93
+
94
+ def event_name_for(obj, event)
95
+ if obj.is_a?(Hash)
96
+ "#{obj.keys.first}##{event}"
97
+ else
98
+ event
99
+ end
100
+ end
101
+
102
+ def messenger_for_class(name)
103
+ klass = messenger_name_for_class(name).safe_constantize
104
+ klass ||= messenger_name_for_class(name.safe_constantize.superclass).safe_constantize
105
+ klass ||= messenger_name_for_class(name.safe_constantize.superclass.superclass).safe_constantize
106
+ klass
107
+ end
108
+
109
+ def messenger_name_for_class(name)
110
+ "#{name}Messenger"
111
+ end
112
+
113
+ def messenger_for_obj(obj)
114
+ return Nuntius::CustomMessenger if obj.is_a? Hash
115
+
116
+ klass = messenger_name_for_obj(obj).safe_constantize
117
+
118
+ # Lets check 2 levels above to see if a messager exists for a possible super class (think STI)
119
+ klass ||= messenger_name_for_obj(obj.class.superclass).safe_constantize
120
+ klass ||= messenger_name_for_obj(obj.class.superclass.superclass).safe_constantize
121
+
122
+ raise Nuntius::MissingMessengerException.new(self), "messenger missing for #{obj.class.name}" unless klass
123
+
124
+ klass
125
+ end
126
+
127
+ def messenger_name_for_obj(obj)
128
+ "#{class_name_for(obj)}Messenger"
129
+ end
130
+
131
+ def locale(locale = nil)
132
+ @locale = locale if locale
133
+ @locale
134
+ end
135
+
136
+ def template_scope(template_scope = nil)
137
+ @template_scope = template_scope if template_scope
138
+ @template_scope
139
+ end
140
+
141
+ def timebased_scopes
142
+ @timebased_scopes ||= {}
143
+ end
144
+
145
+ def timebased_scope(name, &scope_proc)
146
+ raise ArgumentError, 'timebased_scope must start with before or after' unless %w[before after].detect { |prefix| name.to_s.start_with?(prefix) }
147
+
148
+ name = name.to_sym
149
+ timebased_scopes[name] = scope_proc if scope_proc.present?
150
+ define_method(name) { |object, params = {}| } unless respond_to?(name)
151
+ timebased_scopes[name] || nil
152
+ end
153
+
154
+ def timebased_scope_for(template)
155
+ return [] unless timebased_scopes.include?(template.event.to_sym)
156
+
157
+ timebased_scope(template.event)
158
+ .call(template.interval_time_range, template.metadata)
159
+ .where("#{template.klass.constantize.table_name}.created_at > ?", template.created_at)
160
+ .where.not(
161
+ id: Nuntius::Message.select(:nuntiable_id)
162
+ .where(template_id: template.id)
163
+ )
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ # Returns the relevant templates for the object / event combination
170
+ def select_templates
171
+ return @templates if @templates
172
+
173
+ @templates = Template.unscoped.where(klass: class_names_for(@object), event: event_name_for(@object, @event)).where(enabled: true)
174
+ @templates = @templates.instance_exec(@object, &Nuntius.config.default_template_scope)
175
+
176
+ # See if we need to do something additional
177
+ template_scope_proc = self.class.template_scope
178
+ @templates = @templates.instance_exec(@object, &template_scope_proc) if template_scope_proc
179
+
180
+ @templates
181
+ end
182
+
183
+ def liquid_context
184
+ assigns = @params || {}
185
+ instance_variables.reject { |i| %w[@params @object @locale @templates @template_scope].include? i.to_s }.each do |i|
186
+ assigns[i.to_s[1..-1]] = instance_variable_get(i)
187
+ end
188
+
189
+ context = { liquid_variable_name_for(@object) => (@object.is_a?(Hash) ? @object[@object.keys.first].deep_stringify_keys : @object) }
190
+ assigns.merge(context)
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class CustomMessenger < BaseMessenger
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/metadata_scoped'
4
+
5
+ module Nuntius
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+
9
+ include Liquidum::ToLiquid
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module Nuntius
2
+ class Attachment < ApplicationRecord
3
+ has_and_belongs_to_many :messages, class_name: 'Message'
4
+
5
+ delegate :download, :content_type, :filename, :signed_id, to: :content
6
+
7
+ begin
8
+ has_one_attached :content, service: Nuntius.config.active_storage_service
9
+ rescue StandardError
10
+ nil
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class Campaign < ApplicationRecord
5
+ include Nuntius::Concerns::MetadataScoped
6
+
7
+ belongs_to :list
8
+ accepts_nested_attributes_for :list, reject_if: :all_blank
9
+
10
+ belongs_to :layout, optional: true
11
+ has_many :messages, class_name: 'Nuntius::Message'
12
+ validates :name, presence: true
13
+
14
+ state_machine initial: :draft do
15
+ after_transition any => any, do: :do_after_transition
16
+
17
+ event :publish do
18
+ transition draft: :sending
19
+ end
20
+ event :sent do
21
+ transition sending: :sent
22
+ end
23
+ end
24
+
25
+ def deliver
26
+ t = BaseTransport.class_from_name(transport).new
27
+ list.subscribers.each do |subscriber|
28
+ t.deliver(new_message(subscriber))
29
+ end
30
+ end
31
+
32
+ def new_message(subscriber, assigns = {})
33
+ if subscriber.nuntiable
34
+ name = Nuntius::BaseMessenger.liquid_variable_name_for(subscriber.nuntiable)
35
+ assigns[name] = subscriber.nuntiable
36
+ end
37
+ message = Nuntius::Message.new(transport: transport, campaign: self, nuntiable: subscriber.nuntiable, metadata: metadata)
38
+
39
+ locale_proc = Nuntius::BaseMessenger.messenger_for_obj(subscriber.nuntiable).locale
40
+ locale = instance_exec(object, &locale_proc) if locale_proc
41
+
42
+ message.from = render(:from, assigns, locale)
43
+ message.to = if transport == 'mail'
44
+ %["#{subscriber.first_name} #{subscriber.last_name}" <#{subscriber.email}>]
45
+ elsif transport == 'sms'
46
+ subscriber.phone_number
47
+ elsif transport == 'voice'
48
+ subscriber.phone_number
49
+ end
50
+
51
+ message.subject = render(:subject, assigns, locale)
52
+ message.html = render(:html, assigns, locale, layout: layout&.data)
53
+
54
+ message
55
+ end
56
+
57
+ def translation_scope
58
+ scope = %w[nuntius]
59
+ scope << layout.name.underscore.tr(' ', '_') if layout
60
+ scope.join('.')
61
+ end
62
+
63
+ private
64
+
65
+ def render(attr, assigns, locale, options = {})
66
+ I18n.with_locale(locale) do
67
+ ::Liquidum.render(send(attr), assigns: assigns.merge(options.merge('campaign' => self)), registers: { 'campaign' => self })
68
+ end
69
+ end
70
+
71
+ def do_after_transition(transition)
72
+ deliver if transition.event == :publish
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ module Concerns
5
+ module MetadataScoped
6
+ extend ActiveSupport::Concern
7
+
8
+ include Auxilium::Concerns::MetadataScoped
9
+
10
+ included do
11
+ scope :visible, -> { instance_exec(&Nuntius.config.visible_scope) }
12
+ before_save :add_metadata
13
+ end
14
+
15
+ private
16
+
17
+ def add_metadata
18
+ self.metadata ||= {}
19
+ unless Nuntius.config.metadata_fields.empty?
20
+ Nuntius.config.metadata_fields.each do |field, data|
21
+ if data[:current]
22
+ current = data[:current]
23
+ self.metadata[field.to_s] ||= instance_exec(&current)
24
+ end
25
+ end
26
+ end
27
+ instance_exec(&Nuntius.config.add_metadata)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ module Concerns
5
+ module Yamlify
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def yamlify(attr)
10
+ define_method(:"#{attr}_yaml=") do |yaml|
11
+ write_attribute attr, YAML.safe_load(yaml.gsub("\t", ' '))
12
+ end
13
+
14
+ define_method(:"#{attr}_yaml") do
15
+ return '' if attributes[attr.to_s].blank?
16
+
17
+ YAML.dump(attributes[attr.to_s])
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ module Nuntius
2
+ class InboundMessage < ApplicationRecord
3
+ has_one_attached :raw_message, service: Nuntius.config.active_storage_service
4
+ has_many_attached :attachments, service: Nuntius.config.active_storage_service
5
+
6
+ def mail
7
+ @mail ||= Mail.from_source(source)
8
+ end
9
+
10
+ def source
11
+ @source ||= raw_message.download
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class Layout < ApplicationRecord
5
+ include Nuntius::Concerns::MetadataScoped
6
+ include Nuntius::Concerns::Yamlify
7
+
8
+ has_many_attached :attachments, service: Nuntius.config.active_storage_service
9
+ has_many :templates, dependent: :restrict_with_error
10
+
11
+ yamlify :metadata
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class List < ApplicationRecord
5
+ include Nuntius::Concerns::MetadataScoped
6
+
7
+ has_many :subscribers, counter_cache: :subscribers_count
8
+ accepts_nested_attributes_for :subscribers, reject_if: :all_blank
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class Locale < ApplicationRecord
5
+ include Nuntius::Concerns::MetadataScoped
6
+ include Nuntius::Concerns::Yamlify
7
+
8
+ yamlify :data
9
+ yamlify :metadata
10
+ end
11
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ # Stores individual messages to individual recipients
5
+ #
6
+ #
7
+ # Nuntius will have messages in states:
8
+ # pending - nothing done yet
9
+ # sent - we've sent it on to the provider
10
+ # delivered - have delivery confirmation
11
+ # undelivered - have confirmation of non-delivery
12
+ # Not all transports may provide all states
13
+ class Message < ApplicationRecord
14
+ include Nuntius::Concerns::MetadataScoped
15
+
16
+ has_and_belongs_to_many :attachments, class_name: 'Attachment'
17
+
18
+ belongs_to :campaign, optional: true
19
+ belongs_to :template, optional: true
20
+ belongs_to :parent_message, class_name: 'Message', optional: true
21
+ has_many :child_messages, class_name: 'Message', foreign_key: 'parent_message_id', dependent: :destroy
22
+ belongs_to :nuntiable, polymorphic: true, optional: true
23
+
24
+ validates :transport, presence: true
25
+
26
+ before_destroy :cleanup_attachments
27
+
28
+ # Weird loading sequence error, is fixed by the lib/nuntius/helpers
29
+ # begin
30
+ # has_many_attached :attachments
31
+ # rescue NoMethodError
32
+ # end
33
+
34
+ def pending?
35
+ status == 'pending'
36
+ end
37
+
38
+ def sent?
39
+ status == 'sent'
40
+ end
41
+
42
+ def blocked?
43
+ status == 'blocked'
44
+ end
45
+
46
+ def delivered?
47
+ status == 'delivered'
48
+ end
49
+
50
+ def delivered_or_blocked?
51
+ delivered? || blocked?
52
+ end
53
+
54
+ def undelivered?
55
+ status == 'undelivered'
56
+ end
57
+
58
+ # Removes only pending child messages
59
+ def cleanup!
60
+ Nuntius::Message.where(status: 'pending').where(parent_message: self).destroy_all
61
+ end
62
+
63
+ def add_attachment(options)
64
+ attachment = {}
65
+
66
+ uri = options[:url] && URI.parse(options[:url])
67
+
68
+ if uri&.scheme == 'file'
69
+ attachment[:io] = File.open(uri.path)
70
+ elsif uri
71
+ client = HTTPClient.new
72
+ client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
73
+ client.ssl_config.set_default_paths unless Gem.win_platform?
74
+ response = client.get(options[:url], follow_redirect: true)
75
+ content_disposition = response.headers['Content-Disposition'] || ''
76
+ options[:filename] ||= content_disposition[/filename="([^"]+)"/, 1]
77
+ attachment[:content_type] = response.content_type
78
+ attachment[:io] = if response.body.is_a? String
79
+ StringIO.new(response.body)
80
+ else
81
+ # Assume IO object
82
+ response.body
83
+ end
84
+ elsif options[:content].respond_to?(:read)
85
+ attachment[:content_type] = options[:content_type]
86
+ attachment[:io] = options[:content]
87
+ else
88
+ raise 'Cannot add attachment without url or content'
89
+ end
90
+
91
+ # Set the filename
92
+ attachment[:filename] = options[:filename] || uri.path.split('/').last || 'attachment'
93
+
94
+ # (Try to) add file extension if it is missing
95
+ file_extension = File.extname(attachment[:filename]).delete('.')
96
+ attachment[:filename] += ".#{Mime::Type.lookup(attachment[:content_type].split(';').first).to_sym}" if file_extension.blank? && attachment[:content_type]
97
+
98
+ # Fix content type if file extension known but content type blank
99
+ attachment[:content_type] ||= Mime::Type.lookup_by_extension(file_extension)&.to_s if file_extension
100
+
101
+ if options[:auto_zip] && attachment[:io].size > 1024 * 1024
102
+ zip_stream = Zip::OutputStream.write_buffer do |zio|
103
+ zio.put_next_entry attachment[:file_name]
104
+ zio.write attachment[:io].read
105
+ end
106
+ attachment[:content_type] = 'application/zip'
107
+ attachment[:io] = zip_stream
108
+ end
109
+
110
+ nuntius_attachment = Nuntius::Attachment.new
111
+ nuntius_attachment.content.attach(io: attachment[:io],
112
+ filename: attachment[:filename],
113
+ content_type: attachment[:content_type])
114
+
115
+ attachments.push(nuntius_attachment)
116
+ rescue StandardError => e
117
+ Nuntius.config.logger.error "Message: Could not attach #{attachment[:filename]} #{e.message}"
118
+ end
119
+
120
+ def cleanup_attachments
121
+ attachments.each do |attachment|
122
+ attachment.destroy if attachment.messages.where.not(id: id).blank?
123
+ end
124
+ end
125
+
126
+ def nuntius_provider(message)
127
+ klass = Nuntius::BaseProvider.class_from_name(provider, transport)
128
+ klass ||= Nuntius::BaseProvider
129
+ klass.new(message)
130
+ end
131
+
132
+ def resend
133
+ return if pending?
134
+ return unless transport
135
+
136
+ deliver_as(transport)
137
+ end
138
+
139
+ #
140
+ # Convenience method to easily send messages without having a template
141
+ #
142
+ def deliver_as(transport)
143
+ klass = BaseTransport.class_from_name(transport).new
144
+ klass.deliver(self)
145
+ self
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class Subscriber < ApplicationRecord
5
+ belongs_to :list
6
+ belongs_to :nuntiable, polymorphic: true, optional: true
7
+
8
+ def name
9
+ [first_name, last_name].join(' ')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nuntius
4
+ class Template < ApplicationRecord
5
+ include Nuntius::Concerns::MetadataScoped
6
+ include Nuntius::Concerns::Yamlify
7
+
8
+ belongs_to :layout, optional: true
9
+ has_many :messages, class_name: 'Nuntius::Message', dependent: :nullify
10
+
11
+ LIQUID_TAGS = /{%(?:(?!%}).)*%}|{{(?:(?!}}).)*}}/.freeze
12
+
13
+ validates :description, presence: true
14
+ validates :from, liquid: true
15
+ validates :to, liquid: true
16
+ validates :subject, liquid: true
17
+ validates :html, liquid: true
18
+ validates :text, liquid: true
19
+
20
+ validates :event, presence: true
21
+ validates :event, format: { with: /.+#.+/ }, if: ->(t) { t.klass == 'Custom' }
22
+ validates :interval, format: { allow_blank: true, with: /\A[1-9][0-9]*\s(month|week|day|hour|minute)s?\z/ }
23
+
24
+ yamlify :metadata
25
+
26
+ def new_message(object, assigns = {}, params = {})
27
+ message = Nuntius::Message.new(template: self, transport: transport, metadata: metadata)
28
+ message.nuntiable = object unless object.is_a? Hash
29
+
30
+ locale_proc = Nuntius::BaseMessenger.messenger_for_obj(object).locale
31
+ locale = instance_exec(object, &locale_proc) if locale_proc
32
+ locale = params[:locale].to_sym if params[:locale]
33
+
34
+ options = { registers: { 'template' => self, 'message' => message } }
35
+
36
+ message.to = render(:to, assigns, locale, options).split(',').reject(&:empty?).join(',')
37
+ message.from = render(:from, assigns, locale, options).split(',').reject(&:empty?).join(',')
38
+ message.subject = render(:subject, assigns, locale, options)
39
+ message.html = render(:html, assigns, locale, options.merge(layout: layout&.data))
40
+ message.text = render(:text, assigns, locale, options)
41
+ message.payload = render(:payload, assigns, locale, options)
42
+
43
+ message
44
+ end
45
+
46
+ def translation_scope
47
+ scope = %w[]
48
+ scope << layout.name.underscore.tr(' ', '_') if layout
49
+ scope << klass.underscore.tr('/', '_')
50
+ scope << event
51
+ scope << transport
52
+ scope << description.underscore.gsub(/[^a-z]+/, '_') if description
53
+ scope.join('.')
54
+ end
55
+
56
+ # Trix correctly escapes the HTML, but for liquid this is not what we need.
57
+ # This replaces html-entities within the liquid tags ({%...%} and {{...}})
58
+ def html=(html)
59
+ html_unescaped_liquid = html.gsub(LIQUID_TAGS) do |m|
60
+ CGI.unescape_html(m)
61
+ end
62
+ write_attribute :html, html_unescaped_liquid if html
63
+ end
64
+
65
+ def interval_duration
66
+ unless interval.blank?
67
+ number, type = interval.split(' ')
68
+ number = number.to_i
69
+
70
+ return number.public_send(type) if number.respond_to?(type)
71
+ end
72
+
73
+ 0.seconds
74
+ end
75
+
76
+ def interval_time_range
77
+ return 0.seconds..0.seconds if interval.blank?
78
+
79
+ start = if event.start_with?('before')
80
+ interval_duration.after
81
+ else
82
+ interval_duration.ago
83
+ end
84
+
85
+ (start - 1.hour)..start
86
+ end
87
+
88
+ private
89
+
90
+ def render(attr, assigns, locale, options = {})
91
+ I18n.with_locale(locale) do
92
+ if attr == :payload
93
+ YAML.safe_load(::Liquidum.render(send(attr), { assigns: assigns.merge('template' => self), registers: { 'template' => self } }.merge(options)))
94
+ elsif attr == :html
95
+ ::Liquidum.render(send(attr), { filter: 'markdown', assigns: assigns.merge('template' => self), registers: { 'template' => self } }.merge(options))
96
+ else
97
+ ::Liquidum.render(send(attr), { assigns: assigns.merge('template' => self), registers: { 'template' => self } }.merge(options))
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end