brienw-linkedin 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +14 -0
- data/.document +5 -0
- data/.gemtest +0 -0
- data/.gitignore +41 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/LICENSE +20 -0
- data/README.markdown +78 -0
- data/Rakefile +19 -0
- data/changelog.markdown +71 -0
- data/examples/authenticate.rb +21 -0
- data/examples/network.rb +12 -0
- data/examples/profile.rb +18 -0
- data/examples/sinatra.rb +77 -0
- data/examples/status.rb +9 -0
- data/lib/linked_in/api.rb +6 -0
- data/lib/linked_in/api/query_methods.rb +73 -0
- data/lib/linked_in/api/update_methods.rb +55 -0
- data/lib/linked_in/client.rb +46 -0
- data/lib/linked_in/errors.rb +19 -0
- data/lib/linked_in/helpers.rb +6 -0
- data/lib/linked_in/helpers/authorization.rb +68 -0
- data/lib/linked_in/helpers/request.rb +80 -0
- data/lib/linked_in/mash.rb +68 -0
- data/lib/linked_in/search.rb +56 -0
- data/lib/linked_in/version.rb +11 -0
- data/lib/linkedin.rb +32 -0
- data/linkedin.gemspec +25 -0
- data/spec/cases/api_spec.rb +92 -0
- data/spec/cases/linkedin_spec.rb +37 -0
- data/spec/cases/mash_spec.rb +85 -0
- data/spec/cases/oauth_spec.rb +166 -0
- data/spec/cases/search_spec.rb +206 -0
- data/spec/fixtures/cassette_library/LinkedIn_Api/Company_API.yml +73 -0
- data/spec/fixtures/cassette_library/LinkedIn_Client/_authorize_from_request.yml +28 -0
- data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token.yml +28 -0
- data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_a_callback_url.yml +28 -0
- data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_default_options.yml +28 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option.yml +135 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options.yml +122 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields.yml +72 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter.yml +136 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option.yml +136 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination.yml +128 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_options_with_fields.yml +252 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_string_parameter.yml +73 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option.yml +71 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_a_facet.yml +111 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_facets_to_return.yml +71 -0
- data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_pagination.yml +66 -0
- data/spec/helper.rb +30 -0
- metadata +295 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
module Api
|
3
|
+
|
4
|
+
module UpdateMethods
|
5
|
+
|
6
|
+
def add_share(share)
|
7
|
+
path = "/people/~/shares"
|
8
|
+
defaults = {:visibility => {:code => "anyone"}}
|
9
|
+
post(path, defaults.merge(share).to_json, "Content-Type" => "application/json")
|
10
|
+
end
|
11
|
+
|
12
|
+
# def share(options={})
|
13
|
+
# path = "/people/~/shares"
|
14
|
+
# defaults = { :visability => 'anyone' }
|
15
|
+
# post(path, share_to_xml(defaults.merge(options)))
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def update_comment(network_key, comment)
|
19
|
+
# path = "/people/~/network/updates/key=#{network_key}/update-comments"
|
20
|
+
# post(path, comment_to_xml(comment))
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def update_network(message)
|
24
|
+
# path = "/people/~/person-activities"
|
25
|
+
# post(path, network_update_to_xml(message))
|
26
|
+
# end
|
27
|
+
#
|
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
|
45
|
+
#
|
46
|
+
# def clear_status
|
47
|
+
# path = "/people/~/current-status"
|
48
|
+
# delete(path).code
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module LinkedIn
|
4
|
+
|
5
|
+
class Client
|
6
|
+
include Helpers::Request
|
7
|
+
include Helpers::Authorization
|
8
|
+
include Api::QueryMethods
|
9
|
+
include Api::UpdateMethods
|
10
|
+
include Search
|
11
|
+
|
12
|
+
attr_reader :consumer_token, :consumer_secret, :consumer_options
|
13
|
+
|
14
|
+
def initialize(ctoken=LinkedIn.token, csecret=LinkedIn.secret, options={})
|
15
|
+
@consumer_token = ctoken
|
16
|
+
@consumer_secret = csecret
|
17
|
+
@consumer_options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# def current_status
|
22
|
+
# path = "/people/~/current-status"
|
23
|
+
# Crack::XML.parse(get(path))['current_status']
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def network_statuses(options={})
|
27
|
+
# options[:type] = 'STAT'
|
28
|
+
# network_updates(options)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def network_updates(options={})
|
32
|
+
# path = "/people/~/network"
|
33
|
+
# Network.from_xml(get(to_uri(path, options)))
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # helpful in making authenticated calls and writing the
|
37
|
+
# # raw xml to a fixture file
|
38
|
+
# def write_fixture(path, filename)
|
39
|
+
# file = File.new("test/fixtures/#{filename}", "w")
|
40
|
+
# file.puts(access_token.get(path).body)
|
41
|
+
# file.close
|
42
|
+
# end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
module Errors
|
3
|
+
class LinkedInError < StandardError
|
4
|
+
attr_reader :data
|
5
|
+
def initialize(data)
|
6
|
+
@data = data
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class UnauthorizedError < LinkedInError; end
|
12
|
+
class GeneralError < LinkedInError; end
|
13
|
+
class AccessDeniedError < LinkedInError; end
|
14
|
+
|
15
|
+
class UnavailableError < StandardError; end
|
16
|
+
class InformLinkedInError < StandardError; end
|
17
|
+
class NotFoundError < StandardError; end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
module Authorization
|
5
|
+
|
6
|
+
DEFAULT_OAUTH_OPTIONS = {
|
7
|
+
:request_token_path => "/uas/oauth/requestToken",
|
8
|
+
:access_token_path => "/uas/oauth/accessToken",
|
9
|
+
:authorize_path => "/uas/oauth/authorize",
|
10
|
+
:api_host => "https://api.linkedin.com",
|
11
|
+
:auth_host => "https://www.linkedin.com"
|
12
|
+
}
|
13
|
+
|
14
|
+
def consumer
|
15
|
+
@consumer ||= ::OAuth::Consumer.new(@consumer_token, @consumer_secret, parse_oauth_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Note: If using oauth with a web app, be sure to provide :oauth_callback.
|
19
|
+
# Options:
|
20
|
+
# :oauth_callback => String, url that LinkedIn should redirect to
|
21
|
+
def request_token(options={})
|
22
|
+
@request_token ||= consumer.get_request_token(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
# For web apps use params[:oauth_verifier], for desktop apps,
|
26
|
+
# use the verifier is the pin that LinkedIn gives users.
|
27
|
+
def authorize_from_request(request_token, request_secret, verifier_or_pin)
|
28
|
+
request_token = ::OAuth::RequestToken.new(consumer, request_token, request_secret)
|
29
|
+
access_token = request_token.get_access_token(:oauth_verifier => verifier_or_pin)
|
30
|
+
@auth_token, @auth_secret = access_token.token, access_token.secret
|
31
|
+
end
|
32
|
+
|
33
|
+
def access_token
|
34
|
+
@access_token ||= ::OAuth::AccessToken.new(consumer, @auth_token, @auth_secret)
|
35
|
+
end
|
36
|
+
|
37
|
+
def authorize_from_access(atoken, asecret)
|
38
|
+
@auth_token, @auth_secret = atoken, asecret
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# since LinkedIn uses api.linkedin.com for request and access token exchanges,
|
44
|
+
# but www.linkedin.com for authorize/authenticate, we have to take care
|
45
|
+
# of the url creation ourselves.
|
46
|
+
def parse_oauth_options
|
47
|
+
{
|
48
|
+
:request_token_url => full_oauth_url_for(:request_token, :api_host),
|
49
|
+
:access_token_url => full_oauth_url_for(:access_token, :api_host),
|
50
|
+
:authorize_url => full_oauth_url_for(:authorize, :auth_host),
|
51
|
+
:site => @consumer_options[:site] || @consumer_options[:api_host] || DEFAULT_OAUTH_OPTIONS[:api_host]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def full_oauth_url_for(url_type, host_type)
|
56
|
+
if @consumer_options["#{url_type}_url".to_sym]
|
57
|
+
@consumer_options["#{url_type}_url".to_sym]
|
58
|
+
else
|
59
|
+
host = @consumer_options[:site] || @consumer_options[host_type] || DEFAULT_OAUTH_OPTIONS[host_type]
|
60
|
+
path = @consumer_options[:"#{url_type}_path".to_sym] || DEFAULT_OAUTH_OPTIONS["#{url_type}_path".to_sym]
|
61
|
+
"#{host}#{path}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
module Request
|
5
|
+
|
6
|
+
DEFAULT_HEADERS = {
|
7
|
+
'x-li-format' => 'json'
|
8
|
+
}
|
9
|
+
|
10
|
+
API_PATH = '/v1'
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def get(path, options={})
|
15
|
+
response = access_token.get("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
|
16
|
+
raise_errors(response)
|
17
|
+
response.body
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(path, body='', options={})
|
21
|
+
response = access_token.post("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
|
22
|
+
raise_errors(response)
|
23
|
+
response
|
24
|
+
end
|
25
|
+
|
26
|
+
def put(path, body, options={})
|
27
|
+
response = access_token.put("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
|
28
|
+
raise_errors(response)
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(path, options={})
|
33
|
+
response = access_token.delete("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
|
34
|
+
raise_errors(response)
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def raise_errors(response)
|
41
|
+
# Even if the json answer contains the HTTP status code, LinkedIn also sets this code
|
42
|
+
# in the HTTP answer (thankfully).
|
43
|
+
case response.code.to_i
|
44
|
+
when 401
|
45
|
+
data = Mash.from_json(response.body)
|
46
|
+
raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}"
|
47
|
+
when 400
|
48
|
+
data = Mash.from_json(response.body)
|
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}"
|
53
|
+
when 404
|
54
|
+
raise LinkedIn::Errors::NotFoundError, "(#{response.code}): #{response.message}"
|
55
|
+
when 500
|
56
|
+
raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.code}): #{response.message}"
|
57
|
+
when 502..503
|
58
|
+
raise LinkedIn::Errors::UnavailableError, "(#{response.code}): #{response.message}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_query(options)
|
63
|
+
options.inject([]) do |collection, opt|
|
64
|
+
collection << "#{opt[0]}=#{opt[1]}"
|
65
|
+
collection
|
66
|
+
end * '&'
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_uri(path, options)
|
70
|
+
uri = URI.parse(path)
|
71
|
+
|
72
|
+
if options && options != {}
|
73
|
+
uri.query = to_query(options)
|
74
|
+
end
|
75
|
+
uri.to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module LinkedIn
|
5
|
+
class Mash < ::Hashie::Mash
|
6
|
+
|
7
|
+
# a simple helper to convert a json string to a Mash
|
8
|
+
def self.from_json(json_string)
|
9
|
+
result_hash = ::MultiJson.decode(json_string)
|
10
|
+
new(result_hash)
|
11
|
+
end
|
12
|
+
|
13
|
+
# returns a Date if we have year, month and day, and no conflicting key
|
14
|
+
def to_date
|
15
|
+
if !self.has_key?('to_date') && contains_date_fields?
|
16
|
+
Date.civil(self.year, self.month, self.day)
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def timestamp
|
23
|
+
value = self['timestamp']
|
24
|
+
if value.kind_of? Integer
|
25
|
+
value = value / 1000 if value > 9999999999
|
26
|
+
Time.at(value)
|
27
|
+
else
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def contains_date_fields?
|
35
|
+
self.year? && self.month? && self.day?
|
36
|
+
end
|
37
|
+
|
38
|
+
# overload the convert_key mash method so that the LinkedIn
|
39
|
+
# keys are made a little more ruby-ish
|
40
|
+
def convert_key(key)
|
41
|
+
case key.to_s
|
42
|
+
when '_key'
|
43
|
+
'id'
|
44
|
+
when '_total'
|
45
|
+
'total'
|
46
|
+
when 'values'
|
47
|
+
'all'
|
48
|
+
when 'numResults'
|
49
|
+
'total_results'
|
50
|
+
else
|
51
|
+
underscore(key)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# borrowed from ActiveSupport
|
56
|
+
# no need require an entire lib when we only need one method
|
57
|
+
def underscore(camel_cased_word)
|
58
|
+
word = camel_cased_word.to_s.dup
|
59
|
+
word.gsub!(/::/, '/')
|
60
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
61
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
62
|
+
word.tr!("-", "_")
|
63
|
+
word.downcase!
|
64
|
+
word
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
|
3
|
+
module Search
|
4
|
+
def search(options={}, type='people')
|
5
|
+
|
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
|
+
end
|
12
|
+
|
13
|
+
options = { :keywords => options } if options.is_a?(String)
|
14
|
+
options = format_options_for_query(options)
|
15
|
+
|
16
|
+
result_json = get(to_uri(path, options))
|
17
|
+
|
18
|
+
Mash.from_json(result_json)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def format_options_for_query(opts)
|
24
|
+
opts.inject({}) do |list, kv|
|
25
|
+
key, value = kv.first.to_s.gsub("_","-"), kv.last
|
26
|
+
list[key] = sanitize_value(value)
|
27
|
+
list
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def sanitize_value(value)
|
32
|
+
value = value.join("+") if value.is_a?(Array)
|
33
|
+
value = value.gsub(" ", "+") if value.is_a?(String)
|
34
|
+
value
|
35
|
+
end
|
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
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
data/lib/linkedin.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
3
|
+
module LinkedIn
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :token, :secret, :default_profile_fields
|
7
|
+
|
8
|
+
# config/initializers/linkedin.rb (for instance)
|
9
|
+
#
|
10
|
+
# LinkedIn.configure do |config|
|
11
|
+
# config.token = 'consumer_token'
|
12
|
+
# config.secret = 'consumer_secret'
|
13
|
+
# config.default_profile_fields = ['education', 'positions']
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# elsewhere
|
17
|
+
#
|
18
|
+
# client = LinkedIn::Client.new
|
19
|
+
def configure
|
20
|
+
yield self
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
autoload :Api, "linked_in/api"
|
26
|
+
autoload :Client, "linked_in/client"
|
27
|
+
autoload :Mash, "linked_in/mash"
|
28
|
+
autoload :Errors, "linked_in/errors"
|
29
|
+
autoload :Helpers, "linked_in/helpers"
|
30
|
+
autoload :Search, "linked_in/search"
|
31
|
+
autoload :Version, "linked_in/version"
|
32
|
+
end
|