From 5d031b537973c85f862bfa126084b26a9a3687c2 Mon Sep 17 00:00:00 2001 From: Raul E Rangel Date: Mon, 9 Dec 2013 14:26:19 -0700 Subject: [PATCH] Add ability to send targeted company shares This introduces a breaking change to the add_company_share method. We now parse the response body so the user does not have to parse the xml response body. This change should not impact many users seeing as the add_company_share method was just added a while ago and there hasn't been a release since. Usage: response = client.add_company_share("2414183", { :comment => "Testing, 1, 2, 3", :targets => { :geos => ['as', 'eu'], :jobFunc => ['acct', 'bd'] } puts "ID: #{response.body.update_key}" puts "URL: #{response.body.update_url}" --- lib/linked_in/api/update_methods.rb | 25 ++++++++++- lib/linked_in/client.rb | 1 + lib/linked_in/helpers/request.rb | 6 +-- lib/linked_in/mash.rb | 17 +++++++ lib/linked_in/template.rb | 37 +++++++++++++++ lib/linked_in/templates/company_share.xml.erb | 33 ++++++++++++++ lib/linkedin.rb | 5 ++- linkedin.gemspec | 1 + spec/cases/api_spec.rb | 45 ++++++++++++++++--- 9 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 lib/linked_in/template.rb create mode 100644 lib/linked_in/templates/company_share.xml.erb diff --git a/lib/linked_in/api/update_methods.rb b/lib/linked_in/api/update_methods.rb index 0a70273e..9b41b718 100644 --- a/lib/linked_in/api/update_methods.rb +++ b/lib/linked_in/api/update_methods.rb @@ -9,10 +9,26 @@ def add_share(share) post(path, defaults.merge(share).to_json, "Content-Type" => "application/json") end + # Creates a company share + # + # Returns an HttpResponse object with a body containing update_key and update_url + # + # @param company_id [String] The company id + # @param share [Hash] Share data + # + # @option share [String] comment Post must contain comment and/or (content/title and content/submitted-url). Max length is 700 characters. + # @option share [Hash] content Content Hash + # @option content [String] title Post must contain comment and/or (content/title and content/submitted-url). Max length is 200 characters. + # @option content [String] submitted-url Post must contain comment and/or (content/title and content/submitted-url). + # @option content [String] submitted-image-url Invalid without (content/title and content/submitted-url). + # @option content [String] description Max length of 256 characters. + # @option share [Hash] targets Hash containing target_code => ['target_value', ...] + # + # @return [HttpResponse] Response def add_company_share(company_id, share) path = "/companies/#{company_id}/shares" defaults = {:visibility => {:code => "anyone"}} - post(path, defaults.merge(share).to_json, "Content-Type" => "application/json") + hashify post(path, render(:company_share, defaults.merge(share)), 'x-li-format' => 'xml', "Content-Type" => "application/xml") end def follow_company(company_id) @@ -74,6 +90,13 @@ def post_group_discussion(group_id, discussion) post(path, discussion.to_json, "Content-Type" => "application/json") end + private + + def hashify response + response.body = Mash.from_response response + response + end + end end diff --git a/lib/linked_in/client.rb b/lib/linked_in/client.rb index fdf99dca..ea01995d 100644 --- a/lib/linked_in/client.rb +++ b/lib/linked_in/client.rb @@ -8,6 +8,7 @@ class Client include Api::QueryMethods include Api::UpdateMethods include Search + include Template attr_reader :consumer_token, :consumer_secret, :consumer_options diff --git a/lib/linked_in/helpers/request.rb b/lib/linked_in/helpers/request.rb index 94f66592..c7b3318d 100644 --- a/lib/linked_in/helpers/request.rb +++ b/lib/linked_in/helpers/request.rb @@ -42,13 +42,13 @@ def raise_errors(response) # in the HTTP answer (thankfully). case response.code.to_i when 401 - data = Mash.from_json(response.body) + data = Mash.from_response(response) raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}" when 400 - data = Mash.from_json(response.body) + data = Mash.from_response(response) raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}" when 403 - data = Mash.from_json(response.body) + data = Mash.from_response(response) raise LinkedIn::Errors::AccessDeniedError.new(data), "(#{data.status}): #{data.message}" when 404 raise LinkedIn::Errors::NotFoundError, "(#{response.code}): #{response.message}" diff --git a/lib/linked_in/mash.rb b/lib/linked_in/mash.rb index 8079fff8..fc8e969e 100644 --- a/lib/linked_in/mash.rb +++ b/lib/linked_in/mash.rb @@ -1,5 +1,6 @@ require 'hashie' require 'multi_json' +require 'multi_xml' module LinkedIn class Mash < ::Hashie::Mash @@ -10,6 +11,22 @@ def self.from_json(json_string) new(result_hash) end + # a simple helper to convert an xml string to a Mash + def self.from_xml(xml_string) + result_hash = ::MultiXml.parse(xml_string) + + # Drop off the root element + new(result_hash[result_hash.keys.first]) + end + + def self.from_response(response) + if response['x-li-format'] == 'xml' or /\bxml\b/.match response['Content-Type'] + from_xml(response.body) + else + from_json(response.body) + end + end + # returns a Date if we have year, month and day, and no conflicting key def to_date if !self.has_key?('to_date') && contains_date_fields? diff --git a/lib/linked_in/template.rb b/lib/linked_in/template.rb new file mode 100644 index 00000000..14c38adc --- /dev/null +++ b/lib/linked_in/template.rb @@ -0,0 +1,37 @@ +require 'erb' +require 'ostruct' +require 'hashie' + +module LinkedIn + class TemplateBinding < ::Hashie::Mash + include ERB::Util + end + + module Template + + class << self + cache = {} + mutex = Mutex.new + + define_method :load_template do |template| + return cache[template] if cache[template] + mutex.synchronize do + return cache[template] if cache[template] + + file = File.join(LinkedIn.templates, "#{template.to_s}.xml.erb") + io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file) + erb = ERB.new(io) + erb.filename = file + + cache[template] = erb + end + end + end + + def render template, data + template = Template.load_template template + namespace = TemplateBinding.new data + template.result namespace.instance_eval { binding } + end + end +end diff --git a/lib/linked_in/templates/company_share.xml.erb b/lib/linked_in/templates/company_share.xml.erb new file mode 100644 index 00000000..bb2a2f1a --- /dev/null +++ b/lib/linked_in/templates/company_share.xml.erb @@ -0,0 +1,33 @@ + + + + <%=h visibility.code%> + + <% if comment %> + <%= h comment %> + <% end %> + <% if content %> + + <% if content["title"] %><%= h content["title"] %><% end %> + <%= h content["submitted-url"] %> + <% if content["description"] %><%= h content["description"] %><% end %> + <% if content["submitted-image-url"] %><%= h content["submitted-image-url"] %><% end %> + + <% end %> + <% if targets %> + + + <% targets.each do |key, values| %> + + <%= h key %> + + <% values.each do |value| %> + <%= h value %> + <% end %> + + + <% end %> + + + <% end %> + diff --git a/lib/linkedin.rb b/lib/linkedin.rb index cd3524bf..1e7588f6 100644 --- a/lib/linkedin.rb +++ b/lib/linkedin.rb @@ -3,7 +3,7 @@ module LinkedIn class << self - attr_accessor :token, :secret, :default_profile_fields + attr_accessor :token, :secret, :default_profile_fields, :templates # config/initializers/linkedin.rb (for instance) # @@ -22,6 +22,8 @@ def configure end end + @templates = File.join(File.expand_path(File.dirname(__FILE__)), 'linked_in', 'templates') + autoload :Api, "linked_in/api" autoload :Client, "linked_in/client" autoload :Mash, "linked_in/mash" @@ -29,4 +31,5 @@ def configure autoload :Helpers, "linked_in/helpers" autoload :Search, "linked_in/search" autoload :Version, "linked_in/version" + autoload :Template, "linked_in/template" end diff --git a/linkedin.gemspec b/linkedin.gemspec index 91f69fd6..1aca42c1 100644 --- a/linkedin.gemspec +++ b/linkedin.gemspec @@ -4,6 +4,7 @@ require File.expand_path('../lib/linked_in/version', __FILE__) Gem::Specification.new do |gem| gem.add_dependency 'hashie', ['>= 1.2', '< 2.1'] gem.add_dependency 'multi_json', '~> 1.0' + gem.add_dependency 'multi_xml' gem.add_dependency 'oauth', '~> 0.4' # gem.add_development_dependency 'json', '~> 1.6' gem.add_development_dependency 'rake', '~> 10' diff --git a/spec/cases/api_spec.rb b/spec/cases/api_spec.rb index e300859c..824d831f 100644 --- a/spec/cases/api_spec.rb +++ b/spec/cases/api_spec.rb @@ -68,13 +68,6 @@ response.code.should == "201" end - it "should be able to share a new company status" do - stub_request(:post, "https://api.linkedin.com/v1/companies/123456/shares").to_return(:body => "", :status => 201) - response = client.add_company_share("123456", { :comment => "Testing, 1, 2, 3" }) - response.body.should == nil - response.code.should == "201" - end - it "returns the shares for a person" do stub_request(:get, "https://api.linkedin.com/v1/people/~/network/updates?type=SHAR&scope=self&after=1234&count=35").to_return( :body => "{}") @@ -192,6 +185,44 @@ response.code.should == "201" end + it "should be able to share a new company status" do + stub_request(:post, "https://api.linkedin.com/v1/companies/2414183/shares").with(:headers => { 'Content-Type' => 'application/xml' }).to_return(:headers => {'Content-Type' => 'application/xml'}, :body => 'UNIU-c2414183-5811244423991812096-SHAREhttp://www.linkedin.com/company/2414183/comments?topic=5811244423991812096&type=U&scope=2414183&stype=C&a=FlWW', :status => 201) + response = client.add_company_share("2414183", { :comment => "Testing, 1, 2, 3" }) + response.body.update_key.should == 'UNIU-c2414183-5811244423991812096-SHARE' + response.body.update_url.should == 'http://www.linkedin.com/company/2414183/comments?topic=5811244423991812096&type=U&scope=2414183&stype=C&a=FlWW' + response.code.should == "201" + end + + it "should be able to handle an error" do + stub_request(:post, "https://api.linkedin.com/v1/companies/2414183/shares").with(:headers => { 'Content-Type' => 'application/xml' }).to_return(:headers => {'Content-Type' => 'application/xml'}, :body => ' 403 1386620304843 ZIBEJ5MXJ2 0 Member 172914333 cannot post updates on behalf of company 2414183 due to too few targeted followers ', :status => 403) + + expect { + client.add_company_share("2414183", { :comment => "Testing, 1, 2, 3" }) + }.to raise_error(LinkedIn::Errors::AccessDeniedError){ |error| + error.data.message.should_not be_nil + } + end + + it "should be able to target a new company status" do + stub_request(:post, "https://api.linkedin.com/v1/companies/2414183/shares").with(:headers => { 'Content-Type' => 'application/xml' }).to_return(:headers => {'Content-Type' => 'application/xml'}, :body => 'UNIU-c2414183-5811244423991812096-SHAREhttp://www.linkedin.com/company/2414183/comments?topic=5811244423991812096&type=U&scope=2414183&stype=C&a=FlWW', :status => 201) + response = client.add_company_share("2414183", { + :comment => "Testing, 1, 2, 3", + :content => { + :"submitted-url" => "http://www.example.com/content.html", + :title => "Test Share with Content", + :description => "content description", + :"submitted-image-url" => "http://www.example.com/image.jpg" + }, + :targets => { + :geos => ['as', 'eu'], + :jobFunc => ['acct', 'bd'] + } + }) + response.body.update_key.should == 'UNIU-c2414183-5811244423991812096-SHARE' + response.body.update_url.should == 'http://www.linkedin.com/company/2414183/comments?topic=5811244423991812096&type=U&scope=2414183&stype=C&a=FlWW' + response.code.should == "201" + end + end context "Job API" do