diff --git a/.document b/.document deleted file mode 100644 index ecf36731..00000000 --- a/.document +++ /dev/null @@ -1,5 +0,0 @@ -README.rdoc -lib/**/*.rb -bin/* -features/**/*.feature -LICENSE diff --git a/.yardopts b/.yardopts new file mode 100644 index 00000000..31f7f7ad --- /dev/null +++ b/.yardopts @@ -0,0 +1,7 @@ +--no-private +--markup markdown +- +README.md +CHANGELOG.md +LICENSE +EXAMPLES.md diff --git a/changelog.markdown b/CHANGELOG.md similarity index 93% rename from changelog.markdown rename to CHANGELOG.md index 41d1c9fc..2f76fda2 100644 --- a/changelog.markdown +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +* Document all methods +* Re-organize modules under Api to match organization in LinkedIn's REST + API documentation + ## 0.4.4 - Jan 11, 2014 * Group share add @@ -92,4 +96,4 @@ ## 0.0.1 - November 24, 2009 -* Initial release \ No newline at end of file +* Initial release diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 00000000..1ed9bdc5 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,198 @@ +# Linkedin Gem Examples + +## OAuth 1.0a Authentication + +Here's an example of authenticating with the LinkedIn API + +```ruby +require 'rubygems' +require 'linkedin' + +# get your api keys at https://www.linkedin.com/secure/developer +client = LinkedIn::Client.new('your_consumer_key', 'your_consumer_secret') + +# If you want to use one of the scopes from linkedin you have to pass it in at this point +# You can learn more about it here: http://developer.linkedin.com/documents/authentication +request_token = client.request_token({}, :scope => "r_basicprofile+r_emailaddress") + +rtoken = request_token.token +rsecret = request_token.secret + +# to test from your desktop, open the following url in your browser +# and record the pin it gives you +request_token.authorize_url +=> "https://api.linkedin.com/uas/oauth/authorize?oauth_token=" + +# then fetch your access keys +client.authorize_from_request(rtoken, rsecret, pin) +=> ["OU812", "8675309"] # <= save these for future requests + +# or authorize from previously fetched access keys +c.authorize_from_access("OU812", "8675309") + +# you're now free to move about the cabin, call any API method +``` + + +## Profile + +Here are some examples of accessing a user's profile + +```ruby +# AUTHENTICATE FIRST found in examples/authenticate.rb + +# client is a LinkedIn::Client + +# get the profile for the authenticated user +client.profile + +# get a profile for someone found in network via ID +client.profile(:id => 'gNma67_AdI') + +# get a profile for someone via their public profile url +client.profile(:url => 'http://www.linkedin.com/in/netherland') + +# provides the ability to access authenticated user's company field in the profile +user = client.profile(:fields => %w(positions)) +companies = user.positions.all.map{|t| t.company} +# Example: most recent company can be accessed via companies[0] + +# Example of a multi-email search against the special email search API +account_exists = client.profile(:email => 'email=yy@zz.com,email=xx@yy.com', :fields => ['id']) +``` + + +## Sending a Message + +Here's an example of sending a message to two recipients + +```ruby +# AUTHENTICATE FIRST found in examples/authenticate.md + +# client is a LinkedIn::Client + +# send a message to a person in your network. you will need to authenticate the +# user and ask for the "w_messages" permission. +response = client.send_message("subject", "body", ["person_1_id", "person_2_id"]) +``` + + +## User's Network + +Here are some examples of accessing network updates and connections of +the authenticated user + +``` ruby +# AUTHENTICATE FIRST found in examples/authenticate.rb + +# client is a LinkedIn::Client + +# get network updates for the authenticated user +client.network_updates + +# get profile picture changes +client.network_updates(:type => 'PICT') + +# view connections for the currently authenticated user +client.connections +``` + + +## Update User's Status + +Here's an example of updating the current user's status + +```ruby +# AUTHENTICATE FIRST found in examples/authenticate.rb + +# client is a LinkedIn::Client + +# update status for the authenticated user +client.add_share(:comment => 'is playing with the LinkedIn Ruby gem') +``` + + +## Sinatra App + +Here's an example sinatra application that performs authentication, +after which some info about the authenticated user can be retrieved. + +```ruby +require "rubygems" +require "haml" +require "sinatra" +require "linkedin" + +enable :sessions + +helpers do + def login? + !session[:atoken].nil? + end + + def profile + linkedin_client.profile unless session[:atoken].nil? + end + + def connections + linkedin_client.connections unless session[:atoken].nil? + end + + private + def linkedin_client + client = LinkedIn::Client.new(settings.api, settings.secret) + client.authorize_from_access(session[:atoken], session[:asecret]) + client + end + +end + +configure do + # get your api keys at https://www.linkedin.com/secure/developer + set :api, "your_api_key" + set :secret, "your_secret" +end + +get "/" do + haml :index +end + +get "/auth" do + client = LinkedIn::Client.new(settings.api, settings.secret) + request_token = client.request_token(:oauth_callback => "http://#{request.host}:#{request.port}/auth/callback") + session[:rtoken] = request_token.token + session[:rsecret] = request_token.secret + + redirect client.request_token.authorize_url +end + +get "/auth/logout" do + session[:atoken] = nil + redirect "/" +end + +get "/auth/callback" do + client = LinkedIn::Client.new(settings.api, settings.secret) + if session[:atoken].nil? + pin = params[:oauth_verifier] + atoken, asecret = client.authorize_from_request(session[:rtoken], session[:rsecret], pin) + session[:atoken] = atoken + session[:asecret] = asecret + end + redirect "/" +end + + +__END__ +@@index +-if login? + %p Welcome #{profile.first_name}! + %a{:href => "/auth/logout"} Logout + %p= profile.headline + %br + %div= "You have #{connections.total} connections!" + -connections.all.each do |c| + %div= "#{c.first_name} #{c.last_name} - #{c.headline}" +-else + %a{:href => "/auth"} Login using LinkedIn +``` diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 00ba17d0..00000000 --- a/README.markdown +++ /dev/null @@ -1,84 +0,0 @@ -# LinkedIn - -Ruby wrapper for the [LinkedIn API](http://developer.linkedin.com). The LinkedIn gem provides an easy-to-use wrapper for LinkedIn's Oauth/XML APIs. - -Travis CI : [![Build Status](https://secure.travis-ci.org/hexgnu/linkedin.png)](http://travis-ci.org/hexgnu/linkedin) - -## Installation - - gem install linkedin - -## Usage - -### Authenticate - -LinkedIn's API uses Oauth for authentication. Luckily, the LinkedIn gem hides most of the gory details from you. - -```ruby -require 'linkedin' - -# get your api keys at https://www.linkedin.com/secure/developer -client = LinkedIn::Client.new('your_consumer_key', 'your_consumer_secret') - -# If you want to use one of the scopes from linkedin you have to pass it in at this point -# You can learn more about it here: http://developer.linkedin.com/documents/authentication -request_token = client.request_token({}, :scope => "r_basicprofile+r_emailaddress") - -rtoken = request_token.token -rsecret = request_token.secret - -# to test from your desktop, open the following url in your browser -# and record the pin it gives you -request_token.authorize_url -=> "https://api.linkedin.com/uas/oauth/authorize?oauth_token=" - -# then fetch your access keys -client.authorize_from_request(rtoken, rsecret, pin) -=> ["OU812", "8675309"] # <= save these for future requests - -# or authorize from previously fetched access keys -c.authorize_from_access("OU812", "8675309") - -# you're now free to move about the cabin, call any API method -``` - -### Profile examples -```ruby -# get the profile for the authenticated user -client.profile - -# get a profile for someone found in network via ID -client.profile(:id => 'gNma67_AdI') - -# get a profile for someone via their public profile url -client.profile(:url => 'http://www.linkedin.com/in/netherland') -``` - -More examples in the [examples folder](http://github.com/pengwynn/linkedin/blob/master/examples). - -For a nice example on using this in a [Rails App](http://pivotallabs.com/users/will/blog/articles/1096-linkedin-gem-for-a-web-app). - -If you want to play with the LinkedIn api without using the gem, have a look at the [apigee LinkedIn console](http://app.apigee.com/console/linkedin). - -## TODO - -* Change to json api -* Update and correct test suite -* Change to Faraday for authentication -* Implement Messaging APIs - -## Note on Patches/Pull Requests - -* Fork the project. -* Make your feature addition or bug fix. -* Add tests for it. This is important so I don't break it in a - future version unintentionally. -* Make sure your test doesn't just check of instance of LinkedIn::Mash :smile:. -* Commit, do not mess with rakefile, version, or history. - (if you want to have your own version, that is fine but - bump version in a commit by itself I can ignore when I pull) -* Send me a pull request. Bonus points for topic branches. - -## Copyright - -Copyright (c) 2013-Present [Matt Kirk](http://matthewkirk.com) 2009-11 [Wynn Netherland](http://wynnnetherland.com). See LICENSE for details. diff --git a/README.md b/README.md new file mode 100644 index 00000000..7c1e3388 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# LinkedIn + +Ruby wrapper for the [LinkedIn API](http://developer.linkedin.com). The LinkedIn gem provides an easy-to-use wrapper for LinkedIn's REST APIs. + +Travis CI : [![Build Status](https://secure.travis-ci.org/hexgnu/linkedin.png)](http://travis-ci.org/hexgnu/linkedin) + +## Installation + + gem install linkedin + +## Documentation + +[http://rdoc.info/gems/linkedin](http://rdoc.info/gems/linkedin) + +## Usage + +[View the Examples](EXAMPLES.md) + +## Changelog + +[View the Changelog](CHANGELOG.md) + +## TODO + +* Update and correct test suite +* Change to Faraday for authentication +* Implement Messaging APIs + +## Note on Patches/Pull Requests + +* Fork the project. +* Make your feature addition or bug fix. +* Add tests for it. This is important so I don't break it in a + future version unintentionally. +* Make sure your test doesn't just check of instance of LinkedIn::Mash :smile:. +* Commit, do not mess with rakefile, version, or history. + (if you want to have your own version, that is fine but + bump version in a commit by itself I can ignore when I pull) +* Send me a pull request. Bonus points for topic branches. + +## Copyright + +Copyright (c) 2013-Present [Matt Kirk](http://matthewkirk.com) 2009-11 [Wynn Netherland](http://wynnnetherland.com). See LICENSE for details. diff --git a/Rakefile b/Rakefile index ab591f4c..f8197d6b 100755 --- a/Rakefile +++ b/Rakefile @@ -10,11 +10,6 @@ task :test => :spec task :default => :spec load 'vcr/tasks/vcr.rake' -require 'rdoc/task' require File.expand_path('../lib/linked_in/version', __FILE__) -RDoc::Task.new do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "linkedin #{LinkedIn::VERSION::STRING}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end +require 'yard' +YARD::Rake::YardocTask.new diff --git a/examples/authenticate.rb b/examples/authenticate.rb deleted file mode 100644 index 531dfba1..00000000 --- a/examples/authenticate.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'rubygems' -require 'linkedin' - -# get your api keys at https://www.linkedin.com/secure/developer -client = LinkedIn::Client.new('your_consumer_key', 'your_consumer_secret') - -# If you want to use one of the scopes from linkedin you have to pass it in at this point -# You can learn more about it here: http://developer.linkedin.com/documents/authentication -request_token = client.request_token({}, :scope => "r_basicprofile+r_emailaddress") - -rtoken = request_token.token -rsecret = request_token.secret - -# to test from your desktop, open the following url in your browser -# and record the pin it gives you -request_token.authorize_url -=> "https://api.linkedin.com/uas/oauth/authorize?oauth_token=" - -# then fetch your access keys -client.authorize_from_request(rtoken, rsecret, pin) -=> ["OU812", "8675309"] # <= save these for future requests - -# or authorize from previously fetched access keys -c.authorize_from_access("OU812", "8675309") - -# you're now free to move about the cabin, call any API method diff --git a/examples/communication.rb b/examples/communication.rb deleted file mode 100644 index 4c8984ab..00000000 --- a/examples/communication.rb +++ /dev/null @@ -1,7 +0,0 @@ -# AUTHENTICATE FIRST found in examples/authenticate.rb - -# client is a LinkedIn::Client - -# send a message to a person in your network. you will need to authenticate the -# user and ask for the "w_messages" permission. -response = client.send_message("subject", "body", ["person_1_id", "person_2_id"]) \ No newline at end of file diff --git a/examples/network.rb b/examples/network.rb deleted file mode 100644 index 2d19ae70..00000000 --- a/examples/network.rb +++ /dev/null @@ -1,12 +0,0 @@ -# AUTHENTICATE FIRST found in examples/authenticate.rb - -# client is a LinkedIn::Client - -# get network updates for the authenticated user -client.network_updates - -# get profile picture changes -client.network_updates(:type => 'PICT') - -# view connections for the currently authenticated user -client.connections \ No newline at end of file diff --git a/examples/profile.rb b/examples/profile.rb deleted file mode 100644 index 14ce65b1..00000000 --- a/examples/profile.rb +++ /dev/null @@ -1,21 +0,0 @@ -# AUTHENTICATE FIRST found in examples/authenticate.rb - -# client is a LinkedIn::Client - -# get the profile for the authenticated user -client.profile - -# get a profile for someone found in network via ID -client.profile(:id => 'gNma67_AdI') - -# get a profile for someone via their public profile url -client.profile(:url => 'http://www.linkedin.com/in/netherland') - -# provides the ability to access authenticated user's company field in the profile -user = client.profile(:fields => %w(positions)) -companies = user.positions.all.map{|t| t.company} -# Example: most recent company can be accessed via companies[0] - -# Example of a multi-email search against the special email search API -account_exists = client.profile(:email => 'email=yy@zz.com,email=xx@yy.com', :fields => ['id']) - diff --git a/examples/sinatra.rb b/examples/sinatra.rb deleted file mode 100644 index e4780d04..00000000 --- a/examples/sinatra.rb +++ /dev/null @@ -1,77 +0,0 @@ -require "rubygems" -require "haml" -require "sinatra" -require "linkedin" - -enable :sessions - -helpers do - def login? - !session[:atoken].nil? - end - - def profile - linkedin_client.profile unless session[:atoken].nil? - end - - def connections - linkedin_client.connections unless session[:atoken].nil? - end - - private - def linkedin_client - client = LinkedIn::Client.new(settings.api, settings.secret) - client.authorize_from_access(session[:atoken], session[:asecret]) - client - end - -end - -configure do - # get your api keys at https://www.linkedin.com/secure/developer - set :api, "your_api_key" - set :secret, "your_secret" -end - -get "/" do - haml :index -end - -get "/auth" do - client = LinkedIn::Client.new(settings.api, settings.secret) - request_token = client.request_token(:oauth_callback => "http://#{request.host}:#{request.port}/auth/callback") - session[:rtoken] = request_token.token - session[:rsecret] = request_token.secret - - redirect client.request_token.authorize_url -end - -get "/auth/logout" do - session[:atoken] = nil - redirect "/" -end - -get "/auth/callback" do - client = LinkedIn::Client.new(settings.api, settings.secret) - if session[:atoken].nil? - pin = params[:oauth_verifier] - atoken, asecret = client.authorize_from_request(session[:rtoken], session[:rsecret], pin) - session[:atoken] = atoken - session[:asecret] = asecret - end - redirect "/" -end - - -__END__ -@@index --if login? - %p Welcome #{profile.first_name}! - %a{:href => "/auth/logout"} Logout - %p= profile.headline - %br - %div= "You have #{connections.total} connections!" - -connections.all.each do |c| - %div= "#{c.first_name} #{c.last_name} - #{c.headline}" --else - %a{:href => "/auth"} Login using LinkedIn diff --git a/examples/status.rb b/examples/status.rb deleted file mode 100644 index 457ebe4a..00000000 --- a/examples/status.rb +++ /dev/null @@ -1,6 +0,0 @@ -# AUTHENTICATE FIRST found in examples/authenticate.rb - -# client is a LinkedIn::Client - -# update status for the authenticated user -client.add_share(:comment => 'is playing with the LinkedIn Ruby gem') diff --git a/lib/linked_in/api.rb b/lib/linked_in/api.rb index cb1b0dcd..84aaf432 100644 --- a/lib/linked_in/api.rb +++ b/lib/linked_in/api.rb @@ -1,6 +1,38 @@ module LinkedIn module Api - autoload :QueryMethods, "linked_in/api/query_methods" - autoload :UpdateMethods, "linked_in/api/update_methods" + + # @!macro person_path_options + # @param [Hash] options identifies the user profile you want + # @option options [String] :id a member token + # @option options [String] :url a Public Profile URL + # @option options [String] :email + + # @!macro company_path_options + # @param [Hash] options identifies the user profile you want + # @option options [String] :domain company email domain + # @option options [String] :id company ID + # @option options [String] :url + # @option options [String] :name company universal name + # @option options [String] :is_admin list all companies that the + # authenticated is an administrator of + + # @!macro share_input_fields + # @param [Hash] share content of the share + # @option share [String] :comment + # @option share [String] :content + # @option share [String] :title + # @option share [String] :submitted-url + # @option share [String] :submitted-image-url + # @option share [String] :description + # @option share [String] :visibility + # @option share [String] :code + + autoload :QueryHelpers, "linked_in/api/query_helpers" + autoload :People, "linked_in/api/people" + autoload :Groups, "linked_in/api/groups" + autoload :Companies, "linked_in/api/companies" + autoload :Jobs, "linked_in/api/jobs" + autoload :ShareAndSocialStream, "linked_in/api/share_and_social_stream" + autoload :Communications, "linked_in/api/communications" end end diff --git a/lib/linked_in/api/communications.rb b/lib/linked_in/api/communications.rb new file mode 100644 index 00000000..8e67f692 --- /dev/null +++ b/lib/linked_in/api/communications.rb @@ -0,0 +1,44 @@ +module LinkedIn + module Api + + # Communications APIs + # + # @see http://developer.linkedin.com/documents/communications + module Communications + + # (Create) send a message from the authenticated user to a + # connection + # + # Permissions: w_messages + # + # @see http://developer.linkedin.com/documents/messaging-between-connections-api + # @see http://developer.linkedin.com/documents/invitation-api Invitation API + # + # @example + # client.send_message("subject", "body", ["person_1_id", "person_2_id"]) + # + # @param [String] subject Subject of the message + # @param [String] body Body of the message, plain text only + # @param [Array] recipient_paths a collection of + # profile paths that identify the users who will receive the + # message + # @return [void] + def send_message(subject, body, recipient_paths) + path = "/people/~/mailbox" + + message = { + 'subject' => subject, + 'body' => body, + 'recipients' => { + 'values' => recipient_paths.map do |profile_path| + { 'person' => { '_path' => "/people/#{profile_path}" } } + end + } + } + post(path, message.to_json, "Content-Type" => "application/json") + end + + end + + end +end diff --git a/lib/linked_in/api/companies.rb b/lib/linked_in/api/companies.rb new file mode 100644 index 00000000..eec67e77 --- /dev/null +++ b/lib/linked_in/api/companies.rb @@ -0,0 +1,128 @@ +module LinkedIn + module Api + + # Companies API + # + # @see http://developer.linkedin.com/documents/companies Companies API + # @see http://developer.linkedin.com/documents/company-lookup-api-and-fields Company Fields + # + # The following API actions do not have corresponding methods in + # this module + # + # * Permissions Checking Endpoints for Company Shares + # * GET Suggested Companies to Follow + # * GET Company Products + # + # [(contribute here)](https://github.com/hexgnu/linkedin) + module Companies + + # Retrieve a Company Profile + # + # @see http://developer.linkedin.com/documents/company-lookup-api-and-fields + # + # @macro company_path_options + # @option options [String] :scope + # @option options [String] :type + # @option options [String] :count + # @option options [String] :start + # @return [LinkedIn::Mash] + def company(options = {}) + path = company_path(options) + simple_query(path, options) + end + + # Retrieve a feed of event items for a Company + # + # @see http://developer.linkedin.com/reading-company-shares + # + # @macro company_path_options + # @option options [String] :event-type + # @option options [String] :count + # @option options [String] :start + # @return [LinkedIn::Mash] + def company_updates(options={}) + path = "#{company_path(options)}/updates" + simple_query(path, options) + end + + # Retrieve statistics for a particular company page + # + # Permissions: rw_company_admin + # + # @see http://developer.linkedin.com/documents/company-statistics + # + # @macro company_path_options + # @return [LinkedIn::Mash] + def company_statistics(options={}) + path = "#{company_path(options)}/company-statistics" + simple_query(path, options) + end + + # Retrieve comments on a particular company update: + # + # @see http://developer.linkedin.com/reading-company-shares + # + # @param [String] update_key a update/update-key representing a + # particular company update + # @macro company_path_options + # @return [LinkedIn::Mash] + def company_updates_comments(update_key, options={}) + path = "#{company_path(options)}/updates/key=#{update_key}/update-comments" + simple_query(path, options) + end + + # Retrieve likes on a particular company update: + # + # @see http://developer.linkedin.com/reading-company-shares + # + # @param [String] update_key a update/update-key representing a + # particular company update + # @macro company_path_options + # @return [LinkedIn::Mash] + def company_updates_likes(update_key, options={}) + path = "#{company_path(options)}/updates/key=#{update_key}/likes" + simple_query(path, options) + end + + # Create a share for a company that the authenticated user + # administers + # + # Permissions: rw_company_admin + # + # @see http://developer.linkedin.com/creating-company-shares + # @see http://developer.linkedin.com/documents/targeting-company-shares Targeting Company Shares + # + # @param [String] company_id Company ID + # @macro share_input_fields + # @return [void] + def add_company_share(company_id, share) + path = "/companies/#{company_id}/shares" + defaults = {:visibility => {:code => "anyone"}} + post(path, defaults.merge(share).to_json, "Content-Type" => "application/json") + end + + # (Create) authenticated user starts following a company + # + # @see http://developer.linkedin.com/documents/company-follow-and-suggestions + # + # @param [String] company_id Company ID + # @return [void] + def follow_company(company_id) + path = "/people/~/following/companies" + body = {:id => company_id } + post(path, body.to_json, "Content-Type" => "application/json") + end + + # (Destroy) authenticated user stops following a company + # + # @see http://developer.linkedin.com/documents/company-follow-and-suggestions + # + # @param [String] company_id Company ID + # @return [void] + def unfollow_company(company_id) + path = "/people/~/following/companies/id=#{company_id}" + delete(path) + end + end + end +end diff --git a/lib/linked_in/api/groups.rb b/lib/linked_in/api/groups.rb new file mode 100644 index 00000000..d0656ee4 --- /dev/null +++ b/lib/linked_in/api/groups.rb @@ -0,0 +1,115 @@ +module LinkedIn + module Api + + # Groups API + # + # @see http://developer.linkedin.com/documents/groups-api Groups API + # @see http://developer.linkedin.com/documents/groups-fields Groups Fields + # + # The following API actions do not have corresponding methods in + # this module + # + # * PUT Change my Group Settings + # * POST Change my Group Settings + # * DELETE Leave a group + # * PUT Follow/unfollow a Group post + # * PUT Flag a Post as a Promotion or Job + # * DELETE Delete a Post + # * DELETE Flag a post as inappropriate + # * DELETE A comment or flag comment as inappropriate + # * DELETE Remove a Group Suggestion + # + # [(contribute here)](https://github.com/hexgnu/linkedin) + module Groups + + # Retrieve group suggestions for the current user + # + # Permissions: r_fullprofile + # + # @see http://developer.linkedin.com/documents/job-bookmarks-and-suggestions + # + # @macro person_path_options + # @return [LinkedIn::Mash] + def group_suggestions(options = {}) + path = "#{person_path(options)}/suggestions/groups" + simple_query(path, options) + end + + # Retrieve the groups a current user belongs to + # + # Permissions: rw_groups + # + # @see http://developer.linkedin.com/documents/groups-api + # + # @macro person_path_options + # @return [LinkedIn::Mash] + def group_memberships(options = {}) + path = "#{person_path(options)}/group-memberships" + simple_query(path, options) + end + + # Retrieve the profile of a group + # + # Permissions: rw_groups + # + # @see http://developer.linkedin.com/documents/groups-api + # + # @param [Hash] options identifies the group or groups + # @optio options [String] :id identifier for the group + # @return [LinkedIn::Mash] + def group_profile(options) + path = group_path(options) + simple_query(path, options) + end + + # Retrieve the posts in a group + # + # Permissions: rw_groups + # + # @see http://developer.linkedin.com/documents/groups-api + # + # @param [Hash] options identifies the group or groups + # @optio options [String] :id identifier for the group + # @optio options [String] :count + # @optio options [String] :start + # @return [LinkedIn::Mash] + def group_posts(options) + path = "#{group_path(options)}/posts" + simple_query(path, options) + end + + # @deprecated Use {#add_group_share} instead + def post_group_discussion(group_id, discussion) + warn 'Use add_group_share over post_group_discussion. This will be taken out in future versions' + add_group_share(group_id, discussion) + end + + # Create a share for a company that the authenticated user + # administers + # + # Permissions: rw_groups + # + # @see http://developer.linkedin.com/documents/groups-api#create + # + # @param [String] group_id Group ID + # @macro share_input_fields + # @return [void] + def add_group_share(group_id, share) + path = "/groups/#{group_id}/posts" + post(path, share.to_json, "Content-Type" => "application/json") + end + + # (Update) User joins, or requests to join, a group + # + # @see http://developer.linkedin.com/documents/groups-api#membergroups + # + # @param [String] group_id Group ID + # @return [void] + def join_group(group_id) + path = "/people/~/group-memberships/#{group_id}" + body = {'membership-state' => {'code' => 'member' }} + put(path, body.to_json, "Content-Type" => "application/json") + end + end + end +end diff --git a/lib/linked_in/api/jobs.rb b/lib/linked_in/api/jobs.rb new file mode 100644 index 00000000..50024193 --- /dev/null +++ b/lib/linked_in/api/jobs.rb @@ -0,0 +1,64 @@ +module LinkedIn + module Api + + # Jobs API + # + # @see http://developer.linkedin.com/documents/job-lookup-api-and-fields Job Lookup API and Fields + # @see http://developer.linkedin.com/documents/job-bookmarks-and-suggestions Job Bookmarks and Suggestions + # + # The following API actions do not have corresponding methods in + # this module + # + # * DELETE a Job Bookmark + # + # [(contribute here)](https://github.com/hexgnu/linkedin) + module Jobs + + # Retrieve likes on a particular company update: + # + # @see http://developer.linkedin.com/reading-company-shares + # + # @param [Hash] options identifies the job + # @option options [String] id unique identifier for a job + # @return [LinkedIn::Mash] + def job(options = {}) + path = jobs_path(options) + simple_query(path, options) + end + + # Retrieve the current members' job bookmarks + # + # @see http://developer.linkedin.com/documents/job-bookmarks-and-suggestions + # + # @macro person_path_options + # @return [LinkedIn::Mash] + def job_bookmarks(options = {}) + path = "#{person_path(options)}/job-bookmarks" + simple_query(path, options) + end + + # Retrieve job suggestions for the current user + # + # @see http://developer.linkedin.com/documents/job-bookmarks-and-suggestions + # + # @macro person_path_options + # @return [LinkedIn::Mash] + def job_suggestions(options = {}) + path = "#{person_path(options)}/suggestions/job-suggestions" + simple_query(path, options) + end + + # Create a job bookmark for the authenticated user + # + # @see http://developer.linkedin.com/documents/job-bookmarks-and-suggestions + # + # @param [String] job_id Job ID + # @return [void] + def add_job_bookmark(job_id) + path = "/people/~/job-bookmarks" + body = {'job' => {'id' => job_id}} + post(path, body.to_json, "Content-Type" => "application/json") + end + end + end +end diff --git a/lib/linked_in/api/people.rb b/lib/linked_in/api/people.rb new file mode 100644 index 00000000..21b50291 --- /dev/null +++ b/lib/linked_in/api/people.rb @@ -0,0 +1,65 @@ +module LinkedIn + module Api + + # People APIs + # + # @see http://developer.linkedin.com/documents/people People API + # @see http://developer.linkedin.com/documents/profile-fields Profile Fields + # @see http://developer.linkedin.com/documents/field-selectors Field Selectors + # @see http://developer.linkedin.com/documents/accessing-out-network-profiles Accessing Out of Network Profiles + module People + + # Retrieve a member's LinkedIn profile. + # + # Permissions: r_basicprofile, r_fullprofile + # + # @see http://developer.linkedin.com/documents/profile-api + # @macro person_path_options + # @option options [string] :secure-urls if 'true' URLs in responses will be HTTPS + # @return [LinkedIn::Mash] + def profile(options={}) + path = person_path(options) + simple_query(path, options) + end + + # Retrieve a list of 1st degree connections for a user who has + # granted access to his/her account + # + # Permissions: r_network + # + # @see http://developer.linkedin.com/documents/connections-api + # + # @macro person_path_options + # @return [LinkedIn::Mash] + def connections(options={}) + path = "#{person_path(options)}/connections" + simple_query(path, options) + end + + # Retrieve a list of the latest set of 1st degree connections for a + # user + # + # Permissions: r_network + # + # @see http://developer.linkedin.com/documents/connections-api + # + # @param [String] modified_since timestamp indicating since when + # you want to retrieve new connections + # @macro person_path_options + # @return [LinkedIn::Mash] + def new_connections(modified_since, options={}) + options.merge!('modified' => 'new', 'modified-since' => modified_since) + path = "#{person_path(options)}/connections" + simple_query(path, options) + end + + # TODO can't find this method in the REST API documentation and it + # doesn't seem to work when I tried it out from the command line + def picture_urls(options={}) + picture_size = options.delete(:picture_size) || 'original' + path = "#{picture_urls_path(options)}::(#{picture_size})" + simple_query(path, options) + end + end + end +end diff --git a/lib/linked_in/api/query_helpers.rb b/lib/linked_in/api/query_helpers.rb new file mode 100644 index 00000000..93b36385 --- /dev/null +++ b/lib/linked_in/api/query_helpers.rb @@ -0,0 +1,86 @@ +module LinkedIn + module Api + + module QueryHelpers + private + + def group_path(options) + path = "/groups" + if id = options.delete(:id) + path += "/#{id}" + end + end + + def simple_query(path, options={}) + fields = options.delete(:fields) || LinkedIn.default_profile_fields + + if options.delete(:public) + path +=":public" + elsif fields + path +=":(#{build_fields_params(fields)})" + end + + headers = options.delete(:headers) || {} + params = to_query(options) + path += "#{path.include?("?") ? "&" : "?"}#{params}" if !params.empty? + + Mash.from_json(get(path, headers)) + end + + def build_fields_params(fields) + if fields.is_a?(Hash) && !fields.empty? + fields.map {|index,value| "#{index}:(#{build_fields_params(value)})" }.join(',') + elsif fields.respond_to?(:each) + fields.map {|field| build_fields_params(field) }.join(',') + else + fields.to_s.gsub("_", "-") + end + end + + def person_path(options) + path = "/people" + if id = options.delete(:id) + path += "/id=#{id}" + elsif url = options.delete(:url) + path += "/url=#{CGI.escape(url)}" + elsif email = options.delete(:email) + path += "::(#{email})" + else + path += "/~" + end + end + + def company_path(options) + path = "/companies" + + if domain = options.delete(:domain) + path += "?email-domain=#{CGI.escape(domain)}" + elsif id = options.delete(:id) + path += "/id=#{id}" + elsif url = options.delete(:url) + path += "/url=#{CGI.escape(url)}" + elsif name = options.delete(:name) + path += "/universal-name=#{CGI.escape(name)}" + elsif is_admin = options.delete(:is_admin) + path += "?is-company-admin=#{CGI.escape(is_admin)}" + else + path += "/~" + end + end + + def picture_urls_path(options) + path = person_path(options) + path += "/picture-urls" + end + + def jobs_path(options) + path = "/jobs" + if id = options.delete(:id) + path += "/id=#{id}" + else + path += "/~" + end + end + end + end +end diff --git a/lib/linked_in/api/query_methods.rb b/lib/linked_in/api/query_methods.rb deleted file mode 100644 index 88a61283..00000000 --- a/lib/linked_in/api/query_methods.rb +++ /dev/null @@ -1,183 +0,0 @@ -module LinkedIn - module Api - - module QueryMethods - - def profile(options={}) - path = person_path(options) - simple_query(path, options) - end - - def connections(options={}) - path = "#{person_path(options)}/connections" - simple_query(path, options) - end - - def network_updates(options={}) - path = "#{person_path(options)}/network/updates" - simple_query(path, options) - end - - def company(options = {}) - path = company_path(options) - simple_query(path, options) - end - - def company_updates(options={}) - path = "#{company_path(options)}/updates" - simple_query(path, options) - end - - def company_statistics(options={}) - path = "#{company_path(options)}/company-statistics" - simple_query(path, options) - end - - def company_updates_comments(update_key, options={}) - path = "#{company_path(options)}/updates/key=#{update_key}/update-comments" - simple_query(path, options) - end - - def company_updates_likes(update_key, options={}) - path = "#{company_path(options)}/updates/key=#{update_key}/likes" - simple_query(path, options) - end - - def job(options = {}) - path = jobs_path(options) - simple_query(path, options) - end - - def job_bookmarks(options = {}) - path = "#{person_path(options)}/job-bookmarks" - simple_query(path, options) - end - - def job_suggestions(options = {}) - path = "#{person_path(options)}/suggestions/job-suggestions" - simple_query(path, options) - end - - def group_suggestions(options = {}) - path = "#{person_path(options)}/suggestions/groups" - simple_query(path, options) - end - - def group_memberships(options = {}) - path = "#{person_path(options)}/group-memberships" - simple_query(path, options) - end - - def group_profile(options) - path = group_path(options) - simple_query(path, options) - end - - def group_posts(options) - path = "#{group_path(options)}/posts" - simple_query(path, options) - end - - def shares(options={}) - path = "#{person_path(options)}/network/updates" - simple_query(path, {:type => "SHAR", :scope => "self"}.merge(options)) - end - - def share_comments(update_key, options={}) - path = "#{person_path(options)}/network/updates/key=#{update_key}/update-comments" - simple_query(path, options) - end - - def share_likes(update_key, options={}) - path = "#{person_path(options)}/network/updates/key=#{update_key}/likes" - simple_query(path, options) - end - - def picture_urls(options={}) - picture_size = options.delete(:picture_size) || 'original' - path = "#{picture_urls_path(options)}::(#{picture_size})" - simple_query(path, options) - end - - private - - def group_path(options) - path = "/groups" - if id = options.delete(:id) - path += "/#{id}" - end - end - - def simple_query(path, options={}) - fields = options.delete(:fields) || LinkedIn.default_profile_fields - - if options.delete(:public) - path +=":public" - elsif fields - path +=":(#{build_fields_params(fields)})" - end - - headers = options.delete(:headers) || {} - params = to_query(options) - path += "#{path.include?("?") ? "&" : "?"}#{params}" if !params.empty? - - Mash.from_json(get(path, headers)) - end - - def build_fields_params(fields) - if fields.is_a?(Hash) && !fields.empty? - fields.map {|index,value| "#{index}:(#{build_fields_params(value)})" }.join(',') - elsif fields.respond_to?(:each) - fields.map {|field| build_fields_params(field) }.join(',') - else - fields.to_s.gsub("_", "-") - end - end - - def person_path(options) - path = "/people" - if id = options.delete(:id) - path += "/id=#{id}" - elsif url = options.delete(:url) - path += "/url=#{CGI.escape(url)}" - elsif email = options.delete(:email) - path += "::(#{email})" - else - path += "/~" - end - end - - def company_path(options) - path = "/companies" - - if domain = options.delete(:domain) - path += "?email-domain=#{CGI.escape(domain)}" - elsif id = options.delete(:id) - path += "/id=#{id}" - elsif url = options.delete(:url) - path += "/url=#{CGI.escape(url)}" - elsif name = options.delete(:name) - path += "/universal-name=#{CGI.escape(name)}" - elsif is_admin = options.delete(:is_admin) - path += "?is-company-admin=#{CGI.escape(is_admin)}" - else - path += "/~" - end - end - - def picture_urls_path(options) - path = person_path(options) - path += "/picture-urls" - end - - def jobs_path(options) - path = "/jobs" - if id = options.delete(:id) - path += "/id=#{id}" - else - path += "/~" - end - end - end - end -end diff --git a/lib/linked_in/api/share_and_social_stream.rb b/lib/linked_in/api/share_and_social_stream.rb new file mode 100644 index 00000000..060fe2be --- /dev/null +++ b/lib/linked_in/api/share_and_social_stream.rb @@ -0,0 +1,133 @@ +module LinkedIn + module Api + + # Share and Social Stream APIs + # + # @see http://developer.linkedin.com/documents/share-and-social-stream + # @see http://developer.linkedin.com/documents/share-api Share API + # + # The following API actions do not have corresponding methods in + # this module + # + # * GET Network Statistics + # * POST Post Network Update + # + # [(contribute here)](https://github.com/hexgnu/linkedin) + module ShareAndSocialStream + + # Retrieve the authenticated users network updates + # + # Permissions: rw_nus + # + # @see http://developer.linkedin.com/documents/get-network-updates-and-statistics-api + # @see http://developer.linkedin.com/documents/network-update-types Network Update Types + # + # @macro person_path_options + # @option options [String] :scope + # @option options [String] :type + # @option options [String] :count + # @option options [String] :start + # @option options [String] :after + # @option options [String] :before + # @option options [String] :show-hidden-members + # @return [LinkedIn::Mash] + def network_updates(options={}) + path = "#{person_path(options)}/network/updates" + simple_query(path, options) + end + + # TODO refactor to use #network_updates + def shares(options={}) + path = "#{person_path(options)}/network/updates" + simple_query(path, {:type => "SHAR", :scope => "self"}.merge(options)) + end + + # Retrieve all comments for a particular network update + # + # @note The first 5 comments are included in the response to #network_updates + # + # Permissions: rw_nus + # + # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates + # + # @param [String] update_key a update/update-key representing a + # particular network update + # @macro person_path_options + # @return [LinkedIn::Mash] + def share_comments(update_key, options={}) + path = "#{person_path(options)}/network/updates/key=#{update_key}/update-comments" + simple_query(path, options) + end + + # Retrieve all likes for a particular network update + # + # @note Some likes are included in the response to #network_updates + # + # Permissions: rw_nus + # + # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates + # + # @param [String] update_key a update/update-key representing a + # particular network update + # @macro person_path_options + # @return [LinkedIn::Mash] + def share_likes(update_key, options={}) + path = "#{person_path(options)}/network/updates/key=#{update_key}/likes" + simple_query(path, options) + end + + # Create a share for the authenticated user + # + # Permissions: rw_nus + # + # @see http://developer.linkedin.com/documents/share-api + # + # @macro share_input_fields + # @return [void] + def add_share(share) + path = "/people/~/shares" + defaults = {:visibility => {:code => "anyone"}} + post(path, defaults.merge(share).to_json, "Content-Type" => "application/json") + end + + # Create a comment on an update from the authenticated user + # + # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates + # + # @param [String] update_key a update/update-key representing a + # particular network update + # @param [String] comment The text of the comment + # @return [void] + def update_comment(update_key, comment) + path = "/people/~/network/updates/key=#{update_key}/update-comments" + body = {'comment' => comment} + post(path, body.to_json, "Content-Type" => "application/json") + end + + # (Update) like an update as the authenticated user + # + # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates + # + # @param [String] update_key a update/update-key representing a + # particular network update + # @return [void] + def like_share(update_key) + path = "/people/~/network/updates/key=#{update_key}/is-liked" + put(path, 'true', "Content-Type" => "application/json") + end + + # (Destroy) unlike an update the authenticated user previously + # liked + # + # @see http://developer.linkedin.com/documents/commenting-reading-comments-and-likes-network-updates + # + # @param [String] update_key a update/update-key representing a + # particular network update + # @return [void] + def unlike_share(update_key) + path = "/people/~/network/updates/key=#{update_key}/is-liked" + put(path, 'false', "Content-Type" => "application/json") + end + end + end +end diff --git a/lib/linked_in/api/update_methods.rb b/lib/linked_in/api/update_methods.rb deleted file mode 100644 index 83bd7089..00000000 --- a/lib/linked_in/api/update_methods.rb +++ /dev/null @@ -1,85 +0,0 @@ -module LinkedIn - module Api - - module UpdateMethods - - def add_share(share) - path = "/people/~/shares" - defaults = {:visibility => {:code => "anyone"}} - post(path, defaults.merge(share).to_json, "Content-Type" => "application/json") - end - - def add_company_share(company_id, share) - path = "/companies/#{company_id}/shares" - defaults = {:visibility => {:code => "anyone"}} - post(path, defaults.merge(share).to_json, "Content-Type" => "application/json") - end - - def post_group_discussion(group_id, discussion) - warn 'Use add_group_share over post_group_discussion. This will be taken out in future versions' - add_group_share(group_id, discussion) - end - - def add_group_share(group_id, share) - path = "/groups/#{group_id}/posts" - post(path, share.to_json, "Content-Type" => "application/json") - end - - def follow_company(company_id) - path = "/people/~/following/companies" - body = {:id => company_id } - post(path, body.to_json, "Content-Type" => "application/json") - end - - def unfollow_company(company_id) - path = "/people/~/following/companies/id=#{company_id}" - delete(path) - end - - def join_group(group_id) - path = "/people/~/group-memberships/#{group_id}" - body = {'membership-state' => {'code' => 'member' }} - put(path, body.to_json, "Content-Type" => "application/json") - end - - def add_job_bookmark(bookmark) - path = "/people/~/job-bookmarks" - body = {'job' => {'id' => bookmark}} - post(path, body.to_json, "Content-Type" => "application/json") - end - - def update_comment(network_key, comment) - path = "/people/~/network/updates/key=#{network_key}/update-comments" - body = {'comment' => comment} - post(path, body.to_json, "Content-Type" => "application/json") - end - - def like_share(network_key) - path = "/people/~/network/updates/key=#{network_key}/is-liked" - put(path, 'true', "Content-Type" => "application/json") - end - - def unlike_share(network_key) - path = "/people/~/network/updates/key=#{network_key}/is-liked" - put(path, 'false', "Content-Type" => "application/json") - end - - def send_message(subject, body, recipient_paths) - path = "/people/~/mailbox" - - message = { - 'subject' => subject, - 'body' => body, - 'recipients' => { - 'values' => recipient_paths.map do |profile_path| - { 'person' => { '_path' => "/people/#{profile_path}" } } - end - } - } - post(path, message.to_json, "Content-Type" => "application/json") - end - - end - - end -end diff --git a/lib/linked_in/client.rb b/lib/linked_in/client.rb index fdf99dca..645f196d 100644 --- a/lib/linked_in/client.rb +++ b/lib/linked_in/client.rb @@ -5,8 +5,13 @@ module LinkedIn class Client include Helpers::Request include Helpers::Authorization - include Api::QueryMethods - include Api::UpdateMethods + include Api::QueryHelpers + include Api::People + include Api::Groups + include Api::Companies + include Api::Jobs + include Api::ShareAndSocialStream + include Api::Communications include Search attr_reader :consumer_token, :consumer_secret, :consumer_options diff --git a/lib/linked_in/errors.rb b/lib/linked_in/errors.rb index d7a173b4..bded0658 100644 --- a/lib/linked_in/errors.rb +++ b/lib/linked_in/errors.rb @@ -8,12 +8,22 @@ def initialize(data) end end + # Raised when a 401 response status code is received class UnauthorizedError < LinkedInError; end + + # Raised when a 400 response status code is received class GeneralError < LinkedInError; end + + # Raised when a 403 response status code is received class AccessDeniedError < LinkedInError; end - class UnavailableError < StandardError; end - class InformLinkedInError < StandardError; end + # Raised when a 404 response status code is received class NotFoundError < StandardError; end + + # Raised when a 500 response status code is received + class InformLinkedInError < StandardError; end + + # Raised when a 502 or 503 response status code is received + class UnavailableError < StandardError; end end end diff --git a/lib/linked_in/mash.rb b/lib/linked_in/mash.rb index 4b7d170d..b07e8ddd 100644 --- a/lib/linked_in/mash.rb +++ b/lib/linked_in/mash.rb @@ -2,15 +2,23 @@ require 'multi_json' module LinkedIn + + # The generalized pseudo-object that is returned for all query + # requests. class Mash < ::Hashie::Mash - # a simple helper to convert a json string to a Mash + # Convert a json string to a Mash + # + # @param [String] json_string + # @return [LinkedIn::Mash] def self.from_json(json_string) result_hash = ::MultiJson.decode(json_string) new(result_hash) end - # returns a Date if we have year, month and day, and no conflicting key + # Returns a Date if we have year, month and day, and no conflicting key + # + # @return [Date] def to_date if !self.has_key?('to_date') && contains_date_fields? Date.civil(self.year, self.month, self.day) @@ -19,6 +27,9 @@ def to_date end end + # Returns the id of the object from LinkedIn + # + # @return [String] def id if self['id'] self['id'] @@ -27,6 +38,9 @@ def id end end + # Convert the 'timestamp' field from a string to a Time object + # + # @return [Time] def timestamp value = self['timestamp'] if value.kind_of? Integer @@ -37,6 +51,9 @@ def timestamp end end + # Return the results array from the query + # + # @return [Array] def all super || [] end diff --git a/lib/linked_in/search.rb b/lib/linked_in/search.rb index 1edc7e16..2cf3f821 100644 --- a/lib/linked_in/search.rb +++ b/lib/linked_in/search.rb @@ -1,6 +1,21 @@ module LinkedIn module Search + + # Retrieve search results of the given object type + # + # Permissions: (for people search only) r_network + # + # @note People Search API is a part of the Vetted API Access Program. You + # must apply and get approval before using this API + # + # @see http://developer.linkedin.com/documents/people-search-api People Search + # @see http://developer.linkedin.com/documents/job-search-api Job Search + # @see http://developer.linkedin.com/documents/company-search Company Search + # + # @param [Hash] options search input fields + # @param [String] type type of object to return ('people', 'job' or 'company') + # @return [LinkedIn::Mash] def search(options={}, type='people') path = "/#{type.to_s}-search" @@ -53,4 +68,4 @@ def field_selector(fields) end end -end \ No newline at end of file +end diff --git a/lib/linkedin.rb b/lib/linkedin.rb index cd3524bf..513f55ca 100644 --- a/lib/linkedin.rb +++ b/lib/linkedin.rb @@ -7,15 +7,18 @@ class << self # config/initializers/linkedin.rb (for instance) # + # ```ruby # LinkedIn.configure do |config| # config.token = 'consumer_token' # config.secret = 'consumer_secret' # config.default_profile_fields = ['educations', 'positions'] # end - # + # ``` # elsewhere # + # ```ruby # client = LinkedIn::Client.new + # ``` def configure yield self true diff --git a/linkedin.gemspec b/linkedin.gemspec index 0da54b38..394ab635 100644 --- a/linkedin.gemspec +++ b/linkedin.gemspec @@ -7,7 +7,8 @@ Gem::Specification.new do |gem| gem.add_dependency 'oauth', '~> 0.4' # gem.add_development_dependency 'json', '~> 1.6' gem.add_development_dependency 'rake', '~> 10' - gem.add_development_dependency 'rdoc', '~> 4.0' + gem.add_development_dependency 'yard' + gem.add_development_dependency 'redcarpet', '~> 2.0' gem.add_development_dependency 'rspec', '~> 2.13' gem.add_development_dependency 'simplecov', '~> 0.7' gem.add_development_dependency 'vcr', '~> 2.5' diff --git a/spec/cases/api_spec.rb b/spec/cases/api_spec.rb index a3d531a5..e5240df3 100644 --- a/spec/cases/api_spec.rb +++ b/spec/cases/api_spec.rb @@ -30,6 +30,12 @@ client.connections.should be_an_instance_of(LinkedIn::Mash) end + it "should be able to view new connections" do + modified_since = Time.now.to_i * 1000 + stub_request(:get, "https://api.linkedin.com/v1/people/~/connections?modified=new&modified-since=#{modified_since}").to_return(:body => "{}") + client.new_connections(modified_since).should be_an_instance_of(LinkedIn::Mash) + end + it "should be able to view network_updates" do stub_request(:get, "https://api.linkedin.com/v1/people/~/network/updates").to_return(:body => "{}") client.network_updates.should be_an_instance_of(LinkedIn::Mash)