google_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.md +92 -0
- data/Rakefile +2 -0
- data/app/controllers/google_client_controller.rb +79 -0
- data/app/views/google_client/show.html.erb +54 -0
- data/config/routes.rb +9 -0
- data/google_client.gemspec +25 -0
- data/lib/google_client.rb +26 -0
- data/lib/google_client/calendar.rb +181 -0
- data/lib/google_client/contact.rb +56 -0
- data/lib/google_client/engine.rb +39 -0
- data/lib/google_client/error.rb +13 -0
- data/lib/google_client/event.rb +135 -0
- data/lib/google_client/format.rb +19 -0
- data/lib/google_client/http_connection.rb +113 -0
- data/lib/google_client/profile.rb +15 -0
- data/lib/google_client/user.rb +130 -0
- data/lib/google_client/version.rb +3 -0
- data/spec/calendar_spec.rb +33 -0
- data/spec/contact_spec.rb +39 -0
- data/spec/event_spec.rb +20 -0
- data/spec/google_client_spec.rb +9 -0
- data/spec/spec_helper.rb +116 -0
- data/spec/user_spec.rb +103 -0
- metadata +133 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color --format doc
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
|
2
|
+
This gem can be used to get access to a specific set of Google APIs.
|
3
|
+
|
4
|
+
# Installation
|
5
|
+
|
6
|
+
gem install google_client
|
7
|
+
|
8
|
+
# Features
|
9
|
+
|
10
|
+
* Rails Engine to launch OAuth mechanism
|
11
|
+
* Refresh OAuth token
|
12
|
+
* Google Calendar
|
13
|
+
* Fetch the list of user calendars
|
14
|
+
* Create a new calendar
|
15
|
+
* Retrieve a specific calendar
|
16
|
+
* Fetch calendar events, filtering by specific event id, time interval (and more coming)
|
17
|
+
* Delete calendar
|
18
|
+
* Create event
|
19
|
+
* Delete event
|
20
|
+
* Google Contacts
|
21
|
+
* Fetch user contacts
|
22
|
+
|
23
|
+
# Getting started
|
24
|
+
|
25
|
+
Any request that may be done on behalf of the user needs a valid authentication token:
|
26
|
+
|
27
|
+
require "gogole_client"
|
28
|
+
user = GoogleClient::User.new "user-authentication-token"
|
29
|
+
|
30
|
+
# Get user contacts
|
31
|
+
contacts = user.contacts
|
32
|
+
|
33
|
+
# Get user calendars
|
34
|
+
calendars = user.calendars
|
35
|
+
|
36
|
+
# Get a specific user calendar
|
37
|
+
calendar = user.calendars("<calendar-id>")
|
38
|
+
|
39
|
+
# Fetch calendar events
|
40
|
+
events = calendar.events
|
41
|
+
|
42
|
+
# Create calendar
|
43
|
+
calendar = user.create_calendar({:title => "my-new-calendar",
|
44
|
+
:details => "Hello world",
|
45
|
+
:timezone => "Spain", :location => "Barcelona"})
|
46
|
+
|
47
|
+
# ...
|
48
|
+
|
49
|
+
Take a look on the [user.rb](blob/master/lib/google_client/user.rb) file to check the available methods
|
50
|
+
|
51
|
+
# Roadmap
|
52
|
+
|
53
|
+
* Enhance filters
|
54
|
+
* Handle contacts
|
55
|
+
* Create a new contact
|
56
|
+
* Delete an existing contact
|
57
|
+
* Update a contact
|
58
|
+
* Add XML format support. Currently (except the OAuth requests) all the requests/responses are JSON encoded. It may be nice to add support for XML too.
|
59
|
+
|
60
|
+
# Note on Patches/Pull Requests
|
61
|
+
|
62
|
+
* Fork the project
|
63
|
+
* Make your feature addition or bug fix.
|
64
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
65
|
+
* Commit, do not mess with rakefile, version, or history.
|
66
|
+
* If you want to have your own version, that is fine but bump version in a commit by itself so I can ignore when I pull
|
67
|
+
* Send me a pull request. Bonus points for topic branches.
|
68
|
+
|
69
|
+
# License
|
70
|
+
|
71
|
+
The MIT License
|
72
|
+
|
73
|
+
Copyright (c) 2011 Juan de Bravo
|
74
|
+
|
75
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
76
|
+
a copy of this software and associated documentation files (the
|
77
|
+
'Software'), to deal in the Software without restriction, including
|
78
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
79
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
80
|
+
permit persons to whom the Software is furnished to do so, subject to
|
81
|
+
the following conditions:
|
82
|
+
|
83
|
+
The above copyright notice and this permission notice shall be
|
84
|
+
included in all copies or substantial portions of the Software.
|
85
|
+
|
86
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
87
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
88
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
89
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
90
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
91
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
92
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
class GoogleClientController < ApplicationController
|
3
|
+
|
4
|
+
# parameters required in both OAuth steps
|
5
|
+
DEFAULT_PARAMS = {
|
6
|
+
:client_id => Rails.application.config.google_client.client_id
|
7
|
+
}
|
8
|
+
|
9
|
+
# OAuth step1: redirect to Google endpoint with the application idenfitifer and the redirect uri
|
10
|
+
def index
|
11
|
+
_params = {
|
12
|
+
:redirect_uri => Rails.application.config.google_client.redirect_uri,
|
13
|
+
:response_type => "code",
|
14
|
+
:scope => Rails.application.config.google_client.scope,
|
15
|
+
}
|
16
|
+
_params.merge!(DEFAULT_PARAMS)
|
17
|
+
|
18
|
+
redirect_to connection.create_uri("/o/oauth2/auth", _params).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
# OAuth step2: retrieve the code from Google, ask for a valid access token and
|
22
|
+
# forward to the user defined uri
|
23
|
+
def oauth2callback
|
24
|
+
# This code is retrieved from Google
|
25
|
+
|
26
|
+
_params = {
|
27
|
+
:redirect_uri => Rails.application.config.google_client.redirect_uri,
|
28
|
+
:client_secret => Rails.application.config.google_client.client_secret,
|
29
|
+
:grant_type => "authorization_code",
|
30
|
+
:code => params[:code]
|
31
|
+
}
|
32
|
+
|
33
|
+
_params.merge!(DEFAULT_PARAMS)
|
34
|
+
|
35
|
+
response = connection.post "/o/oauth2/token", _params
|
36
|
+
|
37
|
+
if response.code.to_s.eql?("200")
|
38
|
+
response = ActiveSupport::JSON.decode(response.body)
|
39
|
+
|
40
|
+
if Rails.application.config.google_client.forward_action.nil? or !Rails.application.config.google_client.forward_action.is_a?(String)
|
41
|
+
raise GoogleClient::Error.new("Invalid forward_action value")
|
42
|
+
end
|
43
|
+
|
44
|
+
url = Rails.application.config.google_client.forward_action.split("#")
|
45
|
+
|
46
|
+
url.length == 2 or raise GoogleClient::Error.new("Invalid forward_action value")
|
47
|
+
|
48
|
+
@data = response
|
49
|
+
|
50
|
+
redirect_to ({
|
51
|
+
:controller => url.first,
|
52
|
+
:action => url.last,
|
53
|
+
:access_token => response["access_token"],
|
54
|
+
:token_type => response["token_type"],
|
55
|
+
:expires_in => response["expires_in"],
|
56
|
+
:refresh_token => response["refresh_token"]
|
57
|
+
})
|
58
|
+
|
59
|
+
else
|
60
|
+
logger.error "Error #{response.code} while accessing to Google #{response.body}"
|
61
|
+
raise RuntimeError, "Unable to access to Google"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def show
|
66
|
+
@data = {:access_token => params[:access_token],
|
67
|
+
:token_type => params[:token_type],
|
68
|
+
:expires_in => params[:expires_in],
|
69
|
+
:refresh_token => params[:refresh_token]}
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def connection
|
76
|
+
@connection ||= GoogleClient::HttpConnection.new("https://accounts.google.com")
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<style >
|
2
|
+
#oauth {
|
3
|
+
margin-left: 10%;
|
4
|
+
width: 80%;
|
5
|
+
font-size: 1em;
|
6
|
+
font-family: 'Verdana', 'Helvetica', 'Arial';
|
7
|
+
}
|
8
|
+
|
9
|
+
#oauth h2{
|
10
|
+
margin-top: 50px;
|
11
|
+
color: #47C3D3;
|
12
|
+
}
|
13
|
+
|
14
|
+
#oauth dt{
|
15
|
+
float: left;
|
16
|
+
font-weight: bold;
|
17
|
+
width: 30%;
|
18
|
+
}
|
19
|
+
|
20
|
+
#oauth dd{
|
21
|
+
width: 70%;
|
22
|
+
}
|
23
|
+
|
24
|
+
#oauth p{
|
25
|
+
margin-top: 50px;
|
26
|
+
}
|
27
|
+
|
28
|
+
#oauth .email{
|
29
|
+
float: right;
|
30
|
+
}
|
31
|
+
|
32
|
+
</style>
|
33
|
+
|
34
|
+
<div id="oauth">
|
35
|
+
<h2>OAuth Engine</h2>
|
36
|
+
<p><h4>This is the data retrieved as result of the OAuth process:</h4></p>
|
37
|
+
<dl>
|
38
|
+
<dt>User access token</dt>
|
39
|
+
<dd><%=@data[:access_token]%></dd>
|
40
|
+
<dt>Token type</dt>
|
41
|
+
<dd><%=@data[:token_type]%></dd>
|
42
|
+
<dt>Expires in</dt>
|
43
|
+
<dd><%=@data[:expires_in]%></dd>
|
44
|
+
<dt>Refresh token</dt>
|
45
|
+
<dd><%=@data[:refresh_token]%></dd>
|
46
|
+
</dl>
|
47
|
+
<p>
|
48
|
+
So now that you have it working, please go to the configuration initializer and set up a cool forward page instead of this :)
|
49
|
+
</p>
|
50
|
+
<p class="email">
|
51
|
+
juandebravo at gmail dot com
|
52
|
+
</p>
|
53
|
+
|
54
|
+
</div>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/google_client/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["juandebravo"]
|
6
|
+
gem.email = ["juandebravo@gmail.com"]
|
7
|
+
gem.summary = %q{Ease way to get access to Google API.}
|
8
|
+
gem.description = %q{This gem is a wrapper on top of the Google API that allows a developer to handle calendars, events, contacts on behalf of the user.}
|
9
|
+
gem.homepage = "http://www.github.com/juandebravo/google_client"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "google_client"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = GoogleClient::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency("rest-client")
|
19
|
+
gem.add_dependency("addressable")
|
20
|
+
|
21
|
+
gem.add_development_dependency("rake")
|
22
|
+
gem.add_development_dependency("rspec")
|
23
|
+
gem.add_development_dependency("webmock")
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'json'
|
2
|
+
require "google_client/version"
|
3
|
+
|
4
|
+
require 'google_client/engine' if defined?(Rails)
|
5
|
+
|
6
|
+
module GoogleClient
|
7
|
+
|
8
|
+
autoload :AuthenticationError, 'google_client/error'
|
9
|
+
autoload :BadRequestError , 'google_client/error'
|
10
|
+
autoload :Calendar , 'google_client/calendar'
|
11
|
+
autoload :Contact , 'google_client/contact'
|
12
|
+
autoload :Error , 'google_client/error'
|
13
|
+
autoload :Event , 'google_client/event'
|
14
|
+
autoload :Format , 'google_client/format'
|
15
|
+
autoload :HttpConnection , 'google_client/http_connection'
|
16
|
+
autoload :NotFoundError , 'google_client/error'
|
17
|
+
autoload :Profile , 'google_client/profile'
|
18
|
+
autoload :User , 'google_client/user'
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def create_client(oauth_credentials)
|
22
|
+
User.new(oauth_credentials)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
module GoogleClient
|
2
|
+
class Calendar
|
3
|
+
|
4
|
+
BASE_URL = "https://www.google.com/calendar/feeds"
|
5
|
+
|
6
|
+
include Format
|
7
|
+
|
8
|
+
attr_accessor :id
|
9
|
+
attr_accessor :title
|
10
|
+
attr_accessor :details
|
11
|
+
attr_accessor :timezone
|
12
|
+
attr_accessor :location
|
13
|
+
|
14
|
+
def initialize(params)
|
15
|
+
@user = params[:user]
|
16
|
+
@user.nil? or @connection = @user.connection
|
17
|
+
@id = params[:calendar_id] || params[:id]
|
18
|
+
@title = params[:title]
|
19
|
+
@details = params[:details]
|
20
|
+
@timezone = params[:timezone]
|
21
|
+
@location = params[:location]
|
22
|
+
@json_mode = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"#{self.class.name} => { id: #{@id}, title: #{@title}, :timezone => #{@timezone}, :location => #{@location} }"
|
27
|
+
end
|
28
|
+
|
29
|
+
def connection
|
30
|
+
@connection or raise RuntimeError.new "Http connection not established"
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Save the Calendar in the server
|
35
|
+
#
|
36
|
+
# @return Event instance
|
37
|
+
def save
|
38
|
+
if @id.nil?
|
39
|
+
data = decode_response connection.post("/calendar/feeds/default/owncalendars/full", {:data => self.to_hash})
|
40
|
+
self.id = data["data"]["id"].split("full/").last
|
41
|
+
else
|
42
|
+
data = decode_response connection.put("/calendar/feeds/default/owncalendars/full/#{@id}", {:apiVersion => "2.3", :data => self.to_hash})
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete
|
48
|
+
@id.nil? and raise Error.new "calendar cannot be deleted if has not an unique identifier"
|
49
|
+
connection.delete("/calendar/feeds/default/owncalendars/full/#{@id}")
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_hash
|
54
|
+
{
|
55
|
+
:title => @title,
|
56
|
+
:details => @details,
|
57
|
+
:timeZone => @timezone,
|
58
|
+
:location => @location
|
59
|
+
}.delete_if{|k,v| v.nil?}
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Fetch a set of the events from the calendar
|
64
|
+
# ==== Parameters
|
65
|
+
# * *params*: Hash
|
66
|
+
# ** *id*: optional, specific event identifier. If present, no other parameter will be considered
|
67
|
+
# ** *from*: optional, if *to* is present and from is not, all events starting till *to* will be fetched
|
68
|
+
# ** *to*: optional, if *from* is present and to is not, all events starting after *from* will be fetched
|
69
|
+
#
|
70
|
+
# ==== Return
|
71
|
+
# * Nil if no event matches the query parameters.
|
72
|
+
# * Event instance if only one Event matches the query parameter.
|
73
|
+
# * An array of Event instances if more than one Event matches the query parameter.
|
74
|
+
#
|
75
|
+
def events(args = nil)
|
76
|
+
events = if args.nil? || args.eql?(:all)
|
77
|
+
events = decode_response connection.get "/calendar/feeds/#{id}/private/full"
|
78
|
+
events = events["feed"]["entry"]
|
79
|
+
events.map{ |event| Event.build_event(event, self)}
|
80
|
+
elsif args.is_a?(String)
|
81
|
+
raise Error.new "Unable to fetch a Event by id"
|
82
|
+
#Event.new({:id => args, :calendar => self}).fetch
|
83
|
+
elsif args.is_a?(Hash)
|
84
|
+
if args.is_a?(Hash) && args.has_key?(:id)
|
85
|
+
Event.new({:id => args[:id], :calendar => self}).fetch
|
86
|
+
else
|
87
|
+
params = { "start-min" => args[:from],
|
88
|
+
"start-max" => args[:to]}
|
89
|
+
events = decode_response connection.get "/calendar/feeds/#{id}/private/full", params
|
90
|
+
events = events["feed"]["entry"]
|
91
|
+
events = events.nil? ? [] : events.map{ |event| Event.build_event(event, self)}
|
92
|
+
end
|
93
|
+
else
|
94
|
+
raise ArgumentError.new "Invalid argument type #{args.class}"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Fetch forthcoming events in the folowing *time* minutes
|
101
|
+
def forthcoming_events(time = 3600)
|
102
|
+
events({:from => Time.now.strftime("%Y-%m-%dT%k:%M:%S").concat(timezone).gsub(/ /,"0"),
|
103
|
+
:to => (Time.now + time).strftime("%Y-%m-%dT%k:%M:%S").concat(timezone).gsub(/ /,"0")})
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_event(params = {})
|
107
|
+
event = if block_given?
|
108
|
+
Event.create(params.merge({:calendar => self}), &Proc.new)
|
109
|
+
else
|
110
|
+
Event.create(params.merge({:calendar => self}))
|
111
|
+
end
|
112
|
+
event.save
|
113
|
+
end
|
114
|
+
|
115
|
+
def delete_event(event_id)
|
116
|
+
end
|
117
|
+
|
118
|
+
def fetch
|
119
|
+
data = decode_response connection.get "/calendar/feeds/default/owncalendars/full/#{id}"
|
120
|
+
self.class.build_calendar data["entry"], @user
|
121
|
+
end
|
122
|
+
|
123
|
+
# Helper to get the Timezone in rfc3339 format
|
124
|
+
def timezone
|
125
|
+
@@timezone ||= (
|
126
|
+
timezone = Time.now.strftime("%z")
|
127
|
+
timezone[0..2].concat(":").concat(timezone[3..-1])
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
class << self
|
132
|
+
def create(params)
|
133
|
+
calendar = if block_given?
|
134
|
+
new(params, &Proc.new)
|
135
|
+
else
|
136
|
+
new(params)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def build_calendar(data, user = nil)
|
141
|
+
|
142
|
+
id = begin
|
143
|
+
data["id"]["$t"].split("full/").last
|
144
|
+
rescue
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
details = begin
|
149
|
+
data["summary"]["$t"]
|
150
|
+
rescue
|
151
|
+
""
|
152
|
+
end
|
153
|
+
title = begin
|
154
|
+
data["title"]["$t"]
|
155
|
+
rescue
|
156
|
+
""
|
157
|
+
end
|
158
|
+
|
159
|
+
timezone = begin
|
160
|
+
data["gCal$timezone"]["value"]
|
161
|
+
rescue
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
|
165
|
+
location = begin
|
166
|
+
data["gd$where"][0]["valueString"]
|
167
|
+
rescue
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
Calendar.new({:id => id,
|
172
|
+
:user => user,
|
173
|
+
:title => title,
|
174
|
+
:details => details,
|
175
|
+
:location => location,
|
176
|
+
:timezone => timezone})
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|