From 1380065c16f3169bab7d1c2c0a3e5cbc6210fc19 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Fri, 22 Feb 2019 17:56:02 -0800 Subject: [PATCH 1/9] Update LinkedIn::Api::ShareAndSocialStream#add_share for Api v2 * The path changed to /v2/ugcPosts * More attributes are required in the payload Along with sending over the comment, you must send the user's "urn" Example: ``` client = LinkedIn::Client.new client.authorize_from_access(@delivery_method_attributes[:token]) client.add_share({ comment: 'hi', urn: 'urn' }) ``` https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin?context=linkedin/consumer/context --- lib/linked_in/api/share_and_social_stream.rb | 22 ++++++++++++++++---- lib/linked_in/helpers/request.rb | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/linked_in/api/share_and_social_stream.rb b/lib/linked_in/api/share_and_social_stream.rb index 1ba97e7e..556da78d 100644 --- a/lib/linked_in/api/share_and_social_stream.rb +++ b/lib/linked_in/api/share_and_social_stream.rb @@ -82,16 +82,30 @@ def share_likes(update_key, options={}) # Create a share for the authenticated user # - # Permissions: rw_nus + # Permissions: w_member_share # # @see https://developer.linkedin.com/docs/share-on-linkedin Share API # # @macro share_input_fields # @return [void] def add_share(share) - path = "/people/~/shares" - defaults = {:visibility => {:code => "anyone"}} - post(path, MultiJson.dump(defaults.merge(share)), "Content-Type" => "application/json") + path = '/ugcPosts' + payload = { + author: "urn:li:person:#{share[:urn]}", + lifecycleState: 'PUBLISHED', + specificContent: { + 'com.linkedin.ugc.ShareContent' => { + shareCommentary: { text: share[:comment] }, + shareMediaCategory: 'NONE' + } + }, + visibility: { + 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' + } + } + headers = { "Content-Type" => "application/json", + "X-Restli-Protocol-Version" => "2.0.0" } + post(path, MultiJson.dump(payload), headers) end # Create a comment on an update from the authenticated user diff --git a/lib/linked_in/helpers/request.rb b/lib/linked_in/helpers/request.rb index ee4f9c13..fd14aa81 100644 --- a/lib/linked_in/helpers/request.rb +++ b/lib/linked_in/helpers/request.rb @@ -7,7 +7,7 @@ module Request 'x-li-format' => 'json' } - API_PATH = '/v1' + API_PATH = '/v2' protected From 2a7e4bf311716219b448e2fe7d85e0fbcae3f26f Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Thu, 28 Feb 2019 17:03:58 -0800 Subject: [PATCH 2/9] Make urn required by #add_share Can't post without it so no reason to make it an option. Also improve docs --- lib/linked_in/api/share_and_social_stream.rb | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/linked_in/api/share_and_social_stream.rb b/lib/linked_in/api/share_and_social_stream.rb index 556da78d..0d94e4b7 100644 --- a/lib/linked_in/api/share_and_social_stream.rb +++ b/lib/linked_in/api/share_and_social_stream.rb @@ -84,25 +84,28 @@ def share_likes(update_key, options={}) # # Permissions: w_member_share # - # @see https://developer.linkedin.com/docs/share-on-linkedin Share API + # @see https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin + # + # @param [String] urn User's URN (UID) returned from OAuth access token + # request + # @param [Hash] share The body we want to submit to LinkedIn # # @macro share_input_fields # @return [void] - def add_share(share) + def add_share(urn, share = {}) path = '/ugcPosts' - payload = { - author: "urn:li:person:#{share[:urn]}", - lifecycleState: 'PUBLISHED', - specificContent: { + payload = { author: "urn:li:person:#{urn}", lifecycleState: 'PUBLISHED', + visibility: { + 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' + } } + if share[:comment].present? + payload[:specificContent] = { 'com.linkedin.ugc.ShareContent' => { shareCommentary: { text: share[:comment] }, shareMediaCategory: 'NONE' } - }, - visibility: { - 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' } - } + end headers = { "Content-Type" => "application/json", "X-Restli-Protocol-Version" => "2.0.0" } post(path, MultiJson.dump(payload), headers) From 6a0c254164a7b1b3e967f628d663274a0107a047 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Thu, 28 Feb 2019 17:53:28 -0800 Subject: [PATCH 3/9] Add support for sharing urls to v2 Add the url to the payload. The link title and description are also customizable Example: ``` payload = { comment: "Comment", title: "Link Title", description: "Link Description", url: "http://example.com/article" } client.add_share(urn, payload) ``` --- lib/linked_in/api/share_and_social_stream.rb | 32 +++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/linked_in/api/share_and_social_stream.rb b/lib/linked_in/api/share_and_social_stream.rb index 0d94e4b7..260f84fd 100644 --- a/lib/linked_in/api/share_and_social_stream.rb +++ b/lib/linked_in/api/share_and_social_stream.rb @@ -94,17 +94,39 @@ def share_likes(update_key, options={}) # @return [void] def add_share(urn, share = {}) path = '/ugcPosts' - payload = { author: "urn:li:person:#{urn}", lifecycleState: 'PUBLISHED', - visibility: { - 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' - } } - if share[:comment].present? + payload = { + author: "urn:li:person:#{urn}", + lifecycleState: 'PUBLISHED', + visibility: { 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' } + } + if share[:url].present? + media = { status: 'READY', originalUrl: share[:url] } + if share[:description] + media[:description] = { text: share[:description] } + end + if share[:title] + media[:title] = { text: share[:title] } + end + payload[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + shareMediaCategory: 'ARTICLE', + media: [media] + } + } + if share[:comment].present? + payload[:specificContent]['com.linkedin.ugc.ShareContent'][:shareCommentary] = + { text: share[:comment] } + end + elsif share[:comment].present? payload[:specificContent] = { 'com.linkedin.ugc.ShareContent' => { shareCommentary: { text: share[:comment] }, shareMediaCategory: 'NONE' } } + else + raise LinkedIn::Errors::UnavailableError, + 'At least a comment is required' end headers = { "Content-Type" => "application/json", "X-Restli-Protocol-Version" => "2.0.0" } From 7a487dfbb2c25469e950b9dc0ae1abfc786812ec Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Tue, 12 Mar 2019 11:08:21 -0700 Subject: [PATCH 4/9] Move v2 Api methods to separate file Rollback share_and_social_stream file back to master Not sure how you want to keep v2 methods separate, but this allows v1 endpoints to still work. You said to do "something like Linkedin::V2.add_share", but that doesn't really make sense since we run the methods on the LinkedIn::Client. A possibility is to add a LinkedIn::V2::Client, then include the v2 methods into that class. --- lib/linked_in/api.rb | 1 + lib/linked_in/api/share_and_social_stream.rb | 51 ++----------- lib/linked_in/api/v2.rb | 74 ++++++++++++++++++ lib/linked_in/client.rb | 2 + lib/linked_in/helpers.rb | 1 + lib/linked_in/helpers/request.rb | 2 +- lib/linked_in/helpers/v2_request.rb | 80 ++++++++++++++++++++ 7 files changed, 165 insertions(+), 46 deletions(-) create mode 100644 lib/linked_in/api/v2.rb create mode 100644 lib/linked_in/helpers/v2_request.rb diff --git a/lib/linked_in/api.rb b/lib/linked_in/api.rb index 84aaf432..667b7a64 100644 --- a/lib/linked_in/api.rb +++ b/lib/linked_in/api.rb @@ -34,5 +34,6 @@ module Api autoload :Jobs, "linked_in/api/jobs" autoload :ShareAndSocialStream, "linked_in/api/share_and_social_stream" autoload :Communications, "linked_in/api/communications" + autoload :V2, "linked_in/api/v2" end end diff --git a/lib/linked_in/api/share_and_social_stream.rb b/lib/linked_in/api/share_and_social_stream.rb index 260f84fd..1ba97e7e 100644 --- a/lib/linked_in/api/share_and_social_stream.rb +++ b/lib/linked_in/api/share_and_social_stream.rb @@ -82,55 +82,16 @@ def share_likes(update_key, options={}) # Create a share for the authenticated user # - # Permissions: w_member_share - # - # @see https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin + # Permissions: rw_nus # - # @param [String] urn User's URN (UID) returned from OAuth access token - # request - # @param [Hash] share The body we want to submit to LinkedIn + # @see https://developer.linkedin.com/docs/share-on-linkedin Share API # # @macro share_input_fields # @return [void] - def add_share(urn, share = {}) - path = '/ugcPosts' - payload = { - author: "urn:li:person:#{urn}", - lifecycleState: 'PUBLISHED', - visibility: { 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' } - } - if share[:url].present? - media = { status: 'READY', originalUrl: share[:url] } - if share[:description] - media[:description] = { text: share[:description] } - end - if share[:title] - media[:title] = { text: share[:title] } - end - payload[:specificContent] = { - 'com.linkedin.ugc.ShareContent' => { - shareMediaCategory: 'ARTICLE', - media: [media] - } - } - if share[:comment].present? - payload[:specificContent]['com.linkedin.ugc.ShareContent'][:shareCommentary] = - { text: share[:comment] } - end - elsif share[:comment].present? - payload[:specificContent] = { - 'com.linkedin.ugc.ShareContent' => { - shareCommentary: { text: share[:comment] }, - shareMediaCategory: 'NONE' - } - } - else - raise LinkedIn::Errors::UnavailableError, - 'At least a comment is required' - end - headers = { "Content-Type" => "application/json", - "X-Restli-Protocol-Version" => "2.0.0" } - post(path, MultiJson.dump(payload), headers) + def add_share(share) + path = "/people/~/shares" + defaults = {:visibility => {:code => "anyone"}} + post(path, MultiJson.dump(defaults.merge(share)), "Content-Type" => "application/json") end # Create a comment on an update from the authenticated user diff --git a/lib/linked_in/api/v2.rb b/lib/linked_in/api/v2.rb new file mode 100644 index 00000000..3df854e1 --- /dev/null +++ b/lib/linked_in/api/v2.rb @@ -0,0 +1,74 @@ +module LinkedIn + module Api + + # V2 Consumer API + # + # @see https://docs.microsoft.com/en-us/linkedin/consumer/ + module V2 + + # Obtain profile information for a member. Currently, the method only + # accesses the authenticated user. + # + # Permissions: r_liteprofile r_emailaddress + # + # @see https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context#retrieving-member-profiles + # + # @return [void] + def v2_profile + path = '/me' + v2_get(path) + end + + # Share content for the authenticated user + # + # Permissions: w_member_share + # + # @see https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin + # + # @param [String] urn User's URN (UID) returned from OAuth access token + # request + # @param [Hash] share The body we want to submit to LinkedIn + # + # @macro share_input_fields + # @return [void] + def v2_add_share(urn, share = {}) + path = '/ugcPosts' + payload = { + author: "urn:li:person:#{urn}", + lifecycleState: 'PUBLISHED', + visibility: { 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' } + } + if share[:url] + media = { status: 'READY', originalUrl: share[:url] } + if share[:description] + media[:description] = { text: share[:description] } + end + if share[:title] + media[:title] = { text: share[:title] } + end + payload[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + shareMediaCategory: 'ARTICLE', + media: [media] + } + } + if share[:comment] + payload[:specificContent]['com.linkedin.ugc.ShareContent'][:shareCommentary] = + { text: share[:comment] } + end + elsif share[:comment] + payload[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + shareCommentary: { text: share[:comment] }, + shareMediaCategory: 'NONE' + } + } + else + raise LinkedIn::Errors::UnavailableError, + 'LinkedIn API: At least a comment is required' + end + v2_post(path, MultiJson.dump(payload)) + end + end + end +end diff --git a/lib/linked_in/client.rb b/lib/linked_in/client.rb index 645f196d..22983324 100644 --- a/lib/linked_in/client.rb +++ b/lib/linked_in/client.rb @@ -5,6 +5,7 @@ module LinkedIn class Client include Helpers::Request include Helpers::Authorization + include Helpers::V2Request include Api::QueryHelpers include Api::People include Api::Groups @@ -12,6 +13,7 @@ class Client include Api::Jobs include Api::ShareAndSocialStream include Api::Communications + include Api::V2 include Search attr_reader :consumer_token, :consumer_secret, :consumer_options diff --git a/lib/linked_in/helpers.rb b/lib/linked_in/helpers.rb index b8bbf315..374554ed 100644 --- a/lib/linked_in/helpers.rb +++ b/lib/linked_in/helpers.rb @@ -2,5 +2,6 @@ module LinkedIn module Helpers autoload :Authorization, "linked_in/helpers/authorization" autoload :Request, "linked_in/helpers/request" + autoload :V2Request, "linked_in/helpers/v2_request" end end diff --git a/lib/linked_in/helpers/request.rb b/lib/linked_in/helpers/request.rb index fd14aa81..ee4f9c13 100644 --- a/lib/linked_in/helpers/request.rb +++ b/lib/linked_in/helpers/request.rb @@ -7,7 +7,7 @@ module Request 'x-li-format' => 'json' } - API_PATH = '/v2' + API_PATH = '/v1' protected diff --git a/lib/linked_in/helpers/v2_request.rb b/lib/linked_in/helpers/v2_request.rb new file mode 100644 index 00000000..091d18d5 --- /dev/null +++ b/lib/linked_in/helpers/v2_request.rb @@ -0,0 +1,80 @@ +module LinkedIn + module Helpers + + module V2Request + + DEFAULT_HEADERS = { + 'x-li-format' => 'json', + 'Content-Type' => 'application/json', + 'X-Restli-Protocol-Version' => '2.0.0' + } + + API_PATH = '/v2' + + protected + + def v2_get(path, options = {}) + response = access_token.get("#{API_PATH}#{path}", + headers: DEFAULT_HEADERS.merge(options)) + raise_errors(response) + response.body + end + + def v2_post(path, body = '', options = {}) + options = { body: body, headers: DEFAULT_HEADERS.merge(options) } + # response is OAuth2::Response + # response.response is Faraday::Response + # sending back response.response makes it easier to access the env + response = access_token.post("#{API_PATH}#{path}", options).response + raise_errors(response) + response + end + + private + + def raise_errors(response) + # Even if the json answer contains the HTTP status code, LinkedIn also sets this code + # in the HTTP answer (thankfully). + case response.status.to_i + when 401 + data = Mash.from_json(response.body) + raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}" + when 400 + data = Mash.from_json(response.body) + raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}" + when 403 + data = Mash.from_json(response.body) + raise LinkedIn::Errors::AccessDeniedError.new(data), "(#{data.status}): #{data.message}" + when 404 + raise LinkedIn::Errors::NotFoundError, "(#{response.status}): #{response.message}" + when 500 + raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.status}): #{response.message}" + when 502..503 + raise LinkedIn::Errors::UnavailableError, "(#{response.status}): #{response.message}" + end + end + + + # Stolen from Rack::Util.build_query + def to_query(params) + params.map { |k, v| + if v.class == Array + to_query(v.map { |x| [k, x] }) + else + v.nil? ? escape(k) : "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" + end + }.join("&") + end + + def to_uri(path, options) + uri = URI.parse(path) + + if options && options != {} + uri.query = to_query(options) + end + uri.to_s + end + end + + end +end From b7fdad948b7b1af1a3f7bb16c80b8b827c3569a7 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Tue, 12 Mar 2019 17:44:19 -0700 Subject: [PATCH 5/9] Add specs for LinkedIn::Api::V2 v2_profile and v2_add_share --- spec/cases/v2_spec.rb | 226 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 spec/cases/v2_spec.rb diff --git a/spec/cases/v2_spec.rb b/spec/cases/v2_spec.rb new file mode 100644 index 00000000..0603392e --- /dev/null +++ b/spec/cases/v2_spec.rb @@ -0,0 +1,226 @@ +require 'helper' + +describe LinkedIn::Api::V2 do + let(:token) { '77j2rfbjbmkcdh' } + let(:consumer_options) do + { site: 'https://api.linkedin.com', raise_errors: false } + end + + let(:client) { LinkedIn::Client.new('token', 'secret') } + let(:consumer) { OAuth2::Client.new('token', 'secret', consumer_options) } + + let(:headers) do + { + 'Accept'=>'*/*', + 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Authorization'=>"Bearer #{token}", + 'Content-Type' => 'application/json', + 'User-Agent'=>'Faraday v0.15.4', + 'X-Li-Format' => 'json', + 'X-Restli-Protocol-Version' => '2.0.0' + } + end + + before do + LinkedIn.default_profile_fields = nil + client.stub(:consumer).and_return(consumer) + client.authorize_from_access(token) + end + + describe '#profile' do + let(:api_url) { 'https://api.linkedin.com/v2/me' } + + context "when LinkedIn returns 201 status code" do + before { stub_request(:get, api_url) } + + it "should send a request" do + client.v2_profile + + expect(a_request(:get, api_url).with(headers: headers, body: nil) + ).to have_been_made.once + end + end + + context 'when LinkedIn returns 403 status code' do + before { stub_request(:get, api_url).to_return(body: '{}', status: 403) } + + it 'returns 403 status code' do + expect do + client.v2_profile + end.to raise_error(LinkedIn::Errors::AccessDeniedError) + end + end + end + + describe '#add_share' do + let(:urn) { '1234567890' } + let(:comment) { 'Testing, 1, 2, 3' } + let(:url) { 'http://example.com/share' } + let(:title) { 'Foobar Title' } + let(:body) do + { author: "urn:li:person:#{urn}", lifecycleState: 'PUBLISHED', + visibility: { 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' } } + end + + context "when comment only" do + before do + body[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + shareCommentary: { text: comment }, + shareMediaCategory: 'NONE' + } + } + end + + context "when LinkedIn returns 201 status code" do + before do + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 201) + end + + it "should send a request" do + client.v2_add_share(urn, comment: comment) + + expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .with(body: body, headers: headers) + ).to have_been_made.once + end + end + + context "when LinkedIn returns 403 status code" do + before do + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 403) + end + + it "should raise AccessDeniedError" do + expect do + client.v2_add_share(urn, comment: comment) + end.to raise_error(LinkedIn::Errors::AccessDeniedError) + end + end + end + + context "when url only" do + before do + body[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + media: [{ status: 'READY', originalUrl: url }], + shareMediaCategory: 'ARTICLE' + } + } + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 201) + end + + it "should send a request" do + client.v2_add_share(urn, url: url) + + expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .with(body: body, headers: headers) + ).to have_been_made.once + end + end + + context "when comment, url, and title" do + before do + body[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + media: [ + { status: 'READY', originalUrl: url, title: { text: title } } + ], + shareCommentary: { text: comment }, + shareMediaCategory: 'ARTICLE' + } + } + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 201) + end + + it "should send a request" do + client.v2_add_share(urn, comment: comment, url: url, title: title) + + expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .with(body: body, headers: headers) + ).to have_been_made.once + end + end + + context "when url and title" do + before do + body[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + media: [ + { status: 'READY', originalUrl: url, title: { text: title } } + ], + shareMediaCategory: 'ARTICLE' + } + } + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 201) + end + + it "should send a request" do + client.v2_add_share(urn, url: url, title: title) + + expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .with(body: body, headers: headers) + ).to have_been_made.once + end + end + + context "when url and comment" do + before do + body[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + media: [{ status: 'READY', originalUrl: url }], + shareCommentary: { text: comment }, + shareMediaCategory: 'ARTICLE' + } + } + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 201) + end + + it "should send a request" do + client.v2_add_share(urn, url: url, comment: comment) + + expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .with(body: body, headers: headers) + ).to have_been_made.once + end + end + + context "when comment and title" do + before do + body[:specificContent] = { + 'com.linkedin.ugc.ShareContent' => { + shareCommentary: { text: comment }, + shareMediaCategory: 'NONE' + } + } + + stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .to_return(body: '{}', status: 201) + end + + it "should send a request with comment only" do + client.v2_add_share(urn, title: title, comment: comment) + + expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') + .with(body: body, headers: headers) + ).to have_been_made.once + end + end + + context "when title only" do + before { stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') } + + it "should raise error" do + expect do + client.v2_add_share(urn, title: title) + end.to raise_error(LinkedIn::Errors::UnavailableError) + end + end + end +end From 68eee8512598197a4f9e7e1366789bc779c1c6af Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Tue, 12 Mar 2019 17:47:22 -0700 Subject: [PATCH 6/9] Fix spec titles --- spec/cases/v2_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/cases/v2_spec.rb b/spec/cases/v2_spec.rb index 0603392e..5520789b 100644 --- a/spec/cases/v2_spec.rb +++ b/spec/cases/v2_spec.rb @@ -27,7 +27,7 @@ client.authorize_from_access(token) end - describe '#profile' do + describe '#v2_profile' do let(:api_url) { 'https://api.linkedin.com/v2/me' } context "when LinkedIn returns 201 status code" do @@ -52,7 +52,7 @@ end end - describe '#add_share' do + describe '#v2_add_share' do let(:urn) { '1234567890' } let(:comment) { 'Testing, 1, 2, 3' } let(:url) { 'http://example.com/share' } From 299840f0a100cf112a4eb60fb25ed51d2abaded2 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Tue, 12 Mar 2019 18:15:39 -0700 Subject: [PATCH 7/9] Refactor LinkedInApi::V2 --- lib/linked_in/api/v2.rb | 46 +++++++++++++++++++++++++++++------------ spec/cases/v2_spec.rb | 20 ++++++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/lib/linked_in/api/v2.rb b/lib/linked_in/api/v2.rb index 3df854e1..93630d01 100644 --- a/lib/linked_in/api/v2.rb +++ b/lib/linked_in/api/v2.rb @@ -27,18 +27,39 @@ def v2_profile # # @param [String] urn User's URN (UID) returned from OAuth access token # request - # @param [Hash] share The body we want to submit to LinkedIn + # @param [Hash] share The body we want to submit to LinkedIn. At least a + # comment is required # # @macro share_input_fields # @return [void] def v2_add_share(urn, share = {}) + if !urn.instance_of?(String) || urn.empty? + raise LinkedIn::Errors::UnavailableError, 'LinkedIn API: URN required' + elsif share[:comment].nil? && share[:url].nil? + raise LinkedIn::Errors::UnavailableError, + 'LinkedIn API: At least a comment is required' + end + path = '/ugcPosts' - payload = { - author: "urn:li:person:#{urn}", - lifecycleState: 'PUBLISHED', - visibility: { 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' } - } - if share[:url] + v2_post(path, MultiJson.dump(share_payload(urn, share))) + end + + private + + def share_payload(urn, share) + payload = { author: "urn:li:person:#{urn}", + lifecycleState: 'PUBLISHED', + visibility: { + 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' + } + } + + return add_url_to_payload(payload, share) if share[:url] + + add_comment_to_payload(payload, share) + end + + def add_url_to_payload(payload, share) media = { status: 'READY', originalUrl: share[:url] } if share[:description] media[:description] = { text: share[:description] } @@ -56,19 +77,18 @@ def v2_add_share(urn, share = {}) payload[:specificContent]['com.linkedin.ugc.ShareContent'][:shareCommentary] = { text: share[:comment] } end - elsif share[:comment] + payload + end + + def add_comment_to_payload(payload, share) payload[:specificContent] = { 'com.linkedin.ugc.ShareContent' => { shareCommentary: { text: share[:comment] }, shareMediaCategory: 'NONE' } } - else - raise LinkedIn::Errors::UnavailableError, - 'LinkedIn API: At least a comment is required' + payload end - v2_post(path, MultiJson.dump(payload)) - end end end end diff --git a/spec/cases/v2_spec.rb b/spec/cases/v2_spec.rb index 5520789b..52e6f69c 100644 --- a/spec/cases/v2_spec.rb +++ b/spec/cases/v2_spec.rb @@ -222,5 +222,25 @@ end.to raise_error(LinkedIn::Errors::UnavailableError) end end + + context "when no urn" do + before { stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') } + + it "should raise error" do + expect do + client.v2_add_share(comment: comment) + end.to raise_error(LinkedIn::Errors::UnavailableError) + end + end + + context "when urn is blank" do + before { stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') } + + it "should raise error" do + expect do + client.v2_add_share('', comment: comment) + end.to raise_error(LinkedIn::Errors::UnavailableError) + end + end end end From f45a6d42947773960dbc35753baa69d700fdd71a Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Tue, 12 Mar 2019 18:40:06 -0700 Subject: [PATCH 8/9] Change requirements for LinkedIn::Api::V2#v2_add_share A comment is always required --- lib/linked_in/api/v2.rb | 9 ++---- spec/cases/v2_spec.rb | 64 +++++++++++++---------------------------- 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/lib/linked_in/api/v2.rb b/lib/linked_in/api/v2.rb index 93630d01..87ae7a20 100644 --- a/lib/linked_in/api/v2.rb +++ b/lib/linked_in/api/v2.rb @@ -35,9 +35,9 @@ def v2_profile def v2_add_share(urn, share = {}) if !urn.instance_of?(String) || urn.empty? raise LinkedIn::Errors::UnavailableError, 'LinkedIn API: URN required' - elsif share[:comment].nil? && share[:url].nil? + elsif share[:comment].nil? raise LinkedIn::Errors::UnavailableError, - 'LinkedIn API: At least a comment is required' + 'LinkedIn API: Comment required' end path = '/ugcPosts' @@ -69,14 +69,11 @@ def add_url_to_payload(payload, share) end payload[:specificContent] = { 'com.linkedin.ugc.ShareContent' => { + shareCommentary: { text: share[:comment] }, shareMediaCategory: 'ARTICLE', media: [media] } } - if share[:comment] - payload[:specificContent]['com.linkedin.ugc.ShareContent'][:shareCommentary] = - { text: share[:comment] } - end payload end diff --git a/spec/cases/v2_spec.rb b/spec/cases/v2_spec.rb index 52e6f69c..f5a0c31c 100644 --- a/spec/cases/v2_spec.rb +++ b/spec/cases/v2_spec.rb @@ -101,27 +101,6 @@ end end - context "when url only" do - before do - body[:specificContent] = { - 'com.linkedin.ugc.ShareContent' => { - media: [{ status: 'READY', originalUrl: url }], - shareMediaCategory: 'ARTICLE' - } - } - stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') - .to_return(body: '{}', status: 201) - end - - it "should send a request" do - client.v2_add_share(urn, url: url) - - expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') - .with(body: body, headers: headers) - ).to have_been_made.once - end - end - context "when comment, url, and title" do before do body[:specificContent] = { @@ -146,29 +125,6 @@ end end - context "when url and title" do - before do - body[:specificContent] = { - 'com.linkedin.ugc.ShareContent' => { - media: [ - { status: 'READY', originalUrl: url, title: { text: title } } - ], - shareMediaCategory: 'ARTICLE' - } - } - stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') - .to_return(body: '{}', status: 201) - end - - it "should send a request" do - client.v2_add_share(urn, url: url, title: title) - - expect(a_request(:post, 'https://api.linkedin.com/v2/ugcPosts') - .with(body: body, headers: headers) - ).to have_been_made.once - end - end - context "when url and comment" do before do body[:specificContent] = { @@ -213,6 +169,26 @@ end end + context "when url and title only" do + before { stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') } + + it "should raise error" do + expect do + client.v2_add_share(urn, url: url, title: title) + end.to raise_error(LinkedIn::Errors::UnavailableError) + end + end + + context "when url only" do + before { stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') } + + it "should raise error" do + expect do + client.v2_add_share(urn, url: url) + end.to raise_error(LinkedIn::Errors::UnavailableError) + end + end + context "when title only" do before { stub_request(:post, 'https://api.linkedin.com/v2/ugcPosts') } From ea4b00058281530ae609576dac3419807e564374 Mon Sep 17 00:00:00 2001 From: Darrell Sandstrom Date: Tue, 12 Mar 2019 18:42:24 -0700 Subject: [PATCH 9/9] Fix spacing --- spec/cases/v2_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cases/v2_spec.rb b/spec/cases/v2_spec.rb index f5a0c31c..b61ff8df 100644 --- a/spec/cases/v2_spec.rb +++ b/spec/cases/v2_spec.rb @@ -174,7 +174,7 @@ it "should raise error" do expect do - client.v2_add_share(urn, url: url, title: title) + client.v2_add_share(urn, url: url, title: title) end.to raise_error(LinkedIn::Errors::UnavailableError) end end