google_client 0.1.0
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.
- 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
@@ -0,0 +1,56 @@
|
|
1
|
+
module GoogleClient
|
2
|
+
|
3
|
+
class Contact
|
4
|
+
|
5
|
+
attr_reader :id
|
6
|
+
attr_accessor :name
|
7
|
+
attr_accessor :email
|
8
|
+
attr_accessor :phone_number
|
9
|
+
attr_accessor :user
|
10
|
+
|
11
|
+
def initialize(params = {})
|
12
|
+
@id = params[:id]
|
13
|
+
@name = params[:name]
|
14
|
+
@email = params[:email]
|
15
|
+
@phone_number = params[:phone_number]
|
16
|
+
@user = params[:user]
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"#{self.class.name} => { name: #{@name}, email: #{@email}, :phone_number => #{@phone_number} }"
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def save
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def build_contact(data, user = nil)
|
30
|
+
id = begin
|
31
|
+
data["id"]["$t"].split("base/").last
|
32
|
+
rescue
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
name = begin
|
36
|
+
data["title"]["$t"].split("full/").last
|
37
|
+
rescue
|
38
|
+
""
|
39
|
+
end
|
40
|
+
email = begin
|
41
|
+
data["gd$email"].collect { |address| address.select { |item| item["address"] }.values }.flatten
|
42
|
+
rescue
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
phone_number = begin
|
46
|
+
data["gd$phoneNumber"].collect { |number| number.select { |item| item["$t"] }.values }.flatten
|
47
|
+
rescue
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
Contact.new({:name => name, :email => email, :phone_number => phone_number, :user => user})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rails'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module GoogleClient
|
5
|
+
#
|
6
|
+
# This class defines the GoogleClient Rails Engine to handle OAuth authentication.
|
7
|
+
# The Engine defines two new routes to handle each of the OAuth steps
|
8
|
+
# 1.- forward the user request to Google server
|
9
|
+
# 2.- get the oauth code, request a valid access token and forward the token info to a user defined action
|
10
|
+
#
|
11
|
+
# How to configure GoogleClient Engine
|
12
|
+
#
|
13
|
+
# :uri => Google API endpoint
|
14
|
+
# :client_id => token that identifies your application in OAuth mechanism
|
15
|
+
# :client_secret => token that secures your communication in OAuth mechanism
|
16
|
+
# :forward_action => controller#action where google_client#code action will redirect the user token data:
|
17
|
+
# - :access_token
|
18
|
+
# - :expires_in
|
19
|
+
# - :refresh_token
|
20
|
+
#
|
21
|
+
# These configuration can be included in an application initializer, i.e. config/initializers/google_client.rb
|
22
|
+
#
|
23
|
+
# Rails.application.config.google_client.client_id = "<client_id>"
|
24
|
+
# Rails.application.config.google_client.client_secret = "<client_secret"
|
25
|
+
# Rails.application.config.google_client.forward_action = "controller#action" that will receive the user token data
|
26
|
+
#
|
27
|
+
|
28
|
+
class Engine < Rails::Engine
|
29
|
+
|
30
|
+
# we need to create the hashblue config hash before loading the application initializer
|
31
|
+
initializer :google_client, {:before => :load_config_initializers} do |app|
|
32
|
+
|
33
|
+
app.config.google_client = OpenStruct.new
|
34
|
+
# HashBlue API endpoint
|
35
|
+
app.config.google_client.uri = "https://www.google.com"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module GoogleClient
|
2
|
+
class Event
|
3
|
+
|
4
|
+
include Format
|
5
|
+
|
6
|
+
attr_reader :id
|
7
|
+
attr_accessor :calendar_id
|
8
|
+
attr_accessor :calendar
|
9
|
+
attr_accessor :title
|
10
|
+
attr_accessor :description
|
11
|
+
attr_accessor :attendees
|
12
|
+
attr_accessor :start_time
|
13
|
+
attr_accessor :end_time
|
14
|
+
attr_accessor :location
|
15
|
+
attr_accessor :attendees
|
16
|
+
attr_accessor :comments
|
17
|
+
|
18
|
+
def initialize(params = {})
|
19
|
+
@id = params[:event_id] || params[:id] || nil
|
20
|
+
@title = params[:title]
|
21
|
+
@description = params[:description]
|
22
|
+
@location = params[:location]
|
23
|
+
@start_time = params[:start_time]
|
24
|
+
@end_time = params[:end_time]
|
25
|
+
@calendar_id = params[:calendar_id]
|
26
|
+
@calendar = params[:calendar]
|
27
|
+
@comments = params[:comments]
|
28
|
+
@attendees = params[:attendees]
|
29
|
+
|
30
|
+
@calendar.nil? or @connection = @calendar.connection
|
31
|
+
@json_mode = true
|
32
|
+
block_given? and yield self
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"#{self.class.name} => { id: #{@id}, title: #{@title}, description: #{@description}, :start_time => #{@start_time}, :end_time => #{@end_time}, :location => #{@location}, :attendees => #{@attendees} }"
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_hash
|
40
|
+
{
|
41
|
+
:title => @title,
|
42
|
+
:details => @description,
|
43
|
+
:timeZone => @timezone,
|
44
|
+
:when => [{:start => @start_time,
|
45
|
+
:end => @end_time}]
|
46
|
+
}.delete_if{|k,v| v.nil?}
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def connection
|
51
|
+
@connection or raise RuntimeError.new "Http connection not established"
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Save the Event in the server
|
56
|
+
# ==== Return
|
57
|
+
# * *Event* instance
|
58
|
+
def save
|
59
|
+
if @id.nil?
|
60
|
+
data = decode_response connection.post("/calendar/feeds/#{calendar}/private/full", {:data => self.to_hash})
|
61
|
+
@id = data["data"]["id"]
|
62
|
+
else
|
63
|
+
# put
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def calendar
|
69
|
+
calendar= if @calendar.nil?
|
70
|
+
if @calendar_id.nil?
|
71
|
+
raise Error.new "Event must be associated to a calendar"
|
72
|
+
else
|
73
|
+
@calendar_id
|
74
|
+
end
|
75
|
+
else
|
76
|
+
@calendar.id
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Delete the Event from the server
|
82
|
+
# ==== Return
|
83
|
+
# * *true* if sucessful
|
84
|
+
# * raise Error if failure
|
85
|
+
def delete
|
86
|
+
@id.nil? and raise Error.new "event cannot be deleted if has not an unique identifier"
|
87
|
+
connection.delete "/calendar/feeds/#{calendar}/private/full/#{@id}", nil, {"If-Match" => "*"}
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Fetch the specific event from Google Calendar
|
93
|
+
# ==== Return
|
94
|
+
# * *Event*
|
95
|
+
def fetch
|
96
|
+
if @calendar_id.nil?
|
97
|
+
@calendar.nil? and raise Error.new "calendar or calendar_id must be valid in the event"
|
98
|
+
@calendar_id = @calendar.id
|
99
|
+
end
|
100
|
+
data = connection.get "/calendar/feeds/#{@calendar_id}/private/full/#{@id}"
|
101
|
+
self.class.build_event data["entry"], @calendar
|
102
|
+
end
|
103
|
+
|
104
|
+
class << self
|
105
|
+
|
106
|
+
def create(params)
|
107
|
+
event = if block_given?
|
108
|
+
new(params, &Proc.new)
|
109
|
+
else
|
110
|
+
new(params)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_event(data, calendar = nil)
|
115
|
+
attendees = data["gd$who"]
|
116
|
+
attendees.nil? or attendees = attendees.map{|attendee| {:name => attendee["valueString"], :email => attendee["email"]}}
|
117
|
+
|
118
|
+
start_time = data["gd$when"][0]["startTime"]
|
119
|
+
end_time = data["gd$when"][0]["endTime"]
|
120
|
+
|
121
|
+
Event.new({:id => data["id"]["$t"].split("full/").last,
|
122
|
+
:calendar => calendar,
|
123
|
+
:title => data["title"]["$t"],
|
124
|
+
:description => data["content"]["$t"],
|
125
|
+
:location => data["gd$where"][0]["valueString"],
|
126
|
+
:comments => data["gd$comments"],
|
127
|
+
:attendees => attendees,
|
128
|
+
:start_time => start_time,
|
129
|
+
:end_time => end_time})
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module GoogleClient
|
2
|
+
|
3
|
+
module Format
|
4
|
+
|
5
|
+
def decode_response(response)
|
6
|
+
response = if json?
|
7
|
+
JSON.parse(response.body)
|
8
|
+
else
|
9
|
+
raise Error.new "Unknow data format"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def json?
|
14
|
+
defined? @json_mode and return @json_mode
|
15
|
+
raise Error.new "Unknown data format"
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'addressable/uri'
|
3
|
+
|
4
|
+
module GoogleClient
|
5
|
+
class HttpConnection
|
6
|
+
|
7
|
+
attr_accessor :headers
|
8
|
+
attr_accessor :access_token
|
9
|
+
attr_accessor :query_params
|
10
|
+
attr_accessor :headers
|
11
|
+
|
12
|
+
def initialize(uri, query_params = {}, headers = {})
|
13
|
+
@headers = headers
|
14
|
+
uri = URI.parse(uri)
|
15
|
+
@host = uri.host
|
16
|
+
@port = uri.port
|
17
|
+
@scheme = uri.scheme
|
18
|
+
@query_params = query_params
|
19
|
+
@headers = headers
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Http::GET request
|
24
|
+
#
|
25
|
+
# ==== Parameters
|
26
|
+
# * *path* URI path
|
27
|
+
# * *query_params* query parameters to be added to the request
|
28
|
+
#
|
29
|
+
# ==== Return
|
30
|
+
# * Hash JSON decoded response
|
31
|
+
def get(path, query_params = {}, headers = {})
|
32
|
+
uri = create_uri(path, self.query_params.merge(query_params))
|
33
|
+
RestClient.get(uri.to_s, self.headers.merge(headers)) do |response, request, result, &block|
|
34
|
+
handle_response(response, request, result, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def post(path, body = {}, headers = {})
|
39
|
+
uri = create_uri(path, {})
|
40
|
+
|
41
|
+
_headers = self.headers.merge(headers)
|
42
|
+
if _headers.has_key?("Content-Type") && _headers["Content-Type"].eql?("json")
|
43
|
+
body.is_a?(Hash) and body = body.to_json
|
44
|
+
end
|
45
|
+
RestClient.post(uri.to_s, body, _headers) do |response, request, result, &block|
|
46
|
+
handle_response(response, request, result, &block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(path, query_params = {}, headers = {})
|
51
|
+
uri = create_uri(path, {})
|
52
|
+
uri = uri.to_s[0..-2]
|
53
|
+
RestClient.delete(uri.to_s, headers.merge({:Authorization => self.headers[:Authorization]})) do |response, request, result, &block|
|
54
|
+
handle_response(response, request, result, &block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_uri(path, query_params = {})
|
59
|
+
Addressable::URI.new({:host => @host,
|
60
|
+
:port => @port,
|
61
|
+
:scheme => @scheme,
|
62
|
+
:path => path,
|
63
|
+
:query => create_query(query_params)})
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
##
|
69
|
+
# This method is used to handle the HTTP response
|
70
|
+
# ==== Parameters
|
71
|
+
# *response* HTTP response
|
72
|
+
# *request* HTTP request
|
73
|
+
# *result*
|
74
|
+
def handle_response(response, request, result, &block)
|
75
|
+
case response.code
|
76
|
+
when 200..207
|
77
|
+
response.return!(request, result, &block)
|
78
|
+
when 301..307
|
79
|
+
response.follow_redirection(request, result, &block)
|
80
|
+
when 400
|
81
|
+
raise BadRequestError.new(result.body)
|
82
|
+
when 401
|
83
|
+
raise AuthenticationError.new(result.body)
|
84
|
+
when 404
|
85
|
+
raise NotFoundError.new(result.body)
|
86
|
+
when 409
|
87
|
+
raise ConflictError.new(result.body)
|
88
|
+
when 422
|
89
|
+
raise Error.new(result.body)
|
90
|
+
when 402..408,410..421,423..499
|
91
|
+
response.return!(request, result, &block)
|
92
|
+
when 500..599
|
93
|
+
raise Error.new(result.body)
|
94
|
+
else
|
95
|
+
response.return!(request, result, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# ==== Parameters
|
101
|
+
# * *query_params* query parameters to be added to the request
|
102
|
+
#
|
103
|
+
# ==== Return
|
104
|
+
# * String with the parameters concatened and escaped using CGI.escape
|
105
|
+
def create_query(query_params)
|
106
|
+
query_params.map do |k, v|
|
107
|
+
"#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
108
|
+
end.join("&")
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module GoogleClient
|
2
|
+
class Profile
|
3
|
+
attr_accessor :email
|
4
|
+
attr_accessor :external_id
|
5
|
+
|
6
|
+
def initialize(params ={})
|
7
|
+
@email = params[:email]
|
8
|
+
@external_id = params[:external_id]
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"#{self.class.name} => { email: #{@email}, external_id: #{@external_id} }"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module GoogleClient
|
2
|
+
class User
|
3
|
+
|
4
|
+
attr_accessor :oauth_credentials
|
5
|
+
attr_accessor :json_mode
|
6
|
+
|
7
|
+
include Format
|
8
|
+
|
9
|
+
def initialize(oauth_credentials, user_credentials = nil)
|
10
|
+
@oauth_credentials = oauth_credentials
|
11
|
+
@json_mode = true
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get user profile
|
15
|
+
#
|
16
|
+
# ==== Return
|
17
|
+
# * Profile instance
|
18
|
+
def profile
|
19
|
+
data = decode_response http.get "/m8/feeds/contacts/default/full", {"max-results" => 1}
|
20
|
+
email = data["feed"]["id"]["$t"]
|
21
|
+
Profile.new({:email => email, :external_id => email.split("@").first})
|
22
|
+
rescue
|
23
|
+
Profile.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Refresh an invalid access_token
|
27
|
+
# ==== Parameters
|
28
|
+
# * *refresh_token*
|
29
|
+
# * *client_id*
|
30
|
+
# * *client_secret*
|
31
|
+
#
|
32
|
+
# ==== Return
|
33
|
+
# * Hash with the following keys:
|
34
|
+
# * access_token
|
35
|
+
# * token_type
|
36
|
+
# * expires_in
|
37
|
+
# * BadRequestError if invalid refresh token or invalid client
|
38
|
+
def refresh refresh_token, client_id, client_secret
|
39
|
+
_params = {
|
40
|
+
:client_id => client_id,
|
41
|
+
:client_secret => client_secret,
|
42
|
+
:refresh_token => refresh_token,
|
43
|
+
:grant_type => "refresh_token"
|
44
|
+
}
|
45
|
+
data = HttpConnection.new("https://accounts.google.com",
|
46
|
+
{:alt => "json"},
|
47
|
+
{:Authorization => "OAuth #{oauth_credentials}",
|
48
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
49
|
+
:Accept => "application/json"}).post "/o/oauth2/token", _params
|
50
|
+
decode_response data.body
|
51
|
+
end
|
52
|
+
|
53
|
+
# Same as refresh method, but also updates the oauth_credentials variable with the new granted token
|
54
|
+
def refresh! refresh_token, client_id, client_secret
|
55
|
+
data = refresh refresh_token, client_id, client_secret
|
56
|
+
@oauth_credentials = data["access_token"]
|
57
|
+
data
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
#
|
62
|
+
# ==== Parameters
|
63
|
+
# * *calendar_id* Calendar unique identifier
|
64
|
+
#
|
65
|
+
# ==== Return
|
66
|
+
# * *Calendar* instance
|
67
|
+
# * *Array of Calendar* instances
|
68
|
+
def calendar(calendar_id = :all)
|
69
|
+
calendars = if calendar_id.nil? || calendar_id.eql?(:all)
|
70
|
+
calendars = decode_response http.get "/calendar/feeds/default/allcalendars/full"
|
71
|
+
calendars = calendars["feed"]["entry"]
|
72
|
+
calendars.map{ |calendar| Calendar.build_calendar(calendar, self)}
|
73
|
+
elsif calendar_id.eql?(:own)
|
74
|
+
calendars = decode_response http.get "/calendar/feeds/default/owncalendars/full"
|
75
|
+
calendars = calendars["feed"]["entry"]
|
76
|
+
calendars.map{ |calendar| Calendar.build_calendar(calendar, self)}
|
77
|
+
elsif calendar_id.is_a?(String)
|
78
|
+
Calendar.new({:id => calendar_id, :user => self}).fetch
|
79
|
+
elsif calendar_id.is_a?(Hash)
|
80
|
+
# TODO add support to {:title => calendar_title}
|
81
|
+
raise ArgumentError.new "Invalid argument type #{calendar_id.class}"
|
82
|
+
else
|
83
|
+
raise ArgumentError.new "Invalid argument type #{calendar_id.class}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Create a calendar
|
88
|
+
#
|
89
|
+
# ==== Parameters
|
90
|
+
# * *params* Hash
|
91
|
+
# * :title
|
92
|
+
# * :details
|
93
|
+
# * :timezone
|
94
|
+
# * :location
|
95
|
+
#
|
96
|
+
# ==== Return
|
97
|
+
# Calendar instance
|
98
|
+
def create_calendar(params)
|
99
|
+
calendar = if block_given?
|
100
|
+
Calendar.create(params.merge({:user => self}), &Proc.new)
|
101
|
+
else
|
102
|
+
Calendar.create(params.merge({:user => self}))
|
103
|
+
end
|
104
|
+
calendar.save
|
105
|
+
end
|
106
|
+
|
107
|
+
# Fetch user contacts
|
108
|
+
def contacts
|
109
|
+
contacts = decode_response http.get "/m8/feeds/contacts/default/full", {"max-results" => "1000"}
|
110
|
+
contacts = contacts["feed"]["entry"]
|
111
|
+
contacts.map{|contact| Contact.build_contact(contact, self)}
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Method that creates and returns the HttpConnection instance that shall be used
|
116
|
+
#
|
117
|
+
# ==== Return
|
118
|
+
# * *HttpConnection* instance
|
119
|
+
def http
|
120
|
+
@http ||= HttpConnection.new("https://www.google.com",
|
121
|
+
{:alt => "json"},
|
122
|
+
{:Authorization => "OAuth #{oauth_credentials}",
|
123
|
+
"Content-Type" => "json",
|
124
|
+
:Accept => "application/json"})
|
125
|
+
end
|
126
|
+
|
127
|
+
alias :connection :http
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|