gcal_mapper 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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