intercom_export 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +7 -0
- data/README.md +44 -0
- data/bin/intercom_export +5 -0
- data/lib/intercom_export/cli.rb +126 -0
- data/lib/intercom_export/coordinator.rb +47 -0
- data/lib/intercom_export/differ/intercom_zendesk.rb +109 -0
- data/lib/intercom_export/executor/dry_run.rb +19 -0
- data/lib/intercom_export/executor/zendesk.rb +70 -0
- data/lib/intercom_export/finder/intercom_zendesk.rb +42 -0
- data/lib/intercom_export/listener/std.rb +19 -0
- data/lib/intercom_export/model/intercom_admin.rb +21 -0
- data/lib/intercom_export/model/intercom_conversation.rb +28 -0
- data/lib/intercom_export/model/intercom_user.rb +21 -0
- data/lib/intercom_export/model/zendesk_ticket.rb +42 -0
- data/lib/intercom_export/model/zendesk_user.rb +45 -0
- data/lib/intercom_export/reference.rb +18 -0
- data/lib/intercom_export/source/intercom_conversations.rb +19 -0
- data/lib/intercom_export/splitter/intercom.rb +81 -0
- data/lib/intercom_export/version.rb +3 -0
- data/spec/intercom_export/cli_spec.rb +94 -0
- data/spec/intercom_export/coordinator_spec.rb +63 -0
- data/spec/intercom_export/differ/intercom_zendesk_spec.rb +226 -0
- data/spec/intercom_export/executor/zendesk_spec.rb +99 -0
- data/spec/intercom_export/finder/intercom_zendesk_spec.rb +89 -0
- data/spec/intercom_export/splitter/intercom_spec.rb +162 -0
- data/spec/spec_helper.rb +18 -0
- metadata +168 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module IntercomExport
|
2
|
+
module Listener
|
3
|
+
class Std
|
4
|
+
def initialize(stdout: STDOUT, stderr: STDERR)
|
5
|
+
@stdout = stdout
|
6
|
+
@stderr = stderr
|
7
|
+
end
|
8
|
+
|
9
|
+
def executing(command)
|
10
|
+
puts "Importing: #{command.inspect}"
|
11
|
+
puts
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :stdout, :stderr
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'intercom_export/reference'
|
2
|
+
|
3
|
+
require 'virtus'
|
4
|
+
|
5
|
+
module IntercomExport
|
6
|
+
module Model
|
7
|
+
class IntercomAdmin
|
8
|
+
include Virtus.value_object
|
9
|
+
|
10
|
+
values do
|
11
|
+
attribute :id, Integer
|
12
|
+
attribute :name, String
|
13
|
+
attribute :email, String
|
14
|
+
end
|
15
|
+
|
16
|
+
def reference
|
17
|
+
IntercomExport::Reference.new("intercom-admin-#{id}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'intercom_export/reference'
|
2
|
+
|
3
|
+
require 'virtus'
|
4
|
+
|
5
|
+
module IntercomExport
|
6
|
+
module Model
|
7
|
+
class IntercomConversation
|
8
|
+
include Virtus.value_object
|
9
|
+
|
10
|
+
values do
|
11
|
+
attribute :id, String
|
12
|
+
attribute :name, String
|
13
|
+
attribute :user
|
14
|
+
attribute :assignee
|
15
|
+
attribute :conversation_message, Hash[Symbol => Object]
|
16
|
+
attribute :created_at
|
17
|
+
attribute :updated_at
|
18
|
+
attribute :tags
|
19
|
+
attribute :open
|
20
|
+
attribute :conversation_parts, Array[Hash[Symbol => Object]]
|
21
|
+
end
|
22
|
+
|
23
|
+
def reference
|
24
|
+
IntercomExport::Reference.new("intercom-conversation-#{id}")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'intercom_export/reference'
|
2
|
+
|
3
|
+
require 'virtus'
|
4
|
+
|
5
|
+
module IntercomExport
|
6
|
+
module Model
|
7
|
+
class IntercomUser
|
8
|
+
include Virtus.value_object
|
9
|
+
|
10
|
+
values do
|
11
|
+
attribute :id, String
|
12
|
+
attribute :name, String
|
13
|
+
attribute :email, String
|
14
|
+
end
|
15
|
+
|
16
|
+
def reference
|
17
|
+
IntercomExport::Reference.new("intercom-user-#{id}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module IntercomExport
|
4
|
+
module Model
|
5
|
+
class ZendeskTicket
|
6
|
+
include Virtus.value_object
|
7
|
+
|
8
|
+
values do
|
9
|
+
attribute :id, Integer
|
10
|
+
attribute :url, String
|
11
|
+
attribute :external_id, String
|
12
|
+
attribute :type, String
|
13
|
+
attribute :subject, String
|
14
|
+
attribute :raw_subject, String
|
15
|
+
attribute :description, String
|
16
|
+
attribute :priority, String
|
17
|
+
attribute :status, String
|
18
|
+
attribute :recipient, String
|
19
|
+
attribute :requester_id, Integer
|
20
|
+
attribute :submitter_id, Integer
|
21
|
+
attribute :assignee_id, Integer
|
22
|
+
attribute :organization_id, Integer
|
23
|
+
attribute :group_id, Integer
|
24
|
+
attribute :collaborator_ids, Array
|
25
|
+
attribute :forum_topic_id, Integer
|
26
|
+
attribute :problem_id, Integer
|
27
|
+
attribute :has_incidents, Boolean
|
28
|
+
attribute :due_at, Time
|
29
|
+
attribute :tags, Array
|
30
|
+
attribute :via
|
31
|
+
attribute :custom_fields, Array
|
32
|
+
attribute :satisfaction_rating
|
33
|
+
attribute :sharing_agreement_ids, Array
|
34
|
+
attribute :followup_ids, Array
|
35
|
+
attribute :ticket_form_id, Integer
|
36
|
+
attribute :brand_id, Integer
|
37
|
+
attribute :created_at, Time
|
38
|
+
attribute :updated_at, Time
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'intercom_export/reference'
|
2
|
+
require 'virtus'
|
3
|
+
module IntercomExport
|
4
|
+
module Model
|
5
|
+
class ZendeskUser
|
6
|
+
include Virtus.value_object
|
7
|
+
|
8
|
+
values do
|
9
|
+
attribute :id, Integer
|
10
|
+
attribute :url, String
|
11
|
+
attribute :name, String
|
12
|
+
attribute :email, String
|
13
|
+
attribute :created_at, Time
|
14
|
+
attribute :updated_at, Time
|
15
|
+
attribute :time_zone, String
|
16
|
+
attribute :phone
|
17
|
+
attribute :photo
|
18
|
+
attribute :locale_id, Integer
|
19
|
+
attribute :locale, String
|
20
|
+
attribute :organization_id
|
21
|
+
attribute :role, String
|
22
|
+
attribute :verified, Boolean
|
23
|
+
attribute :external_id
|
24
|
+
attribute :tags, Array[String]
|
25
|
+
attribute :alias, String
|
26
|
+
attribute :active, Boolean
|
27
|
+
attribute :shared, Boolean
|
28
|
+
attribute :shared_agent, Boolean
|
29
|
+
attribute :last_login_at, Time
|
30
|
+
attribute :two_factor_auth_enabled
|
31
|
+
attribute :signature, String
|
32
|
+
attribute :details, String
|
33
|
+
attribute :notes, String
|
34
|
+
attribute :custom_role_id
|
35
|
+
attribute :moderator, Boolean
|
36
|
+
attribute :ticket_restriction
|
37
|
+
attribute :only_private_comments, Boolean
|
38
|
+
attribute :restricted_agent, Boolean
|
39
|
+
attribute :suspended, Boolean
|
40
|
+
attribute :chat_only, Boolean
|
41
|
+
attribute :user_fields, Hash
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module IntercomExport
|
2
|
+
class Reference
|
3
|
+
def initialize(value)
|
4
|
+
@value = value
|
5
|
+
end
|
6
|
+
|
7
|
+
def ==(other)
|
8
|
+
other.is_a?(self.class) && value == other.value
|
9
|
+
end
|
10
|
+
alias_method :eql?, :==
|
11
|
+
|
12
|
+
def hash
|
13
|
+
self.value.hash
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :value
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module IntercomExport
|
2
|
+
module Source
|
3
|
+
class IntercomConversations
|
4
|
+
def initialize(client)
|
5
|
+
@client = client
|
6
|
+
end
|
7
|
+
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def each(&block)
|
11
|
+
client.conversations.find_all({}).lazy.map { |c| client.conversations.find(id: c.id) }.each(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :client
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'intercom_export/reference'
|
2
|
+
require 'intercom_export/model/intercom_conversation'
|
3
|
+
require 'intercom_export/model/intercom_user'
|
4
|
+
require 'intercom_export/model/intercom_admin'
|
5
|
+
|
6
|
+
module IntercomExport
|
7
|
+
module Splitter
|
8
|
+
class Intercom
|
9
|
+
def initialize(client)
|
10
|
+
@client = client
|
11
|
+
end
|
12
|
+
|
13
|
+
def split(conversation)
|
14
|
+
conversation_hash = flatten(conversation)
|
15
|
+
|
16
|
+
dependencies_for_conversation!(conversation_hash) + [
|
17
|
+
IntercomExport::Model::IntercomConversation.new(conversation_hash)
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :client
|
24
|
+
|
25
|
+
def flatten(conversation)
|
26
|
+
hash = conversation.to_hash
|
27
|
+
hash['conversation_parts'] = hash.fetch('conversation_parts').map { |p| p.to_hash }
|
28
|
+
hash['conversation_message'] = hash.fetch('conversation_message').to_hash
|
29
|
+
hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def model_for(object)
|
33
|
+
case object
|
34
|
+
when ::Intercom::User
|
35
|
+
load_user(object)
|
36
|
+
when ::Intercom::Admin
|
37
|
+
load_admin(object)
|
38
|
+
when ::Intercom::NobodyAdmin
|
39
|
+
nil
|
40
|
+
else
|
41
|
+
raise "Unrecognised object #{object.inspect}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_user(object)
|
46
|
+
IntercomExport::Model::IntercomUser.new(client.users.find(id: object.id).to_hash)
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_admin(object)
|
50
|
+
admin = client.admins.all.find { |a| a.id == Integer(object.id) }
|
51
|
+
return unless admin
|
52
|
+
IntercomExport::Model::IntercomAdmin.new(admin.to_hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
def replace_reference!(hash, key)
|
56
|
+
value = hash[key]
|
57
|
+
return nil unless value
|
58
|
+
model = model_for(value)
|
59
|
+
hash[key] = model && model.reference
|
60
|
+
model
|
61
|
+
end
|
62
|
+
|
63
|
+
def dependencies_for_conversation!(conversation_hash)
|
64
|
+
dependencies = []
|
65
|
+
|
66
|
+
conversation_hash['conversation_parts'] = conversation_hash.fetch('conversation_parts').select { |p|
|
67
|
+
p.fetch('body', nil)
|
68
|
+
}.map { |part|
|
69
|
+
dependencies.push(replace_reference!(part, 'assigned_to'))
|
70
|
+
dependencies.push(replace_reference!(part, 'author'))
|
71
|
+
part
|
72
|
+
}
|
73
|
+
|
74
|
+
dependencies.push(replace_reference!(conversation_hash, 'user'))
|
75
|
+
dependencies.push(replace_reference!(conversation_hash, 'assignee'))
|
76
|
+
dependencies.push(replace_reference!(conversation_hash.fetch('conversation_message'), 'author'))
|
77
|
+
dependencies.compact.uniq
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'intercom_export/cli'
|
3
|
+
require 'intercom_export/coordinator'
|
4
|
+
|
5
|
+
RSpec.describe IntercomExport::Cli do
|
6
|
+
|
7
|
+
let(:coordinator_class) { class_double(IntercomExport::Coordinator) }
|
8
|
+
let(:expected_coordinator) { instance_double(IntercomExport::Coordinator) }
|
9
|
+
|
10
|
+
subject do
|
11
|
+
IntercomExport::Cli.new(
|
12
|
+
'the_program_name',
|
13
|
+
[
|
14
|
+
'--intercom-app-id', 'foobar',
|
15
|
+
'--intercom-api-key', 'asdf',
|
16
|
+
'--zendesk-address', 'example.zendesk.com',
|
17
|
+
'--zendesk-username', 'admin@example.com',
|
18
|
+
'--zendesk-token', 'secret'
|
19
|
+
],
|
20
|
+
coordinator_class: coordinator_class
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#run' do
|
25
|
+
let(:expected_intercom_client) { instance_double(Intercom::Client) }
|
26
|
+
let(:expected_zendesk_client) { instance_double(ZendeskAPI::Client) }
|
27
|
+
let(:expected_intercom_conversation_source) {
|
28
|
+
instance_double(IntercomExport::Source::IntercomConversations)
|
29
|
+
}
|
30
|
+
let(:expected_intercom_splitter) { instance_double(IntercomExport::Splitter::Intercom) }
|
31
|
+
let(:expected_intercom_zendesk_finder) { instance_double(IntercomExport::Finder::IntercomZendesk) }
|
32
|
+
let(:expected_intercom_zendesk_differ) { instance_double(IntercomExport::Differ::IntercomZendesk) }
|
33
|
+
let(:expected_zendesk_executor) { instance_double(IntercomExport::Executor::Zendesk) }
|
34
|
+
|
35
|
+
before do
|
36
|
+
allow(Intercom::Client).to receive(:new)
|
37
|
+
.with(app_id: 'foobar', api_key: 'asdf')
|
38
|
+
.and_return(expected_intercom_client)
|
39
|
+
|
40
|
+
allow(ZendeskAPI::Client).to receive(:new)
|
41
|
+
.and_return(expected_zendesk_client)
|
42
|
+
|
43
|
+
allow(IntercomExport::Source::IntercomConversations).to receive(:new)
|
44
|
+
.with(expected_intercom_client)
|
45
|
+
.and_return(expected_intercom_conversation_source)
|
46
|
+
|
47
|
+
allow(IntercomExport::Splitter::Intercom).to receive(:new)
|
48
|
+
.with(expected_intercom_client)
|
49
|
+
.and_return(expected_intercom_splitter)
|
50
|
+
|
51
|
+
allow(IntercomExport::Finder::IntercomZendesk).to receive(:new)
|
52
|
+
.with(expected_zendesk_client)
|
53
|
+
.and_return(expected_intercom_zendesk_finder)
|
54
|
+
|
55
|
+
allow(IntercomExport::Differ::IntercomZendesk).to receive(:new)
|
56
|
+
.and_return(expected_intercom_zendesk_differ)
|
57
|
+
|
58
|
+
allow(IntercomExport::Executor::Zendesk).to receive(:new)
|
59
|
+
.with(expected_zendesk_client, anything)
|
60
|
+
.and_return(expected_zendesk_executor)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'takes the arguments and initializes the coordinator' do
|
64
|
+
allow(coordinator_class).to receive(:new).with(
|
65
|
+
source: expected_intercom_conversation_source,
|
66
|
+
splitter: expected_intercom_splitter,
|
67
|
+
finder: expected_intercom_zendesk_finder,
|
68
|
+
differ: expected_intercom_zendesk_differ,
|
69
|
+
executor: expected_zendesk_executor
|
70
|
+
).and_return(expected_coordinator)
|
71
|
+
|
72
|
+
expect(expected_coordinator).to receive(:run)
|
73
|
+
|
74
|
+
subject.run
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'when the required arguments arent specified' do
|
78
|
+
let(:stderr) { StringIO.new }
|
79
|
+
|
80
|
+
subject do
|
81
|
+
IntercomExport::Cli.new(
|
82
|
+
'the_program_name', ['--intercom-app-id', 'foobar'],
|
83
|
+
stderr: stderr
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'prints the help' do
|
88
|
+
subject.run
|
89
|
+
|
90
|
+
expect(stderr.tap(&:rewind).read.length).to be > 10
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'intercom_export/coordinator'
|
3
|
+
|
4
|
+
require 'intercom'
|
5
|
+
require 'zendesk_api'
|
6
|
+
|
7
|
+
RSpec.describe IntercomExport::Coordinator do
|
8
|
+
let(:source) { ['a thing'] }
|
9
|
+
let(:splitter) { double('splitter') }
|
10
|
+
let(:finder) { double('finder') }
|
11
|
+
let(:differ) { double('differ') }
|
12
|
+
let(:executor) { double('executor') }
|
13
|
+
|
14
|
+
describe '#run' do
|
15
|
+
let(:expected_parts) { [double('part1', id: 1), double('part2', id: 2)] }
|
16
|
+
let(:expected_find_for_part2) { double('part2-found1') }
|
17
|
+
let(:expected_commands_for_part1) { double('part1-diffs') }
|
18
|
+
let(:expected_commands_for_part2) { double('part2-diffs') }
|
19
|
+
|
20
|
+
before do
|
21
|
+
allow(splitter).to receive(:split).with('a thing').and_return(expected_parts)
|
22
|
+
|
23
|
+
allow(finder).to receive(:find).with(expected_parts[0]).and_return(nil)
|
24
|
+
allow(finder).to receive(:find).with(expected_parts[1]).and_return(expected_find_for_part2)
|
25
|
+
|
26
|
+
allow(differ).to receive(:diff).with(
|
27
|
+
expected_parts[0], nil
|
28
|
+
).and_return(expected_commands_for_part1)
|
29
|
+
|
30
|
+
allow(differ).to receive(:diff).with(
|
31
|
+
expected_parts[1], expected_find_for_part2
|
32
|
+
).and_return(expected_commands_for_part2)
|
33
|
+
end
|
34
|
+
|
35
|
+
subject do
|
36
|
+
IntercomExport::Coordinator.new(
|
37
|
+
source: source, splitter: splitter, finder: finder, differ: differ, executor: executor
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sends the commands to the correct executor' do
|
42
|
+
expect(executor).to receive(:call).with(expected_commands_for_part1).ordered
|
43
|
+
expect(executor).to receive(:call).with(expected_commands_for_part2).ordered
|
44
|
+
|
45
|
+
subject.run
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with duplicate items' do
|
49
|
+
let(:source) { ['a thing', 'a similar thing'] }
|
50
|
+
|
51
|
+
before do
|
52
|
+
allow(splitter).to receive(:split).with('a similar thing').and_return(expected_parts)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'does minimum number of commands' do
|
56
|
+
expect(executor).to receive(:call).with(expected_commands_for_part1).ordered
|
57
|
+
expect(executor).to receive(:call).with(expected_commands_for_part2).ordered
|
58
|
+
|
59
|
+
subject.run
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|