brienw-linkedin 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.autotest +14 -0
  2. data/.document +5 -0
  3. data/.gemtest +0 -0
  4. data/.gitignore +41 -0
  5. data/.rspec +1 -0
  6. data/.travis.yml +5 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +20 -0
  9. data/README.markdown +78 -0
  10. data/Rakefile +19 -0
  11. data/changelog.markdown +71 -0
  12. data/examples/authenticate.rb +21 -0
  13. data/examples/network.rb +12 -0
  14. data/examples/profile.rb +18 -0
  15. data/examples/sinatra.rb +77 -0
  16. data/examples/status.rb +9 -0
  17. data/lib/linked_in/api.rb +6 -0
  18. data/lib/linked_in/api/query_methods.rb +73 -0
  19. data/lib/linked_in/api/update_methods.rb +55 -0
  20. data/lib/linked_in/client.rb +46 -0
  21. data/lib/linked_in/errors.rb +19 -0
  22. data/lib/linked_in/helpers.rb +6 -0
  23. data/lib/linked_in/helpers/authorization.rb +68 -0
  24. data/lib/linked_in/helpers/request.rb +80 -0
  25. data/lib/linked_in/mash.rb +68 -0
  26. data/lib/linked_in/search.rb +56 -0
  27. data/lib/linked_in/version.rb +11 -0
  28. data/lib/linkedin.rb +32 -0
  29. data/linkedin.gemspec +25 -0
  30. data/spec/cases/api_spec.rb +92 -0
  31. data/spec/cases/linkedin_spec.rb +37 -0
  32. data/spec/cases/mash_spec.rb +85 -0
  33. data/spec/cases/oauth_spec.rb +166 -0
  34. data/spec/cases/search_spec.rb +206 -0
  35. data/spec/fixtures/cassette_library/LinkedIn_Api/Company_API.yml +73 -0
  36. data/spec/fixtures/cassette_library/LinkedIn_Client/_authorize_from_request.yml +28 -0
  37. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token.yml +28 -0
  38. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_a_callback_url.yml +28 -0
  39. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_default_options.yml +28 -0
  40. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option.yml +135 -0
  41. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options.yml +122 -0
  42. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields.yml +72 -0
  43. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter.yml +136 -0
  44. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option.yml +136 -0
  45. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination.yml +128 -0
  46. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_options_with_fields.yml +252 -0
  47. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_string_parameter.yml +73 -0
  48. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option.yml +71 -0
  49. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_a_facet.yml +111 -0
  50. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_facets_to_return.yml +71 -0
  51. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_pagination.yml +66 -0
  52. data/spec/helper.rb +30 -0
  53. 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,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,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
@@ -0,0 +1,11 @@
1
+ module LinkedIn
2
+
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 3
6
+ PATCH = 7
7
+ PRE = nil
8
+ STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
9
+ end
10
+
11
+ end
@@ -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