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