intercom-rails 0.0.7 → 0.0.8
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.
- data/README.mdown +109 -0
- data/lib/data/cacert.pem +3920 -0
- data/lib/intercom-rails.rb +1 -0
- data/lib/intercom-rails/config.rb +30 -0
- data/lib/intercom-rails/import.rb +153 -0
- data/lib/intercom-rails/intercom.rake +12 -0
- data/lib/intercom-rails/railtie.rb +4 -0
- data/lib/intercom-rails/version.rb +1 -1
- data/lib/rails/generators/intercom/config/config_generator.rb +5 -0
- data/lib/rails/generators/intercom/config/intercom.rb.erb +29 -1
- data/test/action_controller_test_setup.rb +0 -1
- data/test/import_test_setup.rb +62 -0
- data/test/intercom-rails/import_network_test.rb +121 -0
- data/test/intercom-rails/import_unit_test.rb +95 -0
- data/test/test_setup.rb +17 -0
- metadata +62 -6
- data/MIT-LICENSE +0 -21
- data/README.rdoc +0 -73
data/lib/intercom-rails.rb
CHANGED
@@ -2,6 +2,7 @@ module IntercomRails
|
|
2
2
|
|
3
3
|
module Config
|
4
4
|
|
5
|
+
# Your Intercom app_id
|
5
6
|
def self.app_id=(value)
|
6
7
|
@app_id = value
|
7
8
|
end
|
@@ -10,6 +11,25 @@ module IntercomRails
|
|
10
11
|
@app_id
|
11
12
|
end
|
12
13
|
|
14
|
+
# Intercom api secret, for secure mode
|
15
|
+
def self.api_secret=(value)
|
16
|
+
@api_secret = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.api_secret
|
20
|
+
@api_secret
|
21
|
+
end
|
22
|
+
|
23
|
+
# Intercom API key, for some rake tasks
|
24
|
+
def self.api_key=(value)
|
25
|
+
@api_key = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.api_key
|
29
|
+
@api_key
|
30
|
+
end
|
31
|
+
|
32
|
+
# How is the current logged in user accessed in your controllers?
|
13
33
|
def self.current_user=(value)
|
14
34
|
raise ArgumentError, "current_user should be a Proc" unless value.kind_of?(Proc)
|
15
35
|
@current_user = value
|
@@ -19,6 +39,16 @@ module IntercomRails
|
|
19
39
|
@current_user
|
20
40
|
end
|
21
41
|
|
42
|
+
# What class defines your user model?
|
43
|
+
def self.user_model=(value)
|
44
|
+
raise ArgumentError, "user_model should be a Proc" unless value.kind_of?(Proc)
|
45
|
+
@user_model = value
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.user_model
|
49
|
+
@user_model
|
50
|
+
end
|
51
|
+
|
22
52
|
end
|
23
53
|
|
24
54
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module IntercomRails
|
6
|
+
class ImportError < StandardError; end
|
7
|
+
class IntercomAPIError < StandardError; end
|
8
|
+
|
9
|
+
class Import
|
10
|
+
|
11
|
+
def self.bulk_create_api_endpoint
|
12
|
+
host = (ENV['INTERCOM_RAILS_DEV'] ? "http://api.intercom.dev" : "https://api.intercom.io")
|
13
|
+
URI.parse(host + "/v1/users/bulk_create")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.run(*args)
|
17
|
+
new(*args).run
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :uri, :http
|
21
|
+
attr_accessor :failed, :total_sent
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@uri = Import.bulk_create_api_endpoint
|
25
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
26
|
+
@failed = []
|
27
|
+
@total_sent = 0
|
28
|
+
|
29
|
+
@status_enabled = !!options[:status_enabled]
|
30
|
+
|
31
|
+
if uri.scheme == 'https'
|
32
|
+
http.use_ssl = true
|
33
|
+
http.ca_file = File.join(File.dirname(__FILE__), '../data/ca_cert.pem')
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert_runnable
|
39
|
+
raise ImportError, "You can only import your users from your production environment" unless Rails.env.production?
|
40
|
+
raise ImportError, "We couldn't find your user class, please set one in config/initializers/intercom_rails.rb" unless user_klass.present?
|
41
|
+
info "Found user class: #{user_klass}"
|
42
|
+
raise ImportError, "Only ActiveRecord models are supported" unless (user_klass < ActiveRecord::Base)
|
43
|
+
raise ImportError, "Please add an Intercom API Key to config/initializers/intercom.rb" unless IntercomRails.config.api_key.present?
|
44
|
+
info "Intercom API key found"
|
45
|
+
end
|
46
|
+
|
47
|
+
def run
|
48
|
+
assert_runnable
|
49
|
+
|
50
|
+
info "Sending users in batches of #{MAX_BATCH_SIZE}:"
|
51
|
+
batches do |batch, number_in_batch|
|
52
|
+
failures = send_users(batch)['failed']
|
53
|
+
self.failed += failures
|
54
|
+
self.total_sent += number_in_batch
|
55
|
+
|
56
|
+
progress '.' * (number_in_batch - failures.count)
|
57
|
+
progress 'F' * failures.count
|
58
|
+
end
|
59
|
+
info "Successfully created #{self.total_sent - self.failed.count} users", :new_line => true
|
60
|
+
info "Failed to create #{self.failed.count} #{(self.failed.count == 1) ? 'user' : 'users'}, this is likely due to bad data" unless failed.count.zero?
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def total_failed
|
66
|
+
self.failed.count
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
MAX_BATCH_SIZE = 100
|
71
|
+
def batches
|
72
|
+
user_klass.find_in_batches(:batch_size => MAX_BATCH_SIZE) do |users|
|
73
|
+
users_for_wire = users.map { |u| user_for_wire(u) }.compact
|
74
|
+
yield(prepare_batch(users_for_wire), users_for_wire.count) unless users_for_wire.count.zero?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_batch(batch)
|
79
|
+
{:users => batch}.to_json
|
80
|
+
end
|
81
|
+
|
82
|
+
def user_for_wire(user)
|
83
|
+
wired = {}.tap do |h|
|
84
|
+
h[:user_id] = user.id if user.respond_to?(:id) && user.id.present?
|
85
|
+
h[:email] = user.email if user.respond_to?(:email) && user.email.present?
|
86
|
+
h[:name] = user.name if user.respond_to?(:name) && user.name.present?
|
87
|
+
end
|
88
|
+
|
89
|
+
(wired[:user_id] || wired[:email]) ? wired : nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def user_klass
|
93
|
+
if IntercomRails.config.user_model.present?
|
94
|
+
IntercomRails.config.user_model.call
|
95
|
+
else
|
96
|
+
User
|
97
|
+
end
|
98
|
+
rescue NameError
|
99
|
+
# Rails lazy loads constants, so this is how we check
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_users(users)
|
104
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
105
|
+
request.basic_auth(IntercomRails.config.app_id, IntercomRails.config.api_key)
|
106
|
+
request["Content-Type"] = "application/json"
|
107
|
+
request.body = users
|
108
|
+
|
109
|
+
response = perform_request(request)
|
110
|
+
JSON.parse(response.body)
|
111
|
+
end
|
112
|
+
|
113
|
+
MAX_REQUEST_ATTEMPTS = 3
|
114
|
+
def perform_request(request, attempts = 0, error = {})
|
115
|
+
if (attempts > 0) && (attempts < MAX_REQUEST_ATTEMPTS)
|
116
|
+
sleep(0.5)
|
117
|
+
elsif error.present?
|
118
|
+
raise error[:exception] if error[:exception]
|
119
|
+
raise exception_for_failed_response(error[:failed_response])
|
120
|
+
end
|
121
|
+
|
122
|
+
response = http.request(request)
|
123
|
+
|
124
|
+
return response if successful_response?(response)
|
125
|
+
perform_request(request, attempts + 1, :failed_response => response)
|
126
|
+
rescue Timeout::Error, Errno::ECONNREFUSED => e
|
127
|
+
perform_request(request, attempts + 1, :exception => e)
|
128
|
+
end
|
129
|
+
|
130
|
+
def successful_response?(response)
|
131
|
+
raise ImportError, "App ID or API Key are incorrect, please check them in config/initializers/intercom.rb" if response.code == '403'
|
132
|
+
['200', '201'].include?(response.code)
|
133
|
+
end
|
134
|
+
|
135
|
+
def exception_for_failed_response(response)
|
136
|
+
code = response.code
|
137
|
+
IntercomAPIError.new("The Intercom API request failed with the code: #{code}, after #{MAX_REQUEST_ATTEMPTS} attempts.")
|
138
|
+
end
|
139
|
+
|
140
|
+
def status_enabled?
|
141
|
+
@status_enabled
|
142
|
+
end
|
143
|
+
|
144
|
+
def progress(str)
|
145
|
+
print(str) if status_enabled?
|
146
|
+
end
|
147
|
+
|
148
|
+
def info(str, options = {})
|
149
|
+
puts "#{"\n" if options[:new_line]}* #{str}" if status_enabled?
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
@@ -7,9 +7,14 @@ module Intercom
|
|
7
7
|
end
|
8
8
|
|
9
9
|
argument :app_id, :desc => "Your Intercom app-id, which can be found here: https://www.intercom.io/apps/api_keys"
|
10
|
+
argument :api_secret, :desc => "Your Intercom api-secret, used for secure mode"
|
11
|
+
argument :api_key, :desc => "An Intercom API key, for various rake tasks"
|
10
12
|
|
11
13
|
def create_config_file
|
12
14
|
@app_id = app_id
|
15
|
+
@api_secret = api_secret
|
16
|
+
@api_key = api_key
|
17
|
+
|
13
18
|
template("intercom.rb.erb", "config/initializers/intercom.rb")
|
14
19
|
end
|
15
20
|
|
@@ -1,8 +1,36 @@
|
|
1
1
|
IntercomRails.config do |config|
|
2
|
-
#
|
2
|
+
# == Intercom app_id
|
3
|
+
#
|
3
4
|
config.app_id = "<%= @app_id %>"
|
4
5
|
|
6
|
+
# == Intercom secret key
|
7
|
+
# This is reuqired to enable secure mode, you can find it on your Intercom
|
8
|
+
# "security" configuration page.
|
9
|
+
#
|
10
|
+
<%- if @api_secret -%>
|
11
|
+
config.api_secret = "<%= @api_secret %>"
|
12
|
+
<%- else -%>
|
13
|
+
# config.api_secret = '...'
|
14
|
+
<%- end -%>
|
15
|
+
|
16
|
+
# == Intercom API Key
|
17
|
+
# This is required for some Intercom rake tasks like importing your users;
|
18
|
+
# you can generate one at https://www.intercom.io/apps/api_keys.
|
19
|
+
#
|
20
|
+
<%- if @api_key -%>
|
21
|
+
config.api_key = "<%= @api_key %>"
|
22
|
+
<%- else -%>
|
23
|
+
# config.api_key = "..."
|
24
|
+
<%- end -%>
|
25
|
+
|
26
|
+
# == Curent user name
|
5
27
|
# The method/variable that contains the logged in user in your controllers.
|
6
28
|
# If it is `current_user` or `@user`, then you can ignore this
|
29
|
+
#
|
7
30
|
# config.current_user = Proc.new { current_user }
|
31
|
+
|
32
|
+
# == User model class
|
33
|
+
# The class which defines your user model
|
34
|
+
#
|
35
|
+
# config.user_model = Proc.new { User }
|
8
36
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_setup'
|
2
|
+
require 'active_support/string_inquirer'
|
3
|
+
|
4
|
+
class Rails
|
5
|
+
def self.env
|
6
|
+
ActiveSupport::StringInquirer.new("production")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ActiveRecord
|
11
|
+
class Base; end
|
12
|
+
end
|
13
|
+
|
14
|
+
class User
|
15
|
+
|
16
|
+
attr_reader :id, :email, :name
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
options.each do |k,v|
|
20
|
+
instance_variable_set(:"@#{k}", v)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
MOCK_USERS = [
|
25
|
+
{:id => 1, :email => "ben@intercom.io", :name => "Ben McRedmond"},
|
26
|
+
{:id => 2, :email => "ciaran@intercom.io", :name => "Ciaran Lee"}
|
27
|
+
]
|
28
|
+
|
29
|
+
def self.find_in_batches(*args)
|
30
|
+
yield(MOCK_USERS.map {|u| new(u)})
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all
|
34
|
+
MOCK_USERS.map { |u| new(u) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.first
|
38
|
+
new(MOCK_USERS.first)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.<(other)
|
42
|
+
other == ActiveRecord::Base
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
module ImportTest
|
48
|
+
|
49
|
+
def setup
|
50
|
+
super
|
51
|
+
IntercomRails.config.stub(:api_key).and_return("abcd")
|
52
|
+
end
|
53
|
+
|
54
|
+
def teardown
|
55
|
+
super
|
56
|
+
Rails.rspec_reset
|
57
|
+
User.rspec_reset
|
58
|
+
IntercomRails::Import.rspec_reset
|
59
|
+
IntercomRails::Import.unstub_all_instance_methods
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'import_test_setup'
|
2
|
+
require 'sinatra/base'
|
3
|
+
|
4
|
+
class MockIntercom < Sinatra::Base
|
5
|
+
|
6
|
+
set :server, 'thin'
|
7
|
+
|
8
|
+
before do
|
9
|
+
content_type 'application/json'
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/health_check' do
|
13
|
+
content_type 'plain/text'
|
14
|
+
'hello world'
|
15
|
+
end
|
16
|
+
|
17
|
+
post '/all_successful' do
|
18
|
+
{:failed => []}.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
post '/one_failure' do
|
22
|
+
{:failed => ['ben@intercom.io']}.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
post '/bad_auth' do
|
26
|
+
status 403
|
27
|
+
{"error" => {"type" => "not_authenticated", "message" => "HTTP Basic: Access denied."}}.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
post '/500_error' do
|
31
|
+
status 500
|
32
|
+
{"error" => {"type" => "server_error", "message" => "Danger deploy, gone wrong?"}}.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class InterRunner < MiniTest::Unit
|
38
|
+
|
39
|
+
self.runner = self.new
|
40
|
+
|
41
|
+
def _run(*args)
|
42
|
+
@mock_intercom_pid = start_mock_intercom
|
43
|
+
super
|
44
|
+
ensure
|
45
|
+
Process.kill('INT', @mock_intercom_pid)
|
46
|
+
Process.wait(@mock_intercom_pid) rescue SystemError
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def start_mock_intercom
|
52
|
+
pid = fork do
|
53
|
+
MockIntercom.run!(:port => 46837) do |server|
|
54
|
+
server.silent = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
response = nil
|
59
|
+
uri = URI.parse("http://localhost:46837/health_check")
|
60
|
+
|
61
|
+
begin
|
62
|
+
response = Net::HTTP.get_response(uri).body until(response == 'hello world')
|
63
|
+
rescue Errno::ECONNREFUSED
|
64
|
+
sleep(0.5)
|
65
|
+
retry
|
66
|
+
end
|
67
|
+
|
68
|
+
pid
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
class ImportNetworkTest < InterRunner::TestCase
|
74
|
+
|
75
|
+
include ImportTest
|
76
|
+
|
77
|
+
def api_path=(path)
|
78
|
+
IntercomRails::Import.stub(:bulk_create_api_endpoint) {
|
79
|
+
URI.parse("http://localhost:46837/#{path}")
|
80
|
+
}
|
81
|
+
|
82
|
+
@import = IntercomRails::Import.new
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_empty_failed
|
86
|
+
self.api_path = '/all_successful'
|
87
|
+
|
88
|
+
@import.run
|
89
|
+
assert_equal [], @import.failed
|
90
|
+
assert_equal 2, @import.total_sent
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_sets_failed_correctly
|
94
|
+
self.api_path = '/one_failure'
|
95
|
+
|
96
|
+
@import.run
|
97
|
+
assert_equal ["ben@intercom.io"], @import.failed
|
98
|
+
assert_equal 2, @import.total_sent
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_raises_import_error_on_bad_auth
|
102
|
+
self.api_path = '/bad_auth'
|
103
|
+
|
104
|
+
exception = assert_raises(IntercomRails::ImportError) {
|
105
|
+
@import.run
|
106
|
+
}
|
107
|
+
|
108
|
+
assert_equal "App ID or API Key are incorrect, please check them in config/initializers/intercom.rb", exception.message
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_throws_exception_when_intercom_api_is_being_a_dick
|
112
|
+
self.api_path = '/500_error'
|
113
|
+
|
114
|
+
exception = assert_raises(IntercomRails::IntercomAPIError) {
|
115
|
+
@import.run
|
116
|
+
}
|
117
|
+
|
118
|
+
assert_equal "The Intercom API request failed with the code: 500, after 3 attempts.", exception.message
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|