caffeinate 0.1.4 → 0.5.0
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/README.md +58 -43
- data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +17 -2
- data/app/models/caffeinate/campaign.rb +40 -1
- data/app/models/caffeinate/campaign_subscription.rb +52 -11
- data/app/models/caffeinate/mailing.rb +26 -3
- data/app/views/caffeinate/campaign_subscriptions/subscribe.html.erb +3 -0
- data/app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb +3 -0
- data/app/views/layouts/_caffeinate.html.erb +11 -0
- data/config/locales/en.yml +6 -0
- data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +1 -0
- data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +3 -0
- data/db/migrate/20201124183419_create_caffeinate_mailings.rb +4 -1
- data/lib/caffeinate.rb +2 -0
- data/lib/caffeinate/action_mailer.rb +4 -4
- data/lib/caffeinate/action_mailer/extension.rb +17 -1
- data/lib/caffeinate/action_mailer/interceptor.rb +4 -2
- data/lib/caffeinate/action_mailer/observer.rb +5 -4
- data/lib/caffeinate/active_record/extension.rb +3 -2
- data/lib/caffeinate/configuration.rb +4 -1
- data/lib/caffeinate/drip.rb +22 -35
- data/lib/caffeinate/drip_evaluator.rb +35 -0
- data/lib/caffeinate/dripper/base.rb +13 -14
- data/lib/caffeinate/dripper/batching.rb +22 -0
- data/lib/caffeinate/dripper/callbacks.rb +74 -6
- data/lib/caffeinate/dripper/campaign.rb +8 -9
- data/lib/caffeinate/dripper/defaults.rb +4 -2
- data/lib/caffeinate/dripper/delivery.rb +8 -8
- data/lib/caffeinate/dripper/drip.rb +46 -16
- data/lib/caffeinate/dripper/inferences.rb +29 -0
- data/lib/caffeinate/dripper/perform.rb +16 -5
- data/lib/caffeinate/dripper/periodical.rb +24 -0
- data/lib/caffeinate/engine.rb +12 -9
- data/lib/caffeinate/helpers.rb +24 -0
- data/lib/caffeinate/mail_ext.rb +12 -0
- data/lib/caffeinate/url_helpers.rb +10 -0
- data/lib/caffeinate/version.rb +1 -1
- data/lib/generators/caffeinate/install_generator.rb +5 -1
- data/lib/generators/caffeinate/templates/caffeinate.rb +11 -1
- metadata +13 -5
- data/app/views/layouts/caffeinate/application.html.erb +0 -15
- data/app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb +0 -1
- data/lib/caffeinate/action_mailer/helpers.rb +0 -12
@@ -12,7 +12,10 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
|
|
12
12
|
t.string :user_id
|
13
13
|
t.string :token, null: false
|
14
14
|
t.datetime :ended_at
|
15
|
+
t.string :ended_reason
|
16
|
+
t.datetime :resubscribed_at
|
15
17
|
t.datetime :unsubscribed_at
|
18
|
+
t.string :unsubscribe_reason
|
16
19
|
|
17
20
|
t.timestamps
|
18
21
|
end
|
@@ -14,6 +14,9 @@ class CreateCaffeinateMailings < ActiveRecord::Migration[6.0]
|
|
14
14
|
|
15
15
|
t.timestamps
|
16
16
|
end
|
17
|
-
add_index :caffeinate_mailings,
|
17
|
+
add_index :caffeinate_mailings, :sent_at
|
18
|
+
add_index :caffeinate_mailings, :send_at
|
19
|
+
add_index :caffeinate_mailings, :skipped_at
|
20
|
+
add_index :caffeinate_mailings, %i[caffeinate_campaign_subscription_id mailer_class mailer_action], name: :index_caffeinate_mailings
|
18
21
|
end
|
19
22
|
end
|
data/lib/caffeinate.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rails/all'
|
4
|
+
require 'caffeinate/mail_ext'
|
4
5
|
require 'caffeinate/engine'
|
5
6
|
require 'caffeinate/drip'
|
7
|
+
require 'caffeinate/url_helpers'
|
6
8
|
require 'caffeinate/configuration'
|
7
9
|
require 'caffeinate/dripper/base'
|
8
10
|
require 'caffeinate/deliver_async'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
|
5
|
-
|
6
|
-
require
|
3
|
+
require 'mail'
|
4
|
+
|
5
|
+
# Includes all files in `caffeinate/action_mailer`
|
6
|
+
Dir["#{__dir__}/action_mailer/*"].each { |path| require "caffeinate/action_mailer/#{File.basename(path)}" }
|
@@ -2,11 +2,27 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
module ActionMailer
|
5
|
+
# Convenience for setting `@mailing`, and convenience methods for inferred `caffeinate_unsubscribe_url` and
|
6
|
+
# `caffeinate_subscribe_url`.
|
5
7
|
module Extension
|
6
8
|
def self.included(klass)
|
7
9
|
klass.before_action do
|
8
|
-
@mailing =
|
10
|
+
@mailing = params[:mailing] if params
|
9
11
|
end
|
12
|
+
|
13
|
+
klass.helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
|
14
|
+
end
|
15
|
+
|
16
|
+
# Assumes `@mailing` is set
|
17
|
+
def caffeinate_unsubscribe_url(mailing: nil, **options)
|
18
|
+
mailing ||= @mailing
|
19
|
+
Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(mailing.caffeinate_campaign_subscription, **options)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Assumes `@mailing` is set
|
23
|
+
def caffeinate_subscribe_url(mailing: nil, **options)
|
24
|
+
mailing ||= @mailing
|
25
|
+
Caffeinate::UrlHelpers.caffeinate_subscribe_url(mailing.caffeinate_campaign_subscription, **options)
|
10
26
|
end
|
11
27
|
end
|
12
28
|
end
|
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
module ActionMailer
|
5
|
+
# Handles the evaluation of a drip against a mailing to determine if it ultimately gets delivered.
|
6
|
+
# Also invokes the `before_send` callbacks.
|
5
7
|
class Interceptor
|
6
8
|
# Handles `before_send` callbacks for a `Caffeinate::Dripper`
|
7
9
|
def self.delivering_email(message)
|
8
|
-
mailing =
|
10
|
+
mailing = message.caffeinate_mailing
|
9
11
|
return unless mailing
|
10
12
|
|
11
|
-
mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing
|
13
|
+
mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing, message)
|
12
14
|
drip = mailing.drip
|
13
15
|
message.perform_deliveries = drip.enabled?(mailing)
|
14
16
|
end
|
@@ -2,15 +2,16 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
module ActionMailer
|
5
|
-
# Handles updating the Caffeinate::Message if it's available in
|
5
|
+
# Handles updating the Caffeinate::Message if it's available in Mail::Message.caffeinate_mailing
|
6
6
|
# and runs any associated callbacks
|
7
7
|
class Observer
|
8
8
|
def self.delivered_email(message)
|
9
|
-
mailing =
|
9
|
+
mailing = message.caffeinate_mailing
|
10
10
|
return unless mailing
|
11
11
|
|
12
|
-
mailing.update!(sent_at: Caffeinate.config.time_now) if message.perform_deliveries
|
13
|
-
mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing
|
12
|
+
mailing.update!(sent_at: Caffeinate.config.time_now, skipped_at: nil) if message.perform_deliveries
|
13
|
+
mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing, message)
|
14
|
+
true
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
@@ -2,14 +2,15 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
module ActiveRecord
|
5
|
+
# Includes the ActiveRecord association and relevant scopes for an ActiveRecord-backed model
|
5
6
|
module Extension
|
6
7
|
# Adds the associations for a subscriber
|
7
8
|
def caffeinate_subscriber
|
8
|
-
has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription'
|
9
|
+
has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription', dependent: :destroy
|
9
10
|
has_many :caffeinate_campaigns, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Campaign'
|
10
11
|
has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Mailing'
|
11
12
|
|
12
|
-
scope :
|
13
|
+
scope :not_subscribed_to_campaign, lambda { |list|
|
13
14
|
subscribed = ::Caffeinate::CampaignSubscription.select(:subscriber_id).joins(:caffeinate_campaign).where(caffeinate_campaigns: { slug: list }, subscriber_type: name)
|
14
15
|
where.not(id: subscribed)
|
15
16
|
}
|
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Caffeinate
|
4
|
+
# Global configuration
|
4
5
|
class Configuration
|
5
|
-
attr_accessor :now, :async_delivery, :mailing_job
|
6
|
+
attr_accessor :now, :async_delivery, :mailing_job, :batch_size, :drippers_path
|
6
7
|
|
7
8
|
def initialize
|
8
9
|
@now = -> { Time.current }
|
9
10
|
@async_delivery = false
|
10
11
|
@mailing_job = nil
|
12
|
+
@batch_size = 1_000
|
13
|
+
@drippers_path = "app/drippers"
|
11
14
|
end
|
12
15
|
|
13
16
|
def now=(val)
|
data/lib/caffeinate/drip.rb
CHANGED
@@ -1,40 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'caffeinate/drip_evaluator'
|
3
4
|
module Caffeinate
|
4
5
|
# A Drip object
|
6
|
+
#
|
7
|
+
# Handles the block and provides convenience methods for the drip
|
5
8
|
class Drip
|
6
|
-
# Handles the block and provides convenience methods for the drip
|
7
|
-
class Evaluator
|
8
|
-
attr_reader :mailing
|
9
|
-
def initialize(mailing)
|
10
|
-
@mailing = mailing
|
11
|
-
end
|
12
|
-
|
13
|
-
def call(&block)
|
14
|
-
return true unless block
|
15
|
-
|
16
|
-
instance_eval(&block)
|
17
|
-
end
|
18
|
-
|
19
|
-
# Ends the CampaignSubscription
|
20
|
-
def end!
|
21
|
-
mailing.caffeinate_campaign_subscription.end!
|
22
|
-
false
|
23
|
-
end
|
24
|
-
|
25
|
-
# Unsubscribes the CampaignSubscription
|
26
|
-
def unsubscribe!
|
27
|
-
mailing.caffeinate_campaign_subscription.unsubscribe!
|
28
|
-
false
|
29
|
-
end
|
30
|
-
|
31
|
-
# Skips the mailing
|
32
|
-
def skip!
|
33
|
-
mailing.skip!
|
34
|
-
false
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
9
|
attr_reader :dripper, :action, :options, :block
|
39
10
|
def initialize(dripper, action, options, &block)
|
40
11
|
@dripper = dripper
|
@@ -43,14 +14,30 @@ module Caffeinate
|
|
43
14
|
@block = block
|
44
15
|
end
|
45
16
|
|
46
|
-
# If the associated ActionMailer uses `ActionMailer::Parameterized` initialization
|
17
|
+
# If the associated ActionMailer uses `ActionMailer::Parameterized` initialization instead of argument-based initialization
|
47
18
|
def parameterized?
|
48
19
|
options[:using] == :parameterized
|
49
20
|
end
|
50
21
|
|
51
|
-
|
22
|
+
def send_at(mailing = nil)
|
23
|
+
if periodical?
|
24
|
+
start = mailing.instance_exec(&options[:start])
|
25
|
+
if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count > 0
|
26
|
+
start += options[:every]
|
27
|
+
end
|
28
|
+
start.from_now
|
29
|
+
else
|
30
|
+
options[:delay].from_now
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def periodical?
|
35
|
+
options[:every].present?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Checks if the drip is enabled
|
52
39
|
def enabled?(mailing)
|
53
|
-
|
40
|
+
DripEvaluator.new(mailing).call(&@block)
|
54
41
|
end
|
55
42
|
end
|
56
43
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Caffeinate
|
4
|
+
# Handles evaluating the `drip` block and provides convenience methods for handling the mailing or its campaign.
|
5
|
+
class DripEvaluator
|
6
|
+
attr_reader :mailing
|
7
|
+
def initialize(mailing)
|
8
|
+
@mailing = mailing
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(&block)
|
12
|
+
return true unless block
|
13
|
+
|
14
|
+
instance_eval(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Ends the CampaignSubscription
|
18
|
+
def end!
|
19
|
+
mailing.caffeinate_campaign_subscription.end!
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Unsubscribes the CampaignSubscription
|
24
|
+
def unsubscribe!
|
25
|
+
mailing.caffeinate_campaign_subscription.unsubscribe!
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Skips the mailing
|
30
|
+
def skip!
|
31
|
+
mailing.skip!
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,33 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'caffeinate/dripper/
|
3
|
+
require 'caffeinate/dripper/batching'
|
4
4
|
require 'caffeinate/dripper/callbacks'
|
5
|
-
require 'caffeinate/dripper/defaults'
|
6
|
-
require 'caffeinate/dripper/subscriber'
|
7
5
|
require 'caffeinate/dripper/campaign'
|
8
|
-
require 'caffeinate/dripper/
|
6
|
+
require 'caffeinate/dripper/defaults'
|
9
7
|
require 'caffeinate/dripper/delivery'
|
8
|
+
require 'caffeinate/dripper/drip'
|
9
|
+
require 'caffeinate/dripper/inferences'
|
10
|
+
require 'caffeinate/dripper/perform'
|
11
|
+
require 'caffeinate/dripper/periodical'
|
12
|
+
require 'caffeinate/dripper/subscriber'
|
10
13
|
|
11
14
|
module Caffeinate
|
12
15
|
module Dripper
|
16
|
+
# Base class
|
13
17
|
class Base
|
18
|
+
include Batching
|
14
19
|
include Callbacks
|
15
20
|
include Campaign
|
16
21
|
include Defaults
|
17
22
|
include Delivery
|
18
23
|
include Drip
|
24
|
+
include Inferences
|
19
25
|
include Perform
|
26
|
+
include Periodical
|
20
27
|
include Subscriber
|
21
|
-
|
22
|
-
# The inferred mailer class
|
23
|
-
def self.inferred_mailer_class
|
24
|
-
klass_name = "#{name.delete_suffix('Dripper')}Mailer"
|
25
|
-
klass = klass_name.safe_constantize
|
26
|
-
return nil unless klass
|
27
|
-
return klass_name if klass < ::ActionMailer::Base
|
28
|
-
|
29
|
-
nil
|
30
|
-
end
|
31
28
|
end
|
32
29
|
end
|
33
30
|
end
|
31
|
+
|
32
|
+
ActiveSupport.run_load_hooks :caffeinate, Caffeinate::Dripper::Base
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Caffeinate
|
4
|
+
module Dripper
|
5
|
+
# Includes batch support for setting the batch size for Perform
|
6
|
+
module Batching
|
7
|
+
def self.included(klass)
|
8
|
+
klass.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def batch_size=(num)
|
13
|
+
@batch_size = num
|
14
|
+
end
|
15
|
+
|
16
|
+
def batch_size
|
17
|
+
@batch_size || ::Caffeinate.config.batch_size
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -2,12 +2,17 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
module Dripper
|
5
|
+
# Callbacks for a Dripper.
|
5
6
|
module Callbacks
|
6
7
|
# :nodoc:
|
7
8
|
def self.included(klass)
|
8
9
|
klass.extend ClassMethods
|
9
10
|
end
|
10
11
|
|
12
|
+
def run_callbacks(name, *args)
|
13
|
+
self.class.run_callbacks(name, *args)
|
14
|
+
end
|
15
|
+
|
11
16
|
module ClassMethods
|
12
17
|
# :nodoc:
|
13
18
|
def run_callbacks(name, *args)
|
@@ -33,15 +38,64 @@ module Caffeinate
|
|
33
38
|
@on_subscribe_blocks ||= []
|
34
39
|
end
|
35
40
|
|
41
|
+
# Callback before the mailings get processed.
|
42
|
+
#
|
43
|
+
# before_perform do |dripper|
|
44
|
+
# Slack.notify(:caffeinate, "Dripper is getting ready for mailing! #{dripper.caffeinate_campaign.name}!")
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @yield Caffeinate::Dripper
|
48
|
+
def before_perform(&block)
|
49
|
+
before_perform_blocks << block
|
50
|
+
end
|
51
|
+
|
52
|
+
# :nodoc:
|
53
|
+
def before_perform_blocks
|
54
|
+
@before_perform_blocks ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
# Callback before the mailings get processed in a batch.
|
58
|
+
#
|
59
|
+
# after_process do |dripper, mailings|
|
60
|
+
# Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# @yield Caffeinate::Dripper
|
64
|
+
# @yield Caffeinate::Mailing [Array]
|
65
|
+
def on_perform(&block)
|
66
|
+
on_perform_blocks << block
|
67
|
+
end
|
68
|
+
|
69
|
+
# :nodoc:
|
70
|
+
def on_perform_blocks
|
71
|
+
@on_perform_blocks ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
# Callback after the all the mailings have been sent.
|
75
|
+
#
|
76
|
+
# after_process do |dripper|
|
77
|
+
# Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @yield Caffeinate::Dripper
|
81
|
+
# @yield Caffeinate::Mailing [Array]
|
82
|
+
def after_perform(&block)
|
83
|
+
after_perform_blocks << block
|
84
|
+
end
|
85
|
+
|
86
|
+
# :nodoc:
|
87
|
+
def after_perform_blocks
|
88
|
+
@after_perform_blocks ||= []
|
89
|
+
end
|
90
|
+
|
36
91
|
# Callback before a Drip has called the mailer.
|
37
92
|
#
|
38
93
|
# before_drip do |campaign_subscription, mailing, drip|
|
39
94
|
# Slack.notify(:caffeinate, "#{drip.action_name} is starting")
|
40
95
|
# end
|
41
96
|
#
|
42
|
-
# @yield Caffeinate::CampaignSubscription
|
43
|
-
# @yield Caffeinate::Mailing
|
44
97
|
# @yield Caffeinate::Drip current drip
|
98
|
+
# @yield Caffeinate::Mailing
|
45
99
|
def before_drip(&block)
|
46
100
|
before_drip_blocks << block
|
47
101
|
end
|
@@ -57,7 +111,6 @@ module Caffeinate
|
|
57
111
|
# Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
|
58
112
|
# end
|
59
113
|
#
|
60
|
-
# @yield Caffeinate::CampaignSubscription
|
61
114
|
# @yield Caffeinate::Mailing
|
62
115
|
# @yield Mail::Message
|
63
116
|
def before_send(&block)
|
@@ -75,7 +128,6 @@ module Caffeinate
|
|
75
128
|
# Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
|
76
129
|
# end
|
77
130
|
#
|
78
|
-
# @yield Caffeinate::CampaignSubscription
|
79
131
|
# @yield Caffeinate::Mailing
|
80
132
|
# @yield Mail::Message
|
81
133
|
def after_send(&block)
|
@@ -119,14 +171,30 @@ module Caffeinate
|
|
119
171
|
@on_unsubscribe_blocks ||= []
|
120
172
|
end
|
121
173
|
|
174
|
+
|
175
|
+
# Callback after a CampaignSubscriber has ended.
|
176
|
+
#
|
177
|
+
# on_end do |campaign_sub|
|
178
|
+
# Slack.notify(:caffeinate, "#{campaign_sub.id} has ended... sad day.")
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# @yield Caffeinate::CampaignSubscription
|
182
|
+
def on_end(&block)
|
183
|
+
on_end_blocks << block
|
184
|
+
end
|
185
|
+
|
186
|
+
# :nodoc:
|
187
|
+
def on_end_blocks
|
188
|
+
@on_end_blocks ||= []
|
189
|
+
end
|
190
|
+
|
122
191
|
# Callback after a `Caffeinate::Mailing` is skipped.
|
123
192
|
#
|
124
193
|
# on_skip do |campaign_subscription, mailing, message|
|
125
194
|
# Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
|
126
195
|
# end
|
127
196
|
#
|
128
|
-
# @yield Caffeinate::
|
129
|
-
# @yield Caffeinate::Mailing
|
197
|
+
# @yield `Caffeinate::Mailing`
|
130
198
|
def on_skip(&block)
|
131
199
|
on_skip_blocks << block
|
132
200
|
end
|