intercom_export 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module IntercomExport
2
+ VERSION = '0.0.1'
3
+ 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