moist 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +22 -0
- data/app/controllers/moist/application_controller.rb +5 -0
- data/app/models/moist/application_record.rb +5 -0
- data/app/models/moist/campaign.rb +11 -0
- data/app/models/moist/campaign_subscriber.rb +23 -0
- data/app/models/moist/mailing.rb +11 -0
- data/app/models/moist/receipt.rb +6 -0
- data/app/views/layouts/moist/application.html.erb +15 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20201113220645_create_moist_moist_campaigns.rb +13 -0
- data/db/migrate/20201113220800_create_moist_moist_campaign_subscribers.rb +13 -0
- data/db/migrate/20201113220943_create_moist_mailings.rb +13 -0
- data/db/migrate/20201113220944_create_moist_moist_receipts.rb +12 -0
- data/lib/moist.rb +25 -0
- data/lib/moist/action_mailer/extension.rb +35 -0
- data/lib/moist/action_mailer/observer.rb +21 -0
- data/lib/moist/active_record/extension.rb +35 -0
- data/lib/moist/delivery.rb +15 -0
- data/lib/moist/engine.rb +14 -0
- data/lib/moist/errors.rb +5 -0
- data/lib/moist/mailer_registry.rb +20 -0
- data/lib/moist/models/campaign.rb +38 -0
- data/lib/moist/models/campaign_subscriber.rb +37 -0
- data/lib/moist/models/mailing.rb +19 -0
- data/lib/moist/registry_proxy.rb +8 -0
- data/lib/moist/scheduler.rb +23 -0
- data/lib/moist/steps/collection.rb +53 -0
- data/lib/moist/steps/registry.rb +20 -0
- data/lib/moist/steps/step.rb +16 -0
- data/lib/moist/subscription_manager.rb +25 -0
- data/lib/moist/version.rb +3 -0
- data/lib/tasks/moist_tasks.rake +0 -0
- metadata +153 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 92bb66edca74385939a1d6cb2328a48e78f806d8c03bfaa4c559895f7f5b1f5f
|
|
4
|
+
data.tar.gz: b40cc1b72132ca1e1de92db784ad12b836e95cc2f322b3cd652be92239fb165e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4e321d9898fcf22fceb0eb1a3d44efe3e425dbd88347b14e9ae3764a5c920148495bbfd65ba92281560244d85bc16cb0784c78c36aa9042828e0e96f3cc68a65
|
|
7
|
+
data.tar.gz: 975a0f7bceb6feb52fdc203b1cac0714535a286565c9dba313de83a6c7bc945a41b2aa7f22151292a31e8a2f8e9c96b6d777d1a5d53bf52b0e2460565eea1c8c
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2020 Josh Brody
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Moist
|
|
2
|
+
|
|
3
|
+
Drip engine, for Ruby on Rails.
|
|
4
|
+
|
|
5
|
+
## Why the name?
|
|
6
|
+
|
|
7
|
+
Because `drip` was taken and `moist` was available.
|
|
8
|
+
|
|
9
|
+
There is also a measurable amount of people who hate the word moist. Which results in a poor adoption strategy. In turn,
|
|
10
|
+
fewer stars. Conclusion: recruiters won't bother me?
|
|
11
|
+
|
|
12
|
+
Goals?
|
|
13
|
+
|
|
14
|
+
## Should you use this?
|
|
15
|
+
|
|
16
|
+
Maybe, here's the gist:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Create a campaign:
|
|
20
|
+
::Moist::Campaign.create(name: "Abandoned cart", slug: "cart")
|
|
21
|
+
|
|
22
|
+
# Add `moist` to mailer:
|
|
23
|
+
class AbandonedCartMailer < ActionMailer::Base
|
|
24
|
+
moist :you_forgot_something, campaign: :cart, step: 1, delay: 1.hour
|
|
25
|
+
def you_forgot_something(cart)
|
|
26
|
+
# ...
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
moist :selling_out_soon, campaign: :cart, step: 2, delay: 4.hours
|
|
30
|
+
def selling_out_soon(cart)
|
|
31
|
+
# ...
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Add to a drip campaign:
|
|
36
|
+
::Moist::Campaign.subscribe(cart, user: cart.user).to(:cart)
|
|
37
|
+
|
|
38
|
+
# Remove from a drip campaign:
|
|
39
|
+
::Moist::Campaign.unsubscribe(cart, user: cart.user).from(:cart)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
Add this line to your application's Gemfile:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
gem 'moist'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
And then execute:
|
|
50
|
+
```bash
|
|
51
|
+
$ bundle
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Bring in the initializer and migrations:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
$ rails g moist:setup
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
And do the migrate:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ rails db:migrate
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Setup Moist
|
|
67
|
+
|
|
68
|
+
How to use `Moist` to create an abandoned cart drip campaign.
|
|
69
|
+
|
|
70
|
+
#### 1. First, create a `Moist::Campaign`.
|
|
71
|
+
|
|
72
|
+
`Moist::Campaign.create(name: "Abandoned cart`, slug: "cart")`
|
|
73
|
+
|
|
74
|
+
The `slug` used here will be referenced later. Note it.
|
|
75
|
+
|
|
76
|
+
#### 2. Use `moist` in your mailer to create steps.
|
|
77
|
+
|
|
78
|
+
You'll need to define `@moist_subscriber` and `@moist_user` in each mailer. This tells Moist what `Moist::Mailing` to
|
|
79
|
+
associate with the mailer.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class AbandonedCartMailer < ActionMailer::Base
|
|
83
|
+
moist :you_forgot_something, campaign: :cart, step: 1, delay: 1.hour
|
|
84
|
+
def you_forgot_something(cart)
|
|
85
|
+
@cart = cart
|
|
86
|
+
@moist_subscriber = cart
|
|
87
|
+
@moist_user = cart.user
|
|
88
|
+
mail(subject: "You forgot something...", to: @cart.user.email)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
moist :free_shipping_if_you_order_now, campaign: :cart, step: 2, delay: 2.days
|
|
92
|
+
def free_shipping_if_you_order_now(cart)
|
|
93
|
+
@cart = cart
|
|
94
|
+
@moist_subscriber = cart
|
|
95
|
+
@moist_user = cart.user
|
|
96
|
+
mail(subject: "Free shipping? Yep. Complete your purchase right meow!", to: @cart.user.email)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
In these mailer methods, make sure you assign a `@moist_subscriber` and a `@moist_user`. If you don't, bad things will happen.
|
|
102
|
+
|
|
103
|
+
#### 3. Add `@cart` to the campaign
|
|
104
|
+
|
|
105
|
+
Probably a background job.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
class AbandonedCartJob < ApplicationJob
|
|
109
|
+
TIME_TO_LIVE = 1.hour
|
|
110
|
+
def perform
|
|
111
|
+
Cart.where(checked_out: false).where('updated_at < ?', TIME_TO_LIVE.ago).each do |cart|
|
|
112
|
+
::Moist::Campaign.subscribe(cart, user: cart.user).to(:cart)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### 4. Run the scheduler
|
|
119
|
+
|
|
120
|
+
This manages the mailers and the drips.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class MoistSchedulerJob < ApplicationJob
|
|
124
|
+
def perform
|
|
125
|
+
::Moist::Scheduler.run
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### 5. Done!
|
|
131
|
+
|
|
132
|
+
All done. Some nice-to-haves:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class Cart
|
|
136
|
+
has_moist_campaigns
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Gives you
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
@cart.moist_campaigns
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
class User
|
|
148
|
+
acts_as_moist_user
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Gives you:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
@user.moist_subscriptions
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Data models
|
|
159
|
+
|
|
160
|
+
Three core concepts:
|
|
161
|
+
|
|
162
|
+
### Campaign
|
|
163
|
+
|
|
164
|
+
This is just an object for reference.
|
|
165
|
+
|
|
166
|
+
### CampaignSubscriber
|
|
167
|
+
|
|
168
|
+
This joins a `Campaign` to a `subscriber` (polymorphic anything), and a `user`. In theory, a `user` can have many `moist_campaign_subscribers`,
|
|
169
|
+
which would relate it to objects that belong to a `user`.
|
|
170
|
+
|
|
171
|
+
This is useful if you have an `Order` object and a `Subscription` object that you want to create different campaigns for.
|
|
172
|
+
|
|
173
|
+
### Mailing
|
|
174
|
+
|
|
175
|
+
Takes your ActionMailer `moist` calls and turns them into database records. They have `send_at` and `sent_at` columns,
|
|
176
|
+
in addition to information about the mailers they belong to.
|
|
177
|
+
|
|
178
|
+
## Contributing
|
|
179
|
+
|
|
180
|
+
Just do it.
|
|
181
|
+
|
|
182
|
+
## Todo
|
|
183
|
+
|
|
184
|
+
* Logo, duh
|
|
185
|
+
* Web UI examples
|
|
186
|
+
* Ability to pause a campaign?
|
|
187
|
+
* Conversion stuff
|
|
188
|
+
* Handle updating steps
|
|
189
|
+
* Use block for handling `delay`
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'rdoc/task'
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
rdoc.title = 'Moist'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
|
18
|
+
load 'rails/tasks/engine.rake'
|
|
19
|
+
|
|
20
|
+
load 'rails/tasks/statistics.rake'
|
|
21
|
+
|
|
22
|
+
require 'bundler/gem_tasks'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class Campaign < ApplicationRecord
|
|
3
|
+
self.table_name = 'moist_campaigns'
|
|
4
|
+
include ::Moist::Models::Campaign
|
|
5
|
+
|
|
6
|
+
has_many :moist_campaign_subscribers, class_name: 'Moist::CampaignSubscriber', foreign_key: :moist_campaign_id
|
|
7
|
+
|
|
8
|
+
validates :name, uniqueness: true, presence: true
|
|
9
|
+
validates :slug, uniqueness: true, presence: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class CampaignSubscriber < ApplicationRecord
|
|
3
|
+
self.table_name = 'moist_campaign_subscribers'
|
|
4
|
+
|
|
5
|
+
include ::Moist::Models::CampaignSubscriber
|
|
6
|
+
|
|
7
|
+
has_many :moist_receipts, class_name: '::Moist::Receipt'
|
|
8
|
+
has_many :moist_mailings, class_name: '::Moist::Mailing', foreign_key: :moist_campaign_subscriber_id
|
|
9
|
+
has_one :next_moist_mailing, -> { unsent.order('send_at asc') }, class_name: '::Moist::Mailing', foreign_key: :moist_campaign_subscriber_id
|
|
10
|
+
has_one :most_recent_moist_mailing, -> { sent.order('sent_at desc') }, class_name: '::Moist::Mailing', foreign_key: :moist_campaign_subscriber_id
|
|
11
|
+
belongs_to :moist_campaign, class_name: '::Moist::Campaign'
|
|
12
|
+
belongs_to :subscriber, polymorphic: true
|
|
13
|
+
belongs_to :user
|
|
14
|
+
|
|
15
|
+
after_create_commit :create_mailings!
|
|
16
|
+
|
|
17
|
+
def create_mailings!
|
|
18
|
+
moist_campaign.steps.each do |step|
|
|
19
|
+
::Moist::Mailing.create!(moist_campaign_subscriber: self, send_at: step.options[:delay].from_now, mailer_class: step.mailer_class, mailer_action: step.mailer_action)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class Mailing < ApplicationRecord
|
|
3
|
+
belongs_to :moist_campaign_subscriber, class_name: '::Moist::CampaignSubscriber'
|
|
4
|
+
|
|
5
|
+
scope :unsent, -> { where(sent_at: nil) }
|
|
6
|
+
scope :sent, -> { where.not(sent_at: nil) }
|
|
7
|
+
scope :upcoming, -> { where('send_at > ?', Time.current) }
|
|
8
|
+
|
|
9
|
+
include ::Moist::Models::Mailing
|
|
10
|
+
end
|
|
11
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateMoistMoistCampaigns < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :moist_campaigns do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :slug, null: false
|
|
6
|
+
t.boolean :enabled, null: false, default: true
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :moist_campaigns, :slug, unique: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateMoistMoistCampaignSubscribers < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :moist_campaign_subscribers do |t|
|
|
4
|
+
t.references :moist_campaign, null: false, foreign_key: true
|
|
5
|
+
t.string :subscriber_type, null: false
|
|
6
|
+
t.integer :subscriber_id, null: false
|
|
7
|
+
t.integer :user_id, null: false
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
add_index :moist_campaign_subscribers, [:moist_campaign_id, :subscriber_id, :subscriber_type, :user_id], name: "index_moist_campaign_subscribers"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateMoistMailings < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :moist_mailings do |t|
|
|
4
|
+
t.references :moist_campaign_subscriber, null: false, foreign_key: true
|
|
5
|
+
t.datetime :send_at, null: false
|
|
6
|
+
t.datetime :sent_at
|
|
7
|
+
t.string :mailer_class, null: false
|
|
8
|
+
t.string :mailer_action, null: false
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateMoistMoistReceipts < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :moist_receipts do |t|
|
|
4
|
+
t.references :moist_campaign_subscriber, null: false, foreign_key: true
|
|
5
|
+
t.references :moist_campaign_mailing, null: false, foreign_key: true
|
|
6
|
+
t.integer :step, null: false
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :moist_receipts, [:moist_campaign_subscriber, :moist_campaign_mailing, :step]
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/moist.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'rails/all'
|
|
2
|
+
require 'moist/errors'
|
|
3
|
+
require 'moist/engine'
|
|
4
|
+
require 'moist/mailer_registry'
|
|
5
|
+
require 'moist/subscription_manager'
|
|
6
|
+
require 'moist/registry_proxy'
|
|
7
|
+
require 'moist/steps/registry'
|
|
8
|
+
require 'moist/steps/collection'
|
|
9
|
+
require 'moist/action_mailer/extension'
|
|
10
|
+
require 'moist/active_record/extension'
|
|
11
|
+
require 'moist/action_mailer/observer'
|
|
12
|
+
require 'moist/models/campaign'
|
|
13
|
+
require 'moist/models/campaign_subscriber'
|
|
14
|
+
require 'moist/models/mailing'
|
|
15
|
+
require 'moist/delivery'
|
|
16
|
+
|
|
17
|
+
module Moist
|
|
18
|
+
def self.step_registry
|
|
19
|
+
@step_registry ||= Steps::Registry.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.mailer_registry
|
|
23
|
+
@mailer_registry ||= MailerRegistry.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module ActionMailer
|
|
3
|
+
module Extension
|
|
4
|
+
MOIST_VARS = [:@moist_user, :@moist_subscriber]
|
|
5
|
+
def self.included(klass)
|
|
6
|
+
klass.extend ClassMethods
|
|
7
|
+
klass.after_action :attach_metadata
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def attach_metadata
|
|
13
|
+
mailer_class = self.class.to_s
|
|
14
|
+
mailer_action = self.action_name
|
|
15
|
+
self.message.instance_variable_set(:@mailer_class, mailer_class)
|
|
16
|
+
self.message.instance_variable_set(:@mailer_action, mailer_action)
|
|
17
|
+
self.message.class.send(:attr_reader, :mailer_class)
|
|
18
|
+
self.message.class.send(:attr_reader, :mailer_action)
|
|
19
|
+
|
|
20
|
+
MOIST_VARS.each do |var|
|
|
21
|
+
if instance_variable_defined?(var)
|
|
22
|
+
self.message.instance_variable_set(var, instance_variable_get(var))
|
|
23
|
+
self.message.class.send(:attr_reader, var.to_s[1..].to_sym)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
def moist(mailer_action, campaign:, step:, delay:)
|
|
30
|
+
::Moist::RegistryProxy.register(campaign, self.name, mailer_action, step: step, delay: delay)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module ActionMailer
|
|
3
|
+
class Observer
|
|
4
|
+
def self.delivered_email(message)
|
|
5
|
+
moist_enabled = ::Moist.mailer_registry.enabled?(message.mailer_class, message.mailer_action)
|
|
6
|
+
return unless moist_enabled
|
|
7
|
+
|
|
8
|
+
moist = [message.moist_user, message.moist_subscriber]
|
|
9
|
+
if moist.any?(&:nil?) && moist.any?
|
|
10
|
+
Rails.logger.warn("Moist was skipped, but at least one Moist variable was set.")
|
|
11
|
+
Rails.logger.warn("@moist_user: #{message.moist_user.inspect}\n@moist_subscriber:#{message.moist_subscriber}")
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
subscriber = ::Moist::CampaignSubscriber.find_by(user: message.moist_user, subscriber: message.moist_subscriber)
|
|
16
|
+
mailing = ::Moist::Mailing.find_by(mailer_class: message.mailer_class, mailer_action: message.mailer_action, moist_campaign_subscriber_id: subscriber)
|
|
17
|
+
mailing.update_column(:sent_at, Time.current)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module Extension
|
|
4
|
+
def self.included(klass)
|
|
5
|
+
klass.extend ClassMethods
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module Campaigns
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
has_many :moist_campaign_subscribers, class_name: '::Moist::CampaignSubscriber', as: :subscriber
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Users
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
has_many :moist_subscriptions, class_name: '::Moist::CampaignSubscriber'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
def has_moist_campaigns
|
|
26
|
+
include ::Moist::ActiveRecord::Extension::Campaigns
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def acts_as_moist_user
|
|
30
|
+
include ::Moist::ActiveRecord::Extension::Users
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class Delivery
|
|
3
|
+
def self.call(mailing)
|
|
4
|
+
new(mailing).call
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def initialize(mailing)
|
|
8
|
+
@mailing = mailing
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
@mailing.mailer_class.constantize.send(@mailing.mailer_action, @mailing.moist_campaign_subscriber.subscriber).deliver!
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/moist/engine.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace Moist
|
|
4
|
+
|
|
5
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
6
|
+
include ::Moist::ActionMailer::Extension
|
|
7
|
+
::ActionMailer::Base.register_observer(::Moist::ActionMailer::Observer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ActiveSupport.on_load(:active_record) do
|
|
11
|
+
include ::Moist::ActiveRecord::Extension
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/moist/errors.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class MailerRegistry
|
|
3
|
+
def initialize
|
|
4
|
+
@table = Hash.new
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def register(mailer, action)
|
|
8
|
+
@table[mailer] ||= Set.new
|
|
9
|
+
@table[mailer] << action.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](val)
|
|
13
|
+
@table[val]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enabled?(klass, action)
|
|
17
|
+
@table[klass] && @table[klass].include?(action)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module Models
|
|
3
|
+
module Campaign
|
|
4
|
+
def self.included(klass)
|
|
5
|
+
klass.extend ClassMethods
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def steps
|
|
9
|
+
Steps::Collection.for(self)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_slug
|
|
13
|
+
slug.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def subscribed?(subscriber, **args)
|
|
17
|
+
moist_campaign_subscribers.exists?(subscriber: subscriber, **args)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ClassMethods
|
|
21
|
+
def [](val)
|
|
22
|
+
campaign = ::Moist::Campaign.find_by(slug: val)
|
|
23
|
+
raise ::Moist::UnknownCampaign, "Unknown campaign slug #{val}" if campaign.nil?
|
|
24
|
+
|
|
25
|
+
campaign
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def subscribe(subscriber, user:)
|
|
29
|
+
SubscriptionManager.new(subscriber, user: user, action: :subscribe)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unsubscribe(subscriber, user:)
|
|
33
|
+
SubscriptionManager.new(subscriber, user: user, action: :unsubscribe)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module Models
|
|
3
|
+
module CampaignSubscriber
|
|
4
|
+
def self.included(klass)
|
|
5
|
+
# klass.extend ClassMethods
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def current_step
|
|
9
|
+
most_recent_moist_mailing
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def next_step
|
|
13
|
+
next_moist_mailing || raise(::Moist::CampaignComplete)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def next_mailing
|
|
17
|
+
return nil unless next_step
|
|
18
|
+
|
|
19
|
+
moist_mailings.find_by(mailer_class: next_step.mailer_class, mailer_action: next_step.mailer_action)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def next_step?
|
|
23
|
+
next_moist_mailing.present?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ready_for_next_mailing?
|
|
27
|
+
return false unless next_mailing
|
|
28
|
+
|
|
29
|
+
next_mailing.send_at < Time.current
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mail!
|
|
33
|
+
next_mailing.deliver!
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module Models
|
|
3
|
+
module Mailing
|
|
4
|
+
def deliver?
|
|
5
|
+
raise ::Moist::MailingAlreadyDelivered if sent?
|
|
6
|
+
|
|
7
|
+
send_at < Time.current
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sent?
|
|
11
|
+
sent_at.present?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def deliver!
|
|
15
|
+
::Moist::Delivery.call(self)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class RegistryProxy
|
|
3
|
+
def self.register(campaign_slug, mailer_class, mailer_action, options = {})
|
|
4
|
+
::Moist.step_registry.register(campaign_slug, mailer_class, mailer_action, options)
|
|
5
|
+
::Moist.mailer_registry.register(mailer_class, mailer_action)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class Scheduler
|
|
3
|
+
def self.run
|
|
4
|
+
new.run
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
::Moist::Campaign.where(enabled: true).each do |campaign|
|
|
9
|
+
campaign.subscribers.each do |subscriber|
|
|
10
|
+
tick!(subscriber)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def tick!(subscriber)
|
|
18
|
+
if subscriber.ready_for_next_mail?
|
|
19
|
+
subscriber.send_next_mail!
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module Steps
|
|
3
|
+
class Collection
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
def self.for(campaign)
|
|
7
|
+
new(campaign, ::Moist.step_registry[campaign.to_slug])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :steps
|
|
11
|
+
def initialize(campaign, steps)
|
|
12
|
+
@campaign = campaign
|
|
13
|
+
@steps = sort(steps)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def each(&block)
|
|
17
|
+
@steps.each { |step| block.call(step) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def size
|
|
21
|
+
@steps.size
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](val)
|
|
25
|
+
@steps[val]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find(step_number)
|
|
29
|
+
return nil unless step_number.is_a?(Integer)
|
|
30
|
+
|
|
31
|
+
steps.detect { |step| step.options[:step] == step_number } || find_next(step_number)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_next(step_number)
|
|
35
|
+
return nil unless step_number.is_a?(Integer)
|
|
36
|
+
|
|
37
|
+
steps.detect { |step| step.options[:step] > step_number } || last_step?(step_number)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def last_step?(step_number)
|
|
43
|
+
raise(::Moist::CampaignComplete) if steps.last.options[:step] == step_number
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sort(steps)
|
|
49
|
+
steps.sort_by { |step| step.options[:step] }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'moist/steps/step'
|
|
2
|
+
|
|
3
|
+
module Moist
|
|
4
|
+
module Steps
|
|
5
|
+
class Registry
|
|
6
|
+
def initialize
|
|
7
|
+
@table = Hash.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(campaign_slug, mailer_class, mailer_action, options = {})
|
|
11
|
+
@table[campaign_slug] ||= Set.new
|
|
12
|
+
@table[campaign_slug] << Step.new(mailer_class, mailer_action, options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](val)
|
|
16
|
+
@table[val] || []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
module Steps
|
|
3
|
+
class Step
|
|
4
|
+
attr_reader :mailer_class, :mailer_action, :options
|
|
5
|
+
def initialize(mailer_class, mailer_action, options = {})
|
|
6
|
+
@mailer_class = mailer_class
|
|
7
|
+
@mailer_action = mailer_action
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def name
|
|
12
|
+
"#{mailer_class}##{mailer_action}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Moist
|
|
2
|
+
class SubscriptionManager
|
|
3
|
+
attr_reader :subscriber, :user, :action
|
|
4
|
+
def initialize(subscriber, user:, action:)
|
|
5
|
+
@subscriber = subscriber
|
|
6
|
+
@user = user
|
|
7
|
+
@action = action
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(*campaign_slugs)
|
|
11
|
+
campaign_slugs.map do |campaign_slug|
|
|
12
|
+
campaign = ::Moist::Campaign[campaign_slug]
|
|
13
|
+
if @action == :subscribe
|
|
14
|
+
::Moist::CampaignSubscriber.first_or_create!(subscriber: subscriber, user: user, moist_campaign: campaign)
|
|
15
|
+
elsif @action == :unsubscribe
|
|
16
|
+
sub = ::Moist::CampaignSubscriber.find_by!(subscriber: subscriber, user: user, moist_campaign: campaign)
|
|
17
|
+
sub.destroy!
|
|
18
|
+
sub
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
alias to call
|
|
23
|
+
alias from call
|
|
24
|
+
end
|
|
25
|
+
end
|
|
File without changes
|
metadata
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: moist
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Josh Brody
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2020-11-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 6.0.3
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 6.0.3.4
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 6.0.3
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 6.0.3.4
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: sqlite3
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rspec-rails
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: pry
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: pry-rails
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
description: Opinionated drip campaign framework.
|
|
90
|
+
email:
|
|
91
|
+
- josh@josh.mn
|
|
92
|
+
executables: []
|
|
93
|
+
extensions: []
|
|
94
|
+
extra_rdoc_files: []
|
|
95
|
+
files:
|
|
96
|
+
- MIT-LICENSE
|
|
97
|
+
- README.md
|
|
98
|
+
- Rakefile
|
|
99
|
+
- app/controllers/moist/application_controller.rb
|
|
100
|
+
- app/models/moist/application_record.rb
|
|
101
|
+
- app/models/moist/campaign.rb
|
|
102
|
+
- app/models/moist/campaign_subscriber.rb
|
|
103
|
+
- app/models/moist/mailing.rb
|
|
104
|
+
- app/models/moist/receipt.rb
|
|
105
|
+
- app/views/layouts/moist/application.html.erb
|
|
106
|
+
- config/routes.rb
|
|
107
|
+
- db/migrate/20201113220645_create_moist_moist_campaigns.rb
|
|
108
|
+
- db/migrate/20201113220800_create_moist_moist_campaign_subscribers.rb
|
|
109
|
+
- db/migrate/20201113220943_create_moist_mailings.rb
|
|
110
|
+
- db/migrate/20201113220944_create_moist_moist_receipts.rb
|
|
111
|
+
- lib/moist.rb
|
|
112
|
+
- lib/moist/action_mailer/extension.rb
|
|
113
|
+
- lib/moist/action_mailer/observer.rb
|
|
114
|
+
- lib/moist/active_record/extension.rb
|
|
115
|
+
- lib/moist/delivery.rb
|
|
116
|
+
- lib/moist/engine.rb
|
|
117
|
+
- lib/moist/errors.rb
|
|
118
|
+
- lib/moist/mailer_registry.rb
|
|
119
|
+
- lib/moist/models/campaign.rb
|
|
120
|
+
- lib/moist/models/campaign_subscriber.rb
|
|
121
|
+
- lib/moist/models/mailing.rb
|
|
122
|
+
- lib/moist/registry_proxy.rb
|
|
123
|
+
- lib/moist/scheduler.rb
|
|
124
|
+
- lib/moist/steps/collection.rb
|
|
125
|
+
- lib/moist/steps/registry.rb
|
|
126
|
+
- lib/moist/steps/step.rb
|
|
127
|
+
- lib/moist/subscription_manager.rb
|
|
128
|
+
- lib/moist/version.rb
|
|
129
|
+
- lib/tasks/moist_tasks.rake
|
|
130
|
+
homepage: https://github.com/joshmn/moist
|
|
131
|
+
licenses:
|
|
132
|
+
- MIT
|
|
133
|
+
metadata: {}
|
|
134
|
+
post_install_message:
|
|
135
|
+
rdoc_options: []
|
|
136
|
+
require_paths:
|
|
137
|
+
- lib
|
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '0'
|
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '0'
|
|
148
|
+
requirements: []
|
|
149
|
+
rubygems_version: 3.0.3
|
|
150
|
+
signing_key:
|
|
151
|
+
specification_version: 4
|
|
152
|
+
summary: Opinionated drip campaign framework.
|
|
153
|
+
test_files: []
|