omnicontacts 0.3.5 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -8,31 +8,25 @@ PATH
8
8
  GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
11
- diff-lcs (1.2.5)
12
- docile (1.1.5)
11
+ diff-lcs (1.1.3)
13
12
  json (1.8.1)
14
- multi_json (1.10.1)
15
- rack (1.5.2)
16
- rack-test (0.6.2)
13
+ multi_json (1.1.0)
14
+ rack (1.4.1)
15
+ rack-test (0.6.1)
17
16
  rack (>= 1.0)
18
- rake (10.3.2)
19
- rspec (3.1.0)
20
- rspec-core (~> 3.1.0)
21
- rspec-expectations (~> 3.1.0)
22
- rspec-mocks (~> 3.1.0)
23
- rspec-core (3.1.7)
24
- rspec-support (~> 3.1.0)
25
- rspec-expectations (3.1.2)
26
- diff-lcs (>= 1.2.0, < 2.0)
27
- rspec-support (~> 3.1.0)
28
- rspec-mocks (3.1.3)
29
- rspec-support (~> 3.1.0)
30
- rspec-support (3.1.2)
31
- simplecov (0.9.1)
32
- docile (~> 1.1.0)
17
+ rake (0.9.2.2)
18
+ rspec (2.8.0)
19
+ rspec-core (~> 2.8.0)
20
+ rspec-expectations (~> 2.8.0)
21
+ rspec-mocks (~> 2.8.0)
22
+ rspec-core (2.8.0)
23
+ rspec-expectations (2.8.0)
24
+ diff-lcs (~> 1.1.2)
25
+ rspec-mocks (2.8.0)
26
+ simplecov (0.6.1)
33
27
  multi_json (~> 1.0)
34
- simplecov-html (~> 0.8.0)
35
- simplecov-html (0.8.0)
28
+ simplecov-html (~> 0.5.3)
29
+ simplecov-html (0.5.3)
36
30
 
37
31
  PLATFORMS
38
32
  ruby
data/README.md CHANGED
@@ -8,6 +8,14 @@ OmniContacts uses the OAuth protocol to communicate with the contacts provider.
8
8
  Facebook, Gmail and Hotmail support OAuth 2.0.
9
9
  In order to use OmniContacts, it is therefore necessary to first register your application with the provider and to obtain client_id and client_secret.
10
10
 
11
+ ## Contribute!
12
+ Me (rubytastic) and the orginal author Diego don't actively use this code at the moment, anyone interested in maintaining and contributing to this codebase please write me up in a personal message ( rubytastic )
13
+ I try to merge pull requests in every once and a while but this code would benefit from someone actively use and contribute to it.
14
+
15
+ ## Gem build updates
16
+ There is now a new gem build out which should address many issues people had when posting on the issue tracker. Please update to the latest GEM version if you have problems before posting new issues.
17
+
18
+
11
19
  ## Usage
12
20
 
13
21
  Add OmniContacts as a dependency:
@@ -25,13 +33,14 @@ require "omnicontacts"
25
33
  Rails.application.middleware.use OmniContacts::Builder do
26
34
  importer :gmail, "client_id", "client_secret", {:redirect_path => "/oauth2callback", :ssl_ca_file => "/etc/ssl/certs/curl-ca-bundle.crt"}
27
35
  importer :yahoo, "consumer_id", "consumer_secret", {:callback_path => '/callback'}
36
+ importer :linkedin, "consumer_id", "consumer_secret", {:redirect_path => "/oauth2callback", :state => '<long_unique_string_value>'}
28
37
  importer :hotmail, "client_id", "client_secret"
29
38
  importer :facebook, "client_id", "client_secret"
30
39
  end
31
40
 
32
41
  ```
33
42
 
34
- Every importer expects `client_id` and `client_secret` as mandatory, while `:redirect_path` and `:ssl_ca_file` are optional.
43
+ Every importer expects `client_id` and `client_secret` as mandatory, while `:redirect_path` and `:ssl_ca_file` are optional (except linkedin - `state` arg mandatory).
35
44
  Since Yahoo implements the version 1.0 of the OAuth protocol, naming is slightly different. Instead of `:redirect_path` you should use `:callback_path` as key in the hash providing the optional parameters.
36
45
  While `:ssl_ca_file` is optional, it is highly recommended to set it on production environments for obvious security reasons.
37
46
  On the other hand it makes things much easier to leave the default value for `:redirect_path` and `:callback path`, the reason of which will be clear after reading the following section.
@@ -46,6 +55,9 @@ On the other hand it makes things much easier to leave the default value for `:r
46
55
 
47
56
  * For Facebook : [Facebook Developers](https://developers.facebook.com/apps)
48
57
 
58
+ * For Linkedin : [Linkedin Developer Network](https://www.linkedin.com/secure/developer)
59
+
60
+
49
61
  ##### Note:
50
62
  Please go through [MSDN](http://msdn.microsoft.com/en-us/library/cc287659.aspx) if above Hotmail link will not work.
51
63
 
@@ -159,6 +171,25 @@ The following table shows which fields are supported by which provider:
159
171
  <td>X</td>
160
172
  <td></td>
161
173
  </tr>
174
+ <tr>
175
+ <td>Linkedin</td>
176
+ <td></td>
177
+ <td>X</td>
178
+ <td>X</td>
179
+ <td>X</td>
180
+ <td>X</td>
181
+ <td>X</td>
182
+ <td></td>
183
+ <td></td>
184
+ <td></td>
185
+ <td></td>
186
+ <td></td>
187
+ <td></td>
188
+ <td></td>
189
+ <td></td>
190
+ <td></td>
191
+ <td></td>
192
+ <tr>
162
193
  </table>
163
194
 
164
195
  Obviously it may happen that some fields are blank even if supported by the provider in the case that the contact did not provide any information about them.
data/lib/omnicontacts.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module OmniContacts
2
2
 
3
- VERSION = "0.3.5"
3
+ VERSION = "0.3.6"
4
4
 
5
5
  MOUNT_PATH = "/contacts/"
6
6
 
@@ -57,7 +57,7 @@ module OmniContacts
57
57
  # If the result of ssl_ca_file is nil no file is used. In this case a warn message is logged.
58
58
  private
59
59
 
60
- # Executes an HTTP GET request.
60
+ # Executes an HTTP GET request.
61
61
  # It raises a RuntimeError if the response code is not equal to 200
62
62
  def http_get host, path, params
63
63
  connection = Net::HTTP.new(host)
@@ -5,6 +5,7 @@ module OmniContacts
5
5
  autoload :Yahoo, "omnicontacts/importer/yahoo"
6
6
  autoload :Hotmail, "omnicontacts/importer/hotmail"
7
7
  autoload :Facebook, "omnicontacts/importer/facebook"
8
+ autoload :Linkedin, "omnicontacts/importer/linkedin"
8
9
 
9
10
  end
10
11
  end
@@ -13,7 +13,7 @@ module OmniContacts
13
13
  @auth_host = "accounts.google.com"
14
14
  @authorize_path = "/o/oauth2/auth"
15
15
  @auth_token_path = "/o/oauth2/token"
16
- @scope = "https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile"
16
+ @scope = (args[3] && args[3][:scope]) || "https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile"
17
17
  @contacts_host = "www.google.com"
18
18
  @contacts_path = "/m8/feeds/contacts/default/full"
19
19
  @max_results = (args[3] && args[3][:max_results]) || 100
@@ -45,6 +45,7 @@ module OmniContacts
45
45
 
46
46
  def contacts_from_response response_as_json
47
47
  response = JSON.parse(response_as_json)
48
+
48
49
  return [] if response['feed'].nil? || response['feed']['entry'].nil?
49
50
  contacts = []
50
51
  return contacts if response.nil?
@@ -88,7 +89,7 @@ module OmniContacts
88
89
  # Support older versions of the gem by keeping singular entries around
89
90
  contact[:email] = contact[:emails][0][:email] if contact[:emails][0]
90
91
  contact[:first_name], contact[:last_name], contact[:name] = email_to_name(contact[:name]) if !contact[:name].nil? && contact[:name].include?('@')
91
- contact[:first_name], contact[:last_name], contact[:name] = email_to_name(contact[:emails][0][:email]) if contact[:name].nil? && contact[:emails][0][:email]
92
+ contact[:first_name], contact[:last_name], contact[:name] = email_to_name(contact[:emails][0][:email]) if (contact[:name].nil? && contact[:emails][0] && contact[:emails][0][:email])
92
93
  #format - year-month-date
93
94
  contact[:birthday] = birthday(entry['gContact$birthday']['when']) if entry['gContact$birthday']
94
95
 
@@ -0,0 +1,63 @@
1
+ require "omnicontacts/parse_utils"
2
+ require "omnicontacts/middleware/oauth2"
3
+
4
+ module OmniContacts
5
+ module Importer
6
+ class Linkedin < Middleware::OAuth2
7
+ include ParseUtils
8
+
9
+ attr_reader :auth_host, :authorize_path, :auth_token_path, :scope, :state
10
+
11
+ def initialize *args
12
+ super *args
13
+ @auth_host = "www.linkedin.com"
14
+ @authorize_path = "/uas/oauth2/authorization"
15
+ @auth_token_path = "/uas/oauth2/accessToken"
16
+ @scope = (args[3] && args[3][:scope]) || "r_network"
17
+ @contacts_host = "api.linkedin.com"
18
+ @contacts_path = "/v1/people/~/connections:(id,first-name,last-name,picture-url)"
19
+ @self_host = "www.linkedin.com"
20
+ @profile_path = "/oauth2/v1/userinfo"
21
+ @state = (args[3] && args[3][:state])
22
+ end
23
+
24
+ def fetch_contacts_using_access_token access_token, token_type
25
+ token_type = "Bearer" if token_type.nil?
26
+ contacts_response = https_get(@contacts_host, @contacts_path, contacts_req_params, contacts_req_headers(access_token, token_type))
27
+ contacts_from_response contacts_response
28
+ end
29
+
30
+ private
31
+
32
+ def contacts_req_params
33
+ {'format' => 'json'}
34
+ end
35
+
36
+ def contacts_req_headers token, token_type
37
+ {"Authorization" => "#{token_type} #{token}"}
38
+ end
39
+
40
+ def contacts_from_response response_as_json
41
+ response = JSON.parse(response_as_json)
42
+ return [] if response['values'].nil?
43
+ contacts = []
44
+ return contacts if response.nil?
45
+ response['values'].map do |entry|
46
+ {
47
+ id: entry['id'],
48
+ first_name: normalize_name(entry['firstName']),
49
+ last_name: normalize_name(entry['lastName']),
50
+ name: full_name(entry['firstName'],entry['lastName']),
51
+ profile_picture: entry['pictureUrl']
52
+ }
53
+ end
54
+ end
55
+
56
+ def authorize_url_params
57
+ # merge state param required by LinkedIn
58
+ _params = super
59
+ _params += "&" + to_query_string(state: @state)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -5,7 +5,7 @@ require "omnicontacts/middleware/base_oauth"
5
5
  #
6
6
  # Extending class are required to implement
7
7
  # the following methods:
8
- # * fetch_contacts_using_access_token -> it
8
+ # * fetch_contacts_using_access_token -> it
9
9
  # fetches the list of contacts from the authorization
10
10
  # server.
11
11
  module OmniContacts
@@ -25,7 +25,7 @@ module OmniContacts
25
25
 
26
26
  def request_authorization_from_user
27
27
  target_url = append_state_query(authorization_url)
28
- [302, {"location" => target_url}, []]
28
+ [302, {"Content-Type" => "application/x-www-form-urlencoded", "location" => target_url}, []]
29
29
  end
30
30
 
31
31
  def redirect_uri
@@ -34,7 +34,7 @@ module OmniContacts
34
34
 
35
35
  # It extract the authorization code from the query string.
36
36
  # It uses it to obtain an access token.
37
- # If the authorization code has a refresh token associated
37
+ # If the authorization code has a refresh token associated
38
38
  # with it in the session, it uses the obtain an access token.
39
39
  # It fetches the list of contacts and stores the refresh token
40
40
  # associated with the access token in the session.
@@ -5,6 +5,8 @@ describe OmniContacts::Importer::Gmail do
5
5
 
6
6
  let(:gmail) { OmniContacts::Importer::Gmail.new({}, "client_id", "client_secret") }
7
7
 
8
+ let(:gmail_with_scope_args) { OmniContacts::Importer::Gmail.new({}, "client_id", "client_secret", {scope: "https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/contacts.readonly"}) }
9
+
8
10
  let(:self_response) {
9
11
  '{
10
12
  "id":"16482944006464829443",
@@ -110,6 +112,7 @@ describe OmniContacts::Importer::Gmail do
110
112
 
111
113
  before(:each) do
112
114
  gmail.instance_variable_set(:@env, {})
115
+ gmail_with_scope_args.instance_variable_set(:@env, {})
113
116
  end
114
117
 
115
118
  it "should request the contacts by specifying version and code in the http headers" do
@@ -124,6 +127,9 @@ describe OmniContacts::Importer::Gmail do
124
127
  contacts_as_json
125
128
  end
126
129
  gmail.fetch_contacts_using_access_token token, token_type
130
+
131
+ gmail.scope.should eq "https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile"
132
+ gmail_with_scope_args.scope.should eq "https://www.google.com/m8/feeds https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/contacts.readonly"
127
133
  end
128
134
 
129
135
  it "should correctly parse id, name, email, gender, birthday, profile picture and relation for 1st contact" do
@@ -0,0 +1,67 @@
1
+ require "spec_helper"
2
+ require "omnicontacts/importer/linkedin"
3
+
4
+ describe OmniContacts::Importer::Linkedin do
5
+
6
+ let(:linkedin) { OmniContacts::Importer::Linkedin.new({}, "client_id", "client_secret", state: "ipsaeumeaque") }
7
+
8
+ let(:contacts_as_json) do
9
+ "{
10
+ \n \"_total\": 2,
11
+ \n \"values\": [
12
+ \n {
13
+ \n \"firstName\": \"Adolf\",
14
+ \n \"id\": \"k71S5q6MKe\",
15
+ \n \"lastName\": \"Witting\",
16
+ \n \"pictureUrl\": \"https://media.licdn.com/mpr/mprx/0_mLnj-7szw130pFRLB8Op7-p1Sxoyv53U3B47Scp1Sxoyv53U3B47Scp1Sxoyv53U3B47Sc\"\n
17
+ },
18
+ \n {
19
+ \n \"firstName\": \"Emmet\",
20
+ \n \"id\": \"ms5r3lI3J2\",
21
+ \n \"lastName\": \"Little\",
22
+ \n \"pictureUrl\": \"https://media.licdn.com/mpr/mprx/0_iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6\"\n
23
+ }
24
+ ]\n
25
+ }"
26
+ end
27
+
28
+ describe "fetch_contacts_using_access_token" do
29
+ let(:token) { "token" }
30
+ let(:token_type) { nil }
31
+
32
+ before(:each) do
33
+ linkedin.instance_variable_set(:@env, {})
34
+ end
35
+
36
+ it "should request the contacts by specifying code in the http headers" do
37
+ linkedin.should_receive(:https_get) do |host, path, params, headers|
38
+ headers["Authorization"].should eq("Bearer #{token}")
39
+ contacts_as_json
40
+ end
41
+ linkedin.fetch_contacts_using_access_token token, token_type
42
+ end
43
+
44
+ it "should correctly parse id, name, and profile picture for 1st contact" do
45
+ linkedin.should_receive(:https_get).and_return(contacts_as_json)
46
+ result = linkedin.fetch_contacts_using_access_token token, token_type
47
+
48
+ result.size.should be(2)
49
+ result.first[:id].should eq('k71S5q6MKe')
50
+ result.first[:first_name].should eq('Adolf')
51
+ result.first[:last_name].should eq('Witting')
52
+ result.first[:name].should eq("Adolf Witting")
53
+ result.first[:profile_picture].should eq("https://media.licdn.com/mpr/mprx/0_mLnj-7szw130pFRLB8Op7-p1Sxoyv53U3B47Scp1Sxoyv53U3B47Scp1Sxoyv53U3B47Sc")
54
+ end
55
+
56
+ it "should correctly parse id, name, and profile picture for 2nd contact" do
57
+ linkedin.should_receive(:https_get).and_return(contacts_as_json)
58
+ result = linkedin.fetch_contacts_using_access_token token, token_type
59
+ result.size.should be(2)
60
+ result.last[:id].should eq('ms5r3lI3J2')
61
+ result.last[:first_name].should eq('Emmet')
62
+ result.last[:last_name].should eq('Little')
63
+ result.last[:name].should eq("Emmet Little")
64
+ result.last[:profile_picture].should eq("https://media.licdn.com/mpr/mprx/0_iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6")
65
+ end
66
+ end
67
+ end
metadata CHANGED
@@ -1,7 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnicontacts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.6
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Diego Castorina
@@ -9,90 +10,102 @@ authors:
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2014-11-21 00:00:00.000000000 Z
13
+ date: 2015-04-30 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: rack
16
17
  requirement: !ruby/object:Gem::Requirement
18
+ none: false
17
19
  requirements:
18
- - - '>='
20
+ - - ! '>='
19
21
  - !ruby/object:Gem::Version
20
22
  version: '0'
21
23
  type: :runtime
22
24
  prerelease: false
23
25
  version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
24
27
  requirements:
25
- - - '>='
28
+ - - ! '>='
26
29
  - !ruby/object:Gem::Version
27
30
  version: '0'
28
31
  - !ruby/object:Gem::Dependency
29
32
  name: json
30
33
  requirement: !ruby/object:Gem::Requirement
34
+ none: false
31
35
  requirements:
32
- - - '>='
36
+ - - ! '>='
33
37
  - !ruby/object:Gem::Version
34
38
  version: '0'
35
39
  type: :runtime
36
40
  prerelease: false
37
41
  version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
38
43
  requirements:
39
- - - '>='
44
+ - - ! '>='
40
45
  - !ruby/object:Gem::Version
41
46
  version: '0'
42
47
  - !ruby/object:Gem::Dependency
43
48
  name: simplecov
44
49
  requirement: !ruby/object:Gem::Requirement
50
+ none: false
45
51
  requirements:
46
- - - '>='
52
+ - - ! '>='
47
53
  - !ruby/object:Gem::Version
48
54
  version: '0'
49
55
  type: :development
50
56
  prerelease: false
51
57
  version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
52
59
  requirements:
53
- - - '>='
60
+ - - ! '>='
54
61
  - !ruby/object:Gem::Version
55
62
  version: '0'
56
63
  - !ruby/object:Gem::Dependency
57
64
  name: rake
58
65
  requirement: !ruby/object:Gem::Requirement
66
+ none: false
59
67
  requirements:
60
- - - '>='
68
+ - - ! '>='
61
69
  - !ruby/object:Gem::Version
62
70
  version: '0'
63
71
  type: :development
64
72
  prerelease: false
65
73
  version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
66
75
  requirements:
67
- - - '>='
76
+ - - ! '>='
68
77
  - !ruby/object:Gem::Version
69
78
  version: '0'
70
79
  - !ruby/object:Gem::Dependency
71
80
  name: rack-test
72
81
  requirement: !ruby/object:Gem::Requirement
82
+ none: false
73
83
  requirements:
74
- - - '>='
84
+ - - ! '>='
75
85
  - !ruby/object:Gem::Version
76
86
  version: '0'
77
87
  type: :development
78
88
  prerelease: false
79
89
  version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
80
91
  requirements:
81
- - - '>='
92
+ - - ! '>='
82
93
  - !ruby/object:Gem::Version
83
94
  version: '0'
84
95
  - !ruby/object:Gem::Dependency
85
96
  name: rspec
86
97
  requirement: !ruby/object:Gem::Requirement
98
+ none: false
87
99
  requirements:
88
- - - '>='
100
+ - - ! '>='
89
101
  - !ruby/object:Gem::Version
90
102
  version: '0'
91
103
  type: :development
92
104
  prerelease: false
93
105
  version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
94
107
  requirements:
95
- - - '>='
108
+ - - ! '>='
96
109
  - !ruby/object:Gem::Version
97
110
  version: '0'
98
111
  description: A generalized Rack middleware for importing contacts from major email
@@ -118,6 +131,7 @@ files:
118
131
  - lib/omnicontacts/importer/facebook.rb
119
132
  - lib/omnicontacts/importer/gmail.rb
120
133
  - lib/omnicontacts/importer/hotmail.rb
134
+ - lib/omnicontacts/importer/linkedin.rb
121
135
  - lib/omnicontacts/importer/yahoo.rb
122
136
  - lib/omnicontacts/integration_test.rb
123
137
  - lib/omnicontacts/middleware/base_oauth.rb
@@ -131,6 +145,7 @@ files:
131
145
  - spec/omnicontacts/importer/facebook_spec.rb
132
146
  - spec/omnicontacts/importer/gmail_spec.rb
133
147
  - spec/omnicontacts/importer/hotmail_spec.rb
148
+ - spec/omnicontacts/importer/linkedin_spec.rb
134
149
  - spec/omnicontacts/importer/yahoo_spec.rb
135
150
  - spec/omnicontacts/integration_test_spec.rb
136
151
  - spec/omnicontacts/middleware/base_oauth_spec.rb
@@ -140,25 +155,26 @@ files:
140
155
  - spec/spec_helper.rb
141
156
  homepage: http://github.com/Diego81/omnicontacts
142
157
  licenses: []
143
- metadata: {}
144
158
  post_install_message:
145
159
  rdoc_options: []
146
160
  require_paths:
147
161
  - lib
148
162
  required_ruby_version: !ruby/object:Gem::Requirement
163
+ none: false
149
164
  requirements:
150
- - - '>='
165
+ - - ! '>='
151
166
  - !ruby/object:Gem::Version
152
167
  version: '0'
153
168
  required_rubygems_version: !ruby/object:Gem::Requirement
169
+ none: false
154
170
  requirements:
155
- - - '>='
171
+ - - ! '>='
156
172
  - !ruby/object:Gem::Version
157
173
  version: 1.3.6
158
174
  requirements: []
159
175
  rubyforge_project:
160
- rubygems_version: 2.0.14
176
+ rubygems_version: 1.8.23
161
177
  signing_key:
162
- specification_version: 4
178
+ specification_version: 3
163
179
  summary: A generalized Rack middleware for importing contacts from major email providers.
164
180
  test_files: []
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 153f2fff78dd7440fe1c653a7458f47db2b8717a
4
- data.tar.gz: f115709601214db1e8826893355146514c5c8b82
5
- SHA512:
6
- metadata.gz: 29f390858859e150129a1a01d3fbbe6d523b997ece36e113e6c8635d5c0ecb2cc47f77e2686c07503703a4a0148dd543842cebfa5d285f0923be01e5f5d4454c
7
- data.tar.gz: 7850d3c9bd1f6309f0dd1065262c6b16f7aa947ba3f33deb186ba8bff3a9809e5baeb1e588b6cc817c7b159650f3692c44c522df1d062f62a37fffbd6a97b90a