talkable 0.0.0 → 0.9.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.
Files changed (38) hide show
  1. checksums.yaml +13 -5
  2. data/.gitignore +4 -2
  3. data/.rspec +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +126 -0
  6. data/LICENSE +22 -0
  7. data/README.md +144 -185
  8. data/Rakefile +17 -0
  9. data/lib/talkable.rb +43 -0
  10. data/lib/talkable/api.rb +13 -0
  11. data/lib/talkable/api/base.rb +98 -0
  12. data/lib/talkable/api/offer.rb +11 -0
  13. data/lib/talkable/api/origin.rb +29 -0
  14. data/lib/talkable/api/person.rb +17 -0
  15. data/lib/talkable/api/referral.rb +19 -0
  16. data/lib/talkable/api/reward.rb +19 -0
  17. data/lib/talkable/api/share.rb +19 -0
  18. data/lib/talkable/api/visitor.rb +13 -0
  19. data/lib/talkable/configuration.rb +40 -0
  20. data/lib/talkable/generators/install_generator.rb +81 -0
  21. data/lib/talkable/generators/templates/app/controllers/invite_controller.rb +8 -0
  22. data/lib/talkable/generators/templates/app/views/invite/show.html.erb +2 -0
  23. data/lib/talkable/generators/templates/app/views/invite/show.html.haml +2 -0
  24. data/lib/talkable/generators/templates/app/views/invite/show.html.slim +2 -0
  25. data/lib/talkable/generators/templates/app/views/shared/_talkable_offer.html.erb +4 -0
  26. data/lib/talkable/generators/templates/app/views/shared/_talkable_offer.html.haml +3 -0
  27. data/lib/talkable/generators/templates/app/views/shared/_talkable_offer.html.slim +3 -0
  28. data/lib/talkable/generators/templates/config/initializers/talkable.rb +13 -0
  29. data/lib/talkable/integration.rb +41 -0
  30. data/lib/talkable/middleware.rb +147 -0
  31. data/lib/talkable/railtie.rb +11 -0
  32. data/lib/talkable/resources.rb +3 -0
  33. data/lib/talkable/resources/offer.rb +49 -0
  34. data/lib/talkable/resources/origin.rb +22 -0
  35. data/lib/talkable/version.rb +3 -0
  36. data/solano.yml +20 -0
  37. data/talkable.gemspec +37 -0
  38. metadata +202 -9
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "bundler"
2
+ require "bundler/gem_tasks"
3
+
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+
12
+ # RSpec tasks
13
+ require 'rspec/core'
14
+ require "rspec/core/rake_task"
15
+ RSpec::Core::RakeTask.new(:spec)
16
+
17
+ task :default => :spec
data/lib/talkable.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'talkable/version'
2
+ require 'talkable/configuration'
3
+ require 'talkable/api'
4
+ require 'talkable/resources'
5
+ require 'talkable/middleware'
6
+ require 'talkable/integration'
7
+ require 'talkable/railtie' if defined? ::Rails::Railtie
8
+
9
+ module Talkable
10
+ UUID = 'talkable_visitor_uuid'.freeze
11
+
12
+ class << self
13
+ def configure(config = nil)
14
+ configuration.apply config if config
15
+ yield(configuration) if block_given?
16
+ end
17
+
18
+ def configuration
19
+ @configuration ||= Talkable::Configuration.new
20
+ end
21
+
22
+ def visitor_uuid
23
+ Thread.current[UUID]
24
+ end
25
+
26
+ def visitor_uuid=(uuid)
27
+ Thread.current[UUID] = uuid
28
+ end
29
+
30
+ def with_uuid(uuid)
31
+ old_uuid, Talkable.visitor_uuid = Talkable.visitor_uuid, uuid
32
+ yield if block_given?
33
+ ensure
34
+ Talkable.visitor_uuid = old_uuid
35
+ end
36
+
37
+ def find_or_generate_uuid
38
+ visitor_uuid || Talkable::API::Visitor.create[:uuid]
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,13 @@
1
+ require 'talkable/api/base'
2
+
3
+ Dir[File.dirname(__FILE__) + '/api/*.rb'].each { |file| require file }
4
+
5
+ module Talkable
6
+ module API
7
+ VERSION = "v2".freeze
8
+ class BadRequest < StandardError
9
+ end
10
+ class NetworkError < StandardError
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,98 @@
1
+ require 'net/https'
2
+ require 'json'
3
+ require 'furi'
4
+
5
+ module Talkable
6
+ module API
7
+ class Base
8
+ class << self
9
+ def get(path, params = {})
10
+ uri = request_uri(path, request_params(params))
11
+ request = Net::HTTP::Get.new(uri.request_uri)
12
+ perform_request(uri, request)
13
+ end
14
+
15
+ def post(path, params = {})
16
+ data_request(:post, path, params)
17
+ end
18
+
19
+ def put(path, params = {})
20
+ data_request(:put, path, params)
21
+ end
22
+
23
+ protected
24
+
25
+ def data_request(method, path, params)
26
+ http_class = {post: Net::HTTP::Post, put: Net::HTTP::Put}[method.to_sym]
27
+ uri = request_uri(path)
28
+ request = http_class.new(uri.request_uri)
29
+ request.body = request_params(params).to_json
30
+ perform_request(uri, request)
31
+ end
32
+
33
+ def request_params(params = {})
34
+ params.merge({
35
+ api_key: Talkable.configuration.api_key,
36
+ site_slug: Talkable.configuration.site_slug,
37
+ })
38
+ end
39
+
40
+ def request_uri(path, params = {})
41
+ URI(
42
+ Furi.update("#{Talkable.configuration.server}/api/#{Talkable::API::VERSION}#{path}",
43
+ query: params
44
+ )
45
+ )
46
+ end
47
+
48
+ def request_headers
49
+ {
50
+ 'User-Agent' => "Talkable Gem/#{Talkable::VERSION}",
51
+ 'Content-Type' => 'application/json',
52
+ 'Accept' => 'application/json',
53
+ }
54
+ end
55
+
56
+ def perform_request(uri, request)
57
+ http = Net::HTTP.new(uri.host, uri.port)
58
+ http.use_ssl = uri.is_a?(URI::HTTPS)
59
+
60
+ request.initialize_http_header request_headers
61
+ process_response http.request(request)
62
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT,
63
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, SocketError, Timeout::Error,
64
+ OpenSSL::SSL::SSLError, EOFError, Net::HTTPBadResponse => e
65
+ raise NetworkError.new(e.message)
66
+ end
67
+
68
+ def process_response(response)
69
+ case response
70
+ when Net::HTTPSuccess, Net::HTTPClientError
71
+ parse_response response.body
72
+ when Net::HTTPServerError
73
+ raise NetworkError.new("Server #{Talkable.configuration.server} is unavailable. Try again later")
74
+ end
75
+ end
76
+
77
+ def parse_response(body)
78
+ raise_invalid_response if body.nil?
79
+ result = JSON.parse(body, symbolize_names: true)
80
+ raise_invalid_response unless result.is_a?(Hash)
81
+
82
+ if result[:ok]
83
+ result[:result]
84
+ else
85
+ raise BadRequest.new(result[:error_message])
86
+ end
87
+
88
+ rescue JSON::ParserError
89
+ raise_invalid_response
90
+ end
91
+
92
+ def raise_invalid_response
93
+ raise BadRequest.new("Invalid response")
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,11 @@
1
+ module Talkable
2
+ module API
3
+ class Offer < Base
4
+ class << self
5
+ def find(id, params = {})
6
+ get "/offers/#{id}", params
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module Talkable
2
+ module API
3
+ class Origin < Base
4
+ AFFILIATE_MEMBER = "AffiliateMember".freeze
5
+ PURCHASE = "Purchase".freeze
6
+ EVENT = "Event".freeze
7
+
8
+ DEFAULT_TRAFFIC_SOURCE = 'talkable-gem'
9
+
10
+ class << self
11
+ def create(origin_type, params)
12
+ post '/origins', {
13
+ type: origin_type,
14
+ data: default_data.merge(params),
15
+ }
16
+ end
17
+
18
+ protected
19
+
20
+ def default_data
21
+ {
22
+ uuid: Talkable.visitor_uuid,
23
+ traffic_source: DEFAULT_TRAFFIC_SOURCE,
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module Talkable
2
+ module API
3
+ class Person < Base
4
+ class << self
5
+ def find(email_or_username)
6
+ get "/people/#{email_or_username}"
7
+ end
8
+
9
+ def update(email_or_username, params)
10
+ put "/people/#{email_or_username}", {
11
+ data: params
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Talkable
2
+ module API
3
+ class Referral < Base
4
+ APPROVED = 'approved'
5
+ VOIDED = 'voided'
6
+ UNBLOCKED = 'unblocked'
7
+
8
+ class << self
9
+ def update(origin_slug, status)
10
+ put "/origins/#{origin_slug}/referral", {
11
+ data: {
12
+ status: status
13
+ }
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Talkable
2
+ module API
3
+ class Reward < Base
4
+ class << self
5
+ def find(params = {})
6
+ get "/rewards", default_params.merge(params)
7
+ end
8
+
9
+ protected
10
+
11
+ def default_params
12
+ {
13
+ visitor_uuid: Talkable.visitor_uuid,
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Talkable
2
+ module API
3
+ class Share < Base
4
+ CHANNEL_FACEBOOK = "facebook".freeze
5
+ CHANNEL_FACEBOOK_MESSAGE = "facebook_message".freeze
6
+ CHANNEL_TWITTER = "twitter".freeze
7
+ CHANNEL_SMS = "sms".freeze
8
+ CHANNEL_OTHER = "other".freeze
9
+
10
+ class << self
11
+ def create(short_url_code, channel)
12
+ post "/offers/#{short_url_code}/shares", {
13
+ channel: channel,
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Talkable
2
+ module API
3
+ class Visitor < Base
4
+ class << self
5
+ def create(params = {})
6
+ post '/visitors', {
7
+ data: params,
8
+ }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ module Talkable
2
+ class Configuration
3
+ DEFAULT_SERVER = "https://www.talkable.com".freeze
4
+
5
+ attr_accessor :site_slug
6
+ attr_accessor :api_key
7
+ attr_accessor :server
8
+ attr_accessor :js_integration_library
9
+
10
+ class UnknownOptionError < StandardError
11
+ end
12
+
13
+ def initialize
14
+ self.site_slug = ENV["TALKABLE_SITE_SLUG"]
15
+ self.api_key = ENV["TALKABLE_API_KEY"]
16
+ self.server = "https://www.talkable.com"
17
+ end
18
+
19
+ def apply(config)
20
+ config.each do |key, value|
21
+ if respond_to?("#{key}=")
22
+ public_send("#{key}=", value)
23
+ else
24
+ raise UnknownOptionError.new("There is no `#{key}` option")
25
+ end
26
+ end
27
+ end
28
+
29
+ def js_integration_library
30
+ @js_integration_library || default_js_integration_library
31
+ end
32
+
33
+ protected
34
+
35
+ def default_js_integration_library
36
+ "//d2jjzw81hqbuqv.cloudfront.net/integration/clients/#{site_slug}.min.js"
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,81 @@
1
+ module Talkable
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path("../templates", __FILE__)
4
+ class_option :haml, type: :boolean, default: false
5
+ class_option :slim, type: :boolean, default: false
6
+
7
+ def ask_config_values
8
+ @site_slug = ask("Your Talkable site slug:")
9
+ @api_key = ask("Your Talkable API Key:")
10
+ if yes?('Do you have a custom domain? [Y/n]')
11
+ @server = ask("Your custom domain [#{Talkable::Configuration::DEFAULT_SERVER}]:")
12
+ end
13
+ end
14
+
15
+ def add_initializer
16
+ template "config/initializers/talkable.rb", "config/initializers/talkable.rb"
17
+ end
18
+
19
+ def inject_talkable_offer
20
+ inject_into_file "app/controllers/application_controller.rb", after: "protect_from_forgery with: :exception" do
21
+ <<-RUBY
22
+
23
+ before_action :load_talkable_offer
24
+ RUBY
25
+ end
26
+
27
+ inject_into_file "app/controllers/application_controller.rb", before: /^end/ do
28
+ <<-RUBY
29
+ protected
30
+
31
+ def load_talkable_offer
32
+ origin = Talkable.register_affiliate_member(campaign_tags: 'popup')
33
+ @offer ||= origin.offer if origin
34
+ end
35
+ RUBY
36
+ end
37
+
38
+ empty_directory "app/views/shared/"
39
+
40
+ if erb?
41
+ copy_file "app/views/shared/_talkable_offer.html.erb", "app/views/shared/_talkable_offer.html.erb"
42
+ inject_into_file "app/views/layouts/application.html.erb", before: "</body>" do
43
+ "<%= render 'shared/talkable_offer', offer: @offer %>\n"
44
+ end
45
+ else
46
+ ext = template_lang
47
+
48
+ copy_file "app/views/shared/_talkable_offer.html.#{ext}", "app/views/shared/_talkable_offer.html.#{ext}"
49
+ gsub_file "app/views/layouts/application.html.#{ext}", /^(\s*)\=\s*yield\s*$/ do |line|
50
+ paddings = line.match(/(\s*)\=/)[1]
51
+ "#{line}#{paddings}= render 'shared/talkable_offer', offer: @offer\n"
52
+ end
53
+ end
54
+ end
55
+
56
+ def add_invite_controller
57
+ ext = template_lang
58
+
59
+ copy_file "app/controllers/invite_controller.rb", "app/controllers/invite_controller.rb"
60
+ copy_file "app/views/invite/show.html.#{ext}", "app/views/invite/show.html.#{ext}"
61
+ route "get '/invite' => 'invite#show'"
62
+ end
63
+
64
+ protected
65
+
66
+ def template_lang
67
+ @template_lang ||= if options[:haml]
68
+ 'haml'
69
+ elsif options[:slim]
70
+ 'slim'
71
+ else
72
+ Rails::Generators.options[:rails][:template_engine].to_s.downcase
73
+ end
74
+ end
75
+
76
+ def erb?
77
+ template_lang == 'erb'
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,8 @@
1
+ class InviteController < ApplicationController
2
+ skip_before_action :load_talkable_offer # skip default trigger widget integration
3
+
4
+ def show
5
+ origin = Talkable.register_affiliate_member(campaign_tags: 'invite')
6
+ @invite_offer = origin.offer if origin
7
+ end
8
+ end