heya 0.0.1 → 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 +4 -4
- data/CHANGELOG.md +11 -0
- data/LICENSE +85 -0
- data/README.md +418 -0
- data/Rakefile +14 -14
- data/app/mailers/heya/application_mailer.rb +0 -2
- data/app/mailers/heya/campaign_mailer.rb +25 -0
- data/app/models/heya/campaign_membership.rb +9 -0
- data/app/models/heya/campaign_receipt.rb +5 -0
- data/app/views/layouts/heya/campaign_mailer.html.erb +20 -0
- data/app/views/layouts/heya/campaign_mailer.text.erb +1 -0
- data/lib/generators/heya/campaign/USAGE +14 -0
- data/lib/generators/heya/campaign/campaign_generator.rb +53 -0
- data/lib/generators/heya/campaign/templates/application_campaign.rb.tt +3 -0
- data/lib/generators/heya/campaign/templates/campaign.rb.tt +7 -0
- data/lib/generators/heya/campaign/templates/message.html.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/message.md.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/message.text.erb.tt +1 -0
- data/lib/generators/heya/install/USAGE +10 -0
- data/lib/generators/heya/install/install_generator.rb +20 -0
- data/lib/generators/heya/install/templates/initializer.rb.tt +16 -0
- data/lib/generators/heya/install/templates/migration.rb.tt +28 -0
- data/lib/heya.rb +44 -1
- data/lib/heya/campaigns/action.rb +27 -0
- data/lib/heya/campaigns/actions/block.rb +24 -0
- data/lib/heya/campaigns/actions/email.rb +15 -0
- data/lib/heya/campaigns/base.rb +151 -0
- data/lib/heya/campaigns/queries.rb +118 -0
- data/lib/heya/campaigns/scheduler.rb +49 -0
- data/lib/heya/campaigns/step.rb +24 -0
- data/lib/heya/campaigns/step_action_job.rb +34 -0
- data/lib/heya/config.rb +17 -0
- data/lib/heya/engine.rb +14 -0
- data/lib/heya/version.rb +3 -1
- data/lib/tasks/heya_tasks.rake +8 -4
- metadata +58 -7
data/Rakefile
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
begin
|
2
|
-
require
|
2
|
+
require "bundler/setup"
|
3
3
|
rescue LoadError
|
4
|
-
puts
|
4
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
5
5
|
end
|
6
6
|
|
7
|
-
require
|
7
|
+
require "rdoc/task"
|
8
8
|
|
9
9
|
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
-
rdoc.rdoc_dir =
|
11
|
-
rdoc.title
|
12
|
-
rdoc.options <<
|
13
|
-
rdoc.rdoc_files.include(
|
14
|
-
rdoc.rdoc_files.include(
|
10
|
+
rdoc.rdoc_dir = "rdoc"
|
11
|
+
rdoc.title = "Heya"
|
12
|
+
rdoc.options << "--line-numbers"
|
13
|
+
rdoc.rdoc_files.include("README.md")
|
14
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
15
15
|
end
|
16
16
|
|
17
17
|
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
-
load
|
18
|
+
load "rails/tasks/engine.rake"
|
19
19
|
|
20
|
-
load
|
20
|
+
load "rails/tasks/statistics.rake"
|
21
21
|
|
22
|
-
require
|
22
|
+
require "bundler/gem_tasks"
|
23
23
|
|
24
|
-
require
|
24
|
+
require "rake/testtask"
|
25
25
|
|
26
26
|
Rake::TestTask.new(:test) do |t|
|
27
|
-
t.libs <<
|
28
|
-
t.pattern =
|
27
|
+
t.libs << "test"
|
28
|
+
t.pattern = "test/**/*_test.rb"
|
29
29
|
t.verbose = false
|
30
30
|
end
|
31
31
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Heya
|
2
|
+
class CampaignMailer < ApplicationMailer
|
3
|
+
layout "heya/campaign_mailer"
|
4
|
+
|
5
|
+
def build
|
6
|
+
user = params.fetch(:user)
|
7
|
+
step = params.fetch(:step)
|
8
|
+
campaign = step.campaign
|
9
|
+
from = step.params.fetch("from")
|
10
|
+
reply_to = step.params.fetch("reply_to", nil)
|
11
|
+
subject = step.params.fetch("subject")
|
12
|
+
|
13
|
+
instance_variable_set(:"@#{user.model_name.element}", user)
|
14
|
+
|
15
|
+
mail(
|
16
|
+
from: from,
|
17
|
+
reply_to: reply_to,
|
18
|
+
to: user.email,
|
19
|
+
subject: subject,
|
20
|
+
template_path: "heya/campaign_mailer/#{campaign.name.underscore}",
|
21
|
+
template_name: step.name.underscore
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
5
|
+
<style>
|
6
|
+
.message-content { max-width: 600px; }
|
7
|
+
.message-content div { padding-bottom: 10px; }
|
8
|
+
.message-content img { max-width: 100%; height: auto; }
|
9
|
+
p {
|
10
|
+
margin-bottom: 1em;
|
11
|
+
}
|
12
|
+
</style>
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body>
|
16
|
+
<div class="message-content">
|
17
|
+
<%= yield %>
|
18
|
+
</div>
|
19
|
+
</body>
|
20
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= yield %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Description:
|
2
|
+
Generate a new campaign
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate heya:campaign Onboarding first second third
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
app/campaigns/onboarding_campaign.rb
|
9
|
+
app/views/heya/campaign_mailer/onboarding_campaign/first.text.erb
|
10
|
+
app/views/heya/campaign_mailer/onboarding_campaign/first.html.erb
|
11
|
+
app/views/heya/campaign_mailer/onboarding_campaign/second.text.erb
|
12
|
+
app/views/heya/campaign_mailer/onboarding_campaign/second.html.erb
|
13
|
+
app/views/heya/campaign_mailer/onboarding_campaign/third.text.erb
|
14
|
+
app/views/heya/campaign_mailer/onboarding_campaign/third.html.erb
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Heya::CampaignGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
argument :steps, type: :array, default: []
|
7
|
+
|
8
|
+
def copy_campaign_template
|
9
|
+
template "application_campaign.rb", "app/campaigns/application_campaign.rb"
|
10
|
+
template "campaign.rb", "app/campaigns/#{file_name.underscore}_campaign.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
def copy_view_templates
|
14
|
+
selection =
|
15
|
+
if defined?(Maildown)
|
16
|
+
puts <<~MSG
|
17
|
+
What type of views would you like to generate?
|
18
|
+
1. Multipart (text/html)
|
19
|
+
2. Maildown (markdown)
|
20
|
+
MSG
|
21
|
+
|
22
|
+
ask(">")
|
23
|
+
else
|
24
|
+
"1"
|
25
|
+
end
|
26
|
+
|
27
|
+
template_method =
|
28
|
+
case selection
|
29
|
+
when "1"
|
30
|
+
method(:action_mailer_template)
|
31
|
+
when "2"
|
32
|
+
method(:maildown_template)
|
33
|
+
else
|
34
|
+
abort "Error: must be a number [1-2]"
|
35
|
+
end
|
36
|
+
|
37
|
+
steps.each do |step|
|
38
|
+
@step = step
|
39
|
+
template_method.call(step)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def action_mailer_template(step)
|
46
|
+
template "message.text.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.text.erb"
|
47
|
+
template "message.html.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.html.erb"
|
48
|
+
end
|
49
|
+
|
50
|
+
def maildown_template(step)
|
51
|
+
template "message.md.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.md.erb"
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
This is the <%= @step.downcase %> messsage.
|
@@ -0,0 +1 @@
|
|
1
|
+
This is the <%= @step.downcase %> messsage.
|
@@ -0,0 +1 @@
|
|
1
|
+
This is the <%= @step.downcase %> messsage.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Heya::InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
def copy_migrations
|
9
|
+
migration_template "migration.rb", "db/migrate/create_heya_tables.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
def copy_initializer_file
|
13
|
+
copy_file "initializer.rb", "config/initializers/heya.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.next_migration_number(dirname)
|
17
|
+
next_migration_number = current_migration_number(dirname) + 1
|
18
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Heya.configure do |config|
|
2
|
+
# The name of the model you want to use with Heya.
|
3
|
+
config.user_type = "User"
|
4
|
+
|
5
|
+
# The default options to use when processing campaign steps.
|
6
|
+
# config.campaigns.default_options = {from: "user@example.com"}
|
7
|
+
|
8
|
+
# Campaign priority. When a user is added to multiple campaigns, they are
|
9
|
+
# sent in this order. Campaigns are sent in the order that the users were
|
10
|
+
# added if no priority is configured.
|
11
|
+
# config.campaings.priority = [
|
12
|
+
# "FirstCampaign",
|
13
|
+
# "SecondCampaign",
|
14
|
+
# "ThirdCampaign"
|
15
|
+
# ]
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class CreateHeyaTables < ActiveRecord::Migration[<%= ActiveRecord::VERSION::MAJOR %>.<%= ActiveRecord::VERSION::MINOR %>]
|
2
|
+
def change
|
3
|
+
create_table :heya_campaign_memberships do |t|
|
4
|
+
t.references :user, null: false, polymorphic: true, index: false
|
5
|
+
|
6
|
+
t.string :campaign_gid, null: false
|
7
|
+
t.boolean :concurrent, null: false, default: false
|
8
|
+
|
9
|
+
t.datetime :last_sent_at, null: false
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :heya_campaign_memberships, [:user_type, :user_id, :campaign_gid], unique: true, name: :user_campaign_idx
|
15
|
+
|
16
|
+
create_table :heya_campaign_receipts do |t|
|
17
|
+
t.references :user, null: false, polymorphic: true, index: false
|
18
|
+
|
19
|
+
t.string :step_gid, null: false
|
20
|
+
|
21
|
+
t.datetime :sent_at
|
22
|
+
|
23
|
+
t.timestamps
|
24
|
+
end
|
25
|
+
|
26
|
+
add_index :heya_campaign_receipts, [:user_type, :user_id, :step_gid], unique: true, name: :user_step_idx
|
27
|
+
end
|
28
|
+
end
|
data/lib/heya.rb
CHANGED
@@ -1,5 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "heya/version"
|
1
4
|
require "heya/engine"
|
5
|
+
require "heya/config"
|
6
|
+
require "heya/campaigns/action"
|
7
|
+
require "heya/campaigns/actions/email"
|
8
|
+
require "heya/campaigns/actions/block"
|
9
|
+
require "heya/campaigns/base"
|
10
|
+
require "heya/campaigns/queries"
|
11
|
+
require "heya/campaigns/scheduler"
|
12
|
+
require "heya/campaigns/step"
|
13
|
+
require "heya/campaigns/step_action_job"
|
2
14
|
|
3
15
|
module Heya
|
4
|
-
|
16
|
+
extend self
|
17
|
+
|
18
|
+
attr_accessor :campaigns
|
19
|
+
self.campaigns = []
|
20
|
+
|
21
|
+
def register_campaign(klass)
|
22
|
+
campaigns.push(klass) unless campaigns.include?(klass)
|
23
|
+
end
|
24
|
+
|
25
|
+
def unregister_campaign(klass)
|
26
|
+
campaigns.delete(klass)
|
27
|
+
end
|
28
|
+
|
29
|
+
def configure
|
30
|
+
yield(config) if block_given?
|
31
|
+
config
|
32
|
+
end
|
33
|
+
|
34
|
+
def config
|
35
|
+
@config ||= Config.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def in_segments?(user, *segments)
|
39
|
+
return false if segments.any? { |s| !in_segment?(user, s) }
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def in_segment?(user, segment)
|
44
|
+
return true if segment.nil?
|
45
|
+
return user.send(segment) if segment.is_a?(Symbol)
|
46
|
+
segment.call(user)
|
47
|
+
end
|
5
48
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Heya
|
4
|
+
module Campaigns
|
5
|
+
class Action
|
6
|
+
def initialize(user:, step:)
|
7
|
+
@user, @step = user, step
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :user, :step
|
11
|
+
|
12
|
+
def build
|
13
|
+
raise NotImplementedError, "Please implement #build on subclass of Heya::Campaigns::Action."
|
14
|
+
end
|
15
|
+
|
16
|
+
def deliver_now
|
17
|
+
build.deliver
|
18
|
+
end
|
19
|
+
|
20
|
+
def deliver_later
|
21
|
+
StepActionJob
|
22
|
+
.set(queue: step.queue)
|
23
|
+
.perform_later(step.campaign.class.name, user, step)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Heya
|
4
|
+
module Campaigns
|
5
|
+
module Actions
|
6
|
+
class Block < Action
|
7
|
+
class Execution
|
8
|
+
def initialize(user:, step:, &block)
|
9
|
+
@user, @step, @block = user, step, block
|
10
|
+
end
|
11
|
+
|
12
|
+
def deliver
|
13
|
+
instance_exec(@user, @step, &@block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def build
|
18
|
+
block = step.params.fetch("block")
|
19
|
+
Execution.new(user: user, step: step, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/descendants_tracker"
|
4
|
+
|
5
|
+
module Heya
|
6
|
+
module Campaigns
|
7
|
+
# {Campaigns::Base} provides a Ruby DSL for building campaign sequences.
|
8
|
+
# Multiple actions are supported; the default is email.
|
9
|
+
class Base
|
10
|
+
extend ActiveSupport::DescendantsTracker
|
11
|
+
|
12
|
+
include Singleton
|
13
|
+
include GlobalID::Identification
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
self.steps = []
|
17
|
+
end
|
18
|
+
|
19
|
+
delegate :name, :__segments, to: :class
|
20
|
+
alias id name
|
21
|
+
|
22
|
+
# Returns String GlobalID.
|
23
|
+
def gid
|
24
|
+
to_gid(app: "heya").to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(user, restart: false, concurrent: false, send_now: true)
|
28
|
+
return false unless Heya.in_segments?(user, *__segments)
|
29
|
+
|
30
|
+
restart && CampaignReceipt
|
31
|
+
.where(user: user, step_gid: steps.map(&:gid))
|
32
|
+
.delete_all
|
33
|
+
|
34
|
+
CampaignMembership.where(user: user, campaign_gid: gid, concurrent: concurrent).first_or_create!
|
35
|
+
|
36
|
+
if send_now && (step = steps.first) && step.wait <= 0
|
37
|
+
Scheduler.process(self, step, user)
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove(user)
|
44
|
+
CampaignMembership.where(user: user, campaign_gid: gid).delete_all
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def users
|
49
|
+
base_class = user_class.base_class
|
50
|
+
user_class
|
51
|
+
.joins(
|
52
|
+
sanitize_sql_array([
|
53
|
+
"inner join heya_campaign_memberships on heya_campaign_memberships.user_type = ? and heya_campaign_memberships.user_id = #{base_class.table_name}.id and heya_campaign_memberships.campaign_gid = ?",
|
54
|
+
base_class.name,
|
55
|
+
gid
|
56
|
+
])
|
57
|
+
).all
|
58
|
+
end
|
59
|
+
|
60
|
+
def user_class
|
61
|
+
@user_class ||= self.class.user_type.constantize
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_accessor :steps
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
delegate :sanitize_sql_array, to: ActiveRecord::Base
|
69
|
+
|
70
|
+
class_attribute :__defaults, default: {}.freeze
|
71
|
+
class_attribute :__segments, default: [].freeze
|
72
|
+
class_attribute :__user_type, default: nil
|
73
|
+
|
74
|
+
STEP_ATTRS = {
|
75
|
+
action: Actions::Email,
|
76
|
+
wait: 2.days,
|
77
|
+
segment: nil,
|
78
|
+
queue: "heya"
|
79
|
+
}.freeze
|
80
|
+
|
81
|
+
class << self
|
82
|
+
def inherited(campaign)
|
83
|
+
Heya.register_campaign(campaign)
|
84
|
+
Heya.unregister_campaign(campaign.superclass)
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
def find(_id)
|
89
|
+
instance
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_exception(exception)
|
93
|
+
raise exception
|
94
|
+
end
|
95
|
+
|
96
|
+
delegate :steps, :add, :remove, :users, :gid, :user_class, to: :instance
|
97
|
+
|
98
|
+
def default(**params)
|
99
|
+
self.__defaults = __defaults.merge(params).freeze
|
100
|
+
end
|
101
|
+
|
102
|
+
def user_type(value = nil)
|
103
|
+
if value.present?
|
104
|
+
self.__user_type = value.is_a?(String) ? value.to_s : value.name
|
105
|
+
end
|
106
|
+
|
107
|
+
__user_type || Heya.config.user_type
|
108
|
+
end
|
109
|
+
|
110
|
+
def segment(arg = nil, &block)
|
111
|
+
if block_given?
|
112
|
+
self.__segments = ([block] | __segments).freeze
|
113
|
+
elsif arg
|
114
|
+
self.__segments = ([arg] | __segments).freeze
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def step(name, **opts, &block)
|
119
|
+
if block_given?
|
120
|
+
opts[:block] ||= block
|
121
|
+
opts[:action] ||= Actions::Block
|
122
|
+
end
|
123
|
+
|
124
|
+
opts =
|
125
|
+
STEP_ATTRS
|
126
|
+
.merge(Heya.config.campaigns.default_options)
|
127
|
+
.merge(__defaults)
|
128
|
+
.merge(opts)
|
129
|
+
|
130
|
+
attrs = opts.select { |k, _| STEP_ATTRS.key?(k) }
|
131
|
+
attrs[:params] = opts.reject { |k, _| STEP_ATTRS.key?(k) }.stringify_keys
|
132
|
+
attrs[:id] = "#{self.name}/#{name}"
|
133
|
+
attrs[:name] = name.to_s
|
134
|
+
attrs[:position] = steps.size
|
135
|
+
attrs[:campaign] = instance
|
136
|
+
|
137
|
+
step = Step.new(attrs)
|
138
|
+
method_name = :"#{step.name.underscore}"
|
139
|
+
raise "Invalid step name: #{step.name}\n Step names must not conflict with method names on Heya::Campaigns::Base" if respond_to?(method_name)
|
140
|
+
|
141
|
+
define_singleton_method method_name do |user|
|
142
|
+
step.action.new(user: user, step: step).build
|
143
|
+
end
|
144
|
+
steps << step
|
145
|
+
|
146
|
+
step
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|