nuntius 0.1.1 → 1.0.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +199 -63
- data/Rakefile +62 -1
- data/app/controllers/nuntius/admin/campaigns_controller.rb +56 -0
- data/app/controllers/nuntius/admin/layouts/attachments_controller.rb +32 -0
- data/app/controllers/nuntius/admin/layouts_controller.rb +50 -0
- data/app/controllers/nuntius/admin/lists/subscribers_controller.rb +50 -0
- data/app/controllers/nuntius/admin/lists_controller.rb +46 -0
- data/app/controllers/nuntius/admin/locales_controller.rb +46 -0
- data/app/controllers/nuntius/admin/messages_controller.rb +24 -0
- data/app/controllers/nuntius/admin/templates_controller.rb +56 -0
- data/app/controllers/nuntius/api/events_controller.rb +16 -0
- data/app/controllers/nuntius/application_admin_controller.rb +11 -0
- data/app/controllers/nuntius/application_controller.rb +12 -0
- data/app/controllers/nuntius/callbacks_controller.rb +21 -0
- data/app/controllers/nuntius/dashboard_controller.rb +12 -0
- data/app/controllers/nuntius/feedback_controller.rb +17 -0
- data/app/controllers/nuntius/inbound_messages/twilio_inbound_smses_controller.rb +56 -0
- data/app/controllers/nuntius/messages_controller.rb +13 -0
- data/app/drops/nuntius/application_drop.rb +17 -0
- data/app/drops/nuntius/campaign_drop.rb +7 -0
- data/app/drops/nuntius/layout_drop.rb +11 -0
- data/app/drops/nuntius/message_drop.rb +11 -0
- data/app/drops/nuntius/template_drop.rb +7 -0
- data/app/exceptions/nuntius/base_exception.rb +4 -0
- data/app/exceptions/nuntius/missing_messenger_exception.rb +4 -0
- data/app/helpers/nuntius/application_helper.rb +78 -0
- data/app/jobs/nuntius/application_job.rb +9 -0
- data/app/jobs/nuntius/deliver_inbound_message_job.rb +10 -0
- data/app/jobs/nuntius/messenger_job.rb +17 -0
- data/app/jobs/nuntius/process_inbound_message_job.rb +10 -0
- data/app/jobs/nuntius/purge_message_job.rb +13 -0
- data/app/jobs/nuntius/retrieve_mail_job.rb +18 -0
- data/app/jobs/nuntius/transport_delivery_job.rb +32 -0
- data/app/jobs/nuntius/transport_refresh_job.rb +22 -0
- data/app/message_boxes/nuntius/base_message_box.rb +75 -0
- data/app/messengers/nuntius/base_messenger.rb +193 -0
- data/app/messengers/nuntius/custom_messenger.rb +6 -0
- data/app/models/nuntius/application_record.rb +11 -0
- data/app/models/nuntius/attachment.rb +13 -0
- data/app/models/nuntius/campaign.rb +75 -0
- data/app/models/nuntius/concerns/metadata_scoped.rb +31 -0
- data/app/models/nuntius/concerns/yamlify.rb +23 -0
- data/app/models/nuntius/inbound_message.rb +14 -0
- data/app/models/nuntius/layout.rb +13 -0
- data/app/models/nuntius/list.rb +10 -0
- data/app/models/nuntius/locale.rb +11 -0
- data/app/models/nuntius/message.rb +148 -0
- data/app/models/nuntius/subscriber.rb +12 -0
- data/app/models/nuntius/template.rb +102 -0
- data/app/presenters/application_presenter.rb +28 -0
- data/app/presenters/template_presenter.rb +17 -0
- data/app/providers/nuntius/apnotic_push_provider.rb +44 -0
- data/app/providers/nuntius/base_provider.rb +72 -0
- data/app/providers/nuntius/firebase_push_provider.rb +26 -0
- data/app/providers/nuntius/houston_push_provider.rb +40 -0
- data/app/providers/nuntius/message_bird_sms_provider.rb +39 -0
- data/app/providers/nuntius/slack_slack_provider.rb +36 -0
- data/app/providers/nuntius/smtp_mail_provider.rb +80 -0
- data/app/providers/nuntius/twilio_sms_provider.rb +37 -0
- data/app/providers/nuntius/twilio_voice_provider.rb +70 -0
- data/app/reportlets/nuntius/application_reportlet.rb +35 -0
- data/app/reportlets/nuntius/daily_messages_per_template_reportlet.rb +50 -0
- data/app/runners/nuntius/application_runner.rb +6 -0
- data/app/runners/nuntius/basic_application_runner.rb +15 -0
- data/app/runners/nuntius/timebased_events_runner.rb +15 -0
- data/app/services/nuntius/application_service.rb +7 -0
- data/app/services/nuntius/aws_sns_processor_service.rb +81 -0
- data/app/services/nuntius/deliver_inbound_message_service.rb +14 -0
- data/app/services/nuntius/retrieve_inbound_mail_service.rb +36 -0
- data/app/tables/nuntius_campaigns_table.rb +21 -0
- data/app/tables/nuntius_layouts_table.rb +24 -0
- data/app/tables/nuntius_lists_table.rb +19 -0
- data/app/tables/nuntius_locales_table.rb +18 -0
- data/app/tables/nuntius_messages_table.rb +36 -0
- data/app/tables/nuntius_subscribers_table.rb +19 -0
- data/app/tables/nuntius_templates_table.rb +30 -0
- data/app/transports/nuntius/base_transport.rb +36 -0
- data/app/transports/nuntius/mail_transport.rb +35 -0
- data/app/transports/nuntius/push_transport.rb +15 -0
- data/app/transports/nuntius/slack_transport.rb +6 -0
- data/app/transports/nuntius/sms_transport.rb +6 -0
- data/app/transports/nuntius/voice_transport.rb +6 -0
- data/app/validators/liquid_validator.rb +11 -0
- data/app/views/nuntius/admin/campaigns/edit.html.slim +49 -0
- data/app/views/nuntius/admin/campaigns/index.html.slim +2 -0
- data/app/views/nuntius/admin/layouts/attachments/_attachments.html.slim +28 -0
- data/app/views/nuntius/admin/layouts/attachments/_index.html.slim +2 -0
- data/app/views/nuntius/admin/layouts/attachments/create.json.jbuilder +5 -0
- data/app/views/nuntius/admin/layouts/edit.html.slim +24 -0
- data/app/views/nuntius/admin/layouts/index.html.slim +2 -0
- data/app/views/nuntius/admin/lists/edit.html.slim +14 -0
- data/app/views/nuntius/admin/lists/index.html.slim +2 -0
- data/app/views/nuntius/admin/lists/subscribers/edit.html.slim +14 -0
- data/app/views/nuntius/admin/locales/edit.html.slim +14 -0
- data/app/views/nuntius/admin/locales/index.html.slim +2 -0
- data/app/views/nuntius/admin/messages/index.html.slim +2 -0
- data/app/views/nuntius/admin/messages/show.html.slim +56 -0
- data/app/views/nuntius/admin/templates/edit.html.slim +81 -0
- data/app/views/nuntius/admin/templates/index.html.slim +2 -0
- data/app/views/nuntius/dashboard/show.html.slim +7 -0
- data/app/views/nuntius/messages/show.html.slim +7 -0
- data/config/locales/en.yml +22 -0
- data/config/locales/nl.yml +22 -0
- data/config/routes.rb +38 -0
- data/config/webpack/development.js +5 -0
- data/config/webpack/environment.js +4 -0
- data/config/webpack/production.js +5 -0
- data/config/webpack/test.js +5 -0
- data/config/webpacker.yml +119 -0
- data/db/migrate/20190301201541_create_nuntius_templates.rb +27 -0
- data/db/migrate/20190301202436_create_nuntius_messages.rb +30 -0
- data/db/migrate/20190322112815_create_nuntius_lists.rb +12 -0
- data/db/migrate/20190322114340_create_nuntius_campaigns.rb +18 -0
- data/db/migrate/20190322121338_create_nuntius_subscribers.rb +17 -0
- data/db/migrate/20190327123535_add_metadata_to_models.rb +8 -0
- data/db/migrate/20190327124407_create_nuntius_layouts.rb +24 -0
- data/db/migrate/20190327143921_add_campaign_to_message.rb +7 -0
- data/db/migrate/20190327155112_add_state_to_campaign.rb +7 -0
- data/db/migrate/20190412103335_add_enabled_to_templates.rb +7 -0
- data/db/migrate/20190417125153_change_message_status_default.rb +9 -0
- data/db/migrate/20190417144554_add_nuntiable_to_subscriber.rb +7 -0
- data/db/migrate/20190521135011_add_payload_to_nuntius_messages.rb +7 -0
- data/db/migrate/20190522122657_add_metadata_to_nuntius_message.rb +7 -0
- data/db/migrate/20190825080757_update_nuntius_message_sending_to_sent.rb +11 -0
- data/db/migrate/20200220154927_change_nuntius_templates_payload_type.rb +7 -0
- data/db/migrate/20200224132337_create_nuntius_locales.rb +13 -0
- data/db/migrate/20200318095339_create_nuntius_attachments.rb +12 -0
- data/db/migrate/20200407050646_add_interval_to_nuntius_template.rb +5 -0
- data/db/migrate/20200430131219_make_html_processing_optional.rb +6 -0
- data/db/migrate/20200430154032_make_html_processing_optional_revert.rb +6 -0
- data/db/migrate/20201121185718_create_nuntius_inbound_messages.rb +21 -0
- data/db/migrate/20220412114148_add_last_sent_to_message.rb +5 -0
- data/lib/nuntius/active_record_helpers.rb +28 -0
- data/lib/nuntius/active_storage_helpers.rb +8 -0
- data/lib/nuntius/configuration.rb +97 -0
- data/lib/nuntius/deprecator.rb +8 -0
- data/lib/nuntius/devise.rb +19 -0
- data/lib/nuntius/engine.rb +46 -0
- data/lib/nuntius/i18n_store.rb +100 -0
- data/lib/nuntius/liquid/tags/attach_tag.rb +44 -0
- data/lib/nuntius/mail_allow_list.rb +23 -0
- data/lib/nuntius/nuntiable.rb +24 -0
- data/lib/nuntius/state_machine.rb +19 -0
- data/lib/nuntius/transactio.rb +30 -0
- data/lib/nuntius/version.rb +3 -1
- data/lib/nuntius.rb +74 -8
- data/lib/preamble.rb +87 -0
- data/lib/tasks/nuntius_tasks.rake +27 -0
- metadata +532 -67
- data/.gitignore +0 -4
- data/Gemfile +0 -4
- data/lib/nuntius/encodings/url_safe_base64.rb +0 -26
- data/lib/nuntius/encodings.rb +0 -9
- data/lib/nuntius/envelope.rb +0 -45
- data/lib/nuntius/key.rb +0 -47
- data/lib/nuntius/messenger.rb +0 -37
- data/nuntius.gemspec +0 -23
- data/spec/keys/alice.pem +0 -27
- data/spec/keys/alice.pub +0 -9
- data/spec/keys/bob.pem +0 -27
- data/spec/keys/bob.pub +0 -9
- data/spec/nuntius/encodings/url_safe_base64_spec.rb +0 -36
- data/spec/nuntius/envelope_spec.rb +0 -6
- data/spec/nuntius/key_spec.rb +0 -12
- data/spec/nuntius/messenger_spec.rb +0 -26
- data/spec/spec_helper.rb +0 -9
- data/spec/support/keys.rb +0 -3
@@ -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,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(¤t)
|
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,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,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
|