cogniteev-intercom 2.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +13 -0
  5. data/MIT-LICENSE +21 -0
  6. data/README.md +378 -0
  7. data/Rakefile +21 -0
  8. data/changes.txt +168 -0
  9. data/intercom.gemspec +28 -0
  10. data/lib/data/cacert.pem +3965 -0
  11. data/lib/ext/sliceable_hash.rb +16 -0
  12. data/lib/intercom.rb +176 -0
  13. data/lib/intercom/admin.rb +9 -0
  14. data/lib/intercom/api_operations/convert.rb +19 -0
  15. data/lib/intercom/api_operations/count.rb +16 -0
  16. data/lib/intercom/api_operations/delete.rb +15 -0
  17. data/lib/intercom/api_operations/find.rb +23 -0
  18. data/lib/intercom/api_operations/find_all.rb +33 -0
  19. data/lib/intercom/api_operations/list.rb +17 -0
  20. data/lib/intercom/api_operations/load.rb +16 -0
  21. data/lib/intercom/api_operations/save.rb +51 -0
  22. data/lib/intercom/collection_proxy.rb +71 -0
  23. data/lib/intercom/company.rb +29 -0
  24. data/lib/intercom/contact.rb +22 -0
  25. data/lib/intercom/conversation.rb +17 -0
  26. data/lib/intercom/count.rb +21 -0
  27. data/lib/intercom/errors.rb +61 -0
  28. data/lib/intercom/event.rb +11 -0
  29. data/lib/intercom/extended_api_operations/reply.rb +16 -0
  30. data/lib/intercom/extended_api_operations/tags.rb +14 -0
  31. data/lib/intercom/extended_api_operations/users.rb +17 -0
  32. data/lib/intercom/generic_handlers/base_handler.rb +22 -0
  33. data/lib/intercom/generic_handlers/count.rb +59 -0
  34. data/lib/intercom/generic_handlers/tag.rb +71 -0
  35. data/lib/intercom/generic_handlers/tag_find_all.rb +47 -0
  36. data/lib/intercom/lib/dynamic_accessors.rb +59 -0
  37. data/lib/intercom/lib/dynamic_accessors_on_method_missing.rb +53 -0
  38. data/lib/intercom/lib/flat_store.rb +31 -0
  39. data/lib/intercom/lib/typed_json_deserializer.rb +53 -0
  40. data/lib/intercom/message.rb +9 -0
  41. data/lib/intercom/note.rb +17 -0
  42. data/lib/intercom/notification.rb +20 -0
  43. data/lib/intercom/request.rb +166 -0
  44. data/lib/intercom/segment.rb +14 -0
  45. data/lib/intercom/subscription.rb +15 -0
  46. data/lib/intercom/tag.rb +23 -0
  47. data/lib/intercom/traits/api_resource.rb +132 -0
  48. data/lib/intercom/traits/dirty_tracking.rb +33 -0
  49. data/lib/intercom/traits/generic_handler_binding.rb +29 -0
  50. data/lib/intercom/traits/incrementable_attributes.rb +12 -0
  51. data/lib/intercom/user.rb +30 -0
  52. data/lib/intercom/utils.rb +62 -0
  53. data/lib/intercom/version.rb +3 -0
  54. data/spec/spec_helper.rb +308 -0
  55. data/spec/unit/intercom/admin_spec.rb +9 -0
  56. data/spec/unit/intercom/collection_proxy_spec.rb +34 -0
  57. data/spec/unit/intercom/company_spec.rb +23 -0
  58. data/spec/unit/intercom/contact_spec.rb +25 -0
  59. data/spec/unit/intercom/event_spec.rb +25 -0
  60. data/spec/unit/intercom/lib/flat_store_spec.rb +29 -0
  61. data/spec/unit/intercom/message_spec.rb +21 -0
  62. data/spec/unit/intercom/note_spec.rb +19 -0
  63. data/spec/unit/intercom/notification_spec.rb +68 -0
  64. data/spec/unit/intercom/request_spec.rb +16 -0
  65. data/spec/unit/intercom/subscription_spec.rb +18 -0
  66. data/spec/unit/intercom/tag_spec.rb +23 -0
  67. data/spec/unit/intercom/traits/api_resource_spec.rb +85 -0
  68. data/spec/unit/intercom/user_spec.rb +230 -0
  69. data/spec/unit/intercom_spec.rb +90 -0
  70. metadata +214 -0
@@ -0,0 +1,53 @@
1
+ module Intercom
2
+ module Lib
3
+ class DynamicAccessorsOnMethodMissing
4
+
5
+ attr_reader :method_sym, :method_string, :arguments, :object, :klass
6
+
7
+ def initialize(method_sym, *arguments, object)
8
+ @method_sym = method_sym
9
+ @method_string = method_sym.to_s
10
+ @arguments = arguments
11
+ @klass = object.class
12
+ @object = object
13
+ end
14
+
15
+ def define_accessors_or_call(&block)
16
+ return yield if not_an_accessor?
17
+ if setter?
18
+ Lib::DynamicAccessors.define_accessors(attribute_name, *arguments, object)
19
+ object.send(method_sym, *arguments)
20
+ else # getter
21
+ if trying_to_access_private_variable?
22
+ yield
23
+ else
24
+ raise Intercom::AttributeNotSetError, attribute_not_set_error_message
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def not_an_accessor?
32
+ (method_string.end_with? '?') || (method_string.end_with? '!') || arguments.length > 1
33
+ end
34
+
35
+ def setter?
36
+ method_string.end_with? '='
37
+ end
38
+
39
+ def attribute_name
40
+ method_string.gsub(/=$/, '')
41
+ end
42
+
43
+ def trying_to_access_private_variable?
44
+ object.instance_variable_defined?("@#{method_string}")
45
+ end
46
+
47
+ def attribute_not_set_error_message
48
+ "'#{method_string}' called on #{klass} but it has not been set an " +
49
+ "attribute or does not exist as a method"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module Intercom
2
+ module Lib
3
+
4
+ # Sub-class of {Hash} for storing custom data attributes.
5
+ # Doesn't allow nested Hashes or Arrays. And requires {String} or {Symbol} keys.
6
+ class FlatStore < Hash
7
+
8
+ def initialize(attributes={})
9
+ (attributes).each do |key, value|
10
+ validate_key_and_value(key, value)
11
+ self[key] = value
12
+ end
13
+ end
14
+
15
+ def []=(key, value)
16
+ validate_key_and_value(key, value)
17
+ super(key.to_s, value)
18
+ end
19
+
20
+ def [](key)
21
+ super(key.to_s)
22
+ end
23
+
24
+ private
25
+ def validate_key_and_value(key, value)
26
+ raise ArgumentError.new("This does not support nested data structures (key: #{key}, value: #{value}") if value.is_a?(Array) || value.is_a?(Hash)
27
+ raise ArgumentError.new("Key must be String or Symbol: #{key}") unless key.is_a?(String) || key.is_a?(Symbol)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ module Intercom
2
+ module Lib
3
+
4
+ # Responsibility: To decide whether we are deserializing a collection or an
5
+ # entity of a particular type and to dispatch deserialization
6
+ class TypedJsonDeserializer
7
+ attr_reader :json
8
+
9
+ def initialize(json)
10
+ @json = json
11
+ end
12
+
13
+ def deserialize
14
+ if blank_object_type?(object_type)
15
+ raise DeserializationError, "No type field was found to facilitate deserialization"
16
+ elsif list_object_type?(object_type)
17
+ deserialize_collection(json[object_entity_key])
18
+ else # singular object type
19
+ deserialize_object(json)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def blank_object_type?(object_type)
26
+ object_type.nil? || object_type == ''
27
+ end
28
+
29
+ def list_object_type?(object_type)
30
+ object_type.end_with?('.list')
31
+ end
32
+
33
+ def deserialize_collection(collection_json)
34
+ return [] if collection_json == nil
35
+ collection_json.map { |item_json| TypedJsonDeserializer.new(item_json).deserialize }
36
+ end
37
+
38
+ def deserialize_object(object_json)
39
+ entity_class = Utils.constantize_singular_resource_name(object_entity_key)
40
+ entity_class.from_api(object_json)
41
+ end
42
+
43
+ def object_type
44
+ @object_type ||= json['type']
45
+ end
46
+
47
+ def object_entity_key
48
+ @object_entity_key ||= Utils.entity_key_from_type(object_type)
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ require 'intercom/api_operations/save'
2
+ require 'intercom/traits/api_resource'
3
+
4
+ module Intercom
5
+ class Message
6
+ include ApiOperations::Save
7
+ include Traits::ApiResource
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ require 'intercom/api_operations/save'
2
+ require 'intercom/api_operations/list'
3
+ require 'intercom/api_operations/find_all'
4
+ require 'intercom/api_operations/find'
5
+ require 'intercom/api_operations/load'
6
+ require 'intercom/traits/api_resource'
7
+
8
+ module Intercom
9
+ class Note
10
+ include ApiOperations::Save
11
+ include ApiOperations::List
12
+ include ApiOperations::FindAll
13
+ include ApiOperations::Find
14
+ include ApiOperations::Load
15
+ include Traits::ApiResource
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ require 'intercom/traits/api_resource'
2
+
3
+ module Intercom
4
+ class Notification
5
+ include Traits::ApiResource
6
+
7
+ def model
8
+ data.item
9
+ end
10
+
11
+ def model_type
12
+ model.class
13
+ end
14
+
15
+ def load
16
+ model.load
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,166 @@
1
+ require 'cgi'
2
+ require 'net/https'
3
+
4
+ module Intercom
5
+ class Request
6
+ attr_accessor :path, :net_http_method
7
+
8
+ def initialize(path, net_http_method)
9
+ self.path = path
10
+ self.net_http_method = net_http_method
11
+ end
12
+
13
+ def set_common_headers(method, base_uri)
14
+ method.basic_auth(CGI.unescape(base_uri.user), CGI.unescape(base_uri.password))
15
+ method.add_field('AcceptEncoding', 'gzip, deflate')
16
+ end
17
+
18
+ def self.get(path, params)
19
+ new(path, Net::HTTP::Get.new(append_query_string_to_url(path, params), default_headers))
20
+ end
21
+
22
+ def self.post(path, form_data)
23
+ new(path, method_with_body(Net::HTTP::Post, path, form_data))
24
+ end
25
+
26
+ def self.delete(path, params)
27
+ new(path, method_with_body(Net::HTTP::Delete, path, params))
28
+ end
29
+
30
+ def self.put(path, form_data)
31
+ new(path, method_with_body(Net::HTTP::Put, path, form_data))
32
+ end
33
+
34
+ def self.method_with_body(http_method, path, params)
35
+ request = http_method.send(:new, path, default_headers)
36
+ request.body = params.to_json
37
+ request["Content-Type"] = "application/json"
38
+ request
39
+ end
40
+
41
+ def self.default_headers
42
+ {'Accept-Encoding' => 'gzip, deflate', 'Accept' => 'application/vnd.intercom.3+json', 'User-Agent' => "Intercom-Ruby/#{Intercom::VERSION}"}
43
+ end
44
+
45
+ def client(uri)
46
+ net = Net::HTTP.new(uri.host, uri.port)
47
+ if uri.is_a?(URI::HTTPS)
48
+ net.use_ssl = true
49
+ net.verify_mode = OpenSSL::SSL::VERIFY_PEER
50
+ net.ca_file = File.join(File.dirname(__FILE__), '../data/cacert.pem')
51
+ end
52
+ net.read_timeout = 90
53
+ net.open_timeout = 30
54
+ net
55
+ end
56
+
57
+ def execute(target_base_url=nil)
58
+ base_uri = URI.parse(target_base_url)
59
+ set_common_headers(net_http_method, base_uri)
60
+ begin
61
+ client(base_uri).start do |http|
62
+ begin
63
+ response = http.request(net_http_method)
64
+ set_rate_limit_details(response)
65
+ decoded_body = decode_body(response)
66
+ parsed_body = parse_body(decoded_body, response)
67
+ raise_errors_on_failure(response)
68
+ parsed_body
69
+ rescue Timeout::Error
70
+ raise Intercom::ServiceUnavailableError.new('Service Unavailable [request timed out]')
71
+ end
72
+ end
73
+ rescue Timeout::Error
74
+ raise Intercom::ServiceConnectionError.new('Failed to connect to service [connection attempt timed out]')
75
+ end
76
+ end
77
+
78
+ def decode_body(response)
79
+ decode(response['content-encoding'], response.body)
80
+ end
81
+
82
+ def parse_body(decoded_body, response)
83
+ parsed_body = nil
84
+ return parsed_body if decoded_body.nil? || decoded_body.strip.empty?
85
+ begin
86
+ parsed_body = JSON.parse(decoded_body)
87
+ rescue JSON::ParserError => _
88
+ raise_errors_on_failure(response)
89
+ end
90
+ raise_application_errors_on_failure(parsed_body, response.code.to_i) if parsed_body['type'] == 'error.list'
91
+ parsed_body
92
+ end
93
+
94
+ def set_rate_limit_details(response)
95
+ rate_limit_details = {}
96
+ rate_limit_details[:limit] = response['X-RateLimit-Limit'].to_i if response['X-RateLimit-Limit']
97
+ rate_limit_details[:remaining] = response['X-RateLimit-Remaining'].to_i if response['X-RateLimit-Remaining']
98
+ rate_limit_details[:reset_at] = Time.at(response['X-RateLimit-Reset'].to_i) if response['X-RateLimit-Reset']
99
+ Intercom.rate_limit_details = rate_limit_details
100
+ end
101
+
102
+ def decode(content_encoding, body)
103
+ return body if (!body) || body.empty? || content_encoding != 'gzip'
104
+ Zlib::GzipReader.new(StringIO.new(body)).read
105
+ end
106
+
107
+ def raise_errors_on_failure(res)
108
+ if res.code.to_i.eql?(404)
109
+ raise Intercom::ResourceNotFound.new('Resource Not Found')
110
+ elsif res.code.to_i.eql?(401)
111
+ raise Intercom::AuthenticationError.new('Unauthorized')
112
+ elsif res.code.to_i.eql?(403)
113
+ raise Intercom::AuthenticationError.new('Forbidden')
114
+ elsif res.code.to_i.eql?(500)
115
+ raise Intercom::ServerError.new('Server Error')
116
+ elsif res.code.to_i.eql?(502)
117
+ raise Intercom::BadGatewayError.new('Bad Gateway Error')
118
+ elsif res.code.to_i.eql?(503)
119
+ raise Intercom::ServiceUnavailableError.new('Service Unavailable')
120
+ end
121
+ end
122
+
123
+ def raise_application_errors_on_failure(error_list_details, http_code)
124
+ # Currently, we don't support multiple errors
125
+ error_details = error_list_details['errors'].first
126
+ error_code = error_details['type'] || error_details['code']
127
+ parsed_http_code = (http_code > 0 ? http_code : nil)
128
+ error_context = {
129
+ :http_code => parsed_http_code,
130
+ :application_error_code => error_code
131
+ }
132
+ case error_code
133
+ when 'unauthorized', 'forbidden'
134
+ raise Intercom::AuthenticationError.new(error_details['message'], error_context)
135
+ when "bad_request", "missing_parameter", 'parameter_invalid'
136
+ raise Intercom::BadRequestError.new(error_details['message'], error_context)
137
+ when "not_found"
138
+ raise Intercom::ResourceNotFound.new(error_details['message'], error_context)
139
+ when "rate_limit_exceeded"
140
+ raise Intercom::RateLimitExceeded.new(error_details['message'], error_context)
141
+ when 'service_unavailable'
142
+ raise Intercom::ServiceUnavailableError.new(error_details['message'], error_context)
143
+ when 'conflict'
144
+ raise Intercom::MultipleMatchingUsersError.new(error_details['message'], error_context)
145
+ when nil, ''
146
+ raise Intercom::UnexpectedError.new(message_for_unexpected_error_without_type(error_details, parsed_http_code), error_context)
147
+ else
148
+ raise Intercom::UnexpectedError.new(message_for_unexpected_error_with_type(error_details, parsed_http_code), error_context)
149
+ end
150
+ end
151
+
152
+ def message_for_unexpected_error_with_type(error_details, parsed_http_code)
153
+ "The error of type '#{error_details['type']}' is not recognized. It occurred with the message: #{error_details['message']} and http_code: '#{parsed_http_code}'. Please contact Intercom with these details."
154
+ end
155
+
156
+ def message_for_unexpected_error_without_type(error_details, parsed_http_code)
157
+ "An unexpected error occured. It occurred with the message: #{error_details['message']} and http_code: '#{parsed_http_code}'. Please contact Intercom with these details."
158
+ end
159
+
160
+ def self.append_query_string_to_url(url, params)
161
+ return url if params.empty?
162
+ query_string = params.map { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
163
+ url + "?#{query_string}"
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,14 @@
1
+ require 'intercom/api_operations/count'
2
+ require 'intercom/api_operations/find'
3
+ require 'intercom/api_operations/save'
4
+ require 'intercom/traits/api_resource'
5
+
6
+ module Intercom
7
+ class Segment
8
+ include ApiOperations::List
9
+ include ApiOperations::Find
10
+ include ApiOperations::Save
11
+ include ApiOperations::Count
12
+ include Traits::ApiResource
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ require 'intercom/api_operations/list'
2
+ require 'intercom/api_operations/find_all'
3
+ require 'intercom/api_operations/save'
4
+ require 'intercom/api_operations/delete'
5
+ require 'intercom/traits/api_resource'
6
+
7
+ module Intercom
8
+ class Subscription
9
+ include ApiOperations::List
10
+ include ApiOperations::Find
11
+ include ApiOperations::Save
12
+ include ApiOperations::Delete
13
+ include Traits::ApiResource
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ require 'intercom/api_operations/count'
2
+ require 'intercom/api_operations/save'
3
+ require 'intercom/api_operations/list'
4
+ require 'intercom/api_operations/find'
5
+ require 'intercom/api_operations/find_all'
6
+ require 'intercom/traits/api_resource'
7
+ require 'intercom/traits/generic_handler_binding'
8
+ require 'intercom/generic_handlers/tag'
9
+ require 'intercom/generic_handlers/tag_find_all'
10
+
11
+ module Intercom
12
+ class Tag
13
+ include ApiOperations::Count
14
+ include ApiOperations::Save
15
+ include ApiOperations::List
16
+ include ApiOperations::Find
17
+ include ApiOperations::FindAll
18
+ include Traits::ApiResource
19
+ include Traits::GenericHandlerBinding
20
+ include GenericHandlers::Tag
21
+ include GenericHandlers::TagFindAll
22
+ end
23
+ end
@@ -0,0 +1,132 @@
1
+ require 'intercom/lib/flat_store'
2
+ require 'intercom/lib/dynamic_accessors'
3
+ require 'intercom/lib/dynamic_accessors_on_method_missing'
4
+ require 'intercom/traits/dirty_tracking'
5
+ require 'intercom/lib/typed_json_deserializer'
6
+
7
+ module Intercom
8
+ module Traits
9
+
10
+ module ApiResource
11
+ include DirtyTracking
12
+
13
+ attr_accessor :id
14
+
15
+ def initialize(attributes = {})
16
+ from_hash(attributes)
17
+ end
18
+
19
+ def from_response(response)
20
+ from_hash(response)
21
+ reset_changed_fields!
22
+ self
23
+ end
24
+
25
+ def from_hash(hash)
26
+ hash.each do |attribute, value|
27
+ next if type_field?(attribute)
28
+ initialize_property(attribute, value)
29
+ end
30
+ initialize_missing_flat_store_attributes if respond_to? :flat_store_attributes
31
+ self
32
+ end
33
+
34
+ def to_hash
35
+ instance_variables_excluding_dirty_tracking_field.inject({}) do |hash, variable|
36
+ hash[variable.to_s.delete("@")] = instance_variable_get(variable)
37
+ hash
38
+ end
39
+ end
40
+
41
+ def to_submittable_hash
42
+ submittable_hash = {}
43
+ to_hash.each do |attribute, value|
44
+ submittable_hash[attribute] = value if submittable_attribute?(attribute, value)
45
+ end
46
+ submittable_hash
47
+ end
48
+
49
+ def method_missing(method_sym, *arguments, &block)
50
+ Lib::DynamicAccessorsOnMethodMissing.new(method_sym, *arguments, self).
51
+ define_accessors_or_call { super }
52
+ end
53
+
54
+ def flat_store_attribute?(attribute)
55
+ (respond_to?(:flat_store_attributes)) && (flat_store_attributes.map(&:to_s).include?(attribute.to_s))
56
+ end
57
+
58
+ private
59
+
60
+ def initialize_property(attribute, value)
61
+ Lib::DynamicAccessors.define_accessors(attribute, value, self) unless accessors_already_defined?(attribute)
62
+ set_property(attribute, value)
63
+ end
64
+
65
+ def accessors_already_defined?(attribute)
66
+ respond_to?(attribute) && respond_to?("#{attribute}=")
67
+ end
68
+
69
+ def set_property(attribute, value)
70
+ if typed_value?(value) && !custom_attribute_field?(attribute) && !message_from_field?(attribute, value) && !message_to_field?(attribute, value)
71
+ value_to_set = Intercom::Lib::TypedJsonDeserializer.new(value).deserialize
72
+ elsif flat_store_attribute?(attribute)
73
+ value_to_set = Intercom::Lib::FlatStore.new(value)
74
+ else
75
+ value_to_set = value
76
+ end
77
+ call_setter_for_attribute(attribute, value_to_set)
78
+ end
79
+
80
+ def custom_attribute_field?(attribute)
81
+ attribute == 'custom_attributes'
82
+ end
83
+
84
+ def message_from_field?(attribute, value)
85
+ attribute.to_s == 'from' && value.is_a?(Hash) && value['type']
86
+ end
87
+
88
+ def message_to_field?(attribute, value)
89
+ attribute.to_s == 'to' && value.is_a?(Hash) && value['type']
90
+ end
91
+
92
+ def typed_value?(value)
93
+ value.is_a? Hash and !!value['type']
94
+ end
95
+
96
+ def call_setter_for_attribute(attribute, value)
97
+ setter_method = "#{attribute.to_s}="
98
+ self.send(setter_method, value)
99
+ end
100
+
101
+ def type_field?(attribute)
102
+ attribute == 'type'
103
+ end
104
+
105
+ def initialize_missing_flat_store_attributes
106
+ flat_store_attributes.each do |attribute|
107
+ unless instance_variables_excluding_dirty_tracking_field.map(&:to_s).include? "@#{attribute}"
108
+ initialize_property(attribute, {})
109
+ end
110
+ end
111
+ end
112
+
113
+ def submittable_attribute?(attribute, value)
114
+ # FlatStores always submitted, even if not changed, as we don't track their dirtyness
115
+ value.is_a?(Intercom::Lib::FlatStore) || field_changed?(attribute)
116
+ end
117
+
118
+ module ClassMethods
119
+ def from_api(api_response)
120
+ object = self.new
121
+ object.from_response(api_response)
122
+ object
123
+ end
124
+ end
125
+
126
+ def self.included(base)
127
+ base.extend(ClassMethods)
128
+ end
129
+
130
+ end
131
+ end
132
+ end