cl_linkedin 0.2.3
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/.gitignore +25 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.markdown +80 -0
- data/Rakefile +41 -0
- data/VERSION +1 -0
- data/changelog.markdown +76 -0
- data/examples/authenticate.rb +21 -0
- data/examples/network.rb +12 -0
- data/examples/profile.rb +12 -0
- data/examples/status.rb +9 -0
- data/lib/linked_in/api_standard_profile_request.rb +17 -0
- data/lib/linked_in/authorization_helpers.rb +48 -0
- data/lib/linked_in/base.rb +13 -0
- data/lib/linked_in/birthdate.rb +21 -0
- data/lib/linked_in/client.rb +154 -0
- data/lib/linked_in/company.rb +11 -0
- data/lib/linked_in/connections.rb +16 -0
- data/lib/linked_in/country.rb +9 -0
- data/lib/linked_in/current_share.rb +56 -0
- data/lib/linked_in/education.rb +41 -0
- data/lib/linked_in/error.rb +21 -0
- data/lib/linked_in/group.rb +32 -0
- data/lib/linked_in/im_account.rb +28 -0
- data/lib/linked_in/languages.rb +28 -0
- data/lib/linked_in/likes.rb +23 -0
- data/lib/linked_in/location.rb +13 -0
- data/lib/linked_in/message.rb +20 -0
- data/lib/linked_in/network.rb +12 -0
- data/lib/linked_in/patents.rb +42 -0
- data/lib/linked_in/people.rb +18 -0
- data/lib/linked_in/person.rb +7 -0
- data/lib/linked_in/phone_number.rb +29 -0
- data/lib/linked_in/position.rb +46 -0
- data/lib/linked_in/profile.rb +94 -0
- data/lib/linked_in/publications.rb +40 -0
- data/lib/linked_in/recipient.rb +7 -0
- data/lib/linked_in/recipients.rb +18 -0
- data/lib/linked_in/recommendations.rb +30 -0
- data/lib/linked_in/request_helpers.rb +78 -0
- data/lib/linked_in/short_profile.rb +13 -0
- data/lib/linked_in/skill.rb +33 -0
- data/lib/linked_in/to_xml_helpers.rb +53 -0
- data/lib/linked_in/twitter_account.rb +28 -0
- data/lib/linked_in/update.rb +23 -0
- data/lib/linked_in/url_resource.rb +26 -0
- data/lib/linkedin.rb +83 -0
- data/linkedin.gemspec +49 -0
- data/spec/cases/client_spec.rb +230 -0
- data/spec/cases/linkedin_spec.rb +37 -0
- data/spec/cases/oauth_spec.rb +109 -0
- data/spec/client_shared_examples.orig.rb +91 -0
- data/spec/client_shared_examples.rb +104 -0
- data/spec/fixtures/403.xml +7 -0
- data/spec/fixtures/404.xml +7 -0
- data/spec/fixtures/blank.xml +0 -0
- data/spec/fixtures/connections.xml +3733 -0
- data/spec/fixtures/likes.xml +18 -0
- data/spec/fixtures/mailbox_items.xml +16 -0
- data/spec/fixtures/network_status_with_group.xml +44 -0
- data/spec/fixtures/network_statuses.xml +317 -0
- data/spec/fixtures/picture_updates.xml +117 -0
- data/spec/fixtures/profile.xml +9 -0
- data/spec/fixtures/profile_full.orig.xml +3909 -0
- data/spec/fixtures/profile_full.xml +395 -0
- data/spec/fixtures/profile_with_positions.xml +79 -0
- data/spec/fixtures/search.xml +538 -0
- data/spec/fixtures/shares.xml +12 -0
- data/spec/fixtures/status.xml +2 -0
- data/spec/spec_helper.rb +58 -0
- metadata +242 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class Profile < LinkedIn::Base
|
3
|
+
|
4
|
+
# xml_reader :three_current_positions, :as => [Position]
|
5
|
+
|
6
|
+
PROFILE_FIELDS = %w[id first_name last_name headline industry
|
7
|
+
current_status current_status_timestamp summary
|
8
|
+
specialties proposal_comments associations honors
|
9
|
+
interests picture_url distance num_recommenders public_profile_url twitter_accounts im_accounts]
|
10
|
+
|
11
|
+
PROFILE_FIELDS.each do |f|
|
12
|
+
define_method(f.to_sym) do
|
13
|
+
@doc.xpath("./person/#{f.gsub(/_/,'-')}").text
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def birthdate
|
18
|
+
@birthday ||= Birthdate.new(@doc.xpath('./person/date-of-birth'))
|
19
|
+
end
|
20
|
+
|
21
|
+
def location
|
22
|
+
@location ||= Location.new(@doc)
|
23
|
+
end
|
24
|
+
|
25
|
+
def api_standard_profile_request
|
26
|
+
@api_standard ||= ApiStandardProfileRequest.new(@doc.xpath('./person/api-standard-profile-request'))
|
27
|
+
end
|
28
|
+
|
29
|
+
def site_standard_profile_request
|
30
|
+
@doc.xpath('/person/site-standard-profile-request/url').text
|
31
|
+
end
|
32
|
+
|
33
|
+
def relation_to_viewer
|
34
|
+
@doc.xpath('//relation-to-viewer/distance').text
|
35
|
+
end
|
36
|
+
|
37
|
+
def member_url_resources
|
38
|
+
@url_resources ||= UrlResource.new(@doc.xpath('//member-url-resources')).resources
|
39
|
+
end
|
40
|
+
|
41
|
+
def positions
|
42
|
+
@positions ||= Position.new(@doc.xpath('//positions')).positions
|
43
|
+
end
|
44
|
+
|
45
|
+
def educations
|
46
|
+
@educations ||= Education.new(@doc.xpath('//educations')).educations
|
47
|
+
end
|
48
|
+
|
49
|
+
def connections
|
50
|
+
@connections ||= Connections.new(@doc.xpath('//connections')).connections
|
51
|
+
end
|
52
|
+
|
53
|
+
def groups
|
54
|
+
@groups ||= Group.new(@doc.xpath('//member-groups')).groups
|
55
|
+
end
|
56
|
+
|
57
|
+
def recommendations
|
58
|
+
@recommendations ||= Recommendations.new(@doc.xpath('//recommendations-received')).recommendations
|
59
|
+
end
|
60
|
+
|
61
|
+
def current_share
|
62
|
+
@current_share ||= CurrentShare.new(@doc.xpath('//current-share'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def languages
|
66
|
+
@languages ||= Languages.new(@doc.xpath('//languages')).languages
|
67
|
+
end
|
68
|
+
|
69
|
+
def skills
|
70
|
+
@skills ||= Skill.new(@doc.xpath('//skills')).skills
|
71
|
+
end
|
72
|
+
|
73
|
+
def phone_numbers
|
74
|
+
@phone_number ||= PhoneNumber.new(@doc.xpath('//phone-numbers')).phone_numbers
|
75
|
+
end
|
76
|
+
|
77
|
+
def publications
|
78
|
+
@publications ||= Publications.new(@doc.xpath('//publications')).publications
|
79
|
+
end
|
80
|
+
|
81
|
+
def patents
|
82
|
+
@patents ||= Patents.new(@doc.xpath('//patents')).patents
|
83
|
+
end
|
84
|
+
|
85
|
+
def twitter_accounts
|
86
|
+
@twitter_accounts ||= TwitterAccount.new(@doc.xpath('//twitter-accounts')).twitter_accounts
|
87
|
+
end
|
88
|
+
|
89
|
+
def im_accounts
|
90
|
+
@im_accounts ||= ImAccount.new(@doc.xpath('//im-accounts')).im_accounts
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class Publications < LinkedIn::Base
|
3
|
+
|
4
|
+
def publications
|
5
|
+
@publications ||= @doc.children.inject([]) do |list, publication|
|
6
|
+
list << Resource.new(publication) unless publication.blank?
|
7
|
+
list
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Resource
|
12
|
+
def initialize(publication)
|
13
|
+
@publication = publication
|
14
|
+
end
|
15
|
+
|
16
|
+
%w[id title].each do |f|
|
17
|
+
define_method(f.to_sym) do
|
18
|
+
@publication.xpath("./#{f.gsub(/_/,'-')}").text
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def year
|
23
|
+
@year ||= @publication.xpath("./date/year").text.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def day
|
27
|
+
@day ||= @publication.xpath("./date/day").text.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def month
|
31
|
+
@month ||= @publication.xpath("./date/month").text.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
def date
|
35
|
+
Date.civil(year, month, day)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class Recipients
|
3
|
+
|
4
|
+
attr_accessor :recipients
|
5
|
+
|
6
|
+
def to_xml
|
7
|
+
self.to_xml_nodes(Nokogiri.XML('<root/>', nil, 'UTF-8')).to_xml
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_xml_nodes(doc)
|
11
|
+
recipients.inject(Nokogiri::XML::NodeSet.new(doc)) do |nodes, recipient|
|
12
|
+
node = Nokogiri::XML::DocumentFragment.new(doc, '<recipient><person/></recipient>')
|
13
|
+
node.at_css('person')['path'] = recipient.person.path
|
14
|
+
nodes << node
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
|
3
|
+
class Recommendations < LinkedIn::Base
|
4
|
+
|
5
|
+
def recommendations
|
6
|
+
@recommendations ||= @doc.children.inject([]) do |list, recommendation|
|
7
|
+
list << Recommendation.new(recommendation) unless recommendation.blank?
|
8
|
+
list
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Recommendation
|
13
|
+
def initialize(recommendation)
|
14
|
+
@recommendation = recommendation
|
15
|
+
end
|
16
|
+
|
17
|
+
%w[id recommendation_type recommendation_text].each do |f|
|
18
|
+
define_method(f.to_sym) do
|
19
|
+
@recommendation.xpath("./#{f.gsub(/_/,'-')}").text
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def recommender
|
24
|
+
@recommender ||= ShortProfile.new(@recommendation.xpath('./recommender'))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
|
3
|
+
module RequestHelpers
|
4
|
+
|
5
|
+
def get(path, options={})
|
6
|
+
path = "/v1#{path}"
|
7
|
+
response = access_token.get(path, options)
|
8
|
+
raise_errors(response)
|
9
|
+
response.body
|
10
|
+
end
|
11
|
+
|
12
|
+
def post(path, body='', options={})
|
13
|
+
path = "/v1#{path}"
|
14
|
+
default_options = { 'Content-Type' => 'application/xml' }
|
15
|
+
response = access_token.post(path, body, default_options.merge(options))
|
16
|
+
raise_errors(response)
|
17
|
+
response
|
18
|
+
end
|
19
|
+
|
20
|
+
def put(path, body, options={})
|
21
|
+
path = "/v1#{path}"
|
22
|
+
response = access_token.put(path, body, options)
|
23
|
+
raise_errors(response)
|
24
|
+
response
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(path, options={})
|
28
|
+
path = "/v1#{path}"
|
29
|
+
response = access_token.delete(path, options)
|
30
|
+
raise_errors(response)
|
31
|
+
response
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def raise_errors(response)
|
37
|
+
# Even if the XML answer contains the HTTP status code, LinkedIn also sets this code
|
38
|
+
# in the HTTP answer (thankfully).
|
39
|
+
if response.code.to_i >= 400
|
40
|
+
data = LinkedIn::Error.from_xml(response.body)
|
41
|
+
message = "(#{response.code}): #{data.message} - #{data.code}"
|
42
|
+
|
43
|
+
case response.code.to_i
|
44
|
+
when 400
|
45
|
+
raise BadRequest.new(data), message
|
46
|
+
when 401
|
47
|
+
raise Unauthorized.new(data), message
|
48
|
+
when 403
|
49
|
+
raise Forbidden.new(data), message
|
50
|
+
when 404
|
51
|
+
raise NotFound.new(data), message
|
52
|
+
when 500
|
53
|
+
raise InformLinkedIn, "LinkedIn had an internal error. Please let them know in the forum. #{message}"
|
54
|
+
when 502..503
|
55
|
+
raise Unavailable, message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_query(options)
|
61
|
+
query_string = options.inject([]) do |collection, opt|
|
62
|
+
collection << "#{opt[0]}=#{opt[1]}"
|
63
|
+
collection
|
64
|
+
end * '&'
|
65
|
+
URI.escape(query_string)
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_uri(path, options)
|
69
|
+
uri = URI.parse(path)
|
70
|
+
|
71
|
+
if options && options != {}
|
72
|
+
uri.query = to_query(options)
|
73
|
+
end
|
74
|
+
uri.to_s
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class Skill < LinkedIn::Base
|
3
|
+
|
4
|
+
def skills
|
5
|
+
@array ||= begin
|
6
|
+
@array = []
|
7
|
+
@doc.children.each do |skill|
|
8
|
+
@array << Resource.new(skill) unless skill.blank?
|
9
|
+
end
|
10
|
+
@array
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Resource
|
15
|
+
|
16
|
+
def initialize(skill)
|
17
|
+
@skill = skill
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
@skill.xpath("./skill/name").text
|
22
|
+
end
|
23
|
+
|
24
|
+
%w[id].each do |f| #proficiency level to come? http://developer.linkedin.com/community/apis/blog/2011/01/04/new-profile-fields-are-here
|
25
|
+
define_method(f.to_sym) do
|
26
|
+
@skill.xpath("./#{f.gsub(/_/,'-')}").text
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end # resource class
|
31
|
+
|
32
|
+
end # skill class
|
33
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
module ToXmlHelpers
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def status_to_xml(status)
|
7
|
+
doc = Nokogiri.XML('<current-status/>', nil, 'UTF-8')
|
8
|
+
doc.root.content = status
|
9
|
+
doc.to_xml
|
10
|
+
end
|
11
|
+
|
12
|
+
def message_to_xml(message)
|
13
|
+
doc = Nokogiri.XML('')
|
14
|
+
doc.encoding = 'UTF-8'
|
15
|
+
doc.root = message.to_xml_node(doc)
|
16
|
+
doc.to_xml
|
17
|
+
end
|
18
|
+
|
19
|
+
def share_to_xml(options={})
|
20
|
+
doc = Nokogiri.XML('<share><comment/><content><title/><submitted-url/><submitted-image-url/></content><visibility><code/></visibility></share>')
|
21
|
+
doc.encoding = 'UTF-8'
|
22
|
+
|
23
|
+
{:comment => 'comment', :title => 'title', :url => 'submitted-url', :image_url => 'submitted-image-url'}.each do |key, name|
|
24
|
+
doc.at_css(name).content = options[key] if options[key]
|
25
|
+
end
|
26
|
+
|
27
|
+
doc.at_css('visibility > code').content = options[:visibility] || options[:visability] # backward-compatible typo fix
|
28
|
+
doc.to_xml
|
29
|
+
end
|
30
|
+
|
31
|
+
def comment_to_xml(comment)
|
32
|
+
doc = Nokogiri.XML('<update-comment><comment/><update-comment/>')
|
33
|
+
doc.encoding = 'UTF-8'
|
34
|
+
doc.at_css('comment').content = comment
|
35
|
+
doc.to_xml
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_liked_to_xml(is_liked)
|
39
|
+
doc = Nokogiri.XML('<is-liked/>')
|
40
|
+
doc.encoding = 'UTF-8'
|
41
|
+
doc.at_css('is-liked').content = is_liked
|
42
|
+
doc.to_xml
|
43
|
+
end
|
44
|
+
|
45
|
+
def network_update_to_xml(message)
|
46
|
+
doc = Nokogiri::XML('<activity locale="en_US"><content-type>linkedin-html</content-type><body/></activity>')
|
47
|
+
doc.encoding = 'UTF-8'
|
48
|
+
doc.at_css('body').content = message
|
49
|
+
doc.to_xml
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class TwitterAccount < LinkedIn::Base
|
3
|
+
|
4
|
+
def twitter_accounts
|
5
|
+
@twitter_accounts ||= @doc.children.inject([]) do |list, twitter_account|
|
6
|
+
list << Resource.new(twitter_account) unless twitter_account.blank?
|
7
|
+
list
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Resource
|
12
|
+
|
13
|
+
def initialize(twitter_account)
|
14
|
+
@twitter_account = twitter_account
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@twitter_account.xpath("./provider-account-name").text
|
19
|
+
end
|
20
|
+
|
21
|
+
def id
|
22
|
+
@twitter_account.xpath("./provider-account-id").text
|
23
|
+
end
|
24
|
+
|
25
|
+
end # resource class
|
26
|
+
|
27
|
+
end # skill class
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class Update < LinkedIn::Base
|
3
|
+
|
4
|
+
%w[update_key update_type is_commentable?].each do |f|
|
5
|
+
define_method(f.to_sym) do
|
6
|
+
@doc.xpath("./update/#{f.gsub(/_/,'-').gsub(/\?$/,"")}").text
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def timestamp
|
11
|
+
@doc.xpath('./update/timestamp').text.to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
def profile
|
15
|
+
Profile.new(Nokogiri::XML(@doc.xpath('./update/update-content/person').to_xml))
|
16
|
+
end
|
17
|
+
|
18
|
+
def likes
|
19
|
+
Likes.new(Nokogiri::XML(@doc.xpath('./update/likes').to_xml)).likes
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module LinkedIn
|
2
|
+
class UrlResource < LinkedIn::Base
|
3
|
+
|
4
|
+
def resources
|
5
|
+
@resources ||= @doc.children.inject([]) do |list, url|
|
6
|
+
list << Resource.new(url) unless url.blank?
|
7
|
+
list
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Resource
|
12
|
+
|
13
|
+
def initialize(member_url)
|
14
|
+
@member_url = member_url
|
15
|
+
end
|
16
|
+
|
17
|
+
%w[url name].each do |f|
|
18
|
+
define_method(f.to_sym) do
|
19
|
+
@member_url.xpath("./#{f.gsub(/_/,'-')}").text
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end # resource class
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|