cogniteev-intercom 2.5.4

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 (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