linkedin-build 1.1.14

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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +14 -0
  3. data/.gemtest +0 -0
  4. data/.gitignore +49 -0
  5. data/.rspec +1 -0
  6. data/.travis.yml +6 -0
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +99 -0
  9. data/EXAMPLES.md +202 -0
  10. data/Gemfile +11 -0
  11. data/LICENSE +22 -0
  12. data/README.md +43 -0
  13. data/Rakefile +15 -0
  14. data/lib/linked_in/api.rb +38 -0
  15. data/lib/linked_in/api/communications.rb +44 -0
  16. data/lib/linked_in/api/companies.rb +129 -0
  17. data/lib/linked_in/api/groups.rb +115 -0
  18. data/lib/linked_in/api/jobs.rb +64 -0
  19. data/lib/linked_in/api/people.rb +73 -0
  20. data/lib/linked_in/api/query_helpers.rb +86 -0
  21. data/lib/linked_in/api/share_and_social_stream.rb +137 -0
  22. data/lib/linked_in/client.rb +51 -0
  23. data/lib/linked_in/errors.rb +29 -0
  24. data/lib/linked_in/helpers.rb +6 -0
  25. data/lib/linked_in/helpers/authorization.rb +69 -0
  26. data/lib/linked_in/helpers/request.rb +85 -0
  27. data/lib/linked_in/mash.rb +95 -0
  28. data/lib/linked_in/search.rb +71 -0
  29. data/lib/linked_in/version.rb +11 -0
  30. data/lib/linkedin.rb +35 -0
  31. data/linkedin-build.gemspec +28 -0
  32. data/spec/cases/api_spec.rb +308 -0
  33. data/spec/cases/linkedin_spec.rb +37 -0
  34. data/spec/cases/mash_spec.rb +113 -0
  35. data/spec/cases/oauth_spec.rb +178 -0
  36. data/spec/cases/search_spec.rb +234 -0
  37. data/spec/fixtures/cassette_library/LinkedIn_Api/Company_API.yml +81 -0
  38. data/spec/fixtures/cassette_library/LinkedIn_Api/Company_API/should_load_correct_company_data.yml +81 -0
  39. data/spec/fixtures/cassette_library/LinkedIn_Client/_authorize_from_request/should_return_a_valid_access_token.yml +37 -0
  40. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_a_callback_url/should_return_a_valid_access_token.yml +37 -0
  41. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_default_options/should_return_a_valid_request_token.yml +37 -0
  42. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option/should_perform_a_search.yml +92 -0
  43. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_email_address/should_perform_a_people_search.yml +57 -0
  44. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options/should_perform_a_search.yml +100 -0
  45. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields/should_perform_a_search.yml +114 -0
  46. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter/should_perform_a_search.yml +52 -0
  47. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_multiple_email_address/should_perform_a_multi-email_search.yml +59 -0
  48. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option/should_perform_a_search.yml +52 -0
  49. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination/should_perform_a_search.yml +43 -0
  50. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/email_search_returns_unauthorized/should_raise_an_unauthorized_error.yml +59 -0
  51. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_options_with_fields/should_perform_a_search.yml +43 -0
  52. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_string_parameter/should_perform_a_company_search.yml +80 -0
  53. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option/should_perform_a_company_search.yml +80 -0
  54. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_facets_to_return/should_return_a_facet.yml +80 -0
  55. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_pagination/should_perform_a_search.yml +74 -0
  56. data/spec/helper.rb +34 -0
  57. metadata +282 -0
@@ -0,0 +1,86 @@
1
+ module LinkedIn
2
+ module Api
3
+
4
+ module QueryHelpers
5
+ private
6
+
7
+ def group_path(options)
8
+ path = "/groups"
9
+ if id = options.delete(:id)
10
+ path += "/#{id}"
11
+ end
12
+ end
13
+
14
+ def simple_query(path, options={})
15
+ fields = options.delete(:fields) || LinkedIn.default_profile_fields
16
+
17
+ if options.delete(:public)
18
+ path +=":public"
19
+ elsif fields
20
+ path +=":(#{build_fields_params(fields)})"
21
+ end
22
+
23
+ headers = options.delete(:headers) || {}
24
+ params = to_query(options)
25
+ path += "#{path.include?("?") ? "&" : "?"}#{params}" if !params.empty?
26
+
27
+ Mash.from_json(get(path, headers))
28
+ end
29
+
30
+ def build_fields_params(fields)
31
+ if fields.is_a?(Hash) && !fields.empty?
32
+ fields.map {|index,value| "#{index}:(#{build_fields_params(value)})" }.join(',')
33
+ elsif fields.respond_to?(:each)
34
+ fields.map {|field| build_fields_params(field) }.join(',')
35
+ else
36
+ fields.to_s.gsub("_", "-")
37
+ end
38
+ end
39
+
40
+ def person_path(options)
41
+ path = "/people"
42
+ if id = options.delete(:id)
43
+ path += "/id=#{id}"
44
+ elsif url = options.delete(:url)
45
+ path += "/url=#{CGI.escape(url)}"
46
+ elsif email = options.delete(:email)
47
+ path += "::(#{email})"
48
+ else
49
+ path += "/~"
50
+ end
51
+ end
52
+
53
+ def company_path(options)
54
+ path = "/companies"
55
+
56
+ if domain = options.delete(:domain)
57
+ path += "?email-domain=#{CGI.escape(domain)}"
58
+ elsif id = options.delete(:id)
59
+ path += "/#{id}"
60
+ elsif url = options.delete(:url)
61
+ path += "/url=#{CGI.escape(url)}"
62
+ elsif name = options.delete(:name)
63
+ path += "/universal-name=#{CGI.escape(name)}"
64
+ elsif is_admin = options.delete(:is_admin)
65
+ path += "?is-company-admin=#{CGI.escape(is_admin)}"
66
+ else
67
+ path += "/~"
68
+ end
69
+ end
70
+
71
+ def picture_urls_path(options)
72
+ path = person_path(options)
73
+ path += "/picture-urls"
74
+ end
75
+
76
+ def jobs_path(options)
77
+ path = "/jobs"
78
+ if id = options.delete(:id)
79
+ path += "/id=#{id}"
80
+ else
81
+ path += "/~"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,137 @@
1
+ module LinkedIn
2
+ module Api
3
+
4
+ # Share and Social Stream APIs
5
+ #
6
+ # @see https://developer.linkedin.com/docs/share-on-linkedin Share API
7
+ #
8
+ # The following API actions do not have corresponding methods in
9
+ # this module
10
+ #
11
+ # * GET Network Statistics
12
+ # * POST Post Network Update
13
+ #
14
+ # [(contribute here)](https://github.com/hexgnu/linkedin)
15
+ module ShareAndSocialStream
16
+
17
+ # Retrieve the authenticated users network updates
18
+ #
19
+ # Permissions: rw_nus
20
+ #
21
+ # @see http://developer.linkedin.com/documents/get-network-updates-and-statistics-api
22
+ # @see http://developer.linkedin.com/documents/network-update-types Network Update Types
23
+ #
24
+ # @macro person_path_options
25
+ # @option options [String] :scope
26
+ # @option options [String] :type
27
+ # @option options [String] :count
28
+ # @option options [String] :start
29
+ # @option options [String] :after
30
+ # @option options [String] :before
31
+ # @option options [String] :show-hidden-members
32
+ # @return [LinkedIn::Mash]
33
+ def network_updates(options={})
34
+ path = "#{person_path(options)}/network/updates"
35
+ simple_query(path, options)
36
+ end
37
+
38
+ # TODO refactor to use #network_updates
39
+ def shares(options={})
40
+ path = "#{person_path(options)}/network/updates"
41
+ simple_query(path, {:type => "SHAR", :scope => "self"}.merge(options))
42
+ end
43
+
44
+ def share(update_key, options={})
45
+ path = "#{person_path(options)}/network/updates/key=#{update_key}"
46
+ simple_query(path, options)
47
+ end
48
+
49
+ # Retrieve all comments for a particular network update
50
+ #
51
+ # @note The first 5 comments are included in the response to #network_updates
52
+ #
53
+ # Permissions: rw_nus
54
+ #
55
+ # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates
56
+ #
57
+ # @param [String] update_key a update/update-key representing a
58
+ # particular network update
59
+ # @macro person_path_options
60
+ # @return [LinkedIn::Mash]
61
+ def share_comments(update_key, options={})
62
+ path = "#{person_path(options)}/network/updates/key=#{update_key}/update-comments"
63
+ simple_query(path, options)
64
+ end
65
+
66
+ # Retrieve all likes for a particular network update
67
+ #
68
+ # @note Some likes are included in the response to #network_updates
69
+ #
70
+ # Permissions: rw_nus
71
+ #
72
+ # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates
73
+ #
74
+ # @param [String] update_key a update/update-key representing a
75
+ # particular network update
76
+ # @macro person_path_options
77
+ # @return [LinkedIn::Mash]
78
+ def share_likes(update_key, options={})
79
+ path = "#{person_path(options)}/network/updates/key=#{update_key}/likes"
80
+ simple_query(path, options)
81
+ end
82
+
83
+ # Create a share for the authenticated user
84
+ #
85
+ # Permissions: rw_nus
86
+ #
87
+ # @see https://developer.linkedin.com/docs/share-on-linkedin Share API
88
+ #
89
+ # @macro share_input_fields
90
+ # @return [void]
91
+ def add_share(share)
92
+ path = "/people/~/shares"
93
+ defaults = {:visibility => {:code => "anyone"}}
94
+ post(path, MultiJson.dump(defaults.merge(share)), "Content-Type" => "application/json")
95
+ end
96
+
97
+ # Create a comment on an update from the authenticated user
98
+ #
99
+ # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates
100
+ #
101
+ # @param [String] update_key a update/update-key representing a
102
+ # particular network update
103
+ # @param [String] comment The text of the comment
104
+ # @return [void]
105
+ def update_comment(update_key, comment)
106
+ path = "/people/~/network/updates/key=#{update_key}/update-comments"
107
+ body = {'comment' => comment}
108
+ post(path, MultiJson.dump(body), "Content-Type" => "application/json")
109
+ end
110
+
111
+ # (Update) like an update as the authenticated user
112
+ #
113
+ # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates
114
+ #
115
+ # @param [String] update_key a update/update-key representing a
116
+ # particular network update
117
+ # @return [void]
118
+ def like_share(update_key)
119
+ path = "/people/~/network/updates/key=#{update_key}/is-liked"
120
+ put(path, 'true', "Content-Type" => "application/json")
121
+ end
122
+
123
+ # (Destroy) unlike an update the authenticated user previously
124
+ # liked
125
+ #
126
+ # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates
127
+ #
128
+ # @param [String] update_key a update/update-key representing a
129
+ # particular network update
130
+ # @return [void]
131
+ def unlike_share(update_key)
132
+ path = "/people/~/network/updates/key=#{update_key}/is-liked"
133
+ put(path, 'false', "Content-Type" => "application/json")
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,51 @@
1
+ require 'cgi'
2
+
3
+ module LinkedIn
4
+
5
+ class Client
6
+ include Helpers::Request
7
+ include Helpers::Authorization
8
+ include Api::QueryHelpers
9
+ include Api::People
10
+ include Api::Groups
11
+ include Api::Companies
12
+ include Api::Jobs
13
+ include Api::ShareAndSocialStream
14
+ include Api::Communications
15
+ include Search
16
+
17
+ attr_reader :consumer_token, :consumer_secret, :consumer_options
18
+
19
+ def initialize(ctoken=LinkedIn.token, csecret=LinkedIn.secret, options={})
20
+ @consumer_token = ctoken
21
+ @consumer_secret = csecret
22
+ @consumer_options = options
23
+ end
24
+
25
+ #
26
+ # def current_status
27
+ # path = "/people/~/current-status"
28
+ # Crack::XML.parse(get(path))['current_status']
29
+ # end
30
+ #
31
+ # def network_statuses(options={})
32
+ # options[:type] = 'STAT'
33
+ # network_updates(options)
34
+ # end
35
+ #
36
+ # def network_updates(options={})
37
+ # path = "/people/~/network"
38
+ # Network.from_xml(get(to_uri(path, options)))
39
+ # end
40
+ #
41
+ # # helpful in making authenticated calls and writing the
42
+ # # raw xml to a fixture file
43
+ # def write_fixture(path, filename)
44
+ # file = File.new("test/fixtures/#{filename}", "w")
45
+ # file.puts(access_token.get(path).body)
46
+ # file.close
47
+ # end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,29 @@
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
+ # Raised when a 401 response status code is received
12
+ class UnauthorizedError < LinkedInError; end
13
+
14
+ # Raised when a 400 response status code is received
15
+ class GeneralError < LinkedInError; end
16
+
17
+ # Raised when a 403 response status code is received
18
+ class AccessDeniedError < LinkedInError; end
19
+
20
+ # Raised when a 404 response status code is received
21
+ class NotFoundError < LinkedInError; end
22
+
23
+ # Raised when a 500 response status code is received
24
+ class InformLinkedInError < LinkedInError; end
25
+
26
+ # Raised when a 502 or 503 response status code is received
27
+ class UnavailableError < LinkedInError; end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module LinkedIn
2
+ module Helpers
3
+ autoload :Authorization, "linked_in/helpers/authorization"
4
+ autoload :Request, "linked_in/helpers/request"
5
+ end
6
+ end
@@ -0,0 +1,69 @@
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 => "/oauth/v2/accessToken", #"/uas/oauth/accessToken",
9
+ :authorize_path => "/oauth/v2/authorize",
10
+ :api_host => "https://api.linkedin.com",
11
+ :auth_host => "https://www.linkedin.com"
12
+ }
13
+
14
+ def consumer
15
+ @consumer ||= ::OAuth2::Client.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={}, *arguments, &block)
22
+ @request_token ||= consumer.get_request_token(options, *arguments, &block)
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 ||= ::OAuth2::AccessToken.new(consumer, @auth_token, :expires_at => @expires_at)
35
+ end
36
+
37
+ def authorize_from_access(atoken, expires_at)
38
+ @auth_token, @expires_at = atoken, expires_at
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
+ :proxy => @consumer_options.fetch(:proxy, nil)
53
+ }
54
+ end
55
+
56
+ def full_oauth_url_for(url_type, host_type)
57
+ if @consumer_options["#{url_type}_url".to_sym]
58
+ @consumer_options["#{url_type}_url".to_sym]
59
+ else
60
+ host = @consumer_options[:site] || @consumer_options[host_type] || DEFAULT_OAUTH_OPTIONS[host_type]
61
+ path = @consumer_options[:"#{url_type}_path".to_sym] || DEFAULT_OAUTH_OPTIONS["#{url_type}_path".to_sym]
62
+ "#{host}#{path}"
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,85 @@
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
+ end
19
+
20
+ def post(path, body='', options={})
21
+ response = access_token.post("#{API_PATH}#{path}", {:body => body, :headers => 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 => body, :headers => 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}", {:headers => 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.status.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.status}): #{response.message}"
55
+ when 500
56
+ raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.status}): #{response.message}"
57
+ when 502..503
58
+ raise LinkedIn::Errors::UnavailableError, "(#{response.status}): #{response.message}"
59
+ end
60
+ end
61
+
62
+
63
+ # Stolen from Rack::Util.build_query
64
+ def to_query(params)
65
+ params.map { |k, v|
66
+ if v.class == Array
67
+ to_query(v.map { |x| [k, x] })
68
+ else
69
+ v.nil? ? escape(k) : "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
70
+ end
71
+ }.join("&")
72
+ end
73
+
74
+ def to_uri(path, options)
75
+ uri = URI.parse(path)
76
+
77
+ if options && options != {}
78
+ uri.query = to_query(options)
79
+ end
80
+ uri.to_s
81
+ end
82
+ end
83
+
84
+ end
85
+ end