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
| @@ -0,0 +1,118 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Heya
         | 
| 4 | 
            +
              module Campaigns
         | 
| 5 | 
            +
                module Queries
         | 
| 6 | 
            +
                  NEXT_STEP_SUBQUERY = <<~SQL
         | 
| 7 | 
            +
                    (WITH steps AS (SELECT * FROM (VALUES :steps_values) AS m (step_gid,campaign_gid,position))
         | 
| 8 | 
            +
                     SELECT m.step_gid FROM steps AS m
         | 
| 9 | 
            +
                     WHERE m.campaign_gid = :campaign_gid
         | 
| 10 | 
            +
                       AND m.position > coalesce((SELECT m.position FROM heya_campaign_receipts AS r
         | 
| 11 | 
            +
                         INNER JOIN steps AS m ON m.step_gid = r.step_gid
         | 
| 12 | 
            +
                           AND m.campaign_gid = :campaign_gid
         | 
| 13 | 
            +
                         WHERE r.user_type = heya_campaign_memberships.user_type
         | 
| 14 | 
            +
                           AND r.user_id = heya_campaign_memberships.user_id
         | 
| 15 | 
            +
                         ORDER BY m.position DESC
         | 
| 16 | 
            +
                         LIMIT 1), -1)
         | 
| 17 | 
            +
                     ORDER BY m.position ASC
         | 
| 18 | 
            +
                     LIMIT 1
         | 
| 19 | 
            +
                    ) = :step_gid
         | 
| 20 | 
            +
                  SQL
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  ACTIVE_CAMPAIGN_SUBQUERY = <<~SQL
         | 
| 23 | 
            +
                    (WITH heya_campaigns AS (SELECT * FROM (VALUES :campaigns_values) AS campaigns (campaign_gid,position))
         | 
| 24 | 
            +
                    SELECT memberships.campaign_gid FROM heya_campaign_memberships AS memberships
         | 
| 25 | 
            +
                     INNER JOIN heya_campaigns AS campaigns
         | 
| 26 | 
            +
                       ON campaigns.campaign_gid = memberships.campaign_gid
         | 
| 27 | 
            +
                     WHERE memberships.user_type = heya_campaign_memberships.user_type
         | 
| 28 | 
            +
                       AND memberships.user_id = heya_campaign_memberships.user_id
         | 
| 29 | 
            +
                       AND memberships.concurrent = FALSE
         | 
| 30 | 
            +
                     ORDER BY campaigns.position DESC, memberships.created_at ASC
         | 
| 31 | 
            +
                     LIMIT 1
         | 
| 32 | 
            +
                    ) = :campaign_gid
         | 
| 33 | 
            +
                  SQL
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  # Given a campaign and a step, {Queries::UsersForStep} returns the
         | 
| 36 | 
            +
                  # users who should complete the step.
         | 
| 37 | 
            +
                  UsersForStep = ->(campaign, step) {
         | 
| 38 | 
            +
                    wait_threshold = Time.now.utc - step.wait
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    # Safeguard to make sure we never complete the same step twice.
         | 
| 41 | 
            +
                    receipt_query = CampaignReceipt
         | 
| 42 | 
            +
                      .select("heya_campaign_receipts.user_id")
         | 
| 43 | 
            +
                      .where(user_type: campaign.user_class.name)
         | 
| 44 | 
            +
                      .where("heya_campaign_receipts.step_gid = ?", step.gid)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    # https://www.postgresql.org/docs/9.4/queries-values.html
         | 
| 47 | 
            +
                    steps_values = campaign.steps.map { |m|
         | 
| 48 | 
            +
                      ActiveRecord::Base.sanitize_sql_array(
         | 
| 49 | 
            +
                        ["(?, ?, ?)", m.gid, campaign.gid, m.position]
         | 
| 50 | 
            +
                      )
         | 
| 51 | 
            +
                    }.join(", ")
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    priority = Heya.config.campaigns.priority.reverse.map { |c| c.is_a?(String) ? c : c.name }
         | 
| 54 | 
            +
                    campaigns_values = Heya.campaigns.map { |c|
         | 
| 55 | 
            +
                      ActiveRecord::Base.sanitize_sql_array(
         | 
| 56 | 
            +
                        ["(?, ?)", c.gid, priority.index(c.name) || -1]
         | 
| 57 | 
            +
                      )
         | 
| 58 | 
            +
                    }.join(", ")
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    users = campaign.users
         | 
| 61 | 
            +
                    users
         | 
| 62 | 
            +
                      .where.not(id: receipt_query)
         | 
| 63 | 
            +
                      .where(NEXT_STEP_SUBQUERY.gsub(":steps_values", steps_values), {
         | 
| 64 | 
            +
                        campaign_gid: campaign.gid,
         | 
| 65 | 
            +
                        step_gid: step.gid
         | 
| 66 | 
            +
                      })
         | 
| 67 | 
            +
                      .merge(
         | 
| 68 | 
            +
                        users
         | 
| 69 | 
            +
                          .where("heya_campaign_memberships.concurrent = ?", true)
         | 
| 70 | 
            +
                          .or(
         | 
| 71 | 
            +
                            users.where(ACTIVE_CAMPAIGN_SUBQUERY.gsub(":campaigns_values", campaigns_values), {
         | 
| 72 | 
            +
                              campaign_gid: campaign.gid
         | 
| 73 | 
            +
                            })
         | 
| 74 | 
            +
                          )
         | 
| 75 | 
            +
                      )
         | 
| 76 | 
            +
                      .where(
         | 
| 77 | 
            +
                        "heya_campaign_memberships.last_sent_at <= ?", wait_threshold
         | 
| 78 | 
            +
                      )
         | 
| 79 | 
            +
                  }
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  # Given a campaign and a step, {Queries::UsersCompletedStep}
         | 
| 82 | 
            +
                  # returns the users who have completed the step.
         | 
| 83 | 
            +
                  UsersCompletedStep = ->(campaign, step) {
         | 
| 84 | 
            +
                    receipt_query = CampaignReceipt
         | 
| 85 | 
            +
                      .select("heya_campaign_receipts.user_id")
         | 
| 86 | 
            +
                      .where(user_type: campaign.user_class.name)
         | 
| 87 | 
            +
                      .where("heya_campaign_receipts.step_gid = ?", step.gid)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    campaign.users
         | 
| 90 | 
            +
                      .where(id: receipt_query)
         | 
| 91 | 
            +
                  }
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  # Given a campaign and a user, {Queries::CampaignMembershipsForUpdate}
         | 
| 94 | 
            +
                  # returns the user's campaign memberships which should be updated
         | 
| 95 | 
            +
                  # concurrently.
         | 
| 96 | 
            +
                  CampaignMembershipsForUpdate = ->(campaign, user) {
         | 
| 97 | 
            +
                    membership = CampaignMembership.where(user: user, campaign_gid: campaign.gid).first
         | 
| 98 | 
            +
                    if membership.concurrent?
         | 
| 99 | 
            +
                      CampaignMembership
         | 
| 100 | 
            +
                        .where(user: user, campaign_gid: campaign.gid)
         | 
| 101 | 
            +
                    else
         | 
| 102 | 
            +
                      CampaignMembership
         | 
| 103 | 
            +
                        .where(user: user, concurrent: false)
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                  }
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  # Given a campaign, {Queries::OrphanedCampaignMemberships} returns the
         | 
| 108 | 
            +
                  # campaign memberships which belong to users who no longer exist in the
         | 
| 109 | 
            +
                  # database.
         | 
| 110 | 
            +
                  OrphanedCampaignMemberships = ->(campaign) {
         | 
| 111 | 
            +
                    CampaignMembership
         | 
| 112 | 
            +
                      .where(campaign_gid: campaign.gid)
         | 
| 113 | 
            +
                      .where(user_type: campaign.user_class.base_class.name)
         | 
| 114 | 
            +
                      .where.not(user_id: campaign.users.select("id"))
         | 
| 115 | 
            +
                  }
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Heya
         | 
| 4 | 
            +
              module Campaigns
         | 
| 5 | 
            +
                # {Campaigns::Scheduler} schedules campaign jobs to run for each campaign.
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                # For each step in each campaign:
         | 
| 8 | 
            +
                #   1. Find users who haven't completed step, and are outside the `wait`
         | 
| 9 | 
            +
                #   window
         | 
| 10 | 
            +
                #   2. Match segment
         | 
| 11 | 
            +
                #   3. Create CampaignReceipt (excludes user in subsequent steps)
         | 
| 12 | 
            +
                #   4. Process job
         | 
| 13 | 
            +
                class Scheduler
         | 
| 14 | 
            +
                  def run
         | 
| 15 | 
            +
                    Heya.campaigns.each do |campaign|
         | 
| 16 | 
            +
                      Queries::OrphanedCampaignMemberships.call(campaign).delete_all
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      campaign.steps.each do |step|
         | 
| 19 | 
            +
                        Queries::UsersForStep.call(campaign, step).find_each do |user|
         | 
| 20 | 
            +
                          self.class.process(campaign, step, user)
         | 
| 21 | 
            +
                        end
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      if (last_step = campaign.steps.last)
         | 
| 25 | 
            +
                        CampaignMembership.where(
         | 
| 26 | 
            +
                          user: Queries::UsersCompletedStep.call(campaign, last_step),
         | 
| 27 | 
            +
                          campaign_gid: campaign.gid
         | 
| 28 | 
            +
                        ).delete_all
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def self.process(campaign, step, user)
         | 
| 34 | 
            +
                    ActiveRecord::Base.transaction do
         | 
| 35 | 
            +
                      return if CampaignReceipt.where(user: user, step_gid: step.gid).exists?
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      if step.in_segment?(user)
         | 
| 38 | 
            +
                        now = Time.now.utc
         | 
| 39 | 
            +
                        Queries::CampaignMembershipsForUpdate.call(campaign, user).update_all(last_sent_at: now)
         | 
| 40 | 
            +
                        CampaignReceipt.create!(user: user, step_gid: step.gid, sent_at: now)
         | 
| 41 | 
            +
                        step.action.new(user: user, step: step).deliver_later
         | 
| 42 | 
            +
                      else
         | 
| 43 | 
            +
                        CampaignReceipt.create!(user: user, step_gid: step.gid)
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "ostruct"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Heya
         | 
| 6 | 
            +
              module Campaigns
         | 
| 7 | 
            +
                class Step < OpenStruct
         | 
| 8 | 
            +
                  include GlobalID::Identification
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.find(id)
         | 
| 11 | 
            +
                    campaign_name, _step_name = id.to_s.split("/")
         | 
| 12 | 
            +
                    campaign_name.constantize.steps.find { |s| s.id == id }
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def gid
         | 
| 16 | 
            +
                    to_gid(app: "heya").to_s
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def in_segment?(user)
         | 
| 20 | 
            +
                    Heya.in_segments?(user, *campaign.__segments, segment)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Heya
         | 
| 4 | 
            +
              module Campaigns
         | 
| 5 | 
            +
                class StepActionJob < ActiveJob::Base
         | 
| 6 | 
            +
                  queue_as { Heya.config.campaigns.queue }
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  rescue_from StandardError, with: :handle_exception_with_campaign_class
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def perform(_campaign, user, step)
         | 
| 11 | 
            +
                    step.action.new(user: user, step: step).deliver_now
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  private
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # From ActionMailer: "deserialize" the mailer class name by hand in case
         | 
| 17 | 
            +
                  # another argument (like a Global ID reference) raised
         | 
| 18 | 
            +
                  # DeserializationError.
         | 
| 19 | 
            +
                  def campaign_class
         | 
| 20 | 
            +
                    if (campaign = (arguments_serialized? && Array(@serialized_arguments).first) || Array(arguments).first)
         | 
| 21 | 
            +
                      campaign.constantize
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def handle_exception_with_campaign_class(exception)
         | 
| 26 | 
            +
                    if (klass = campaign_class)
         | 
| 27 | 
            +
                      klass.handle_exception(exception)
         | 
| 28 | 
            +
                    else
         | 
| 29 | 
            +
                      raise exception
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
    
        data/lib/heya/config.rb
    ADDED
    
    
    
        data/lib/heya/engine.rb
    CHANGED
    
    | @@ -1,5 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Heya
         | 
| 2 4 | 
             
              class Engine < ::Rails::Engine
         | 
| 3 5 | 
             
                isolate_namespace Heya
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                initializer "heya.reload_campaigns" do |app|
         | 
| 8 | 
            +
                  app.reloader.to_run do
         | 
| 9 | 
            +
                    Heya.campaigns.clear
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                config.to_prepare do
         | 
| 14 | 
            +
                  Dir.glob(Rails.root + "app/campaigns/*.rb").each do |c|
         | 
| 15 | 
            +
                    require_dependency(c)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 4 18 | 
             
              end
         | 
| 5 19 | 
             
            end
         | 
    
        data/lib/heya/version.rb
    CHANGED
    
    
    
        data/lib/tasks/heya_tasks.rake
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,29 +1,35 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: heya
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0 | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Joshua Wood
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2020-03-20 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rails
         | 
| 15 15 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 16 | 
             
                requirements:
         | 
| 17 | 
            -
                - - " | 
| 17 | 
            +
                - - ">="
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 19 | 
             
                    version: 5.2.3
         | 
| 20 | 
            +
                - - "<"
         | 
| 21 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 22 | 
            +
                    version: 6.1.0
         | 
| 20 23 | 
             
              type: :runtime
         | 
| 21 24 | 
             
              prerelease: false
         | 
| 22 25 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 26 | 
             
                requirements:
         | 
| 24 | 
            -
                - - " | 
| 27 | 
            +
                - - ">="
         | 
| 25 28 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 29 | 
             
                    version: 5.2.3
         | 
| 30 | 
            +
                - - "<"
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: 6.1.0
         | 
| 27 33 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 34 | 
             
              name: pg
         | 
| 29 35 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -38,13 +44,32 @@ dependencies: | |
| 38 44 | 
             
                - - ">="
         | 
| 39 45 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 46 | 
             
                    version: '0'
         | 
| 41 | 
            -
             | 
| 47 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 48 | 
            +
              name: appraisal
         | 
| 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 | 
            +
            description: Heya is a customer communication and automation framework for Ruby on
         | 
| 62 | 
            +
              Rails. It's as robust as the hosted alternatives, without the integration and compliance
         | 
| 63 | 
            +
              nightmare.
         | 
| 42 64 | 
             
            email:
         | 
| 43 65 | 
             
            - josh@honeybadger.io
         | 
| 44 66 | 
             
            executables: []
         | 
| 45 67 | 
             
            extensions: []
         | 
| 46 68 | 
             
            extra_rdoc_files: []
         | 
| 47 69 | 
             
            files:
         | 
| 70 | 
            +
            - CHANGELOG.md
         | 
| 71 | 
            +
            - LICENSE
         | 
| 72 | 
            +
            - README.md
         | 
| 48 73 | 
             
            - Rakefile
         | 
| 49 74 | 
             
            - app/assets/config/heya_manifest.js
         | 
| 50 75 | 
             
            - app/assets/javascripts/heya/application.js
         | 
| @@ -53,15 +78,41 @@ files: | |
| 53 78 | 
             
            - app/helpers/heya/application_helper.rb
         | 
| 54 79 | 
             
            - app/jobs/heya/application_job.rb
         | 
| 55 80 | 
             
            - app/mailers/heya/application_mailer.rb
         | 
| 81 | 
            +
            - app/mailers/heya/campaign_mailer.rb
         | 
| 56 82 | 
             
            - app/models/heya/application_record.rb
         | 
| 83 | 
            +
            - app/models/heya/campaign_membership.rb
         | 
| 84 | 
            +
            - app/models/heya/campaign_receipt.rb
         | 
| 57 85 | 
             
            - app/views/layouts/heya/application.html.erb
         | 
| 86 | 
            +
            - app/views/layouts/heya/campaign_mailer.html.erb
         | 
| 87 | 
            +
            - app/views/layouts/heya/campaign_mailer.text.erb
         | 
| 58 88 | 
             
            - config/routes.rb
         | 
| 89 | 
            +
            - lib/generators/heya/campaign/USAGE
         | 
| 90 | 
            +
            - lib/generators/heya/campaign/campaign_generator.rb
         | 
| 91 | 
            +
            - lib/generators/heya/campaign/templates/application_campaign.rb.tt
         | 
| 92 | 
            +
            - lib/generators/heya/campaign/templates/campaign.rb.tt
         | 
| 93 | 
            +
            - lib/generators/heya/campaign/templates/message.html.erb.tt
         | 
| 94 | 
            +
            - lib/generators/heya/campaign/templates/message.md.erb.tt
         | 
| 95 | 
            +
            - lib/generators/heya/campaign/templates/message.text.erb.tt
         | 
| 96 | 
            +
            - lib/generators/heya/install/USAGE
         | 
| 97 | 
            +
            - lib/generators/heya/install/install_generator.rb
         | 
| 98 | 
            +
            - lib/generators/heya/install/templates/initializer.rb.tt
         | 
| 99 | 
            +
            - lib/generators/heya/install/templates/migration.rb.tt
         | 
| 59 100 | 
             
            - lib/heya.rb
         | 
| 101 | 
            +
            - lib/heya/campaigns/action.rb
         | 
| 102 | 
            +
            - lib/heya/campaigns/actions/block.rb
         | 
| 103 | 
            +
            - lib/heya/campaigns/actions/email.rb
         | 
| 104 | 
            +
            - lib/heya/campaigns/base.rb
         | 
| 105 | 
            +
            - lib/heya/campaigns/queries.rb
         | 
| 106 | 
            +
            - lib/heya/campaigns/scheduler.rb
         | 
| 107 | 
            +
            - lib/heya/campaigns/step.rb
         | 
| 108 | 
            +
            - lib/heya/campaigns/step_action_job.rb
         | 
| 109 | 
            +
            - lib/heya/config.rb
         | 
| 60 110 | 
             
            - lib/heya/engine.rb
         | 
| 61 111 | 
             
            - lib/heya/version.rb
         | 
| 62 112 | 
             
            - lib/tasks/heya_tasks.rake
         | 
| 63 | 
            -
            homepage: https:// | 
| 64 | 
            -
            licenses: | 
| 113 | 
            +
            homepage: https://github.com/honeybadger-io/heya
         | 
| 114 | 
            +
            licenses:
         | 
| 115 | 
            +
            - Prosperity Public License
         | 
| 65 116 | 
             
            metadata: {}
         | 
| 66 117 | 
             
            post_install_message: 
         | 
| 67 118 | 
             
            rdoc_options: []
         |