linkedin-oauth2 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.autotest +14 -0
- data/.document +5 -0
- data/.gemtest +0 -0
- data/.gitignore +41 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/LICENSE +20 -0
- data/README.markdown +121 -0
- data/Rakefile +26 -0
- data/changelog.markdown +94 -0
- data/examples/authenticate.rb +16 -0
- data/examples/network.rb +12 -0
- data/examples/profile.rb +18 -0
- data/examples/sinatra.rb +69 -0
- data/examples/status.rb +6 -0
- data/lib/linked_in/api.rb +6 -0
- data/lib/linked_in/api/query_methods.rb +123 -0
- data/lib/linked_in/api/update_methods.rb +76 -0
- data/lib/linked_in/client.rb +31 -0
- data/lib/linked_in/errors.rb +19 -0
- data/lib/linked_in/helpers.rb +6 -0
- data/lib/linked_in/helpers/authorization.rb +106 -0
- data/lib/linked_in/helpers/request.rb +93 -0
- data/lib/linked_in/mash.rb +68 -0
- data/lib/linked_in/search.rb +55 -0
- data/lib/linked_in/version.rb +11 -0
- data/lib/linkedin-oauth2.rb +32 -0
- data/linkedin-oauth2.gemspec +25 -0
- data/spec/cases/api_spec.rb +192 -0
- data/spec/cases/linkedin_spec.rb +37 -0
- data/spec/cases/mash_spec.rb +85 -0
- data/spec/cases/oauth_spec.rb +130 -0
- data/spec/cases/search_spec.rb +190 -0
- data/spec/helper.rb +30 -0
- metadata +236 -0
data/examples/status.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
module Api
|
3
|
+
|
4
|
+
module QueryMethods
|
5
|
+
|
6
|
+
def profile(options={})
|
7
|
+
path = person_path(options)
|
8
|
+
simple_query(path, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def connections(options={})
|
12
|
+
path = "#{person_path(options)}/connections"
|
13
|
+
simple_query(path, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def network_updates(options={})
|
17
|
+
path = "#{person_path(options)}/network/updates"
|
18
|
+
simple_query(path, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def company(options = {})
|
22
|
+
path = company_path(options)
|
23
|
+
simple_query(path, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def job(options = {})
|
27
|
+
path = jobs_path(options)
|
28
|
+
simple_query(path, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def job_bookmarks(options = {})
|
32
|
+
path = "#{person_path(options)}/job-bookmarks"
|
33
|
+
simple_query(path, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def job_suggestions(options = {})
|
37
|
+
path = "#{person_path(options)}/suggestions/job-suggestions"
|
38
|
+
simple_query(path, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def group_memberships(options = {})
|
42
|
+
path = "#{person_path(options)}/group-memberships"
|
43
|
+
simple_query(path, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def shares(options={})
|
47
|
+
path = "#{person_path(options)}/network/updates?type=SHAR&scope=self"
|
48
|
+
simple_query(path, options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def share_comments(update_key, options={})
|
52
|
+
path = "#{person_path(options)}/network/updates/key=#{update_key}/update-comments"
|
53
|
+
simple_query(path, options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def share_likes(update_key, options={})
|
57
|
+
path = "#{person_path(options)}/network/updates/key=#{update_key}/likes"
|
58
|
+
simple_query(path, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def simple_query(path, options={})
|
64
|
+
fields = options.delete(:fields) || LinkedIn.default_profile_fields
|
65
|
+
|
66
|
+
if options.delete(:public)
|
67
|
+
path +=":public"
|
68
|
+
elsif fields
|
69
|
+
path +=":(#{fields.map{ |f| f.to_s.gsub("_","-") }.join(',')})"
|
70
|
+
end
|
71
|
+
|
72
|
+
headers = options.delete(:headers) || {}
|
73
|
+
|
74
|
+
params = to_query(options)
|
75
|
+
path += "?#{params}" if !params.empty?
|
76
|
+
|
77
|
+
Mash.from_json(get(path, headers))
|
78
|
+
end
|
79
|
+
|
80
|
+
def person_path(options)
|
81
|
+
path = "/people"
|
82
|
+
if id = options.delete(:id)
|
83
|
+
if id.is_a? Array
|
84
|
+
path += "::(#{id.join(",")})"
|
85
|
+
else
|
86
|
+
path += "/id=#{id}"
|
87
|
+
end
|
88
|
+
elsif url = options.delete(:url)
|
89
|
+
path += "/url=#{CGI.escape(url)}"
|
90
|
+
else
|
91
|
+
path += "/~"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def company_path(options)
|
96
|
+
path = "/companies"
|
97
|
+
|
98
|
+
if domain = options.delete(:domain)
|
99
|
+
path += "?email-domain=#{CGI.escape(domain)}"
|
100
|
+
elsif id = options.delete(:id)
|
101
|
+
path += "/id=#{id}"
|
102
|
+
elsif url = options.delete(:url)
|
103
|
+
path += "/url=#{CGI.escape(url)}"
|
104
|
+
elsif name = options.delete(:name)
|
105
|
+
path += "/universal-name=#{CGI.escape(name)}"
|
106
|
+
else
|
107
|
+
path += "/~"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def jobs_path(options)
|
112
|
+
path = "/jobs"
|
113
|
+
if id = options.delete(:id)
|
114
|
+
path += "/id=#{id}"
|
115
|
+
else
|
116
|
+
path += "/~"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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 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
|
+
|
18
|
+
def add_job_bookmark(bookmark)
|
19
|
+
path = "/people/~/job-bookmarks"
|
20
|
+
body = {'job' => {'id' => bookmark}}
|
21
|
+
post(path, body.to_json, "Content-Type" => "application/json")
|
22
|
+
end
|
23
|
+
|
24
|
+
# def share(options={})
|
25
|
+
# path = "/people/~/shares"
|
26
|
+
# defaults = { :visability => 'anyone' }
|
27
|
+
# post(path, share_to_xml(defaults.merge(options)))
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
def update_comment(network_key, comment)
|
31
|
+
path = "/people/~/network/updates/key=#{network_key}/update-comments"
|
32
|
+
body = {'comment' => comment}
|
33
|
+
post(path, body.to_json, "Content-Type" => "application/json")
|
34
|
+
end
|
35
|
+
#
|
36
|
+
# def update_network(message)
|
37
|
+
# path = "/people/~/person-activities"
|
38
|
+
# post(path, network_update_to_xml(message))
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
|
42
|
+
def like_share(network_key)
|
43
|
+
path = "/people/~/network/updates/key=#{network_key}/is-liked"
|
44
|
+
put(path, 'true', "Content-Type" => "application/json")
|
45
|
+
end
|
46
|
+
|
47
|
+
def unlike_share(network_key)
|
48
|
+
path = "/people/~/network/updates/key=#{network_key}/is-liked"
|
49
|
+
put(path, 'false', "Content-Type" => "application/json")
|
50
|
+
end
|
51
|
+
|
52
|
+
def send_message(subject, body, recipient_paths)
|
53
|
+
path = "/people/~/mailbox"
|
54
|
+
|
55
|
+
message = {
|
56
|
+
'subject' => subject,
|
57
|
+
'body' => body,
|
58
|
+
'recipients' => {
|
59
|
+
'values' => recipient_paths.map do |profile_path|
|
60
|
+
{ 'person' => { '_path' => "/people/#{profile_path}" } }
|
61
|
+
end
|
62
|
+
}
|
63
|
+
}
|
64
|
+
post(path, message.to_json, "Content-Type" => "application/json")
|
65
|
+
end
|
66
|
+
#
|
67
|
+
# def clear_status
|
68
|
+
# path = "/people/~/current-status"
|
69
|
+
# delete(path).code
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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 :client_id, :client_secret, :access_token
|
13
|
+
|
14
|
+
# The first two arguments must be your client_id, and client_secret.
|
15
|
+
# The third option may either be an access_token or an options hash.
|
16
|
+
def initialize(client_id=LinkedIn.client_id,
|
17
|
+
client_secret=LinkedIn.client_secret,
|
18
|
+
initial_access_token=nil,
|
19
|
+
options={})
|
20
|
+
@client_id = client_id
|
21
|
+
@client_secret = client_secret
|
22
|
+
if initial_access_token.is_a? Hash
|
23
|
+
@client_options = initial_access_token
|
24
|
+
else
|
25
|
+
@client_options = options
|
26
|
+
self.set_access_token initial_access_token
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
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,106 @@
|
|
1
|
+
|
2
|
+
# TODO
|
3
|
+
# `consumer` renamed to `client`
|
4
|
+
#
|
5
|
+
# rename OAuth to OAuth2
|
6
|
+
#
|
7
|
+
# deprecate `request_token`
|
8
|
+
# deprecate `access_token`
|
9
|
+
#
|
10
|
+
# deprecate `authorize_from_request`
|
11
|
+
# deprecate `authorize_from_access`
|
12
|
+
#
|
13
|
+
# cleanup bottom
|
14
|
+
# deprecate `oauth_callback`
|
15
|
+
#
|
16
|
+
# DONE
|
17
|
+
# `consumer_token renamed to `client_id`
|
18
|
+
# `consumer_secret renamed to `client_secret`
|
19
|
+
# request_token_url is deprecated. Replaced with token_url
|
20
|
+
# access_token_url is deprecated. Replaced with token_url
|
21
|
+
# request_token_path is deprecated
|
22
|
+
# access_token_path has changed
|
23
|
+
# authorize_path has been deprecated
|
24
|
+
|
25
|
+
module LinkedIn
|
26
|
+
module Helpers
|
27
|
+
|
28
|
+
module Authorization
|
29
|
+
|
30
|
+
DEFAULT_OAUTH2_OPTIONS = {
|
31
|
+
authorize_path: "/uas/oauth2/authorization",
|
32
|
+
access_token_path: "/uas/oauth2/accessToken",
|
33
|
+
api_host: "https://api.linkedin.com",
|
34
|
+
auth_host: "https://www.linkedin.com"
|
35
|
+
}
|
36
|
+
|
37
|
+
def oauth2_client
|
38
|
+
@oauth2_client ||= ::OAuth2::Client.new(@client_id,
|
39
|
+
@client_secret,
|
40
|
+
parse_oauth2_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# A way to fetch the authorize_url
|
44
|
+
# @param :redirect_uri - Where you want it to redirect to after
|
45
|
+
# @param :scope - A list of member permissions you would like to
|
46
|
+
# request.
|
47
|
+
def authorize_url(params={})
|
48
|
+
# response_type param included by default by using the OAuth 2.0
|
49
|
+
# auth_code strategy
|
50
|
+
# client_id param included automatically by the OAuth 2.0 gem
|
51
|
+
params[:state] ||= state
|
52
|
+
params[:redirect_uri] ||= "http://localhost"
|
53
|
+
oauth2_client.auth_code.authorize_url(params)
|
54
|
+
rescue OAuth2::Error => e
|
55
|
+
raise LinkedIn::Errors::UnauthorizedError.new(e.code), e.description
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fetches the access_token given the auth_code fetched by
|
59
|
+
# navigating to `authorize_url`
|
60
|
+
# @param :redirect_uri - Where you want to redirect after you have
|
61
|
+
# fetched the token.
|
62
|
+
def request_access_token(code, params={})
|
63
|
+
params[:redirect_uri] ||= "http://localhost"
|
64
|
+
opts = {}
|
65
|
+
opts[:mode] = :query
|
66
|
+
opts[:param_name] = "oauth2_access_token"
|
67
|
+
@access_token = oauth2_client.auth_code.get_token(code, params, opts)
|
68
|
+
rescue OAuth2::Error => e
|
69
|
+
raise LinkedIn::Errors::UnauthorizedError.new(e.code), e.description
|
70
|
+
end
|
71
|
+
|
72
|
+
# If one already has an access_token string, it can be set here and
|
73
|
+
# turned into an OAuth2::AccessToken object.
|
74
|
+
def set_access_token(token, options={})
|
75
|
+
options[:access_token] = token
|
76
|
+
options[:mode] = :query
|
77
|
+
options[:param_name] = "oauth2_access_token"
|
78
|
+
@access_token = OAuth2::AccessToken.from_hash oauth2_client, options
|
79
|
+
end
|
80
|
+
|
81
|
+
# NOTE: There is an attr_reader for :access_token.
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# The keys of this hash are designed to match the OAuth2
|
86
|
+
# initialize spec.
|
87
|
+
def parse_oauth2_options
|
88
|
+
default = {site: DEFAULT_OAUTH2_OPTIONS[:api_host],
|
89
|
+
token_url: full_oauth_url_for(:access_token, :auth_host),
|
90
|
+
authorize_url: full_oauth_url_for(:authorize, :auth_host)}
|
91
|
+
return default.merge(@client_options)
|
92
|
+
end
|
93
|
+
|
94
|
+
def full_oauth_url_for(url_type, host_type)
|
95
|
+
host = DEFAULT_OAUTH2_OPTIONS[host_type]
|
96
|
+
path = DEFAULT_OAUTH2_OPTIONS["#{url_type}_path".to_sym]
|
97
|
+
"#{host}#{path}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def state
|
101
|
+
o = [('a'..'z'),('A'..'Z')].map{|i| i.to_a}.flatten
|
102
|
+
@state ||= (0...50).map{ o[rand(o.length)] }.join
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,93 @@
|
|
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}", headers: DEFAULT_HEADERS.merge(options))
|
16
|
+
raise_errors(response)
|
17
|
+
response.body
|
18
|
+
rescue OAuth2::Error => e
|
19
|
+
raise LinkedIn::Errors::AccessDeniedError.new(e.code), e.description
|
20
|
+
end
|
21
|
+
|
22
|
+
def post(path, body='', options={})
|
23
|
+
response = access_token.post("#{API_PATH}#{path}", body: body, headers: DEFAULT_HEADERS.merge(options))
|
24
|
+
raise_errors(response)
|
25
|
+
response
|
26
|
+
rescue OAuth2::Error => e
|
27
|
+
raise LinkedIn::Errors::AccessDeniedError.new(e.code), e.description
|
28
|
+
end
|
29
|
+
|
30
|
+
def put(path, body, options={})
|
31
|
+
response = access_token.put("#{API_PATH}#{path}", body: body, headers: DEFAULT_HEADERS.merge(options))
|
32
|
+
raise_errors(response)
|
33
|
+
response
|
34
|
+
rescue OAuth2::Error => e
|
35
|
+
raise LinkedIn::Errors::AccessDeniedError.new(e.code), e.description
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete(path, options={})
|
39
|
+
response = access_token.delete("#{API_PATH}#{path}", headers: DEFAULT_HEADERS.merge(options))
|
40
|
+
raise_errors(response)
|
41
|
+
response
|
42
|
+
rescue OAuth2::Error => e
|
43
|
+
raise LinkedIn::Errors::AccessDeniedError.new(e.code), e.description
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def raise_errors(response)
|
49
|
+
# Even if the json answer contains the HTTP status code, LinkedIn also sets this status
|
50
|
+
# in the HTTP answer (thankfully).
|
51
|
+
case response.status.to_i
|
52
|
+
when 401
|
53
|
+
data = Mash.from_json(response.body)
|
54
|
+
raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}"
|
55
|
+
when 400
|
56
|
+
data = Mash.from_json(response.body)
|
57
|
+
raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}"
|
58
|
+
when 403
|
59
|
+
data = Mash.from_json(response.body)
|
60
|
+
raise LinkedIn::Errors::AccessDeniedError.new(data), "(#{data.status}): #{data.message}"
|
61
|
+
when 404
|
62
|
+
raise LinkedIn::Errors::NotFoundError, "(#{response.status}): #{response.message}"
|
63
|
+
when 500
|
64
|
+
raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.status}): #{response.message}"
|
65
|
+
when 502..503
|
66
|
+
raise LinkedIn::Errors::UnavailableError, "(#{response.status}): #{response.message}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Stolen from Rack::Util.build_query
|
72
|
+
def to_query(params)
|
73
|
+
params.map { |k, v|
|
74
|
+
if v.class == Array
|
75
|
+
to_query(v.map { |x| [k, x] })
|
76
|
+
else
|
77
|
+
v.nil? ? escape(k) : "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
78
|
+
end
|
79
|
+
}.join("&")
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_uri(path, options)
|
83
|
+
uri = URI.parse(path)
|
84
|
+
|
85
|
+
if options && options != {}
|
86
|
+
uri.query = to_query(options)
|
87
|
+
end
|
88
|
+
uri.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|