linkedin-api 0.8.9

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 93d087170b8192f65f736e4c4372591df800da4e41fff2dde3e33673cc3e364a
4
+ data.tar.gz: a40a61cabd1006e165050c547713912779f1ac8b7d76f8432e2a14dc4f6a9b5a
5
+ SHA512:
6
+ metadata.gz: 41eecf64b4c967af66c5275d941c3c6912efe496fd2edbd1ba5dbe2e4b3f4935eaa3fddd7bf37c2a43d63dc63f57de8f570d7824aa8ce5ccfd358c2419453d7a
7
+ data.tar.gz: 01e4686bac59ccaa6ef956110801fcbc3bfc0d1b319b9d3033efe019674a59a81bbc7f96a58a398cd61c2131bfd866ececc03d7f43d767d06968d0fe3ee73793
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ module API
5
+ class CurrentUser
6
+ attr_reader :client
7
+
8
+ LOCATION_URN_PREFIX = 'urn:li:standardizedLocationKey'
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ def me
15
+ client.get('v2/me', projection: '(*,profilePicture(displayImage~:playableStreams))')
16
+ end
17
+
18
+ def email
19
+ client.get('v2/emailAddress', q: 'members', projection: '(elements*(handle~))')
20
+ end
21
+
22
+ def location(urn)
23
+ return unless urn.start_with?(LOCATION_URN_PREFIX)
24
+
25
+ client.get('v2/regions', q: 'standardizedLocation', standardizedLocation: urn)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+
5
+ module LinkedIn
6
+ module API
7
+ class Media
8
+ TYPES = { 'image/png': '.png', 'image/jpeg': '.jpeg' }.freeze
9
+ attr_reader :client
10
+
11
+ def initialize(client)
12
+ @client = client
13
+ end
14
+
15
+ def upload(image_url, organization_id)
16
+ image = URI.open(image_url)
17
+ type = image.content_type
18
+ raise "Unexpected content type: '#{type}'" unless TYPES.key?(type.to_sym)
19
+
20
+ response = image_register(organization_id)
21
+ client.media_upload(response[:uploadUrl], image, filename(image))
22
+ response
23
+ end
24
+
25
+ private
26
+
27
+ def image_register(organization_id)
28
+ body = {
29
+ initializeUploadRequest: {
30
+ owner: "urn:li:organization:#{organization_id}",
31
+ },
32
+ }
33
+ response = client.post('rest/images?action=initializeUpload', body)
34
+ response.dig(:value)
35
+ end
36
+
37
+ def filename(media)
38
+ filename = File.basename(media.base_uri.to_s)
39
+ filename += TYPES[media.content_type.to_sym] unless filename.match?('\.(jpg|jpeg|png)')
40
+ filename
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ module API
5
+ class Organization
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ def organizations
13
+ client.get('rest/organizationAcls?q=roleAssignee&count=50')
14
+ end
15
+
16
+ def organization(id)
17
+ client.get("rest/organizations/#{id}")
18
+ end
19
+
20
+ def organization_statistics(urn)
21
+ client.get("rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=#{urn}")
22
+ end
23
+
24
+ def organization_network_size(id)
25
+ client.get("rest/networkSizes/urn:li:organization:#{id}", edgeType: 'COMPANY_FOLLOWED_BY_MEMBER')
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ module API
5
+ class Post
6
+ attr_reader :client, :media, :logger
7
+
8
+ def initialize(client, media, logger=nil)
9
+ @client = client
10
+ @media = media
11
+ @logger = logger
12
+ end
13
+
14
+ def create(organization_id, share_data)
15
+ share = Hashie::Mash.new(share_data)
16
+ post = create_post(content(share, organization_id), organization_id).to_json
17
+ client.post('rest/posts', post)
18
+ end
19
+
20
+ def social_summary(id)
21
+ get_post_summary(id)
22
+ end
23
+
24
+ def likes_summary(id)
25
+ get_post_summary(id, '(likesSummary)')
26
+ end
27
+
28
+ def comments_summary(id)
29
+ get_post_summary(id, '(commentsSummary)')
30
+ end
31
+
32
+ private
33
+
34
+ def content(share, organization_id)
35
+ comment = comment_content(share.comment)
36
+ content = image_content(share.content, organization_id) || {}
37
+ compact(comment.deep_merge!(content))
38
+ end
39
+
40
+ def create_post(content, organization_id)
41
+ payload = Hashie::Mash.new(
42
+ author: "urn:li:organization:#{organization_id}",
43
+ lifecycleState: 'PUBLISHED',
44
+ visibility: 'PUBLIC',
45
+ isReshareDisabledByAuthor: false,
46
+ distribution: {
47
+ feedDistribution: "MAIN_FEED",
48
+ targetEntities: [],
49
+ thirdPartyDistributionChannels: [],
50
+ },
51
+ ).deep_merge!(content)
52
+
53
+ logger&.debug(msg: "## PAYLOAD -> ", payload: payload.to_json)
54
+ payload
55
+ end
56
+
57
+ def comment_content(comment)
58
+ Hashie::Mash.new(
59
+ commentary: escape_characters(comment)
60
+ )
61
+ end
62
+
63
+ ##
64
+ # The api from 2023-02 has some special characters and underscore is one of them that not works as expected on links
65
+ #
66
+ # @link https://learn.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/little-text-format?view=li-lms-2023-02#text
67
+ #
68
+ #
69
+ def escape_characters(comment)
70
+ replacements = {
71
+ '_' => '\\_',
72
+ '~' => '\\~',
73
+ '*' => '\\*',
74
+ '[' => '\\[',
75
+ ']' => '\\]',
76
+ '(' => '\\(',
77
+ ')' => '\\)',
78
+ '{' => '\\{',
79
+ '}' => '\\}',
80
+ '|' => '\\|',
81
+ '#' => '\\#',
82
+ '@' => '\\@',
83
+ '>' => '\\>',
84
+ '<' => '\\<',
85
+ '\\' => '\\\\'
86
+ }
87
+ comment.to_s.gsub(/[\_\~\*\[\]\(\)\{\}\|\#\@\>\<\\]/, replacements)
88
+ end
89
+
90
+ def image_content(content, organization_id)
91
+ image_url = content&.dig(:"submitted-image-url") || ''
92
+ return if image_url.empty?
93
+
94
+ image = @media.upload(image_url, organization_id)
95
+ {
96
+ content: {
97
+ media: compact(id: image[:image]),
98
+ },
99
+ }
100
+ end
101
+
102
+ def compact(hash)
103
+ hash.delete_if { |_k, value| value.is_a?(Hash) ? compact(value).empty? : value.nil? }
104
+ end
105
+
106
+ def get_post_summary(id, projection = {})
107
+ options = { projection: projection } unless projection.empty?
108
+ client.get("v2/socialActions/" + URI.encode_www_form_component("urn:li:share:#{id}"), options || {})
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ module Auth
5
+ class AccessToken
6
+ attr_reader :token, :expires_in
7
+
8
+ def initialize(attrs = {})
9
+ @token = attrs.delete('access_token')
10
+ @expires_in = attrs.delete('expires_in')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'securerandom'
5
+ require 'faraday_middleware'
6
+
7
+ require 'linked_in/middleware/raise_error'
8
+ require 'linked_in/auth/access_token'
9
+
10
+ module LinkedIn
11
+ module Auth
12
+ class Client
13
+ def initialize(state = SecureRandom.hex, config = LinkedIn.config)
14
+ @state = state
15
+ @config = config
16
+ end
17
+
18
+ def authorize_url
19
+ params = authorize_url_params.merge(state: @state)
20
+ connection.build_url(config.authorize_url, params).to_s
21
+ end
22
+
23
+ def access_token(code, state = nil)
24
+ raise LinkedIn::InvalidRequestError if code.nil?
25
+
26
+ params = access_token_params.merge(code: code, state: state)
27
+ response = connection.post(config.token_url, params)
28
+
29
+ Auth::AccessToken.new(response.body)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :config
35
+
36
+ def connection
37
+ @connection ||= Faraday.new(config.site) do |conn|
38
+ conn.use LinkedIn::Middleware::RaiseError
39
+ conn.request :url_encoded
40
+ conn.response :json, content_type: /\bjson$/
41
+ conn.adapter Faraday.default_adapter
42
+ end
43
+ end
44
+
45
+ def authorize_url_params
46
+ {
47
+ scope: config.scope,
48
+ response_type: :code,
49
+ client_id: config.client_id,
50
+ redirect_uri: config.redirect_uri,
51
+ }
52
+ end
53
+
54
+ def access_token_params
55
+ {
56
+ client_id: config.client_id,
57
+ client_secret: config.client_secret,
58
+ redirect_uri: config.redirect_uri,
59
+ grant_type: :authorization_code,
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ class Configuration
5
+ FIELDS = {
6
+ site: 'https://www.linkedin.com',
7
+ scope: nil,
8
+ client_id: nil,
9
+ client_secret: nil,
10
+ redirect_uri: nil,
11
+ token_url: '/oauth/v2/accessToken',
12
+ authorize_url: '/oauth/v2/authorization',
13
+ }.freeze
14
+
15
+ FIELDS.each do |name, default_value|
16
+ define_method(name) do
17
+ instance_variable_get("@#{name}") || default_value
18
+ end
19
+
20
+ define_method("#{name}=") do |value|
21
+ instance_variable_set("@#{name}", value)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ class APIError < RuntimeError
5
+ def initialize(response = nil)
6
+ @response = response
7
+ end
8
+
9
+ def http_status
10
+ response&.status
11
+ end
12
+
13
+ def http_headers
14
+ response&.headers
15
+ end
16
+
17
+ def http_body
18
+ response&.body
19
+ end
20
+
21
+ def message
22
+ return super() unless response
23
+
24
+ "#{self.class} - #{http_status}: #{http_body}"
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :response
30
+ end
31
+
32
+ class InvalidRequestError < APIError; end
33
+
34
+ class UnauthorizedError < APIError; end
35
+
36
+ class AccessDeniedError < APIError; end
37
+
38
+ class NotFoundError < APIError; end
39
+
40
+ class InformLinkedInError < APIError; end
41
+
42
+ class UnavailableError < APIError; end
43
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+ require 'linked_in/middleware/raise_error'
6
+
7
+ module LinkedIn
8
+ class HTTPClient
9
+ BASE_URL = 'https://api.linkedin.com/'
10
+ LINKEDIN_VERSION_API = '202511'
11
+ HEADER_RESTLI_PROTOCOL = '2.0.0'
12
+
13
+ attr_reader :json_client, :multipart_client, :protocol, :debug_mode
14
+
15
+ def initialize(access_token:, protocol: true, debug_mode: false)
16
+ @protocol = protocol
17
+ @json_client = build_client(access_token, BASE_URL, :json)
18
+ @multipart_client = build_client(access_token, BASE_URL, :multipart)
19
+ @debug_mode = debug_mode
20
+ end
21
+
22
+ def get(path, params = {}, headers = {})
23
+ response = json_client.get(path, params, headers)
24
+ to_result(response)
25
+ end
26
+
27
+ def post(path, body = {})
28
+ response = json_client.post(path, body)
29
+ to_result(response)
30
+ end
31
+
32
+ def register_upload(path, body)
33
+ post(path, body)
34
+ end
35
+
36
+ def media_upload(path, media, filename)
37
+ header_media_content(multipart_client, media)
38
+ content = Faraday::UploadIO.new(media, media.content_type, filename)
39
+
40
+ response = multipart_client.put(path, content)
41
+ response
42
+ end
43
+
44
+ private
45
+
46
+ def header_media_content(client, media)
47
+ client.headers["Content-Type"] = media.content_type
48
+ client.headers['Content-Length'] = media.length.to_s
49
+ end
50
+
51
+ def to_result(response)
52
+ unless response.body.empty?
53
+ body = response.body.is_a?(Hash) ? response.body : JSON.parse(response.body)
54
+
55
+ return Hashie::Mash.new(response.body)
56
+ end
57
+
58
+ Hashie::Mash.new(
59
+ 'id': response.headers['x-restli-id'],
60
+ 'x-li-uuid': response.headers['x-li-uuid']
61
+ )
62
+ end
63
+
64
+ def build_client(access_token, url, type)
65
+ Faraday.new(url: url) do |builder|
66
+ builder.authorization('Bearer', access_token)
67
+ builder.request type
68
+ builder.use Faraday::Request::UrlEncoded
69
+ builder.use LinkedIn::Middleware::RaiseError
70
+ builder.use FaradayMiddleware::ParseJson, content_type: /\bjson$/
71
+ builder.adapter :net_http
72
+
73
+ # @link https://learn.microsoft.com/en-us/linkedin/marketing/versioning?view=li-lms-unversioned
74
+ builder.headers['LinkedIn-Version'] = LINKEDIN_VERSION_API
75
+ builder.headers['X-Restli-Protocol-Version'] = HEADER_RESTLI_PROTOCOL if protocol?
76
+
77
+ builder.response :logger, nil, { headers: true, bodies: true, errors: true }
78
+ end
79
+ end
80
+
81
+ def protocol?
82
+ protocol
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'linked_in/errors'
4
+
5
+ # rubocop:disable Metrics/CyclomaticComplexity
6
+ module LinkedIn
7
+ module Middleware
8
+ class RaiseError < Faraday::Response::RaiseError
9
+ def on_complete(env)
10
+ response = env.response
11
+ case response.status
12
+ when 400
13
+ raise LinkedIn::InvalidRequestError, response
14
+ when 401
15
+ raise LinkedIn::UnauthorizedError, response
16
+ when 403
17
+ raise LinkedIn::AccessDeniedError, response
18
+ when 404
19
+ raise LinkedIn::NotFoundError, response
20
+ when 500
21
+ raise LinkedIn::InformLinkedInError, response
22
+ when 502..503
23
+ raise LinkedIn::UnavailableError, response
24
+ else
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedIn
4
+ VERSION = '0.8.9'
5
+ end
data/lib/linked_in.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hashie'
4
+ require 'linked_in/version'
5
+ require 'linked_in/http_client'
6
+ require 'linked_in/auth/client'
7
+ require 'linked_in/api/organization'
8
+ require 'linked_in/api/media'
9
+ require 'linked_in/api/post'
10
+ require 'linked_in/api/current_user'
11
+ require 'linked_in/configuration'
12
+
13
+ module LinkedIn
14
+ class << self
15
+ def setup
16
+ yield config
17
+ end
18
+
19
+ def config
20
+ @config ||= Configuration.new
21
+ end
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linkedin-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.9
5
+ platform: ruby
6
+ authors:
7
+ - LeadGen Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.17'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.17'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.13'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hashie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rexml
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: A gem to use LinkedIn API
98
+ email: leadgen@resultadosdigitais.com.br
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - lib/linked_in.rb
104
+ - lib/linked_in/api/current_user.rb
105
+ - lib/linked_in/api/media.rb
106
+ - lib/linked_in/api/organization.rb
107
+ - lib/linked_in/api/post.rb
108
+ - lib/linked_in/auth/access_token.rb
109
+ - lib/linked_in/auth/client.rb
110
+ - lib/linked_in/configuration.rb
111
+ - lib/linked_in/errors.rb
112
+ - lib/linked_in/http_client.rb
113
+ - lib/linked_in/middleware/raise_error.rb
114
+ - lib/linked_in/version.rb
115
+ homepage: https://github.com/ResultadosDigitais/linkedin
116
+ licenses: []
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '3.0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.4.20
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: A gem to use LinkedIn API
137
+ test_files: []