gcal_mapper 0.1.1

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 (58) hide show
  1. data/.gitignore +32 -0
  2. data/.travis.yml +16 -0
  3. data/CHANGELOG.md +15 -0
  4. data/Gemfile +20 -0
  5. data/Guardfile +19 -0
  6. data/LICENSE +22 -0
  7. data/README.md +370 -0
  8. data/Rakefile +32 -0
  9. data/bin/ci/file/auth.yaml +7 -0
  10. data/bin/ci/file/bad_yaml.yaml +0 -0
  11. data/bin/ci/file/config.yaml +8 -0
  12. data/bin/ci/file/privatekey.p12 +0 -0
  13. data/bin/ci/travis_build.sh +4 -0
  14. data/bin/ci/vcr/GcalMapper_Authentification/access_token_should_exist_with_assertion_auth.yml +49 -0
  15. data/bin/ci/vcr/GcalMapper_Authentification/should_be_fasle_if_bad_client_email_is_given.yml +47 -0
  16. data/bin/ci/vcr/GcalMapper_Authentification_Assertion/should_have_access_token_attribute.yml +49 -0
  17. data/bin/ci/vcr/GcalMapper_Authentification_Assertion/should_have_refresh_token_attribute.yml +49 -0
  18. data/bin/ci/vcr/GcalMapper_Calendar/should_get_the_calendar_list.yml +190 -0
  19. data/bin/ci/vcr/GcalMapper_Calendar/should_get_the_events_list.yml +45 -0
  20. data/bin/ci/vcr/GcalMapper_Calendar/should_raise_error_if_the_calendar_id_isn_t_accessible.yml +56 -0
  21. data/bin/ci/vcr/GcalMapper_Calendar/should_raise_error_if_the_token_is_bad.yml +60 -0
  22. data/bin/ci/vcr/GcalMapper_Mapper/should_save_all_events.yml +232 -0
  23. data/bin/ci/vcr/GcalMapper_Mapper/should_save_events_only_once.yml +232 -0
  24. data/bin/gcal-mapper +108 -0
  25. data/gcal_mapper.gemspec +25 -0
  26. data/lib/gcal_mapper.rb +15 -0
  27. data/lib/gcal_mapper/authentification.rb +57 -0
  28. data/lib/gcal_mapper/authentification/assertion.rb +110 -0
  29. data/lib/gcal_mapper/authentification/base.rb +20 -0
  30. data/lib/gcal_mapper/authentification/oauth2.rb +68 -0
  31. data/lib/gcal_mapper/calendar.rb +51 -0
  32. data/lib/gcal_mapper/configuration.rb +23 -0
  33. data/lib/gcal_mapper/errors.rb +40 -0
  34. data/lib/gcal_mapper/mapper.rb +76 -0
  35. data/lib/gcal_mapper/mapper/active_record.rb +74 -0
  36. data/lib/gcal_mapper/mapper/dsl.rb +57 -0
  37. data/lib/gcal_mapper/mapper/simple.rb +97 -0
  38. data/lib/gcal_mapper/railtie.rb +8 -0
  39. data/lib/gcal_mapper/rest_request.rb +73 -0
  40. data/lib/gcal_mapper/sync.rb +159 -0
  41. data/lib/gcal_mapper/version.rb +4 -0
  42. data/spec/authentification/assertion_spec.rb +15 -0
  43. data/spec/authentification/base_spec.rb +18 -0
  44. data/spec/authentification/oauth2_spec.rb +15 -0
  45. data/spec/authentification_spec.rb +34 -0
  46. data/spec/calendar_spec.rb +33 -0
  47. data/spec/gcal_mapper_spec.rb +5 -0
  48. data/spec/mapper/active_record_spec.rb +46 -0
  49. data/spec/mapper/dsl_spec.rb +31 -0
  50. data/spec/mapper/simple_spec.rb +48 -0
  51. data/spec/mapper_spec.rb +32 -0
  52. data/spec/rest_request_spec.rb +5 -0
  53. data/spec/spec_helper.rb +51 -0
  54. data/spec/support/models/event.rb +25 -0
  55. data/spec/support/models/event_jeu.rb +22 -0
  56. data/spec/support/schema.rb +28 -0
  57. data/spec/sync_spec.rb +28 -0
  58. metadata +200 -0
data/bin/gcal-mapper ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ bin_dir = File.expand_path("..", __FILE__)
3
+ lib_dir = File.expand_path("../lib", bin_dir)
4
+
5
+ $LOAD_PATH.unshift(lib_dir)
6
+ $LOAD_PATH.uniq!
7
+
8
+ require 'launchy'
9
+ require 'socket'
10
+ require 'gcal_mapper'
11
+
12
+ OAUTH_SERVER_PORT = 12736
13
+
14
+ options = {}
15
+
16
+ opt_parser = OptionParser.new do |opt|
17
+
18
+ opt.on('--client_id CLIENT_ID', 'your client id') do |client_id|
19
+ options[:client_id] = client_id
20
+ end
21
+
22
+ opt.on('--client_secret CLIENT_SECRET', 'your client secret') do |client_secret|
23
+ options[:client_secret] = client_secret
24
+ end
25
+
26
+ opt.on('-f FILE', '--file FILE', 'the file to save credentials') do |file|
27
+ options[:file] = file
28
+ end
29
+
30
+ opt.on('--scope SCOPE', 'the scope to ask authorization for') do |scope|
31
+ options[:scope] = scope
32
+
33
+ end
34
+
35
+ end
36
+
37
+ opt_parser.parse!
38
+ options[:scope] = 'https://www.googleapis.com/auth/calendar' if options[:scope].nil?
39
+ options[:file] = '~/google_auth.yaml' if options[:file].nil?
40
+
41
+ authorize_url = 'https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=auto&'
42
+ authorize_url += 'client_id=' + options[:client_id] + '&'
43
+ authorize_url += 'redirect_uri=http://localhost:' + OAUTH_SERVER_PORT.to_s + '/&response_type=code&'
44
+ authorize_url += 'scope=' + options[:scope]
45
+
46
+ # open web browser with the authorisation page
47
+ Launchy.open(authorize_url)
48
+
49
+ msg = ''
50
+
51
+ # tcp server to receive the response and close the window
52
+ server = TCPServer.new('0.0.0.0', OAUTH_SERVER_PORT)
53
+ loop do
54
+ Thread.new(server.accept) do |client|
55
+ msg = client.readline
56
+ body = "<html><head><script>function closeWindow() {window.open('', '_self', '');window.close();} setTimeout(closeWindow, 10);</script></head><body>You may close this window.</body></html>\r\n"
57
+ headers = [
58
+ "",
59
+ "HTTP/1.1 200 OK",
60
+ "Date: Fri, 30 Sep 2011 08:11:27 GMT",
61
+ "Server: TCP socket test",
62
+ "Content-Type: text/html; charset=iso-8859-1",
63
+ "Content-Length: #{body.length}\r\n\r\n"].join("\r\n")
64
+
65
+ client.write headers
66
+ client.write body
67
+
68
+ client.close
69
+ end
70
+
71
+ sleep(1.0/24.0)
72
+ break if msg.include?('code')
73
+ end
74
+
75
+ # keep just the authorization code
76
+ msg.slice!('GET /?code=')
77
+ msg.chop!
78
+ msg.slice!(' HTTP/1.1')
79
+
80
+ # form rest request and send it
81
+ url = 'https://accounts.google.com/o/oauth2/token'
82
+ data = {
83
+ 'code' => msg,
84
+ 'grant_type'=> 'authorization_code',
85
+ 'redirect_uri' => 'http://localhost:' + OAUTH_SERVER_PORT.to_s + '/',
86
+ 'client_id' => options[:client_id],
87
+ 'client_secret' => options[:client_secret]
88
+ }
89
+ opts = {
90
+ :method => :post,
91
+ :headers => {'Content-Type' => 'application/x-www-form-urlencoded'},
92
+ :parameters => data
93
+ }
94
+ req = GcalMapper::RestRequest.new(url, opts)
95
+ response = req.execute
96
+
97
+ # store credentials in yaml file
98
+ config = {
99
+ 'mechanism' => 'oauth_2',
100
+ 'scope' => options[:scope],
101
+ 'client_id' => options[:client_id],
102
+ 'client_secret' => options[:client_secret],
103
+ 'access_token' => response['access_token'],
104
+ 'refresh_token' => response['refresh_token']
105
+ }
106
+ config_file = File.expand_path(options[:file])
107
+ open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
108
+ exit(0)
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/gcal_mapper/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['Néville Dubuis']
6
+ gem.email = ['neville.dubuis@liquid-concept.ch']
7
+ gem.description = %q{A library to map Google Calendar events with an ORM}
8
+ gem.summary = %q{A library to map Google Calendar events with an ORM}
9
+ gem.homepage = 'http://rubygems.org/gems/gcal_mapper'
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = ['gcal-mapper']
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = 'gcal_mapper'
15
+ gem.require_paths = ['lib']
16
+ gem.version = GcalMapper::VERSION
17
+
18
+ gem.add_dependency 'launchy'
19
+
20
+ gem.add_development_dependency 'activerecord'
21
+ gem.add_development_dependency 'rspec', '>= 2.0'
22
+ gem.add_development_dependency 'vcr'
23
+ gem.add_development_dependency 'fakeweb'
24
+
25
+ end
@@ -0,0 +1,15 @@
1
+ require 'gcal_mapper/version'
2
+ require 'gcal_mapper/rest_request'
3
+ require 'gcal_mapper/authentification'
4
+ require 'gcal_mapper/calendar'
5
+ require 'gcal_mapper/errors'
6
+ require 'gcal_mapper/sync'
7
+ require 'gcal_mapper/mapper'
8
+ require 'gcal_mapper/configuration'
9
+ require 'gcal_mapper/railtie' if defined?(Rails)
10
+
11
+ #
12
+ # A library to map Google Calendar events with an ORM.
13
+ #
14
+ module GcalMapper
15
+ end
@@ -0,0 +1,57 @@
1
+ require 'gcal_mapper/authentification/assertion'
2
+ require 'gcal_mapper/authentification/oauth2'
3
+
4
+ module GcalMapper
5
+ #
6
+ # Abstract which type of authentification is required
7
+ #
8
+ class Authentification
9
+ REQUEST_URL = 'https://accounts.google.com/o/oauth2/token' # url where to request to authentificate
10
+
11
+ attr_reader :file # file that is needed for authentification
12
+ attr_reader :client_email # for assertion authentification
13
+ attr_reader :password # password for the p12 file
14
+
15
+ # intialize client info needed for connection to Oauth2.
16
+ #
17
+ # @param [String] file path to the yaml or p12 file
18
+ # @param [String] client_email email from the api_consol (service account only)
19
+ # @param [String] user_email email from the impersonated user (service account only)
20
+ # @param [String] password p12 key password (service account only)
21
+ def initialize(file, client_email=nil, user_email=nil, password='notasecret')
22
+ @file = File.expand_path(file)
23
+ @client_email = client_email
24
+ @user_email = user_email
25
+ @password = password
26
+ raise GcalMapper::AuthFileError if !File.exist?(@file)
27
+ end
28
+
29
+ # do the authentification for one of the right authentification method
30
+ #
31
+ # @return [Bool] true if instantiation ok
32
+ def authenticate
33
+ if client_email==nil
34
+ @auth = Authentification::Oauth2.new(@file)
35
+ else
36
+ @auth = Authentification::Assertion.new(@file, @client_email, @user_email, @password)
37
+ end
38
+
39
+ !access_token.nil?
40
+ end
41
+
42
+ # Gives the access token
43
+ #
44
+ # @return [string] the access token
45
+ def access_token
46
+ @auth.access_token
47
+ end
48
+
49
+ # refresh the access token
50
+ #
51
+ # @return [string] the access token
52
+ def refresh_token
53
+ @auth.refresh_token
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,110 @@
1
+ require 'gcal_mapper/authentification/base'
2
+ require 'json'
3
+
4
+ module GcalMapper
5
+ class Authentification
6
+ #
7
+ # make the authentification for service account and request data from google calendar.
8
+ #
9
+ class Assertion < Authentification::Base
10
+ ASSERTION_SCOPE = 'https://www.google.com/calendar/feeds/' # scope for assertion
11
+ JWT_HEADER = {'alg' => 'RS256', 'typ' => 'JWT'} # header of the jwt to send
12
+ ASSERTION_TYPE = 'http://oauth.net/grant_type/jwt/1.0/bearer' # url to specify which type of assertion
13
+
14
+
15
+ attr_accessor :client_email # the email given by google for the service account
16
+ attr_accessor :user_email # the email of the user to impersonate
17
+
18
+ # New object
19
+ #
20
+ # @param [String] p12_file the path to the p12 key file
21
+ # @param [String] client_email email from the api_consol
22
+ # @param [String] user_email email from the impersonated user
23
+ # @param [String] password p12 key password
24
+ def initialize(p12_file, client_email, user_email, password)
25
+ @client_email = client_email
26
+ @p12_file = p12_file
27
+ @user_email = user_email
28
+ @password = password
29
+ request_token
30
+ @validity = Time.now.getutc.to_i
31
+ end
32
+
33
+ # give the acess token for th application and refresh if it is outdated
34
+ #
35
+ # @return [String] access token
36
+ def access_token
37
+ if Time.now.getutc.to_i - @validity > 3600
38
+ refresh_token
39
+ end
40
+
41
+ @access_token
42
+ end
43
+
44
+ # refresh the token by asking for a new one
45
+ #
46
+ # @return [String] access token
47
+ def refresh_token
48
+ request_token
49
+ end
50
+
51
+ private
52
+ # generate the JWT that must be include with the request acess token.
53
+ # the format of the JWT is (base64url encoded header).(base64url encoded claim set).(base64url encoded signature)
54
+ #
55
+ # @return [String] the generate JWT.
56
+ def generate_assertion()
57
+ encoded_header = [JWT_HEADER.to_json].pack("m0").tr("+/", "-_")
58
+
59
+
60
+ utc_time = Time.now.getutc.to_i
61
+ claim = {
62
+ 'aud' => Authentification::REQUEST_URL,
63
+ 'scope'=> ASSERTION_SCOPE,
64
+ 'prn' => @user_email,
65
+ 'iat' => utc_time,
66
+ 'exp' => utc_time+3600,
67
+ 'iss' => @client_email
68
+ }
69
+ encoded_claim = [claim.to_json].pack("m0").tr("+/", "-_")
70
+ begin
71
+ p12 = OpenSSL::PKCS12.new(File.read(@p12_file), @password)
72
+ rescue
73
+ raise GcalMapper::AuthFileError
74
+ end
75
+ key = p12.key
76
+ sign = key.sign(OpenSSL::Digest::SHA256.new, encoded_header + '.' + encoded_claim)
77
+ encoded_sign = [sign].pack("m0").tr("+/", "-_")
78
+
79
+ encoded_header + '.' + encoded_claim + '.' + encoded_sign
80
+ end
81
+
82
+ # Prepare all the parameters for sending the request token request to google
83
+ # and save the token in instance variable.
84
+ #
85
+ # @return [Hash] the HTTP response.
86
+ def request_token
87
+ data = {
88
+ 'grant_type' => 'assertion',
89
+ 'assertion_type' => ASSERTION_TYPE,
90
+ 'assertion' => generate_assertion
91
+ }
92
+ options = {
93
+ :method => :post,
94
+ :headers => {'Content-Type' => 'application/x-www-form-urlencoded'},
95
+ :parameters => data
96
+ }
97
+ req = GcalMapper::RestRequest.new(Authentification::REQUEST_URL, options)
98
+ begin
99
+ response = req.execute
100
+ rescue
101
+ raise GcalMapper::AuthentificationError
102
+ end
103
+ @valididity = Time.now.getutc.to_i
104
+
105
+ @access_token = response['access_token']
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,20 @@
1
+ module GcalMapper
2
+ class Authentification
3
+ #
4
+ # Base class for authentification methods
5
+ #
6
+ class Base
7
+
8
+ # raise error if this method is called -> mean that child class has not implemeted this method
9
+ def access_token
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # raise error if this method is called -> mean that child class has not implemeted this method
14
+ def refresh_token
15
+ raise NotImplementedError
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,68 @@
1
+ require 'gcal_mapper/authentification/base'
2
+ require 'yaml'
3
+
4
+ module GcalMapper
5
+ class Authentification
6
+ #
7
+ # make the authentification with Oauth2 and request data from google calendar.
8
+ #
9
+ class Oauth2 < Authentification::Base
10
+
11
+ # intialize client info needed for connection to Oauth2.
12
+ #
13
+ # @param [String] yaml_file path to the yaml file which contains acess token, ...
14
+ def initialize (yaml_file)
15
+ begin
16
+ oauth_yaml = YAML.load_file(yaml_file)
17
+ @client_id = oauth_yaml["client_id"]
18
+ @client_secret = oauth_yaml["client_secret"]
19
+ @scope = oauth_yaml["scope"]
20
+ @refresh_token = oauth_yaml["refresh_token"]
21
+ @access_token = oauth_yaml["access_token"]
22
+ @validity = Time.now.getutc.to_i
23
+ rescue
24
+ raise GcalMapper::AuthFileError
25
+ end
26
+ end
27
+
28
+ # give the acess token for th application and refresh if it is outdated
29
+ #
30
+ # @return [String] access token
31
+ def access_token
32
+ if (Time.now.getutc.to_i - @validity) > 3600
33
+ refresh_token
34
+ end
35
+
36
+ @access_token
37
+ end
38
+
39
+ # refresh the token by using refresh token
40
+ #
41
+ # @return [String] access token
42
+ def refresh_token
43
+ data = {
44
+ 'client_id' => @client_id,
45
+ 'client_secret' => @client_secret,
46
+ 'refresh_token' => @refresh_token,
47
+ 'grant_type' => 'refresh_token'
48
+ }
49
+ options = {
50
+ :method => :post,
51
+ :headers => {'Content-Type' => 'application/x-www-form-urlencoded'},
52
+ :parameters => data
53
+ }
54
+ req = GcalMapper::RestRequest.new(Authentification::REQUEST_URL, options)
55
+ begin
56
+ response = req.execute
57
+ rescue
58
+ raise GcalMapper::AuthentificationError
59
+ end
60
+ @validity = Time.now.getutc.to_i
61
+
62
+ @access_token = response['access_token']
63
+ response
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,51 @@
1
+ module GcalMapper
2
+ #
3
+ # Provide methods to get google calendar data
4
+ #
5
+ class Calendar
6
+
7
+ # Get the calendar list for the connected user.
8
+ #
9
+ # @param [Hash] access_token the token obtain with Authentification.access_token
10
+ # @return [Array] google calendar id that the user can access.
11
+ def get_calendars_list(access_token)
12
+ url = 'https://www.googleapis.com/calendar/v3/users/me/calendarList'
13
+ options = {
14
+ :method => :get,
15
+ :headers => {'Authorization' => 'Bearer ' + access_token}
16
+ }
17
+ req = GcalMapper::RestRequest.new(url, options)
18
+ response = req.execute
19
+
20
+ tab_cal=response['items']
21
+ ids = []
22
+
23
+ tab_cal.each do |t|
24
+ t.each do |k, v|
25
+ if k == 'id'
26
+ ids.push(v)
27
+ end
28
+ end
29
+ end
30
+
31
+ ids
32
+ end
33
+
34
+ # Get all events from specified calendar(s).
35
+ #
36
+ # @param [Hash] access_token the token obtain with Authentification.access_token
37
+ # @param [Array] calendar_id contain the calendar(s) id you want to map
38
+ # @return [Array] all events from given calendar(s) id.
39
+ def get_events_list(access_token, calendar_id)
40
+ url = 'https://www.googleapis.com/calendar/v3/calendars/'+calendar_id+'/events?showDeleted=true'
41
+ options = {
42
+ :method => :get,
43
+ :headers => {'Authorization' => 'Bearer ' + access_token},
44
+ }
45
+ req = GcalMapper::RestRequest.new(url, options)
46
+ response = req.execute
47
+ response['items']
48
+ end
49
+
50
+ end
51
+ end