diff --git a/README.md b/README.md index 09ef9279..8ad928a4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,26 @@ # intercom-ruby -Ruby bindings for the Intercom API (https://developers.intercom.io/reference). +[](https://circleci.com/gh/intercom/intercom-ruby) +[](https://rubygems.org/gems/intercom) + + +> Ruby bindings for the [Intercom API](https://developers.intercom.io/reference). + +## Project Updates + +### Maintenance + +We're currently building a new team to provide in-depth and dedicated SDK support. + +In the meantime, we'll be operating on limited capacity, meaning all pull requests will be evaluated on a best effort basis and will be limited to critical issues. + +We'll communicate all relevant updates as we build this new team and support strategy in the coming months. [API Documentation](https://developers.intercom.io/docs) [Gem Documentation](http://rubydoc.info/github/intercom/intercom-ruby/master/frames) -For generating Intercom JavaScript script tags for Rails, please see https://github.com/intercom/intercom-rails. +For generating Intercom JavaScript script tags for Rails, please see [intercom/intercom-rails](https://github.com/intercom/intercom-rails) ## Upgrading information @@ -16,12 +30,15 @@ This version of the gem is compatible with `Ruby 2.1` and above. ## Installation - - gem install intercom +```bash +gem install intercom +``` Using bundler: - gem 'intercom', '~> 4.0' +```bundler +gem 'intercom', '~> 4.1' +``` ## Basic Usage @@ -36,7 +53,7 @@ intercom = Intercom::Client.new(token: 'my_token') ```ruby # With a versioned app: -intercom = Intercom::Client.new(token: 'my_token', api_version: '2.0') +intercom = Intercom::Client.new(token: 'my_token', api_version: '2.2') ``` If you are building a third party application you can get your access_tokens by [setting-up-oauth](https://developers.intercom.io/page/setting-up-oauth) for Intercom. @@ -46,26 +63,34 @@ You can also use the [omniauth-intercom lib](https://github.com/intercom/omniaut Resources this API supports: - https://api.intercom.io/contacts - https://api.intercom.io/visitors - https://api.intercom.io/companies - https://api.intercom.io/data_attributes - https://api.intercom.io/events - https://api.intercom.io/tags - https://api.intercom.io/notes - https://api.intercom.io/segments - https://api.intercom.io/conversations - https://api.intercom.io/messages - https://api.intercom.io/admins - https://api.intercom.io/teams - https://api.intercom.io/counts - https://api.intercom.io/subscriptions - https://api.intercom.io/jobs - +```text +https://api.intercom.io/contacts +https://api.intercom.io/visitors +https://api.intercom.io/companies +https://api.intercom.io/data_attributes +https://api.intercom.io/events +https://api.intercom.io/tags +https://api.intercom.io/notes +https://api.intercom.io/segments +https://api.intercom.io/conversations +https://api.intercom.io/messages +https://api.intercom.io/admins +https://api.intercom.io/teams +https://api.intercom.io/counts +https://api.intercom.io/subscriptions +https://api.intercom.io/jobs +https://api.intercom.io/articles +https://api.intercom.io/help_center/collections +https://api.intercom.io/help_center/sections +https://api.intercom.io/phone_call_redirects +https://api.intercom.io/subscription_types +https://api.intercom.io/export/content/data +``` ### Examples #### Contacts + Note that this is a new resource compatible only with the new [Contacts API](https://developers.intercom.com/intercom-api-reference/reference#contacts-model) released in API v2.0. ```ruby @@ -83,9 +108,18 @@ intercom.contacts.save(contact) contact.role = "user" intercom.contacts.save(contact) +# Archive a contact +intercom.contacts.archive(contact) + +# Unarchive a contact +intercom.contacts.unarchive(contact) + # Delete a contact permanently intercom.contacts.delete(contact) +# Deletes an archived contact permanently +intercom.contacts.delete_archived_contact(contact.id) + # List all contacts contacts = intercom.contacts.all contacts.each { |contact| p contact.name } @@ -122,6 +156,9 @@ contact.create_note(body: "
Text for the note
") # List notes for a contact contact.notes.each {|n| p n.body} +# List segments for a contact +contact.segments.each {|segment| p segment.name} + # Add a contact to a company company = intercom.companies.find(id: "123") contact.add_company(id: company.id) @@ -131,9 +168,20 @@ contact.remove_company(id: company.id) # List companies for a contact contact.companies.each {|c| p c.name} + +# attach a subscription_types on a contact +contact.create_subscription_type(id: subscription_type.id) + +# List subscription_types for a contact +contact.subscription_types.each {|n| p n.id} + +# Remove subscription_types +contact.remove_subscription_type({ "id": subscription_type.id }) + ``` #### Visitors + ```ruby # Get and update a visitor visitor = intercom.visitors.find(id: "5dd570e7b1b922452676af23") @@ -149,6 +197,7 @@ intercom.visitors.convert(visitor, user) ``` #### Companies + ```ruby # Find a company by company_id company = intercom.companies.find(company_id: "44") @@ -163,6 +212,9 @@ company = intercom.companies.find(id: "41e66f0313708347cb0000d0") company.name = 'Updated company name' intercom.companies.save(company) +# Delete a company +intercom.companies.delete(company) + # Iterate over all companies intercom.companies.all.each {|company| puts %Q(#{company.name} - #{company.custom_attributes["referral_source"]}) } intercom.companies.all.map {|company| company.name } @@ -173,7 +225,9 @@ intercom.companies.scroll.each { |comp| puts comp.name} ``` #### Data Attributes + Data Attributes are a type of metadata used to describe your customer and company models. These include standard and custom attributes. + ```ruby # Create a new custom data attribute intercom.data_attributes.create({ name: "test_attribute", model: "contact", data_type: "string" }) @@ -193,10 +247,11 @@ intercom.data_attributes.save(attribute) # Find all customer attributes including archived customer_attributes_incl_archived = intercom.data_attributes.find_all({"model": "contact", "include_archived": true}) -customer_attributes_incl_archived.each { |attr| p attribute.name } +customer_attributes_incl_archived.each { |attr| p attr.name } ``` #### Events + ```ruby intercom.events.create( event_name: "invited-friend", @@ -218,6 +273,14 @@ intercom.events.create( # Retrieve event list for user with id:'123abc' intercom.events.find_all("type" => "user", "intercom_user_id" => "123abc") + +# Retrieve the event summary for user with id: 'abc' this will return an event object with the following characteristics: +# name - name of the event +# first - time when event first occured. +# last - time when event last occured +# count - number of times the event occured +# description - description of the event + events = intercom.events.find_all(type: 'user',intercom_user_id: 'abc',summary: true) ``` Metadata Objects support a few simple types that Intercom can present on your behalf @@ -242,7 +305,8 @@ intercom.events.create( ) ``` -The metadata key values in the example are treated as follows- +The metadata key values in the example are treated as follows: + - order_date: a Date (key ends with '_date') - stripe_invoice: The identifier of the Stripe invoice (has a 'stripe_invoice' key) - order_number: a Rich Link (value contains 'url' and 'value' keys) @@ -251,6 +315,7 @@ The metadata key values in the example are treated as follows- *NB:* This version of the gem reserves the field name `type` in Event data. #### Tags + ```ruby # Iterate over all tags intercom.tags.all.each {|tag| "#{tag.id} - #{tag.name}" } @@ -258,15 +323,27 @@ intercom.tags.all.map {|tag| tag.name } # Tag companies tag = intercom.tags.tag(name: 'blue', companies: [{company_id: "42ea2f1b93891f6a99000427"}]) + +# Untag Companies +tag = intercom.tags.untag(name: 'blue', companies: [{ company_id: "42ea2f1b93891f6a99000427" }]) + + +# Delete Tags + +# Note : If there any depedent objects for the tag we are trying to delete, then an error TagHasDependentObjects will be thrown. +tag = intercom.tags.find(id:"123") +intercom.tags.delete(tag) ``` #### Notes + ```ruby # Find a note by id note = intercom.notes.find(id: "123") ``` #### Segments + ```ruby # Find a segment segment = intercom.segments.find(id: segment_id) @@ -276,10 +353,13 @@ intercom.segments.all.each {|segment| puts "id: #{segment.id} name: #{segment.na ``` #### Conversations + ```ruby # Iterate over all conversations for your app intercom.conversations.all.each { |convo| ... } +# The below method of finding conversations by using the find_all method work only for API versions 2.5 and below + # FINDING CONVERSATIONS FOR AN ADMIN # Iterate over all conversations (open and closed) assigned to an admin intercom.conversations.find_all(type: 'admin', id: '7').each {|convo| ... } @@ -406,6 +486,7 @@ conversation.remove_contact(id: contact.id, admin_id: admin.id) ``` #### Full loading of an embedded entity + ```ruby # Given a conversation with a partial contact, load the full contact. This can be # done for any entity @@ -413,6 +494,7 @@ intercom.contacts.load(conversation.contacts.first) ``` #### Sending messages + ```ruby # InApp message from admin to user @@ -476,9 +558,20 @@ intercom.messages.create({ }, body: "halp" }) + +#From version 2.6 the type contact is not supported and you would have to use leads to send messages to a lead. + +intercom.messages.create({ + from: { + type: "lead", + id: "536e5643as316c83104c400671" + }, + body: "halp" +}) ``` #### Admins + ```ruby # Find access token owner (only with Personal Access Token and OAuth) intercom.admins.me @@ -489,6 +582,7 @@ intercom.admins.all.each {|admin| puts admin.email } ``` #### Teams + ```ruby # Find a team by id intercom.teams.find(id: team_id) @@ -525,6 +619,142 @@ intercom.subscriptions.delete(subscription) intercom.subscriptions.all ``` + +#### Subscription Types + +List all the subscription types that a contact can opt in to + +```ruby + +# fetch a subscription +intercom.subscription_types.find(id: "1") + +intercom.subscription_types.all +``` + +#### Articles + +```ruby +# Create an article +article = intercom.articles.create(title: "New Article", author_id: "123456") + +# Create an article with translations +article = intercom.articles.create(title: "New Article", + author_id: "123456", + translated_content: {fr: {title: "Nouvel Article"}, es: {title: "Nuevo artículo"}}) + +# Fetch an article +intercom.articles.find(id: "123456") + +# List all articles +articles = intercom.articles.all +articles.each { |article| p article.title } + +# Update an article +article.title = "Article Updated!" +intercom.articles.save(article) + +# Update an article's existing translation +article.translated_content.en.title = "English Updated!" +intercom.articles.save(article) + +# Update an article by adding a new translation +article.translated_content.es = {title: "Artículo en español"} +intercom.articles.save(article) + +# Delete an article +intercom.articles.delete(article) +``` + +#### Collections + +```ruby +# Create a collection +collection = intercom.collections.create(name: "New Collection") + +# Create a collection with translations +collection = intercom.collections.create(name: "New Collection", + translated_content: {fr: {name: "Nouvelle collection"}, es: {name: "Nueva colección"}}) + +# Fetch a collection +intercom.collections.find(id: "123456") + +# List all collections +collections = intercom.collections.all +collections.each { |collection| p collection.name } + +# Update a collection +collection.name = "Collection updated!" +intercom.collections.save(collection) + +# Update a collection's existing translation +collection.translated_content.en.name = "English Updated!" +intercom.collections.save(collection) + +# Update a collection by adding a new translation +collection.translated_content.es = {name: "Colección en español", description: "Descripción en español"} +intercom.collections.save(collection) + +# Delete an collection +intercom.collections.delete(collection) +``` + +#### Sections + +```ruby +# Create a section +section = intercom.sections.create(name: "New Section", parent_id: "123456") + +# Create a section with translations +section = intercom.sections.create(name: "New Section", + translated_content: {fr: {name: "Nouvelle section"}, es: {name: "Nueva sección"}}) + +# Fetch a section +intercom.sections.find(id: "123456") + +# List all sections +sections = intercom.sections.all +sections.each { |section| p section.name } + +# Update a section +section.name = "Section updated!" +intercom.sections.save(section) + +# Update a section's existing translation +section.translated_content.en.name = "English Updated!" +intercom.collections.save(section) + +# Update a section by adding a new translation +section.translated_content.es = {name: "Sección en español"} +intercom.collections.save(section) + +# Delete an section +intercom.sections.delete(section) +``` + +#### Phone Call Redirect (switch) + +```ruby +# Create a redirect +redirect = intercom.phone_call_redirect.create(phone_number: "+353871234567") + +``` + +#### Data Content Export + +```ruby +# Create a data export +export = intercom.export_content.create(created_at_after: 1667566801, created_at_before: 1668085202) + + +#View a data export +export = intercom.export_content.find(id: 'k0e27ohsyvh8ef3m') + +# Cancel a data export +export = intercom.export_content.cancel('k0e27ohsyvh8ef3m') + +``` + ### Errors There are different styles for error handling - some people prefer exceptions; some prefer nil and check; some prefer error objects/codes. Balancing these preferences alongside our wish to provide an idiomatic gem has brought us to use the current mechanism of throwing specific exceptions. Our approach in the client is to propagate errors and signal our failure loudly so that erroneous data does not get propagated through our customers' systems - in other words, if you see a `Intercom::ServiceUnavailableError` you know where the problem is. @@ -545,6 +775,7 @@ Intercom::RateLimitExceeded Intercom::AttributeNotSetError # Raised when you try to call a getter that does not exist on an object Intercom::MultipleMatchingUsersError Intercom::HttpError # Raised when response object is unexpectedly nil +Intercom::GatewayTimeoutError ``` ### Rate Limiting @@ -559,7 +790,7 @@ intercom.rate_limit_details You can handle the rate limits yourself but a simple option is to use the handle_rate_limit flag. This will automatically catch the 429 rate limit exceeded error and wait until the reset time to retry. After three retries a rate limit exception will be raised. Encountering this error frequently may require a revisiting of your usage of the API. -``` +```ruby intercom = Intercom::Client.new(token: ENV['AT'], handle_rate_limit: true) ``` @@ -585,13 +816,13 @@ intercom = Intercom::Client.new(token: ENV['AT'], handle_rate_limit: true) ```bash # all tests -bundle exec spec +bundle exec rake spec # unit tests -bundle exec spec:unit +bundle exec rake spec:unit # integration tests -bundle exec spec:integration +bundle exec rake spec:integration # single test file bundle exec m spec/unit/intercom/job_spec.rb diff --git a/changes.txt b/changes.txt index 1073568c..b9f617f3 100644 --- a/changes.txt +++ b/changes.txt @@ -1,3 +1,29 @@ +4.2.2 +- Fixed FlatStore to skip hash values when building API request payloads +- Removed hash validation on FlatStore reads to allow for custom objects + +4.1.3 +- Updated ReadMe with more errors. +- Fixed issue where paginated requests could only be iterated through once. +- Moved Dynamic accessors from class level to instance level. + +4.1.2 +- Adding support for company delete. +- Adding support for archiving/unarchiving contacts. +- Adding support for listing contact segments. +- Fixed issue with scroll collection proxy. +- Fixed issue with running assignment rules on a conversation. + +4.1.1 +- Fixed bug with deprecated lead resource. + +4.1.0 +- Added support for new Articles API. +- Added support for new Collections API. +- Added support for new Sections API. +- Added support to equate two resources. +- Fixed issue for dirty tracking nested typed objects. + 4.0.1 - Fixed bug with nested resources. - Support for add/remove contact on conversation object. diff --git a/intercom.gemspec b/intercom.gemspec index a6878d55..e5ed7737 100644 --- a/intercom.gemspec +++ b/intercom.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'minitest', '~> 5.4' spec.add_development_dependency "m", "~> 1.5.0" spec.add_development_dependency 'rake', '~> 10.3' - spec.add_development_dependency 'mocha', '~> 1.0' + spec.add_development_dependency 'mocha', '~> 2.0' spec.add_development_dependency "fakeweb", ["~> 1.3"] spec.add_development_dependency "pry" diff --git a/lib/intercom.rb b/lib/intercom.rb index f9587801..520b9036 100644 --- a/lib/intercom.rb +++ b/lib/intercom.rb @@ -2,6 +2,8 @@ require 'intercom/version' require 'intercom/service/admin' +require 'intercom/service/article' +require 'intercom/service/collection' require 'intercom/service/company' require 'intercom/service/contact' require 'intercom/service/conversation' @@ -10,36 +12,46 @@ require 'intercom/service/message' require 'intercom/service/note' require 'intercom/service/job' +require 'intercom/service/subscription_type' require 'intercom/service/subscription' require 'intercom/service/segment' +require 'intercom/service/section' require 'intercom/service/tag' require 'intercom/service/team' require 'intercom/service/visitor' require 'intercom/service/user' require 'intercom/service/lead' require 'intercom/deprecated_resources.rb' +require 'intercom/service/export_content' +require 'intercom/service/phone_call_redirect' require 'intercom/options' require 'intercom/client' require 'intercom/contact' require 'intercom/user' require 'intercom/lead' require 'intercom/count' +require 'intercom/collection' require 'intercom/company' require 'intercom/service/data_attribute' require 'intercom/note' require 'intercom/job' require 'intercom/tag' require 'intercom/segment' +require 'intercom/section' require 'intercom/event' require 'intercom/conversation' require 'intercom/message' require 'intercom/admin' +require 'intercom/article' require 'intercom/request' require 'intercom/subscription' +require 'intercom/subscription_type' require 'intercom/team' require 'intercom/errors' require 'intercom/visitor' require 'intercom/data_attribute' +require 'intercom/export_content' +require 'intercom/phone_call_redirect' require 'json' ## diff --git a/lib/intercom/api_operations/nested_resource.rb b/lib/intercom/api_operations/nested_resource.rb index 3dedef7f..961d7362 100644 --- a/lib/intercom/api_operations/nested_resource.rb +++ b/lib/intercom/api_operations/nested_resource.rb @@ -13,9 +13,7 @@ def nested_resource_methods(resource, raise ArgumentError, 'operations array required' if operations.nil? resource_url_method = :"#{resource_plural}_url" - resource_name = Utils.resource_class_to_collection_name(self) - define_method(resource_url_method.to_sym) do |id, nested_id = nil| url = "/#{resource_name}/#{id}/#{path}" url += "/#{nested_id}" unless nested_id.nil? diff --git a/lib/intercom/article.rb b/lib/intercom/article.rb new file mode 100644 index 00000000..d1ace3bc --- /dev/null +++ b/lib/intercom/article.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Article + include Traits::ApiResource + end +end diff --git a/lib/intercom/base_collection_proxy.rb b/lib/intercom/base_collection_proxy.rb index 806e0125..8fb59451 100644 --- a/lib/intercom/base_collection_proxy.rb +++ b/lib/intercom/base_collection_proxy.rb @@ -45,7 +45,11 @@ def deserialize_response_hash(response_hash, block) Utils.entity_key_from_type(top_level_type) end response_hash[top_level_entity_key].each do |object_json| - block.call Lib::TypedJsonDeserializer.new(object_json, @client).deserialize + if top_level_type == 'event.summary' + block.call Lib::TypedJsonDeserializer.new(object_json, @client, top_level_type).deserialize + else + block.call Lib::TypedJsonDeserializer.new(object_json, @client).deserialize + end end end @@ -58,15 +62,13 @@ def has_next_link?(response_hash) @params[:starting_after] = paging_next['starting_after'] return true else + @params[:starting_after] = nil return false end end def payload - payload = {} - payload[:per_page] = @params[:per_page] if @params[:per_page] - payload[:starting_after] = @params[:starting_after] if @params[:starting_after] - payload + @params.keep_if { |k, v| !v.nil? }.to_h end end end diff --git a/lib/intercom/client.rb b/lib/intercom/client.rb index 433c4097..5b3f607f 100644 --- a/lib/intercom/client.rb +++ b/lib/intercom/client.rb @@ -48,6 +48,10 @@ def admins Intercom::Service::Admin.new(self) end + def articles + Intercom::Service::Article.new(self) + end + def companies Intercom::Service::Company.new(self) end @@ -80,10 +84,18 @@ def subscriptions Intercom::Service::Subscription.new(self) end + def subscription_types + Intercom::Service::SubscriptionType.new(self) + end + def segments Intercom::Service::Segment.new(self) end + def sections + Intercom::Service::Section.new(self) + end + def tags Intercom::Service::Tag.new(self) end @@ -96,6 +108,10 @@ def users Intercom::Service::User.new(self) end + def leads + Intercom::Service::Lead.new(self) + end + def visitors Intercom::Service::Visitor.new(self) end @@ -108,6 +124,18 @@ def data_attributes Intercom::Service::DataAttribute.new(self) end + def collections + Intercom::Service::Collection.new(self) + end + + def export_content + Intercom::Service::ExportContent.new(self) + end + + def phone_call_redirect + Intercom::Service::PhoneCallRedirect.new(self) + end + def get(path, params) execute_request Intercom::Request.get(path, params) end diff --git a/lib/intercom/collection.rb b/lib/intercom/collection.rb new file mode 100644 index 00000000..3fc7ab26 --- /dev/null +++ b/lib/intercom/collection.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Collection + include Traits::ApiResource + end +end diff --git a/lib/intercom/contact.rb b/lib/intercom/contact.rb index 487b5b0f..6f656d9e 100644 --- a/lib/intercom/contact.rb +++ b/lib/intercom/contact.rb @@ -12,7 +12,9 @@ class Contact nested_resource_methods :tag, operations: %i[add delete list] nested_resource_methods :note, operations: %i[create list] + nested_resource_methods :subscription_type, path: 'subscriptions', operations: %i[create delete list] nested_resource_methods :company, operations: %i[add delete list] + nested_resource_methods :segment, operations: %i[list] def self.collection_proxy_class Intercom::BaseCollectionProxy diff --git a/lib/intercom/errors.rb b/lib/intercom/errors.rb index 5d7e594a..b3af0bd5 100644 --- a/lib/intercom/errors.rb +++ b/lib/intercom/errors.rb @@ -99,6 +99,9 @@ class InvalidDocumentError < IntercomError; end # Raised when a merge is invalid class InvalidMergeError < IntercomError; end + # Raised when a tag has dependent objects + class TagHasDependentObjects < IntercomError; end + # # Non-public errors (internal to the gem) # diff --git a/lib/intercom/export_content.rb b/lib/intercom/export_content.rb new file mode 100644 index 00000000..9ea7c3d5 --- /dev/null +++ b/lib/intercom/export_content.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class ExportContent + include Traits::ApiResource + end +end diff --git a/lib/intercom/lib/dynamic_accessors.rb b/lib/intercom/lib/dynamic_accessors.rb index 1b053d2e..9d26b4fc 100644 --- a/lib/intercom/lib/dynamic_accessors.rb +++ b/lib/intercom/lib/dynamic_accessors.rb @@ -5,20 +5,19 @@ module DynamicAccessors class << self def define_accessors(attribute, value, object) - klass = object.class if attribute.to_s.end_with?('_at') && attribute.to_s != 'update_last_request_at' - define_date_based_accessors(attribute, value, klass) + define_date_based_accessors(attribute, value, object) elsif object.flat_store_attribute?(attribute) - define_flat_store_based_accessors(attribute, value, klass) + define_flat_store_based_accessors(attribute, value, object) else - define_standard_accessors(attribute, value, klass) + define_standard_accessors(attribute, value, object) end end private - def define_flat_store_based_accessors(attribute, value, klass) - klass.class_eval %Q" + def define_flat_store_based_accessors(attribute, value, object) + object.instance_eval %Q" def #{attribute}=(value) mark_field_as_changed!(:#{attribute}) @#{attribute} = Intercom::Lib::FlatStore.new(value) @@ -29,8 +28,8 @@ def #{attribute} " end - def define_date_based_accessors(attribute, value, klass) - klass.class_eval %Q" + def define_date_based_accessors(attribute, value, object) + object.instance_eval %Q" def #{attribute}=(value) mark_field_as_changed!(:#{attribute}) @#{attribute} = value.nil? ? nil : value.to_i @@ -41,8 +40,8 @@ def #{attribute} " end - def define_standard_accessors(attribute, value, klass) - klass.class_eval %Q" + def define_standard_accessors(attribute, value, object) + object.instance_eval %Q" def #{attribute}=(value) mark_field_as_changed!(:#{attribute}) @#{attribute} = value diff --git a/lib/intercom/lib/flat_store.rb b/lib/intercom/lib/flat_store.rb index 6971ca75..2077a2bd 100644 --- a/lib/intercom/lib/flat_store.rb +++ b/lib/intercom/lib/flat_store.rb @@ -2,7 +2,7 @@ module Intercom module Lib # Sub-class of {Hash} for storing custom data attributes. - # Doesn't allow nested Hashes or Arrays. And requires {String} or {Symbol} keys. + # Doesn't allow Arrays. And requires {String} or {Symbol} keys. class FlatStore < Hash def initialize(attributes={}) @@ -21,9 +21,16 @@ def [](key) super(key.to_s) end + def to_submittable_hash + # Filter out Custom Object references when submitting to API + self.reject do |key, value| + value.is_a?(Hash) + end + end + private def validate_key_and_value(key, value) - raise ArgumentError.new("This does not support nested data structures (key: #{key}, value: #{value}") if value.is_a?(Array) || value.is_a?(Hash) + raise ArgumentError.new("This does not support nested data structures (key: #{key}, value: #{value}") if value.is_a?(Array) raise ArgumentError.new("Key must be String or Symbol: #{key}") unless key.is_a?(String) || key.is_a?(Symbol) end end diff --git a/lib/intercom/lib/typed_json_deserializer.rb b/lib/intercom/lib/typed_json_deserializer.rb index 17ed5ea3..155d2325 100644 --- a/lib/intercom/lib/typed_json_deserializer.rb +++ b/lib/intercom/lib/typed_json_deserializer.rb @@ -9,9 +9,10 @@ module Lib class TypedJsonDeserializer attr_reader :json - def initialize(json, client) + def initialize(json, client, type = nil) @json = json @client = client + @type = type end def deserialize @@ -27,7 +28,7 @@ def deserialize private def blank_object_type?(object_type) - object_type.nil? || object_type == '' + object_type.nil? || object_type == '' && @type.nil? end def list_object_type?(object_type) @@ -48,7 +49,11 @@ def deserialize_object(object_json) end def object_type - @object_type ||= json['type'] + if !@type.nil? + @object_type = @type + else + @object_type ||= json['type'] + end end def object_entity_key diff --git a/lib/intercom/phone_call_redirect.rb b/lib/intercom/phone_call_redirect.rb new file mode 100644 index 00000000..61db81bb --- /dev/null +++ b/lib/intercom/phone_call_redirect.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class PhoneCallRedirect + include Traits::ApiResource + end +end diff --git a/lib/intercom/request.rb b/lib/intercom/request.rb index 0be75505..40379939 100644 --- a/lib/intercom/request.rb +++ b/lib/intercom/request.rb @@ -222,6 +222,8 @@ def execute(target_base_url = nil, token:, read_timeout: 90, open_timeout: 30, a raise Intercom::ApiVersionInvalid.new(error_details['message'], error_context) when 'scroll_exists' raise Intercom::ScrollAlreadyExistsError.new(error_details['message'], error_context) + when 'tag_has_dependent_objects' + raise Intercom::TagHasDependentObjects.new(error_details['message'], error_context) when nil, '' raise Intercom::UnexpectedError.new(message_for_unexpected_error_without_type(error_details, parsed_http_code), error_context) else diff --git a/lib/intercom/scroll_collection_proxy.rb b/lib/intercom/scroll_collection_proxy.rb index 929c1425..48129b3b 100644 --- a/lib/intercom/scroll_collection_proxy.rb +++ b/lib/intercom/scroll_collection_proxy.rb @@ -26,7 +26,7 @@ def next(scroll_parameter = nil) raise Intercom::HttpError, 'Http Error - No response entity returned' unless response_hash @scroll_param = extract_scroll_param(response_hash) - top_level_entity_key = deserialize_response_hash(response_hash) + top_level_entity_key = entity_key_from_response(response_hash) response_hash[top_level_entity_key] = response_hash[top_level_entity_key].map do |object_json| Lib::TypedJsonDeserializer.new(object_json, @client).deserialize end @@ -44,7 +44,8 @@ def each(&block) end raise Intercom::HttpError, 'Http Error - No response entity returned' unless response_hash - response_hash[deserialize_response_hash(response_hash)].each do |object_json| + top_level_entity_key = entity_key_from_response(response_hash) + response_hash[top_level_entity_key].each do |object_json| block.call Lib::TypedJsonDeserializer.new(object_json, @client).deserialize end scroll_param = extract_scroll_param(response_hash) @@ -55,8 +56,8 @@ def each(&block) private - def deserialize_response_hash(response_hash) - top_level_type = response_hash.delete('type') + def entity_key_from_response(response_hash) + top_level_type = response_hash['type'] if resource_name == 'subscriptions' 'items' else @@ -65,7 +66,7 @@ def deserialize_response_hash(response_hash) end def records_present?(response_hash) - !response_hash[@resource_name].empty? + !response_hash[entity_key_from_response(response_hash)].empty? end def extract_scroll_param(response_hash) diff --git a/lib/intercom/section.rb b/lib/intercom/section.rb new file mode 100644 index 00000000..75e387ed --- /dev/null +++ b/lib/intercom/section.rb @@ -0,0 +1,23 @@ +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' +require 'intercom/api_operations/save' +require 'intercom/api_operations/delete' + +module Intercom + module Service + class Section < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Save + include ApiOperations::Delete + + def collection_class + Intercom::Section + end + + def collection_name + 'help_center/sections' + end + end + end +end diff --git a/lib/intercom/segment.rb b/lib/intercom/segment.rb index 88de03d9..589ae474 100644 --- a/lib/intercom/segment.rb +++ b/lib/intercom/segment.rb @@ -3,5 +3,9 @@ module Intercom class Segment include Traits::ApiResource + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end end end diff --git a/lib/intercom/service/article.rb b/lib/intercom/service/article.rb new file mode 100644 index 00000000..d21bae74 --- /dev/null +++ b/lib/intercom/service/article.rb @@ -0,0 +1,20 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/find' +require 'intercom/api_operations/list' +require 'intercom/api_operations/delete' +require 'intercom/api_operations/save' + +module Intercom + module Service + class Article < BaseService + include ApiOperations::Find + include ApiOperations::List + include ApiOperations::Delete + include ApiOperations::Save + + def collection_class + Intercom::Article + end + end + end +end diff --git a/lib/intercom/service/collection.rb b/lib/intercom/service/collection.rb new file mode 100644 index 00000000..cbe7a6f2 --- /dev/null +++ b/lib/intercom/service/collection.rb @@ -0,0 +1,24 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/list' +require 'intercom/api_operations/find' +require 'intercom/api_operations/delete' +require 'intercom/api_operations/save' + +module Intercom + module Service + class Collection < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Delete + include ApiOperations::Save + + def collection_class + Intercom::Collection + end + + def collection_name + "help_center/collections" + end + end + end +end diff --git a/lib/intercom/service/company.rb b/lib/intercom/service/company.rb index f23a97c6..b627c452 100644 --- a/lib/intercom/service/company.rb +++ b/lib/intercom/service/company.rb @@ -1,4 +1,5 @@ require 'intercom/service/base_service' +require 'intercom/api_operations/delete' require 'intercom/api_operations/list' require 'intercom/api_operations/scroll' require 'intercom/api_operations/find' @@ -11,6 +12,7 @@ module Intercom module Service class Company < BaseService + include ApiOperations::Delete include ApiOperations::Find include ApiOperations::FindAll include ApiOperations::Load diff --git a/lib/intercom/service/contact.rb b/lib/intercom/service/contact.rb index 737dc96e..3d01bf32 100644 --- a/lib/intercom/service/contact.rb +++ b/lib/intercom/service/contact.rb @@ -33,6 +33,20 @@ def merge(lead, user) user.from_response(response) end + def archive(contact) + @client.post("/#{collection_name}/#{contact.id}/archive", {}) + contact + end + + def unarchive(contact) + @client.post("/#{collection_name}/#{contact.id}/unarchive", {}) + contact + end + + def delete_archived_contact(id) + @client.delete("/#{collection_name}/#{id}", {}) + end + private def raise_invalid_merge_error raise Intercom::InvalidMergeError, 'Merging can only be performed on a lead into a user' end diff --git a/lib/intercom/service/conversation.rb b/lib/intercom/service/conversation.rb index bed5e93b..77685db0 100644 --- a/lib/intercom/service/conversation.rb +++ b/lib/intercom/service/conversation.rb @@ -21,6 +21,10 @@ def collection_class Intercom::Conversation end + def collection_proxy_class + Intercom::BaseCollectionProxy + end + def mark_read(id) @client.put("/conversations/#{id}", read: true) end @@ -58,7 +62,7 @@ def assign(reply_data) def run_assignment_rules(id) collection_name = Utils.resource_class_to_collection_name(collection_class) - response = @client.post("/#{collection_name}/#{id}/run_assignment_rules") + response = @client.post("/#{collection_name}/#{id}/run_assignment_rules", {}) collection_class.new.from_response(response) end end diff --git a/lib/intercom/service/export_content.rb b/lib/intercom/service/export_content.rb new file mode 100644 index 00000000..7c1e3897 --- /dev/null +++ b/lib/intercom/service/export_content.rb @@ -0,0 +1,30 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/find' +require 'intercom/api_operations/list' +require 'intercom/api_operations/save' + +module Intercom + module Service + class ExportContent < BaseService + include ApiOperations::Load + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::Save + + def collection_class + Intercom::ExportContent + end + + def collection_name + 'export/content/data' + end + + def cancel(id) + response = @client.post("/export/cancel/#{id}", {}) + collection_class.new.from_response(response) + end + + end + end +end + diff --git a/lib/intercom/service/phone_call_redirect.rb b/lib/intercom/service/phone_call_redirect.rb new file mode 100644 index 00000000..425b57d2 --- /dev/null +++ b/lib/intercom/service/phone_call_redirect.rb @@ -0,0 +1,15 @@ +require 'intercom/service/base_service' +require 'intercom/api_operations/save' + +module Intercom + module Service + class PhoneCallRedirect < BaseService + include ApiOperations::Save + + def collection_class + Intercom::PhoneCallRedirect + end + + end + end +end diff --git a/lib/intercom/service/section.rb b/lib/intercom/service/section.rb new file mode 100644 index 00000000..2f03e746 --- /dev/null +++ b/lib/intercom/service/section.rb @@ -0,0 +1,7 @@ +require 'intercom/traits/api_resource' + +module Intercom + class Section + include Traits::ApiResource + end +end diff --git a/lib/intercom/service/subscription_type.rb b/lib/intercom/service/subscription_type.rb new file mode 100644 index 00000000..c1de4879 --- /dev/null +++ b/lib/intercom/service/subscription_type.rb @@ -0,0 +1,18 @@ +require 'intercom/api_operations/list' +require 'intercom/api_operations/find_all' +require 'intercom/api_operations/find' + +module Intercom + module Service + class SubscriptionType < BaseService + include ApiOperations::List + include ApiOperations::Find + include ApiOperations::FindAll + include ApiOperations::Delete + + def collection_class + Intercom::SubscriptionType + end + end + end +end diff --git a/lib/intercom/subscription_type.rb b/lib/intercom/subscription_type.rb new file mode 100644 index 00000000..d53c2762 --- /dev/null +++ b/lib/intercom/subscription_type.rb @@ -0,0 +1,12 @@ + +require 'intercom/traits/api_resource' + +module Intercom + class SubscriptionType + include Traits::ApiResource + + def self.collection_proxy_class + Intercom::BaseCollectionProxy + end + end +end diff --git a/lib/intercom/traits/api_resource.rb b/lib/intercom/traits/api_resource.rb index 52707794..8749c196 100644 --- a/lib/intercom/traits/api_resource.rb +++ b/lib/intercom/traits/api_resource.rb @@ -17,6 +17,10 @@ def initialize(attributes = {}) from_hash(attributes) end + def ==(other) + self.class == other.class && to_json == other.to_json + end + def from_response(response) from_hash(response) reset_changed_fields! @@ -37,10 +41,21 @@ def to_hash end end + def to_json(*args) + instance_variables_excluding_dirty_tracking_field.each_with_object({}) do |variable, hash| + next if variable == :@client + + value = instance_variable_get(variable) + hash[variable.to_s.delete('@')] = value.respond_to?(:to_json) ? value.to_json(*args) : value + end + end + def to_submittable_hash submittable_hash = {} to_hash.each do |attribute, value| - submittable_hash[attribute] = value if submittable_attribute?(attribute, value) + next unless submittable_attribute?(attribute, value) + + submittable_hash[attribute] = value.respond_to?(:to_submittable_hash) ? value.to_submittable_hash : value end submittable_hash end diff --git a/lib/intercom/traits/dirty_tracking.rb b/lib/intercom/traits/dirty_tracking.rb index 0d120094..d155c2d9 100644 --- a/lib/intercom/traits/dirty_tracking.rb +++ b/lib/intercom/traits/dirty_tracking.rb @@ -22,7 +22,14 @@ def mark_field_as_changed!(field_name) def field_changed?(field_name) @changed_fields ||= Set.new - @changed_fields.include?(field_name.to_s) + field = instance_variable_get("@#{field_name}") + if field.respond_to?(:field_changed?) + field.to_hash.any? do |attribute, _| + field.field_changed?(attribute) + end + else + @changed_fields.include?(field_name.to_s) + end end def instance_variables_excluding_dirty_tracking_field diff --git a/lib/intercom/utils.rb b/lib/intercom/utils.rb index ce0bcd74..6ccba8cc 100644 --- a/lib/intercom/utils.rb +++ b/lib/intercom/utils.rb @@ -25,6 +25,10 @@ def constantize(camel_cased_word) constant end + def camelize(snake_cased_word) + snake_cased_word.split(/_/).map(&:capitalize).join + end + def resource_class_to_singular_name(resource_class) resource_name = resource_class.to_s.split('::')[-1] resource_name = maybe_underscore_name(resource_name) @@ -40,7 +44,7 @@ def resource_class_to_collection_name(resource_class) end def constantize_resource_name(resource_name) - class_name = Utils.singularize(resource_name.capitalize) + class_name = camelize Utils.singularize(resource_name.capitalize) define_lightweight_class(class_name) unless Intercom.const_defined?(class_name, false) namespaced_class_name = "Intercom::#{class_name}" constantize namespaced_class_name @@ -65,6 +69,7 @@ def entity_key_from_type(type) is_list = type.split('.')[1] == 'list' entity_name = type.split('.')[0] + return Utils.pluralize(entity_name) if entity_name == 'event' is_list ? Utils.pluralize(entity_name) : entity_name end end diff --git a/lib/intercom/version.rb b/lib/intercom/version.rb index 5f2ea446..7406b336 100644 --- a/lib/intercom/version.rb +++ b/lib/intercom/version.rb @@ -1,3 +1,3 @@ module Intercom #:nodoc: - VERSION = "4.0.1" + VERSION = "4.2.2" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ea084a15..1dc07c62 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,12 +2,38 @@ require 'intercom' require 'minitest/autorun' -require 'mocha/setup' +require 'mocha/minitest' require 'webmock' require 'time' require 'pry' include WebMock::API +def test_article + { + "id": "1", + "type": "article", + "workspace_id": "tx2p130c", + "title": "new title", + "description": "test Finished articles are visible when they're added to a Help Center collection", + "body": "thingbop
", + "author_id": 1, + "state": "draft" + } +end + +def test_updated_article + { + "id": "1", + "type": "article", + "workspace_id": "tx2p130c", + "title": "new updated title", + "description": "test Finished articles are visible when they're added to a Help Center collection", + "body": "thingbop
", + "author_id": 1, + "state": "draft" + } +end + def test_user(email = 'bob@example.com') { 'type' => 'user', @@ -131,6 +157,41 @@ def test_contact(email = 'bob@example.com', role = 'user') } end +def test_collection + { + 'id' => '1', + 'workspace_id' => 'tx2p130c', + 'name' => 'Collection 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 1, + 'type' => 'collection', + 'description' => 'Collection desc', + 'icon' => 'book-bookmark' + } +end + +def test_collection_list + { + 'type' => 'list', + 'total_count' => 1, + 'pages' => { + 'page' => 1, + 'per_page' => 20, + 'total_pages' => 1 + }, + 'data' => [{ + 'id' => '1', + 'workspace_id' => 'tx2p130c', + 'name' => 'Collection 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 1, + 'type' => 'collection', + 'description' => 'Collection desc', + 'icon' => 'book-bookmark' + }] + } +end + def test_visitor { 'type' => 'visitor', @@ -789,6 +850,43 @@ def test_app_count } end +def test_section + { + 'id' => '18', + 'workspace_id' => 'tx2p130c', + 'name' => 'Section 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 0, + 'created_at' => 1_589_801_953, + 'updated_at' => 1_589_801_953, + 'type' => 'section', + 'parent_id' => 1 + } +end + +def test_section_list + { + 'type' => 'list', + 'total_count' => 1, + 'pages' => { + 'page' => 1, + 'per_page' => 20, + 'total_pages' => 1 + }, + 'data' => [{ + 'id' => '18', + 'workspace_id' => 'tx2p130c', + 'name' => 'Section 1', + 'url' => 'http://www.intercom.test/help/', + 'order' => 0, + 'created_at' => 1_589_801_953, + 'updated_at' => 1_589_801_953, + 'type' => 'section', + 'parent_id' => 1 + }] + } +end + def test_segment_count { 'type' => 'count', diff --git a/spec/unit/intercom/article_spec.rb b/spec/unit/intercom/article_spec.rb new file mode 100644 index 00000000..9fa3a695 --- /dev/null +++ b/spec/unit/intercom/article_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe "Intercom::Article" do + let(:client) { Intercom::Client.new(token: 'token') } + + describe "Getting an Article" do + it "successfully finds an article" do + client.expects(:get).with("/articles/1", {}).returns(test_article) + client.articles.find(id: "1") + end + end + + describe "Creating an Article" do + it "successfully creates and article with information passed individually" do + client.expects(:post).with("/articles", {"title" => "new title", "author_id" => 1, "body" => "thingbop
", "state" => "draft"}).returns(test_article) + client.articles.create(:title => "new title", :author_id => 1, :body => "thingbop
", :state => "draft") + end + + it "successfully creates and article with information in json" do + client.expects(:post).with("/articles", {"title" => "new title", "author_id" => 1, "body" => "thingbop
", "state" => "draft"}).returns(test_article) + client.articles.create({title: "new title", author_id: 1, body: "thingbop
", state: "draft"}) + end + end + + describe "Updating an article" do + it "successfully updates an article" do + article = Intercom::Article.new(id: 12345) + client.expects(:put).with('/articles/12345', {}) + client.articles.save(article) + end + end + + describe "Deleting an article" do + it "successfully deletes an article" do + article = Intercom::Article.new(id: 12345) + client.expects(:delete).with('/articles/12345', {}) + client.articles.delete(article) + end + end +end \ No newline at end of file diff --git a/spec/unit/intercom/base_collection_proxy_spec.rb b/spec/unit/intercom/base_collection_proxy_spec.rb index ca37b720..02efed90 100644 --- a/spec/unit/intercom/base_collection_proxy_spec.rb +++ b/spec/unit/intercom/base_collection_proxy_spec.rb @@ -4,7 +4,7 @@ let(:client) { Intercom::Client.new(token: 'token') } it "stops iterating if no starting after value" do - client.expects(:get).with("/contacts", {}). returns(page_of_contacts(false)) + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(false)) emails = [] client.contacts.all.each { |contact| emails << contact.email } _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com] @@ -15,6 +15,7 @@ client.expects(:get).with('/contacts', { starting_after: "EnCrYpTeDsTrInG" }).returns(page_of_contacts(false)) emails = [] client.contacts.all.each { |contact| emails << contact.email } + _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com test1@example.com test2@example.com test3@example.com] end it "supports indexed array access" do @@ -27,4 +28,25 @@ emails = client.contacts.all.map { |contact| contact.email } _(emails).must_equal %w[test1@example.com test2@example.com test3@example.com] end + + it "keeps entire collection iterable after first iteration" do + contacts = client.contacts.all + emails_iter1 = [] + emails_iter2 = [] + expects_pagination = proc do + client.expects(:get).with("/contacts", {}).returns(page_of_contacts(true)) + client.expects(:get).with("/contacts", { starting_after: "EnCrYpTeDsTrInG" }).returns(page_of_contacts(false)) + end + + expects_pagination.call + contacts.each { |contact| emails_iter1 << contact.email } + expects_pagination.call + contacts.each { |contact| emails_iter2 << contact.email } + _(emails_iter1).must_equal emails_iter2 + end + + it "supports query params" do + client.expects(:get).with("/conversations", {:intercom_user_id => 'abcdef0000'}).returns(test_conversation_list) + _(client.conversations.find_all(:intercom_user_id => 'abcdef0000').map(&:id)).must_equal %w[147] + end end diff --git a/spec/unit/intercom/collection_spec.rb b/spec/unit/intercom/collection_spec.rb new file mode 100644 index 00000000..87388c9c --- /dev/null +++ b/spec/unit/intercom/collection_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Intercom::Collection do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'creates a collection' do + client.expects(:post).with('/help_center/collections', { 'name' => 'Collection 1', 'description' => 'Collection desc' }).returns(test_collection) + client.collections.create(:name => 'Collection 1', :description => 'Collection desc') + end + + it 'lists collections' do + client.expects(:get).with('/help_center/collections', {}).returns(test_collection_list) + client.collections.all.each { |t| } + end + + it 'finds a collection' do + client.expects(:get).with('/help_center/collections/1', {}).returns(test_collection) + client.collections.find(id: '1') + end + + it 'updates a collection' do + collection = Intercom::Collection.new(id: '12345') + client.expects(:put).with('/help_center/collections/12345', {}) + client.collections.save(collection) + end + + it 'deletes a collection' do + collection = Intercom::Collection.new(id: '12345') + client.expects(:delete).with('/help_center/collections/12345', {}) + client.collections.delete(collection) + end +end \ No newline at end of file diff --git a/spec/unit/intercom/company_spec.rb b/spec/unit/intercom/company_spec.rb index c7e1c646..ac0f0e11 100644 --- a/spec/unit/intercom/company_spec.rb +++ b/spec/unit/intercom/company_spec.rb @@ -9,7 +9,7 @@ _(proc { client.companies.find(:company_id => "4")}).must_raise Intercom::HttpError end - it "on find_all" do + it "on all" do client.expects(:get).with("/companies", {}).returns(nil) _(proc { client.companies.all.each {|company| }}).must_raise Intercom::HttpError end @@ -35,4 +35,10 @@ _(proxy.url).must_equal '/companies/1/contacts' _(proxy.resource_class).must_equal Intercom::Contact end + + it "deletes a company" do + company = Intercom::Company.new("id" => "1") + client.expects(:delete).with("/companies/1", {}) + client.companies.delete(company) + end end diff --git a/spec/unit/intercom/contact_spec.rb b/spec/unit/intercom/contact_spec.rb index b0193fe4..6361905d 100644 --- a/spec/unit/intercom/contact_spec.rb +++ b/spec/unit/intercom/contact_spec.rb @@ -90,12 +90,10 @@ _(contact.to_hash['custom_attributes']).must_equal 'mad' => 123, 'other' => now.to_i, 'thing' => 'yay' end - it 'rejects nested data structures in custom_attributes' do + it 'rejects lists in custom_attributes' do contact = Intercom::Contact.new _(proc { contact.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) - _(proc { contact.custom_attributes['thing'] = { 1 => 2 } }).must_raise(ArgumentError) - _(proc { contact.custom_attributes['thing'] = { 1 => { 2 => 3 } } }).must_raise(ArgumentError) contact = Intercom::Contact.new(test_contact) _(proc { contact.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) @@ -248,6 +246,24 @@ client.contacts.delete(contact) end + it 'archives a contact' do + contact = Intercom::Contact.new('id' => '1') + client.expects(:post).with('/contacts/1/archive', {}) + client.contacts.archive(contact) + end + + it 'unarchives a contact' do + contact = Intercom::Contact.new('id' => '1') + client.expects(:post).with('/contacts/1/unarchive', {}) + client.contacts.unarchive(contact) + end + + it 'deletes an archived contact' do + contact = Intercom::Contact.new('id' => '1','archived' =>true) + client.expects(:delete).with('/contacts/1', {}) + client.contacts.delete_archived_contact("1") + end + describe 'merging' do let(:lead) { Intercom::Contact.from_api(external_id: 'contact_id', role: 'lead') } let(:user) { Intercom::Contact.from_api(id: 'external_id', role: 'user') } @@ -262,7 +278,9 @@ describe 'nested resources' do let(:contact) { Intercom::Contact.new(id: '1', client: client) } + let(:contact_no_tags) { Intercom::Contact.new(id: '2', client: client, tags: []) } let(:company) { Intercom::Company.new(id: '1') } + let(:subscription) { Intercom::Subscription.new(id: '1', client: client) } let(:tag) { Intercom::Tag.new(id: '1') } let(:note) { Intercom::Note.new(body: "Text for the note
") } @@ -273,6 +291,13 @@ _(proxy.resource_class).must_equal Intercom::Note end + it 'returns a collection proxy for listing segments' do + proxy = contact.segments + _(proxy.resource_name).must_equal 'segments' + _(proxy.url).must_equal '/contacts/1/segments' + _(proxy.resource_class).must_equal Intercom::Segment + end + it 'returns a collection proxy for listing tags' do proxy = contact.tags _(proxy.resource_name).must_equal 'tags' @@ -280,6 +305,27 @@ _(proxy.resource_class).must_equal Intercom::Tag end + it 'returns correct tags from differring contacts' do + client.expects(:get).with('/contacts/1/tags', {}).returns({ + 'type' => 'tag.list', + 'tags' => [ + { + 'type' => 'tag', + 'id' => '1', + 'name' => 'VIP Customer' + }, + { + 'type' => 'tag', + 'id' => '2', + 'name' => 'Test tag' + } + ] + }) + + _(contact_no_tags.tags.map{ |t| t.id }).must_equal [] + _(contact.tags.map{ |t| t.id }).must_equal ['1', '2'] + end + it 'returns a collection proxy for listing companies' do proxy = contact.companies _(proxy.resource_name).must_equal 'companies' @@ -297,6 +343,11 @@ contact.add_tag({ "id": tag.id }) end + it 'removes a subscription to a contact' do + client.expects(:delete).with("/contacts/1/subscriptions/#{subscription.id}", "id": subscription.id).returns(subscription.to_hash) + contact.remove_subscription_type({ "id": subscription.id }) + end + it 'removes a tag from a contact' do client.expects(:delete).with("/contacts/1/tags/#{tag.id}", "id": tag.id ).returns(tag.to_hash) contact.remove_tag({ "id": tag.id }) @@ -346,6 +397,11 @@ contact.create_note({body: note.body}) end + it 'adds a subscription to a contact' do + client.expects(:post).with('/contacts/1/subscriptions', "id": subscription.id).returns(subscription.to_hash) + contact.create_subscription_type({ "id": subscription.id }) + end + it 'adds a tag to a contact' do client.expects(:post).with('/contacts/1/tags', "id": tag.id).returns(tag.to_hash) contact.add_tag({ "id": tag.id }) diff --git a/spec/unit/intercom/conversation_spec.rb b/spec/unit/intercom/conversation_spec.rb index c777bda1..d585eea6 100644 --- a/spec/unit/intercom/conversation_spec.rb +++ b/spec/unit/intercom/conversation_spec.rb @@ -13,6 +13,11 @@ client.conversations.all.each { |c| } end + it "can filter conversations based on params" do + client.expects(:get).with("/conversations", {type: 'user', intercom_user_id: '123456789'}).returns(test_conversation_list) + client.conversations.find_all(type: 'user', intercom_user_id: '123456789').each { |c| } + end + it 'marks a conversation as read' do client.expects(:put).with('/conversations/147', { read: true }) client.conversations.mark_read('147') @@ -54,7 +59,7 @@ end it 'runs assignment rules on a conversation' do - client.expects(:post).with('/conversations/147/run_assignment_rules').returns(test_conversation) + client.expects(:post).with('/conversations/147/run_assignment_rules', {}).returns(test_conversation) client.conversations.run_assignment_rules('147') end diff --git a/spec/unit/intercom/event_spec.rb b/spec/unit/intercom/event_spec.rb index 125d9996..5d62551f 100644 --- a/spec/unit/intercom/event_spec.rb +++ b/spec/unit/intercom/event_spec.rb @@ -22,6 +22,13 @@ _(event_names).must_equal %W(invited-friend) end + it "able to fetch event summary" do + client.expects(:get).with("/events", type: 'user', email: 'joe@example.com', summary: true).returns(page_of_events(false)) + event_names = [] + client.events.find_all(type: 'user', email: 'joe@example.com',summary: true).each { |event| event_names << event.event_name } + _(event_names).must_equal %W(invited-friend) + end + it "keeps iterating if next link" do client.expects(:get).with("/events", type: 'user', email: 'joe@example.com').returns(page_of_events(true)) client.expects(:get).with("https://api.intercom.io/events?type=user&intercom_user_id=55a3b&before=144474756550", {}).returns(page_of_events(false)) diff --git a/spec/unit/intercom/export_content_spec.rb b/spec/unit/intercom/export_content_spec.rb new file mode 100644 index 00000000..6c7787b7 --- /dev/null +++ b/spec/unit/intercom/export_content_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe "Intercom::ExportContent" do + let(:client) { Intercom::Client.new(token: 'token') } + let(:job) { + { + job_identifier: "k0e27ohsyvh8ef3m", + status: "no_data", + download_url: "", + download_expires_at: 0 + } + } + + it "creates an export job" do + client.expects(:post).with("/export/content/data", {"created_at_after" => 1667566801, "created_at_before" => 1668085202}).returns(job) + client.export_content.create({"created_at_after" => 1667566801, "created_at_before" => 1668085202}) + end + + it "can view an export job" do + client.expects(:get).with("/export/content/data/#{job[:job_identifier]}", {}).returns(job) + client.export_content.find(id: job[:job_identifier]) + end + + it "Cancels a export job redirect" do + client.expects(:post).with("/export/cancel/#{job[:job_identifier]}", {}).returns(job) + client.export_content.cancel(job[:job_identifier]) + end +end diff --git a/spec/unit/intercom/lib/flat_store_spec.rb b/spec/unit/intercom/lib/flat_store_spec.rb index 3188b579..412ed717 100644 --- a/spec/unit/intercom/lib/flat_store_spec.rb +++ b/spec/unit/intercom/lib/flat_store_spec.rb @@ -3,11 +3,15 @@ require 'spec_helper' describe Intercom::Lib::FlatStore do - it 'raises if you try to set or merge in nested hash structures' do + it 'raises if you try to set arrays but allows hashes' do data = Intercom::Lib::FlatStore.new _(proc { data['thing'] = [1] }).must_raise ArgumentError - _(proc { data['thing'] = { 1 => 2 } }).must_raise ArgumentError - _(proc { Intercom::Lib::FlatStore.new(1 => { 2 => 3 }) }).must_raise ArgumentError + + data['thing'] = { 'key' => 'value' } + _(data['thing']).must_equal({ 'key' => 'value' }) + + flat_store = Intercom::Lib::FlatStore.new('custom_object' => { 'type' => 'Order.list', 'instances' => [{'id' => '123'}] }) + _(flat_store['custom_object']).must_equal({ 'type' => 'Order.list', 'instances' => [{'id' => '123'}] }) end it 'raises if you try to use a non string key' do @@ -28,4 +32,30 @@ _(data['b']).must_equal 2 _(data[:b]).must_equal 2 end + + describe '#to_submittable_hash' do + it 'filters out all hash values' do + data = Intercom::Lib::FlatStore.new( + 'regular_attr' => 'value', + 'number_attr' => 42, + 'custom_object' => { + 'type' => 'Order.list', + 'instances' => [ + { 'id' => '31', 'external_id' => 'ext_123' } + ] + }, + 'regular_hash' => { 'key' => 'value' }, + 'metadata' => { 'source' => 'api', 'version' => 2 } + ) + + submittable = data.to_submittable_hash + + _(submittable['regular_attr']).must_equal 'value' + _(submittable['number_attr']).must_equal 42 + + _(submittable.key?('custom_object')).must_equal false + _(submittable.key?('regular_hash')).must_equal false + _(submittable.key?('metadata')).must_equal false + end + end end diff --git a/spec/unit/intercom/phone_call_redirect.rb b/spec/unit/intercom/phone_call_redirect.rb new file mode 100644 index 00000000..02a38863 --- /dev/null +++ b/spec/unit/intercom/phone_call_redirect.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe "Intercom::PhoneCallRedirect" do + let(:client) { Intercom::Client.new(token: 'token') } + + it "creates a phone redirect" do + + client.expects(:post).with("/phone_call_redirect", {phone_number: "+353871234567"}) + client.phone_call_redirect.create(phone_number: "+353871234567") + end + +end diff --git a/spec/unit/intercom/request_spec.rb b/spec/unit/intercom/request_spec.rb index 2310bc79..6ec61cb8 100644 --- a/spec/unit/intercom/request_spec.rb +++ b/spec/unit/intercom/request_spec.rb @@ -124,6 +124,9 @@ def execute! let(:uri) {"https://api.intercom.io/conversations/reply"} let(:req) { Intercom::Request.put(uri, {}) } + let(:tag_uri) {"https://api.intercom.io/tags/"} + let(:del_req) { Intercom::Request.delete(tag_uri, {}) } + it 'should raise ResourceNotUniqueError error on resource_conflict code' do stub_request(:put, uri).to_return( status: [409, "Resource Already Exists"], @@ -153,5 +156,15 @@ def execute! expect { execute! }.must_raise(Intercom::ResourceNotFound) end + + it 'should raise TagHasDependentObjects error on tag_has_dependent_objects code' do + stub_request(:delete, tag_uri).to_return( + status: [400, "Bad Request"], + headers: { 'X-RateLimit-Reset' => (Time.now.utc + 10).to_i.to_s }, + body: { type: "error.list", errors: [ code: "tag_has_dependent_objects" ] }.to_json + ) + + expect { del_req.execute(tag_uri, token: 'test-token') }.must_raise(Intercom::TagHasDependentObjects) + end end end diff --git a/spec/unit/intercom/section_spec.rb b/spec/unit/intercom/section_spec.rb new file mode 100644 index 00000000..ff73f672 --- /dev/null +++ b/spec/unit/intercom/section_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Intercom::Section do + let(:client) { Intercom::Client.new(token: 'token') } + + it 'creates a section' do + client.expects(:post).with('/help_center/sections', { 'name' => 'Section 1', 'parent_id' => '1' }).returns(test_section) + client.sections.create(:name => 'Section 1', :parent_id => '1') + end + + it 'lists sections' do + client.expects(:get).with('/help_center/sections', {}).returns(test_section_list) + client.sections.all.each { |t| } + end + + it 'finds a section' do + client.expects(:get).with('/help_center/sections/1', {}).returns(test_section) + client.sections.find(id: '1') + end + + it 'updates a section' do + section = Intercom::Section.new(id: '12345') + client.expects(:put).with('/help_center/sections/12345', {}) + client.sections.save(section) + end + + it 'deletes a section' do + section = Intercom::Section.new(id: '12345') + client.expects(:delete).with('/help_center/sections/12345', {}) + client.sections.delete(section) + end +end \ No newline at end of file diff --git a/spec/unit/intercom/tag_spec.rb b/spec/unit/intercom/tag_spec.rb index 07a7d4d9..b18c97a7 100644 --- a/spec/unit/intercom/tag_spec.rb +++ b/spec/unit/intercom/tag_spec.rb @@ -28,4 +28,10 @@ client.expects(:post).with('/tags', 'name' => 'Test Tag', 'companies' => [{ company_id: 'abc123', untag: true }, { company_id: 'def456', untag: true }], 'tag_or_untag' => 'untag').returns(test_tag) client.tags.untag(name: 'Test Tag', companies: [{ company_id: 'abc123' }, { company_id: 'def456' }]) end + + it 'delete tags' do + tag = Intercom::Tag.new('id' => '1') + client.expects(:delete).with('/tags/1', {}).returns(tag) + client.tags.delete(tag) + end end diff --git a/spec/unit/intercom/traits/api_resource_spec.rb b/spec/unit/intercom/traits/api_resource_spec.rb index b5724556..7afc144f 100644 --- a/spec/unit/intercom/traits/api_resource_spec.rb +++ b/spec/unit/intercom/traits/api_resource_spec.rb @@ -17,7 +17,15 @@ 'metadata' => { 'type' => 'user', 'color' => 'cyan' - } } + }, + 'nested_fields' => { + 'type' => 'nested_fields_content', + 'field_1' => { + 'type' => 'field_content', + 'name' => 'Nested Field' + } + } + } end let(:object_hash) do @@ -35,6 +43,13 @@ metadata: { type: 'user', color: 'cyan' + }, + nested_fields: { + type: 'nested_fields_content', + field_1: { + type: 'field_content', + name: 'Nested Field' + } } } end @@ -107,12 +122,50 @@ class ConcreteApiResource; include Intercom::Traits::ApiResource; end api_resource.from_hash(object_hash) initialized_api_resource = ConcreteApiResource.new(object_hash) - except(object_json, 'type').keys.each do |attribute| assert_equal initialized_api_resource.send(attribute), api_resource.send(attribute) end end + describe 'correctly equates two resources' do + class DummyResource; include Intercom::Traits::ApiResource; end + + specify 'if each resource has the same class and same value' do + api_resource1 = DummyResource.new(object_json) + api_resource2 = DummyResource.new(object_json) + assert_equal (api_resource1 == api_resource2), true + end + + specify 'if each resource has the same class and different value' do + object2_json = object_json.merge('id' => 'bbbbbb') + api_resource1 = DummyResource.new(object_json) + api_resource2 = DummyResource.new(object2_json) + assert_equal (api_resource1 == api_resource2), false + end + + specify 'if each resource has a different class' do + dummy_resource = DummyResource.new(object_json) + assert_equal (dummy_resource == api_resource), false + end + end + + it 'correctly generates submittable hash when no updates' do + assert_equal api_resource.to_submittable_hash, {} + end + + it 'correctly generates submittable hash when there are updates' do + api_resource.name = 'SuperSuite updated' + api_resource.nested_fields.field_1.name = 'Updated Name' + assert_equal api_resource.to_submittable_hash, { + 'name' => 'SuperSuite updated', + 'nested_fields' => { + 'field_1' => { + 'name' => 'Updated Name' + } + } + } + end + def except(h, *keys) keys.each { |key| h.delete(key) } h diff --git a/spec/unit/intercom/user_spec.rb b/spec/unit/intercom/user_spec.rb index fa6deddb..a570abfe 100644 --- a/spec/unit/intercom/user_spec.rb +++ b/spec/unit/intercom/user_spec.rb @@ -106,12 +106,10 @@ _(user.to_hash['companies']).must_equal companies end - it 'rejects nested data structures in custom_attributes' do + it 'rejects lists in custom_attributes' do user = Intercom::User.new _(proc { user.custom_attributes['thing'] = [1] }).must_raise(ArgumentError) - _(proc { user.custom_attributes['thing'] = { 1 => 2 } }).must_raise(ArgumentError) - _(proc { user.custom_attributes['thing'] = { 1 => { 2 => 3 } } }).must_raise(ArgumentError) user = Intercom::User.from_api(test_user) _(proc { user.custom_attributes['thing'] = [1] }).must_raise(ArgumentError)