linkedin 0.3.6 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. data/examples/authenticate.rb +1 -1
  2. data/examples/profile.rb +4 -0
  3. data/examples/sinatra.rb +77 -0
  4. data/lib/linked_in/api/query_methods.rb +25 -16
  5. data/lib/linked_in/api/update_methods.rb +20 -17
  6. data/lib/linked_in/errors.rb +1 -1
  7. data/lib/linked_in/helpers/request.rb +4 -1
  8. data/lib/linked_in/search.rb +27 -8
  9. data/lib/linked_in/version.rb +1 -1
  10. data/linkedin.gemspec +3 -3
  11. data/spec/cases/api_spec.rb +31 -0
  12. data/spec/cases/oauth_spec.rb +0 -1
  13. data/spec/cases/search_spec.rb +206 -0
  14. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option.yml +135 -0
  15. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options.yml +122 -0
  16. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields.yml +72 -0
  17. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter.yml +136 -0
  18. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option.yml +136 -0
  19. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination.yml +128 -0
  20. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_options_with_fields.yml +252 -0
  21. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_string_parameter.yml +73 -0
  22. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option.yml +71 -0
  23. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_a_facet.yml +111 -0
  24. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_facets_to_return.yml +71 -0
  25. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_pagination.yml +66 -0
  26. metadata +53 -26
@@ -18,4 +18,4 @@ client.authorize_from_request(rtoken, rsecret, pin)
18
18
  # or authorize from previously fetched access keys
19
19
  c.authorize_from_access("OU812", "8675309")
20
20
 
21
- # you're now free to move about the cabin, call any API method
21
+ # you're now free to move about the cabin, call any API method
@@ -11,4 +11,8 @@ client.profile(:id => 'gNma67_AdI')
11
11
  # get a profile for someone via their public profile url
12
12
  client.profile(:url => 'http://www.linkedin.com/in/netherland')
13
13
 
14
+ # provides the ability to access authenticated user's company field in the profile
15
+ user = client.profile(:fields => %w(positions))
16
+ companies = user.positions.all.map{|t| t.company}
17
+ # Example: most recent company can be accessed via companies[0]
14
18
 
@@ -0,0 +1,77 @@
1
+ require "rubygems"
2
+ require "haml"
3
+ require "sinatra"
4
+ require "linkedin"
5
+
6
+ enable :sessions
7
+
8
+ helpers do
9
+ def login?
10
+ !session[:atoken].nil?
11
+ end
12
+
13
+ def profile
14
+ linkedin_client.profile unless session[:atoken].nil?
15
+ end
16
+
17
+ def connections
18
+ linkedin_client.connections unless session[:atoken].nil?
19
+ end
20
+
21
+ private
22
+ def linkedin_client
23
+ client = LinkedIn::Client.new(settings.api, settings.secret)
24
+ client.authorize_from_access(session[:atoken], session[:asecret])
25
+ client
26
+ end
27
+
28
+ end
29
+
30
+ configure do
31
+ # get your api keys at https://www.linkedin.com/secure/developer
32
+ set :api, "your_api_key"
33
+ set :secret, "your_secret"
34
+ end
35
+
36
+ get "/" do
37
+ haml :index
38
+ end
39
+
40
+ get "/auth" do
41
+ client = LinkedIn::Client.new(settings.api, settings.secret)
42
+ request_token = client.request_token(:oauth_callback => "http://#{request.host}:#{request.port}/auth/callback")
43
+ session[:rtoken] = request_token.token
44
+ session[:rsecret] = request_token.secret
45
+
46
+ redirect client.request_token.authorize_url
47
+ end
48
+
49
+ get "/auth/logout" do
50
+ session[:atoken] = nil
51
+ redirect "/"
52
+ end
53
+
54
+ get "/auth/callback" do
55
+ client = LinkedIn::Client.new(settings.api, settings.secret)
56
+ if session[:atoken].nil?
57
+ pin = params[:oauth_verifier]
58
+ atoken, asecret = client.authorize_from_request(session[:rtoken], session[:rsecret], pin)
59
+ session[:atoken] = atoken
60
+ session[:asecret] = asecret
61
+ end
62
+ redirect "/"
63
+ end
64
+
65
+
66
+ __END__
67
+ @@index
68
+ -if login?
69
+ %p Welcome #{profile.first_name}!
70
+ %a{:href => "/auth/logout"} Logout
71
+ %p= profile.headline
72
+ %br
73
+ %div= "You have #{connections.total} connections!"
74
+ -connections.all.each do |c|
75
+ %div= "#{c.first_name} #{c.last_name} - #{c.headline}"
76
+ -else
77
+ %a{:href => "/auth"} Login using LinkedIn
@@ -23,26 +23,35 @@ module LinkedIn
23
23
  simple_query(path, options)
24
24
  end
25
25
 
26
+ def group_memberships(options = {})
27
+ path = "#{person_path(options)}/group-memberships"
28
+ simple_query(path, options)
29
+ end
30
+
26
31
  private
27
32
 
28
33
  def simple_query(path, options={})
29
- fields = options[:fields] || LinkedIn.default_profile_fields
34
+ fields = options.delete(:fields) || LinkedIn.default_profile_fields
30
35
 
31
- if options[:public]
36
+ if options.delete(:public)
32
37
  path +=":public"
33
38
  elsif fields
34
39
  path +=":(#{fields.map{ |f| f.to_s.gsub("_","-") }.join(',')})"
35
40
  end
36
41
 
37
- Mash.from_json(get(path))
42
+ headers = options.delete(:headers) || {}
43
+ params = options.map { |k,v| "#{k}=#{v}" }.join("&")
44
+ path += "?#{params}" if not params.empty?
45
+
46
+ Mash.from_json(get(path, headers))
38
47
  end
39
48
 
40
49
  def person_path(options)
41
50
  path = "/people/"
42
- if options[:id]
43
- path += "id=#{options[:id]}"
44
- elsif options[:url]
45
- path += "url=#{CGI.escape(options[:url])}"
51
+ if id = options.delete(:id)
52
+ path += "id=#{id}"
53
+ elsif url = options.delete(:url)
54
+ path += "url=#{CGI.escape(url)}"
46
55
  else
47
56
  path += "~"
48
57
  end
@@ -50,14 +59,14 @@ module LinkedIn
50
59
 
51
60
  def company_path(options)
52
61
  path = "/companies/"
53
- if options[:id]
54
- path += "id=#{options[:id]}"
55
- elsif options[:url]
56
- path += "url=#{CGI.escape(options[:url])}"
57
- elsif options[:name]
58
- path += "universal-name=#{CGI.escape(options[:name])}"
59
- elsif options[:domain]
60
- path += "email-domain=#{CGI.escape(options[:domain])}"
62
+ if id = options.delete(:id)
63
+ path += "id=#{id}"
64
+ elsif url = options.delete(:url)
65
+ path += "url=#{CGI.escape(url)}"
66
+ elsif name = options.delete(:name)
67
+ path += "universal-name=#{CGI.escape(name)}"
68
+ elsif domain = options.delete(:domain)
69
+ path += "email-domain=#{CGI.escape(domain)}"
61
70
  else
62
71
  path += "~"
63
72
  end
@@ -66,4 +75,4 @@ module LinkedIn
66
75
  end
67
76
 
68
77
  end
69
- end
78
+ end
@@ -9,6 +9,12 @@ module LinkedIn
9
9
  post(path, defaults.merge(share).to_json, "Content-Type" => "application/json")
10
10
  end
11
11
 
12
+ def join_group(group_id)
13
+ path = "/people/~/group-memberships/#{group_id}"
14
+ body = {'membership-state' => {'code' => 'member' }}
15
+ put(path, body.to_json, "Content-Type" => "application/json")
16
+ end
17
+
12
18
  # def share(options={})
13
19
  # path = "/people/~/shares"
14
20
  # defaults = { :visability => 'anyone' }
@@ -25,23 +31,20 @@ module LinkedIn
25
31
  # post(path, network_update_to_xml(message))
26
32
  # end
27
33
  #
28
- # def send_message(subject, body, recipient_paths)
29
- # path = "/people/~/mailbox"
30
- #
31
- # message = LinkedIn::Message.new
32
- # message.subject = subject
33
- # message.body = body
34
- # recipients = LinkedIn::Recipients.new
35
- #
36
- # recipients.recipients = recipient_paths.map do |profile_path|
37
- # recipient = LinkedIn::Recipient.new
38
- # recipient.person = LinkedIn::Person.new
39
- # recipient.person.path = "/people/#{profile_path}"
40
- # recipient
41
- # end
42
- # message.recipients = recipients
43
- # post(path, message_to_xml(message)).code
44
- # end
34
+ def send_message(subject, body, recipient_paths)
35
+ path = "/people/~/mailbox"
36
+
37
+ message = {
38
+ 'subject' => subject,
39
+ 'body' => body,
40
+ 'recipients' => {
41
+ 'values' => recipient_paths.map do |profile_path|
42
+ { 'person' => { '_path' => "/people/#{profile_path}" } }
43
+ end
44
+ }
45
+ }
46
+ post(path, message.to_json, "Content-Type" => "application/json")
47
+ end
45
48
  #
46
49
  # def clear_status
47
50
  # path = "/people/~/current-status"
@@ -8,9 +8,9 @@ module LinkedIn
8
8
  end
9
9
  end
10
10
 
11
- class RateLimitExceededError < LinkedInError; end
12
11
  class UnauthorizedError < LinkedInError; end
13
12
  class GeneralError < LinkedInError; end
13
+ class AccessDeniedError < LinkedInError; end
14
14
 
15
15
  class UnavailableError < StandardError; end
16
16
  class InformLinkedInError < StandardError; end
@@ -44,9 +44,12 @@ module LinkedIn
44
44
  when 401
45
45
  data = Mash.from_json(response.body)
46
46
  raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}"
47
- when 400, 403
47
+ when 400
48
48
  data = Mash.from_json(response.body)
49
49
  raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}"
50
+ when 403
51
+ data = Mash.from_json(response.body)
52
+ raise LinkedIn::Errors::AccessDeniedError.new(data), "(#{data.status}): #{data.message}"
50
53
  when 404
51
54
  raise LinkedIn::Errors::NotFoundError, "(#{response.code}): #{response.message}"
52
55
  when 500
@@ -1,18 +1,20 @@
1
1
  module LinkedIn
2
2
 
3
3
  module Search
4
+ def search(options={}, type='people')
4
5
 
5
- def search(options={})
6
- path = "/people-search"
7
- options = { :keywords => options } if options.is_a?(String)
8
-
9
- if fields = options.delete(:fields)
10
- path +=":(#{fields.map{ |f| f.to_s.gsub("_","-") }.join(',')})"
6
+ path = "/#{type.to_s}-search"
7
+
8
+ if options.is_a?(Hash)
9
+ fields = options.delete(:fields)
10
+ path += field_selector(fields) if fields
11
11
  end
12
-
12
+
13
+ options = { :keywords => options } if options.is_a?(String)
13
14
  options = format_options_for_query(options)
14
15
 
15
16
  result_json = get(to_uri(path, options))
17
+
16
18
  Mash.from_json(result_json)
17
19
  end
18
20
 
@@ -32,6 +34,23 @@ module LinkedIn
32
34
  value
33
35
  end
34
36
 
37
+ def field_selector(fields)
38
+ result = ":("
39
+ fields = fields.to_a.map do |field|
40
+ if field.is_a?(Hash)
41
+ innerFields = []
42
+ field.each do |key, value|
43
+ innerFields << key.to_s.gsub("_","-") + field_selector(value)
44
+ end
45
+ innerFields.join(',')
46
+ else
47
+ field.to_s.gsub("_","-")
48
+ end
49
+ end
50
+ result += fields.join(',')
51
+ result += ")"
52
+ result
53
+ end
35
54
  end
36
55
 
37
- end
56
+ end
@@ -3,7 +3,7 @@ module LinkedIn
3
3
  module VERSION #:nodoc:
4
4
  MAJOR = 0
5
5
  MINOR = 3
6
- PATCH = 6
6
+ PATCH = 7
7
7
  PRE = nil
8
8
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
9
9
  end
@@ -2,9 +2,9 @@
2
2
  require File.expand_path('../lib/linked_in/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.add_dependency 'hashie', '~> 1.2.0'
6
- gem.add_dependency 'multi_json', '~> 1.0.3'
7
- gem.add_dependency 'oauth', '~> 0.4.5'
5
+ gem.add_dependency 'hashie', '~> 1.2'
6
+ gem.add_dependency 'multi_json', '~> 1.0'
7
+ gem.add_dependency 'oauth', '~> 0.4'
8
8
  gem.add_development_dependency 'json', '~> 1.6'
9
9
  gem.add_development_dependency 'rake', '~> 0.9'
10
10
  gem.add_development_dependency 'rdoc', '~> 3.8'
@@ -52,6 +52,14 @@ describe LinkedIn::Api do
52
52
  response.code.should == "201"
53
53
  end
54
54
 
55
+ it "should be able to send a message" do
56
+ stub_request(:post, "https://api.linkedin.com/v1/people/~/mailbox").to_return(:body => "", :status => 201)
57
+ response = client.send_message("subject", "body", ["recip1", "recip2"])
58
+ response.body.should == ""
59
+ response.code.should == "201"
60
+ end
61
+
62
+
55
63
  context "Company API" do
56
64
  use_vcr_cassette
57
65
 
@@ -83,4 +91,27 @@ describe LinkedIn::Api do
83
91
  end
84
92
  end
85
93
 
94
+ context "Group API" do
95
+
96
+ it "should be able to list group memberships for a profile" do
97
+ stub_request(:get, "https://api.linkedin.com/v1/people/~/group-memberships").to_return(:body => "{}")
98
+ client.group_memberships.should be_an_instance_of(LinkedIn::Mash)
99
+ end
100
+
101
+ it "should be able to join a group" do
102
+ stub_request(:put, "https://api.linkedin.com/v1/people/~/group-memberships/123").to_return(:body => "", :status => 201)
103
+
104
+ response = client.join_group(123)
105
+ response.body.should == ""
106
+ response.code.should == "201"
107
+ end
108
+
109
+ end
110
+
111
+ context "Errors" do
112
+ it "should raise AccessDeniedError when LinkedIn returns 403 status code" do
113
+ stub_request(:get, "https://api.linkedin.com/v1/people-search?first-name=Javan").to_return(:body => "{}", :status => 403)
114
+ expect{ client.search(:first_name => "Javan") }.to raise_error(LinkedIn::Errors::AccessDeniedError)
115
+ end
116
+ end
86
117
  end
@@ -148,7 +148,6 @@ describe "LinkedIn::Client" do
148
148
  it "should return a valid auth token" do
149
149
  access_token.should be_a_kind_of OAuth::AccessToken
150
150
  access_token.token.should be_a_kind_of String
151
- access_token.token.should be_a_kind_of String
152
151
  end
153
152
  end
154
153
 
@@ -0,0 +1,206 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn::Search do
4
+
5
+ # if you remove the related cassettes you will need to inform valid
6
+ # tokens and secrets to regenerate them
7
+ #
8
+ let(:client) do
9
+ consumer_token = ENV['LINKED_IN_CONSUMER_KEY'] || 'key'
10
+ consumer_secret = ENV['LINKED_IN_CONSUMER_SECRET'] || 'secret'
11
+ client = LinkedIn::Client.new(consumer_token, consumer_secret)
12
+
13
+ auth_token = ENV['LINKED_IN_AUTH_KEY'] || 'key'
14
+ auth_secret = ENV['LINKED_IN_AUTH_SECRET'] || 'secret'
15
+ client.authorize_from_access(auth_token, auth_secret)
16
+ client
17
+ end
18
+
19
+ describe "#search_company" do
20
+
21
+ describe "by keywords string parameter" do
22
+ use_vcr_cassette :record => :new_episodes
23
+
24
+ let(:results) do
25
+ client.search('apple', :company)
26
+ end
27
+
28
+ it "should perform a company search" do
29
+ results.companies.all.size.should == 10
30
+ results.companies.all.first.name.should == 'Apple'
31
+ results.companies.all.first.id.should == 162479
32
+ end
33
+ end
34
+
35
+ describe "by single keywords option" do
36
+ use_vcr_cassette :record => :new_episodes
37
+
38
+ let(:results) do
39
+ options = {:keywords => 'apple'}
40
+ client.search(options, :company)
41
+ end
42
+
43
+ it "should perform a company search" do
44
+ results.companies.all.size.should == 10
45
+ results.companies.all.first.name.should == 'Apple'
46
+ results.companies.all.first.id.should == 162479
47
+ end
48
+ end
49
+
50
+ describe "by single keywords option with facets to return" do
51
+ use_vcr_cassette :record => :new_episodes
52
+
53
+ let(:results) do
54
+ options = {:keywords => 'apple', :facets => [:industry]}
55
+ client.search(options, :company)
56
+ end
57
+
58
+ it "should return a facet" do
59
+ results.facets.all.first.buckets.all.first.name.should == 'Information Technology and Services'
60
+ end
61
+ end
62
+
63
+ describe "by single keywords option with pagination" do
64
+ use_vcr_cassette :record => :new_episodes
65
+
66
+ let(:results) do
67
+ options = {:keywords => 'apple', :start => 5, :count => 5}
68
+ client.search(options, :company)
69
+ end
70
+
71
+ it "should perform a search" do
72
+ results.companies.all.size.should == 5
73
+ results.companies.all.first.name.should == 'Apple Vacations'
74
+ results.companies.all.first.id.should == 19271
75
+ results.companies.all.last.name.should == 'Micro Center'
76
+ results.companies.all.last.id.should == 15552
77
+ end
78
+ end
79
+
80
+ describe "by keywords options with fields" do
81
+ use_vcr_cassette :record => :new_episodes
82
+
83
+ let(:results) do
84
+ fields = [{:companies => [:id, :name, :industries, :description, :specialties]}, :num_results]
85
+ client.search({:keywords => 'apple', :fields => fields}, 'company')
86
+ end
87
+
88
+ it "should perform a search" do
89
+ results.companies.all.first.name.should == 'Apple'
90
+ results.companies.all.first.description.should == 'Apple designs Macs, the best personal computers in the world, along with Mac OS X, iLife, iWork, and professional software. Apple leads the digital music revolution with its iPods and iTunes online store. Apple is reinventing the mobile phone with its revolutionary iPhone and App Store, and has recently introduced its magical iPad which is defining the future of mobile media and computing devices.'
91
+ results.companies.all.first.id.should == 162479
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ describe "#search" do
98
+
99
+ describe "by keywords string parameter" do
100
+ use_vcr_cassette :record => :new_episodes
101
+
102
+ let(:results) do
103
+ client.search('github')
104
+ end
105
+
106
+ it "should perform a search" do
107
+ results.people.all.size.should == 10
108
+ results.people.all.first.first_name.should == 'Giliardi'
109
+ results.people.all.first.last_name.should == 'Pires'
110
+ results.people.all.first.id.should == 'YkdnFl04s_'
111
+ end
112
+ end
113
+
114
+ describe "by single keywords option" do
115
+ use_vcr_cassette :record => :new_episodes
116
+
117
+ let(:results) do
118
+ client.search(:keywords => 'github')
119
+ end
120
+
121
+ it "should perform a search" do
122
+ results.people.all.size.should == 10
123
+ results.people.all.first.first_name.should == 'Giliardi'
124
+ results.people.all.first.last_name.should == 'Pires'
125
+ results.people.all.first.id.should == 'YkdnFl04s_'
126
+ end
127
+ end
128
+
129
+ describe "by single keywords option with pagination" do
130
+ use_vcr_cassette :record => :new_episodes
131
+
132
+ let(:results) do
133
+ client.search(:keywords => 'github', :start => 5, :count => 5)
134
+ end
135
+
136
+ it "should perform a search" do
137
+ results.people.all.size.should == 5
138
+ results.people.all.first.first_name.should == 'Stephen'
139
+ results.people.all.first.last_name.should == 'M.'
140
+ results.people.all.first.id.should == 'z2XMcxa_dR'
141
+ results.people.all.last.first_name.should == 'Pablo'
142
+ results.people.all.last.last_name.should == 'C.'
143
+ results.people.all.last.id.should == 'pdzrGpyP0h'
144
+ end
145
+ end
146
+
147
+ describe "by first_name and last_name options" do
148
+ use_vcr_cassette :record => :new_episodes
149
+
150
+ let(:results) do
151
+ client.search(:first_name => 'Giliardi', :last_name => 'Pires')
152
+ end
153
+
154
+ it "should perform a search" do
155
+ results.people.all.size.should == 1
156
+ results.people.all.first.first_name.should == 'Giliardi'
157
+ results.people.all.first.last_name.should == 'Pires'
158
+ results.people.all.first.id.should == 'YkdnFl04s_'
159
+ end
160
+ end
161
+
162
+ describe "by first_name and last_name options with fields" do
163
+ use_vcr_cassette :record => :new_episodes
164
+
165
+ let(:results) do
166
+ fields = [{:people => [:id, :first_name, :last_name, :public_profile_url, :picture_url]}, :num_results]
167
+ client.search(:first_name => 'Giliardi', :last_name => 'Pires', :fields => fields)
168
+ end
169
+
170
+ it "should perform a search" do
171
+ results.people.all.size.should == 1
172
+ results.people.all.first.first_name.should == 'Giliardi'
173
+ results.people.all.first.last_name.should == 'Pires'
174
+ results.people.all.first.id.should == 'YkdnFl04s_'
175
+ results.people.all.first.picture_url == 'http://media.linkedin.com/mpr/mprx/0_Oz05kn9xkWziAEOUKtOVkqzjXd8Clf7UyqIVkqchR2NtmwZRt1fWoN_aobhg-HmB09jUwPLKrAhU'
176
+ results.people.all.first.public_profile_url == 'http://www.linkedin.com/in/gibanet'
177
+ end
178
+ end
179
+
180
+ describe "by company_name option" do
181
+ use_vcr_cassette :record => :new_episodes
182
+
183
+ let(:results) do
184
+ client.search(:company_name => 'linkedin')
185
+ end
186
+
187
+ it "should perform a search" do
188
+ results.people.all.size.should == 10
189
+ results.people.all.first.first_name.should == 'Donald'
190
+ results.people.all.first.last_name.should == 'Denker'
191
+ results.people.all.first.id.should == 'VQcsz5Hp_h'
192
+ end
193
+ end
194
+
195
+ describe "#field_selector" do
196
+ it "should not modify the parameter object" do
197
+ fields = [{:people => [:id, :first_name]}]
198
+ fields_dup = fields.dup
199
+ client.send(:field_selector, fields)
200
+ fields.should eq fields_dup
201
+ end
202
+ end
203
+
204
+ end
205
+
206
+ end