crankin 0.3.6

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 (47) 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 +82 -0
  16. data/examples/status.rb +9 -0
  17. data/lib/linked_in/api/query_methods.rb +101 -0
  18. data/lib/linked_in/api/update_methods.rb +82 -0
  19. data/lib/linked_in/api.rb +6 -0
  20. data/lib/linked_in/client.rb +46 -0
  21. data/lib/linked_in/errors.rb +18 -0
  22. data/lib/linked_in/helpers/authorization.rb +68 -0
  23. data/lib/linked_in/helpers/request.rb +78 -0
  24. data/lib/linked_in/helpers.rb +6 -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 +86 -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 +127 -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/with_a_callback_url.yml +28 -0
  38. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_default_options.yml +28 -0
  39. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token.yml +28 -0
  40. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option.yml +92 -0
  41. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options.yml +43 -0
  42. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields.yml +45 -0
  43. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter.yml +92 -0
  44. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option.yml +92 -0
  45. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination.yml +67 -0
  46. data/spec/helper.rb +30 -0
  47. metadata +205 -0
@@ -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,78 @@
1
+ module LinkedIn
2
+ module Helpers
3
+
4
+ module Request
5
+
6
+ DEFAULT_HEADERS = {
7
+ 'x-li-format' => 'json'
8
+ }
9
+
10
+ # thanks to @coderifous
11
+ API_PATH = 'https://api.linkedin.com/v1'
12
+
13
+ protected
14
+
15
+ def get(path, options={})
16
+ response = access_token.get("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
17
+ raise_errors(response)
18
+ response.body
19
+ end
20
+
21
+ def post(path, body='', options={})
22
+ response = access_token.post("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
23
+ raise_errors(response)
24
+ response
25
+ end
26
+
27
+ def put(path, body, options={})
28
+ response = access_token.put("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
29
+ raise_errors(response)
30
+ response
31
+ end
32
+
33
+ def delete(path, options={})
34
+ response = access_token.delete("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
35
+ raise_errors(response)
36
+ response
37
+ end
38
+
39
+ private
40
+
41
+ def raise_errors(response)
42
+ # Even if the json answer contains the HTTP status code, LinkedIn also sets this code
43
+ # in the HTTP answer (thankfully).
44
+ case response.code.to_i
45
+ when 401
46
+ data = Mash.from_json(response.body)
47
+ raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}"
48
+ when 400, 403
49
+ data = Mash.from_json(response.body)
50
+ raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}"
51
+ when 404
52
+ raise LinkedIn::Errors::NotFoundError, "(#{response.code}): #{response.message}"
53
+ when 500
54
+ raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.code}): #{response.message}"
55
+ when 502..503
56
+ raise LinkedIn::Errors::UnavailableError, "(#{response.code}): #{response.message}"
57
+ end
58
+ end
59
+
60
+ def to_query(options)
61
+ options.inject([]) do |collection, opt|
62
+ collection << "#{opt[0]}=#{opt[1]}"
63
+ collection
64
+ end * '&'
65
+ end
66
+
67
+ def to_uri(path, options)
68
+ uri = URI.parse(path)
69
+
70
+ if options && options != {}
71
+ uri.query = to_query(options)
72
+ end
73
+ uri.to_s
74
+ end
75
+ end
76
+
77
+ end
78
+ 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
+ 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
+
5
+ def search(options={})
6
+ path = "/people-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 = 6
7
+ PRE = nil
8
+ STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
9
+ end
10
+
11
+ end
data/lib/linkedin.rb ADDED
@@ -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
data/linkedin.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../lib/linked_in/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.add_dependency 'hashie', '~> 1.2.0'
6
+ gem.add_dependency 'multi_json', '~> 1.0.3'
7
+ gem.add_dependency 'oauth', '~> 0.4.5'
8
+ gem.add_development_dependency 'json', '~> 1.6'
9
+ gem.add_development_dependency 'rake', '~> 0.9'
10
+ gem.add_development_dependency 'rdoc', '~> 3.8'
11
+ gem.add_development_dependency 'rspec', '~> 2.6'
12
+ gem.add_development_dependency 'simplecov', '~> 0.5'
13
+ gem.add_development_dependency 'vcr', '~> 1.10'
14
+ gem.add_development_dependency 'webmock', '~> 1.7'
15
+ gem.authors = ["Wynn Netherland", "Josh Kalderimis", "Matt Burke", "Ben Shymkiw"]
16
+ gem.description = %q{Ruby wrapper for the LinkedIn API}
17
+ gem.email = ['wynn.netherland@gmail.com', 'josh.kalderimis@gmail.com']
18
+ gem.files = `git ls-files`.split("\n")
19
+ gem.homepage = 'http://github.com/iamsolarpowered/linkedin'
20
+ gem.name = 'crankin'
21
+ gem.require_paths = ['lib']
22
+ gem.summary = gem.description
23
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ gem.version = LinkedIn::VERSION::STRING
25
+ end
@@ -0,0 +1,86 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn::Api do
4
+ before do
5
+ LinkedIn.default_profile_fields = nil
6
+ client.stub(:consumer).and_return(consumer)
7
+ client.authorize_from_access('atoken', 'asecret')
8
+ end
9
+
10
+ let(:client){LinkedIn::Client.new('token', 'secret')}
11
+ let(:consumer){OAuth::Consumer.new('token', 'secret', {:site => 'https://api.linkedin.com'})}
12
+
13
+ it "should be able to view the account profile" do
14
+ stub_request(:get, "https://api.linkedin.com/v1/people/~").to_return(:body => "{}")
15
+ client.profile.should be_an_instance_of(LinkedIn::Mash)
16
+ end
17
+
18
+ it "should be able to view public profiles" do
19
+ stub_request(:get, "https://api.linkedin.com/v1/people/id=123").to_return(:body => "{}")
20
+ client.profile(:id => 123).should be_an_instance_of(LinkedIn::Mash)
21
+ end
22
+
23
+ it "should be able to view connections" do
24
+ stub_request(:get, "https://api.linkedin.com/v1/people/~/connections").to_return(:body => "{}")
25
+ client.connections.should be_an_instance_of(LinkedIn::Mash)
26
+ end
27
+
28
+ it "should be able to view network_updates" do
29
+ stub_request(:get, "https://api.linkedin.com/v1/people/~/network/updates").to_return(:body => "{}")
30
+ client.network_updates.should be_an_instance_of(LinkedIn::Mash)
31
+ end
32
+
33
+ it "should be able to search with a keyword if given a String" do
34
+ stub_request(:get, "https://api.linkedin.com/v1/people-search?keywords=business").to_return(:body => "{}")
35
+ client.search("business").should be_an_instance_of(LinkedIn::Mash)
36
+ end
37
+
38
+ it "should be able to search with an option" do
39
+ stub_request(:get, "https://api.linkedin.com/v1/people-search?first-name=Javan").to_return(:body => "{}")
40
+ client.search(:first_name => "Javan").should be_an_instance_of(LinkedIn::Mash)
41
+ end
42
+
43
+ it "should be able to search with an option and fetch specific fields" do
44
+ stub_request(:get, "https://api.linkedin.com/v1/people-search:(num-results,total)?first-name=Javan").to_return(:body => "{}")
45
+ client.search(:first_name => "Javan", :fields => ["num_results", "total"]).should be_an_instance_of(LinkedIn::Mash)
46
+ end
47
+
48
+ it "should be able to share a new status" do
49
+ stub_request(:post, "https://api.linkedin.com/v1/people/~/shares").to_return(:body => "", :status => 201)
50
+ response = client.add_share(:comment => "Testing, 1, 2, 3")
51
+ response.body.should == ""
52
+ response.code.should == "201"
53
+ end
54
+
55
+ context "Company API" do
56
+ use_vcr_cassette
57
+
58
+ it "should be able to view a company profile" do
59
+ stub_request(:get, "https://api.linkedin.com/v1/companies/id=1586").to_return(:body => "{}")
60
+ client.company(:id => 1586).should be_an_instance_of(LinkedIn::Mash)
61
+ end
62
+
63
+ it "should be able to view a company by universal name" do
64
+ stub_request(:get, "https://api.linkedin.com/v1/companies/universal-name=acme").to_return(:body => "{}")
65
+ client.company(:name => 'acme').should be_an_instance_of(LinkedIn::Mash)
66
+ end
67
+
68
+ it "should be able to view a company by e-mail domain" do
69
+ stub_request(:get, "https://api.linkedin.com/v1/companies/email-domain=acme.com").to_return(:body => "{}")
70
+ client.company(:domain => 'acme.com').should be_an_instance_of(LinkedIn::Mash)
71
+ end
72
+
73
+ it "should load correct company data" do
74
+ client.company(:id => 1586).name.should == "Amazon"
75
+
76
+ data = client.company(:id => 1586, :fields => %w{ id name industry locations:(address:(city state country-code) is-headquarters) employee-count-range })
77
+ data.id.should == 1586
78
+ data.name.should == "Amazon"
79
+ data.employee_count_range.name.should == "10001+"
80
+ data.industry.should == "Internet"
81
+ data.locations.all[0].address.city.should == "Seattle"
82
+ data.locations.all[0].is_headquarters.should == true
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,37 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn do
4
+
5
+ before(:each) do
6
+ LinkedIn.token = nil
7
+ LinkedIn.secret = nil
8
+ LinkedIn.default_profile_fields = nil
9
+ end
10
+
11
+ it "should be able to set the consumer token and consumer secret" do
12
+ LinkedIn.token = 'consumer_token'
13
+ LinkedIn.secret = 'consumer_secret'
14
+
15
+ LinkedIn.token.should == 'consumer_token'
16
+ LinkedIn.secret.should == 'consumer_secret'
17
+ end
18
+
19
+ it "should be able to set the default profile fields" do
20
+ LinkedIn.default_profile_fields = ['education', 'positions']
21
+
22
+ LinkedIn.default_profile_fields.should == ['education', 'positions']
23
+ end
24
+
25
+ it "should be able to set the consumer token and consumer secret via a configure block" do
26
+ LinkedIn.configure do |config|
27
+ config.token = 'consumer_token'
28
+ config.secret = 'consumer_secret'
29
+ config.default_profile_fields = ['education', 'positions']
30
+ end
31
+
32
+ LinkedIn.token.should == 'consumer_token'
33
+ LinkedIn.secret.should == 'consumer_secret'
34
+ LinkedIn.default_profile_fields.should == ['education', 'positions']
35
+ end
36
+
37
+ end
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn::Mash do
4
+
5
+ describe ".from_json" do
6
+ it "should convert a json string to a Mash" do
7
+ json_string = "{\"name\":\"Josh Kalderimis\"}"
8
+ mash = LinkedIn::Mash.from_json(json_string)
9
+
10
+ mash.should have_key('name')
11
+ mash.name.should == 'Josh Kalderimis'
12
+ end
13
+ end
14
+
15
+ describe "#convert_keys" do
16
+ let(:mash) do
17
+ LinkedIn::Mash.new({
18
+ 'firstName' => 'Josh',
19
+ 'LastName' => 'Kalderimis',
20
+ '_key' => 1234,
21
+ '_total' => 1234,
22
+ 'values' => {},
23
+ 'numResults' => 'total_results'
24
+ })
25
+ end
26
+
27
+ it "should convert camal cased hash keys to underscores" do
28
+ mash.should have_key('first_name')
29
+ mash.should have_key('last_name')
30
+ end
31
+
32
+ it "should convert the key _key to id" do
33
+ mash.should have_key('id')
34
+ end
35
+
36
+ it "should convert the key _total to total" do
37
+ mash.should have_key('total')
38
+ end
39
+
40
+ it "should convert the key values to all" do
41
+ mash.should have_key('all')
42
+ end
43
+
44
+ it "should convert the key numResults to total_results" do
45
+ mash.should have_key('total_results')
46
+ end
47
+ end
48
+
49
+ describe '#timestamp' do
50
+ it "should return a valid Time if a key of timestamp exists and the value is an int" do
51
+ time_mash = LinkedIn::Mash.new({ 'timestamp' => 1297083249 })
52
+
53
+ time_mash.timestamp.should be_a_kind_of(Time)
54
+ time_mash.timestamp.to_i.should == 1297083249
55
+ end
56
+
57
+ it "should return a valid Time if a key of timestamp exists and the value is an int which is greater than 9999999999" do
58
+ time_mash = LinkedIn::Mash.new({ 'timestamp' => 1297083249 * 1000 })
59
+
60
+ time_mash.timestamp.should be_a_kind_of(Time)
61
+ time_mash.timestamp.to_i.should == 1297083249
62
+ end
63
+
64
+ it "should not try to convert to a Time object if the value isn't an Integer" do
65
+ time_mash = LinkedIn::Mash.new({ 'timestamp' => 'Foo' })
66
+
67
+ time_mash.timestamp.class.should be String
68
+ end
69
+ end
70
+
71
+ describe "#to_date" do
72
+ let(:date_mash) do
73
+ LinkedIn::Mash.new({
74
+ 'year' => 2010,
75
+ 'month' => 06,
76
+ 'day' => 23
77
+ })
78
+ end
79
+
80
+ it "should return a valid Date if the keys year, month, day all exist" do
81
+ date_mash.to_date.should == Date.civil(2010, 06, 23)
82
+ end
83
+ end
84
+
85
+ end