omnigroupcontacts 0.3.10 → 0.3.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +39 -0
  5. data/README.md +132 -0
  6. data/Rakefile +7 -0
  7. data/lib/omnigroupcontacts.rb +19 -0
  8. data/lib/omnigroupcontacts/authorization/oauth1.rb +122 -0
  9. data/lib/omnigroupcontacts/authorization/oauth2.rb +87 -0
  10. data/lib/omnigroupcontacts/builder.rb +30 -0
  11. data/lib/omnigroupcontacts/http_utils.rb +101 -0
  12. data/lib/omnigroupcontacts/importer.rb +5 -0
  13. data/lib/omnigroupcontacts/importer/gmailgroup.rb +238 -0
  14. data/lib/omnigroupcontacts/integration_test.rb +36 -0
  15. data/lib/omnigroupcontacts/middleware/base_oauth.rb +120 -0
  16. data/lib/omnigroupcontacts/middleware/oauth1.rb +70 -0
  17. data/lib/omnigroupcontacts/middleware/oauth2.rb +80 -0
  18. data/lib/omnigroupcontacts/parse_utils.rb +56 -0
  19. data/omnigroupcontacts-0.3.10.gem +0 -0
  20. data/omnigroupcontacts-0.3.8.gem +0 -0
  21. data/omnigroupcontacts-0.3.9.gem +0 -0
  22. data/omnigroupcontacts.gemspec +25 -0
  23. data/spec/omnicontacts/authorization/oauth1_spec.rb +82 -0
  24. data/spec/omnicontacts/authorization/oauth2_spec.rb +92 -0
  25. data/spec/omnicontacts/http_utils_spec.rb +79 -0
  26. data/spec/omnicontacts/importer/facebook_spec.rb +120 -0
  27. data/spec/omnicontacts/importer/gmail_spec.rb +194 -0
  28. data/spec/omnicontacts/importer/hotmail_spec.rb +106 -0
  29. data/spec/omnicontacts/importer/linkedin_spec.rb +67 -0
  30. data/spec/omnicontacts/importer/yahoo_spec.rb +124 -0
  31. data/spec/omnicontacts/integration_test_spec.rb +51 -0
  32. data/spec/omnicontacts/middleware/base_oauth_spec.rb +53 -0
  33. data/spec/omnicontacts/middleware/oauth1_spec.rb +78 -0
  34. data/spec/omnicontacts/middleware/oauth2_spec.rb +67 -0
  35. data/spec/omnicontacts/parse_utils_spec.rb +53 -0
  36. data/spec/spec_helper.rb +12 -0
  37. metadata +37 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bad16dc3dfef3e116e85938481d5a92686565d70
4
- data.tar.gz: bbb37f94d90262360403edd694b1f9d648257eef
3
+ metadata.gz: 387500fc78883a2288cdf6b47f55178ffe173d3e
4
+ data.tar.gz: 6a755ae4559c0a760eb5b36b1fff75cf88c64e15
5
5
  SHA512:
6
- metadata.gz: 3af199c3e27f360687a7eff63259b28979459d3745a15fa1c663e1a053eb1ded65c65dbb0a05d4af4bd166d7b89325eed41eb95640a028e639f0728ffd17bb60
7
- data.tar.gz: 5b1f3edd5d3a9da077f44ae22be491b12f512355f91ab6016f69d66432905ebb268a7376503daa15bbe02b3701e94c10df0a0eacb60066cf130a1147a227fee9
6
+ metadata.gz: 043996490bf66a9c8dc5ce97a3c7e1c7e8e2fbb9ff08c253c7ca8fee83dfe7401ec31eecb9a4659118a12e78305b7b12ba1b6dd7ef4452f5777d818c5d694422
7
+ data.tar.gz: 7bb04a34aacc2d8ab1332f5881f67fab2d0cde42dd9397c6ec52fa8239f92ff2d09ae58211f10b3cf220230a53665fe67117f8221f0a2cbcc3835b5ee145d7d5
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ coverage
2
+ *.iml
3
+ .idea
4
+ .rvmrc
5
+ .DS_Store
6
+ nbproject
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ omnigroupcontacts (0.3.5)
5
+ json
6
+ rack
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ diff-lcs (1.1.3)
12
+ json (1.8.1)
13
+ multi_json (1.1.0)
14
+ rack (1.4.1)
15
+ rack-test (0.6.1)
16
+ rack (>= 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)
27
+ multi_json (~> 1.0)
28
+ simplecov-html (~> 0.5.3)
29
+ simplecov-html (0.5.3)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ omnigroupcontacts!
36
+ rack-test
37
+ rake
38
+ rspec
39
+ simplecov
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # omnigroupcontacts
2
+
3
+ Inspired by the popular OmniContacts, OmniGroupContacts is a library that enables users of an application to import contacts
4
+ from their email accounts with respect to group. The email providers currently supported are Gmail.
5
+ OmniGroupContacts is a Rack middleware, therefore you can use it with Rails, Sinatra and any other Rack-based framework.
6
+
7
+ OmniGroupContacts uses the OAuth protocol to communicate with the contacts provider.
8
+ In order to use OmniGroupContacts, it is therefore necessary to first register your application with the provider and to obtain client_id and client_secret.
9
+
10
+ ## Usage
11
+
12
+ Add OmniGroupContacts as a dependency:
13
+
14
+ ```ruby
15
+ gem "omnigroupcontacts"
16
+
17
+ ```
18
+
19
+ As for OmniAuth, there is a Builder facilitating the usage of multiple contacts importers. In the case of a Rails application, the following code could be placed at `config/initializers/omnigroupcontacts.rb`:
20
+
21
+ ```ruby
22
+ require "omnigroupcontacts"
23
+
24
+ Rails.application.middleware.use OmniGroupContacts::Builder do
25
+ importer :gmailgroup, "client_id", "client_secret", {:redirect_path => "/oauth2callback", :ssl_ca_file => "/etc/ssl/certs/curl-ca-bundle.crt"}
26
+
27
+ end
28
+
29
+ ```
30
+
31
+ ## Register your application
32
+
33
+ * For Gmail : [Google API Console](https://code.google.com/apis/console/)
34
+
35
+
36
+ ## Integrating with your Application
37
+
38
+ To use the Gem you first need to redirect your users to `/group_contacts/:importer`, where `:importer` can be gmailgroup.
39
+ No changes to `config/routes.rb` are needed for this step since omnigroupcontacts will be listening on that path and redirect the user to the email provider's website in order to authorize your app to access his contact list.
40
+ Once that is done the user will be redirected back to your application, to the path specified in `:redirect_path`.
41
+ If nothing is specified the default value is `/group_contacts/:importer/callback` (e.g. `/group_contacts/gmailgroup/callback`). This makes things simpler and you can just add the following line to `config/routes.rb`:
42
+
43
+ ```ruby
44
+ match "/group_contacts/:importer/callback" => "your_controller#callback"
45
+ ```
46
+
47
+ The list of contacts can be accessed via the `omnigroupcontacts.contacts` key in the environment hash and it consists of a simple array of hashes.
48
+ The following table shows which fields are supported by which provider:
49
+
50
+ <table>
51
+ <tr>
52
+ <th>Provider</th>
53
+ <th>:email</th>
54
+ <th>:id</th>
55
+ <th>:profile_picture</th>
56
+ <th>:name</th>
57
+ <th>:first_name</th>
58
+ <th>:last_name</th>
59
+ <th>:address_1</th>
60
+ <th>:address_2</th>
61
+ <th>:city</th>
62
+ <th>:region</th>
63
+ <th>:postcode</th>
64
+ <th>:country</th>
65
+ <th>:phone_number</th>
66
+ <th>:birthday</th>
67
+ <th>:gender</th>
68
+ <th>:relation</th>
69
+ </tr>
70
+ <tr>
71
+ <td>Gmail</td>
72
+ <td>X</td>
73
+ <td>X</td>
74
+ <td></td>
75
+ <td>X</td>
76
+ <td>X</td>
77
+ <td>X</td>
78
+ <td>X</td>
79
+ <td>X</td>
80
+ <td>X</td>
81
+ <td>X</td>
82
+ <td>X</td>
83
+ <td>X</td>
84
+ <td>X</td>
85
+ <td>X</td>
86
+ <td>X</td>
87
+ <td>X</td>
88
+ </tr>
89
+ </table>
90
+
91
+ 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.
92
+
93
+ The information for the logged in user can also be accessed via 'omnigroupcontacts.user' key in the environment hash. It consists of a simple hash which includes the same fields as above.
94
+
95
+ The following snippet shows how to simply print name and email of each contact, and also the the name of logged in user:
96
+ ```ruby
97
+ def contacts_callback
98
+ @contacts = request.env['omnigroupcontacts.contacts']
99
+ @user = request.env['omnigroupcontacts.user']
100
+ puts "List of contacts of #{@user[:name]} obtained from #{params[:importer]}:"
101
+ @contacts.each do |group_name, contacts|
102
+ puts "Group: #{contacts}"
103
+ contacts.each do |contact|
104
+ puts "Contact found: name => #{contact[:name]}, email => #{contact[:email]}"
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ If the user does not authorize your application to access his/her contacts list, or any other inconvenience occurs, he/she is redirected to `/contacts/failure`. The query string will contain a parameter named `error_message` which specifies why the list of contacts could not be retrieved. `error_message` can have one of the following values: `not_authorized`, `timeout` and `internal_error`.
111
+
112
+ ## License
113
+
114
+ Copyright (c) 2015 Mitesh Jain
115
+
116
+ Permission is hereby granted, free of charge, to any person obtaining a
117
+ copy of this software and associated documentation files (the "Software"),
118
+ to deal in the Software without restriction, including without limitation
119
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
120
+ and/or sell copies of the Software, and to permit persons to whom the
121
+ Software is furnished to do so, subject to the following conditions:
122
+
123
+ The above copyright notice and this permission notice shall be included
124
+ in all copies or substantial portions of the Software.
125
+
126
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
127
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
128
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
129
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
130
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
131
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
132
+ DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
7
+ task :test => :spec
@@ -0,0 +1,19 @@
1
+ module OmniGroupContacts
2
+
3
+ VERSION = "0.3.11"
4
+
5
+ MOUNT_PATH = "/group_contacts/"
6
+
7
+ autoload :Builder, "omnigroupcontacts/builder"
8
+ autoload :Importer, "omnigroupcontacts/importer"
9
+ autoload :IntegrationTest, "omnigroupcontacts/integration_test"
10
+
11
+ class AuthorizationError < RuntimeError
12
+ end
13
+
14
+
15
+ def self.integration_test
16
+ IntegrationTest.instance
17
+ end
18
+
19
+ end
@@ -0,0 +1,122 @@
1
+ require "omnigroupcontacts/http_utils"
2
+ require "base64"
3
+
4
+ # This module represent a OAuth 1.0 Client.
5
+ #
6
+ # Classes including the module must implement
7
+ # the following methods:
8
+ # * auth_host -> the host of the authorization server
9
+ # * auth_token_path -> the path to query to obtain a request token
10
+ # * consumer_key -> the registered consumer key of the client
11
+ # * consumer_secret -> the registered consumer secret of the client
12
+ # * callback -> the callback to include during the redirection step
13
+ # * auth_path -> the path on the authorization server to redirect the user to
14
+ # * access_token_path -> the path to query in order to obtain the access token
15
+ module OmniGroupContacts
16
+ module Authorization
17
+ module OAuth1
18
+ include HTTPUtils
19
+
20
+ OAUTH_VERSION = "1.0"
21
+
22
+ # Obtain an authorization token from the server.
23
+ # The token is returned in an array along with the relative authorization token secret.
24
+ def fetch_authorization_token
25
+ request_token_response = https_post(auth_host, auth_token_path, request_token_req_params)
26
+ values_from_query_string(request_token_response, ["oauth_token", "oauth_token_secret"])
27
+ end
28
+
29
+ private
30
+
31
+ def request_token_req_params
32
+ {
33
+ :oauth_consumer_key => consumer_key,
34
+ :oauth_nonce => encode(random_string),
35
+ :oauth_signature_method => "PLAINTEXT",
36
+ :oauth_signature => encode(consumer_secret + "&"),
37
+ :oauth_timestamp => timestamp,
38
+ :oauth_version => OAUTH_VERSION,
39
+ :oauth_callback => callback
40
+ }
41
+ end
42
+
43
+ def random_string
44
+ (0...50).map { ('a'..'z').to_a[rand(26)] }.join
45
+ end
46
+
47
+ def timestamp
48
+ Time.now.to_i.to_s
49
+ end
50
+
51
+ def values_from_query_string query_string, keys_to_extract
52
+ map = query_string_to_map(query_string)
53
+ keys_to_extract.collect do |key|
54
+ if map.has_key?(key)
55
+ map[key]
56
+ else
57
+ raise "No value found for #{key} in #{query_string}"
58
+ end
59
+ end
60
+ end
61
+
62
+ public
63
+
64
+ # Returns the url the user has to be redirected to do in order grant permission to the client application.
65
+ def authorization_url auth_token
66
+ "https://" + auth_host + auth_path + "?oauth_token=" + auth_token
67
+ end
68
+
69
+ # Fetches the access token from the authorization server.
70
+ # The method expects the authorization token, the authorization token secret and the authorization verifier.
71
+ # The result comprises the access token, the access token secret and a list of additional fields extracted from the server's response.
72
+ # The list of additional fields to extract is specified as last parameter
73
+ def fetch_access_token auth_token, auth_token_secret, auth_verifier, additional_fields_to_extract = []
74
+ access_token_resp = https_post(auth_host, access_token_path, access_token_req_params(auth_token, auth_token_secret, auth_verifier))
75
+ values_from_query_string(access_token_resp, (["oauth_token", "oauth_token_secret"] + additional_fields_to_extract))
76
+ end
77
+
78
+ private
79
+
80
+ def access_token_req_params auth_token, auth_token_secret, auth_verifier
81
+ {
82
+ :oauth_consumer_key => consumer_key,
83
+ :oauth_nonce => encode(random_string),
84
+ :oauth_signature_method => "PLAINTEXT",
85
+ :oauth_signature => encode(consumer_secret + "&" + auth_token_secret),
86
+ :oauth_version => OAUTH_VERSION,
87
+ :oauth_timestamp => timestamp,
88
+ :oauth_token => auth_token,
89
+ :oauth_verifier => auth_verifier
90
+ }
91
+ end
92
+
93
+ public
94
+
95
+ # Calculates a signature using HMAC-SHA1 according to the OAuth 1.0 specifications.
96
+ #
97
+ # The base string is given is a RFC 3986 encoded concatenation of:
98
+ # * Uppercase HTTP method
99
+ # * An '&'
100
+ # * A url without any parameters
101
+ # * An '&'
102
+ # * All parameters to use in the request encoded themselves and sorted by key.
103
+ #
104
+ # The signature key is given by the concatenation of:
105
+ # * RFC 3986 encoded consumer secret
106
+ # * An '&'
107
+ # * RFC 3986 encoded token secret
108
+ def oauth_signature method, url, params, secret
109
+ encoded_method = encode(method.upcase)
110
+ encoded_url = encode(url)
111
+ # params must be in alphabetical order
112
+ encoded_params = encode(to_query_string(params.sort { |x, y| x.to_s <=> y.to_s }))
113
+ base_string = encoded_method + '&' + encoded_url + '&' + encoded_params
114
+ key = encode(consumer_secret) + '&' + secret
115
+ hmac_sha1 = OpenSSL::HMAC.digest('sha1', key, base_string)
116
+ # base64 encode results must be stripped
117
+ encode(Base64.encode64(hmac_sha1).strip)
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,87 @@
1
+ require "omnigroupcontacts/http_utils"
2
+ require "json"
3
+
4
+ # This module represents an OAuth 2.0 client.
5
+ #
6
+ # Classes including the module must implement
7
+ # the following methods:
8
+ # * auth_host -> the host of the authorization server
9
+ # * authorize_path -> the path on the authorization server the redirect the use to
10
+ # * client_id -> the registered client id of the client
11
+ # * client_secret -> the registered client secret of the client
12
+ # * redirect_path -> the path the authorization server has to redirect the user back after authorization
13
+ # * auth_token_path -> the path to query once the user has granted permission to the application
14
+ # * scope -> the scope necessary to acquire the contacts list.
15
+ module OmniGroupContacts
16
+ module Authorization
17
+ module OAuth2
18
+ include HTTPUtils
19
+
20
+ # Calculates the URL the user has to be redirected to in order to authorize
21
+ # the application to access his contacts list.
22
+ def authorization_url
23
+ "https://" + auth_host + authorize_path + "?" + authorize_url_params
24
+ end
25
+
26
+ private
27
+
28
+ def authorize_url_params
29
+ to_query_string({
30
+ :client_id => client_id,
31
+ :scope => encode(scope),
32
+ :response_type => "code",
33
+ :access_type => "online",
34
+ :approval_prompt => "auto",
35
+ :redirect_uri => encode(redirect_uri)
36
+ })
37
+ end
38
+
39
+ public
40
+
41
+ # Fetches the access token from the authorization server using the given authorization code.
42
+ def fetch_access_token code
43
+ access_token_from_response https_post(auth_host, auth_token_path, token_req_params(code))
44
+ end
45
+
46
+ private
47
+
48
+ def token_req_params code
49
+ {
50
+ :client_id => client_id,
51
+ :client_secret => client_secret,
52
+ :code => code,
53
+ :redirect_uri => encode(redirect_uri),
54
+ :grant_type => "authorization_code"
55
+ }
56
+ end
57
+
58
+ def access_token_from_response response
59
+ if auth_host == "graph.facebook.com"
60
+ response = query_string_to_map(response).to_json
61
+ end
62
+ json = JSON.parse(response)
63
+ raise json["error"] if json["error"]
64
+ [json["access_token"], json["token_type"], json["refresh_token"]]
65
+ end
66
+
67
+ public
68
+
69
+ # Refreshes the access token using the provided refresh_token.
70
+ def refresh_access_token refresh_token
71
+ access_token_from_response https_post(auth_host, auth_token_path, refresh_token_req_params(refresh_token))
72
+ end
73
+
74
+ private
75
+
76
+ def refresh_token_req_params refresh_token
77
+ {
78
+ :client_id => client_id,
79
+ :client_secret => client_secret,
80
+ :refresh_token => refresh_token,
81
+ :grant_type => "refresh_token"
82
+ }
83
+
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,30 @@
1
+ require "omnigroupcontacts"
2
+
3
+ module OmniGroupContacts
4
+ class Builder < Rack::Builder
5
+ def initialize(app, &block)
6
+ if rack14?
7
+ super
8
+ else
9
+ @app = app
10
+ super(&block)
11
+ end
12
+ end
13
+
14
+ def rack14?
15
+ Rack.release.split('.')[1].to_i >= 4
16
+ end
17
+
18
+ def importer importer, *args
19
+ middleware = OmniGroupContacts::Importer.const_get(importer.to_s.capitalize)
20
+ use middleware, *args
21
+ rescue NameError
22
+ raise LoadError, "Could not find importer #{importer}."
23
+ end
24
+
25
+ def call env
26
+ @ins << @app unless rack14? || @ins.include?(@app)
27
+ to_app.call(env)
28
+ end
29
+ end
30
+ end