gcal4ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,83 @@
1
+ #=GCal4Ruby
2
+ #
3
+ #==Introduction
4
+ #GCal4Ruby is a full featured wrapper for the google calendar API. GCal4Ruby implements
5
+ #all of the functionality available through the Google Calnedar API, including permissions,
6
+ #attendees, reminders and event recurrence.
7
+ #
8
+ #==Author and Contact Information
9
+ #GCal4Ruby was created and is maintained by {Mike Reich}[mailto:mike@seabourneconsulting.com]
10
+ #and is licenses under the GPL v2. Feel free to use and update, but be sure to contribute your
11
+ #code back to the project and attribute as required by the license.
12
+ #
13
+ #==Description
14
+ #GCal4Ruby has three major components: the service, calendar and event objects. Each service
15
+ #has many calendars, which in turn have many events. Each service is the representation of a
16
+ #google account, and thus must be successfully authenticated using valid Google Calendar
17
+ #account credentials.
18
+ #
19
+ #==Examples
20
+ #Below are some common usage examples. For more examples, check the documentation.
21
+ #===Service
22
+ #1. Authenticate
23
+ # service = Service.new
24
+ # service.authenticate("user@gmail.com", "password")
25
+ #
26
+ #2. Get Calendar List
27
+ # calendars = service.calendars
28
+ #
29
+ #===Calendar
30
+ #All usages assume a successfully authenticated Service.
31
+ #1. Create a new Calendar
32
+ # cal = Calendar.new(service)
33
+ #
34
+ #2. Find an existing Calendar
35
+ # cal = Calendar.find(service, "New Calendar", :first)
36
+ #
37
+ #3. Find all calendars containing the search term
38
+ # cal = Calendar.find(service, "Soccer Team")
39
+ #
40
+ #4. Find a calendar by ID
41
+ # cal = Calendar.find(service, id, :first)
42
+ #===Event
43
+ #All usages assume a successfully authenticated Service and valid Calendar.
44
+ #1. Create a new Event
45
+ # event = Event.new(calendar)
46
+ # event.title = "Soccer Game"
47
+ # event.start = Time.parse("12-06-2009 at 12:30 PM")
48
+ # event.end = Time.parse("12-06-2009 at 1:30 PM")
49
+ # event.where = "Merry Playfields"
50
+ # event.save
51
+ #
52
+ #2. Find an existing Event
53
+ # event = Event.find(cal, "Soccer Game", :first)
54
+ #
55
+ #3. Find all events containing the search term
56
+ # event = Event.find(cal, "Soccer Game")
57
+ #
58
+ #4. Create a recurring event for every saturday
59
+ # event = Event.new(calendar)
60
+ # event.title = "Baseball Game"
61
+ # event.where = "Municipal Stadium"
62
+ # event.recurrence = Recurrence.new
63
+ # event.recurrence.start = Time.parse("13-06-2009 at 4:30 PM")
64
+ # event.recurrence.end = Time.parse("13-06-2009 at 6:30 PM")
65
+ # event.recurrence.frequency = {"weekly" => ["SA"]}
66
+ # event.save
67
+ #
68
+ #5. Create an event with a 15 minute email reminder
69
+ # event = Event.new(calendar)
70
+ # event.title = "Dinner with Kate"
71
+ # event.start = Time.parse("20-06-2009 at 5 pm")
72
+ # event.end = Time.parse("20-06-209 at 8 pm")
73
+ # event.where = "Luigi's"
74
+ # event.reminder = {:minutes => 15, :method => 'email'}
75
+ # event.save
76
+ #
77
+ #6. Create an event with attendees
78
+ # event = Event.new(calendar)
79
+ # event.title = "Dinner with Kate"
80
+ # event.start = Time.parse("20-06-2009 at 5 pm")
81
+ # event.end = Time.parse("20-06-209 at 8 pm")
82
+ # event.attendees => {:name => "Kate", :email => "kate@gmail.com"}
83
+ # event.save
@@ -0,0 +1,256 @@
1
+ require "rexml/document"
2
+ require "cgi"
3
+ require "uri"
4
+ require "net/http"
5
+ require "net/https"
6
+ require "open-uri"
7
+ require "nkf"
8
+ require "time"
9
+
10
+ Net::HTTP.version_1_2
11
+
12
+ # GCal4Ruby is a full featured wrapper for the google calendar API
13
+
14
+ # =Usage:
15
+
16
+ module GCal4Ruby
17
+
18
+ CALENDAR_XML = "<entry xmlns='http://www.w3.org/2005/Atom'
19
+ xmlns:gd='http://schemas.google.com/g/2005'
20
+ xmlns:gCal='http://schemas.google.com/gCal/2005'>
21
+ <title type='text'></title>
22
+ <summary type='text'></summary>
23
+ <gCal:timezone value=''></gCal:timezone>
24
+ <gCal:hidden value=''></gCal:hidden>
25
+ <gCal:color value=''></gCal:color>
26
+ <gd:where rel='' label='' valueString=''></gd:where>
27
+ </entry>"
28
+
29
+ ACL_XML = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gAcl='http://schemas.google.com/acl/2007'>
30
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/acl/2007#accessRule'/>
31
+ <gAcl:scope type='default'></gAcl:scope>
32
+ <gAcl:role value=''></gAcl:role>
33
+ </entry>"
34
+
35
+ EVENT_XML = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005'>
36
+ <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'></category>
37
+ <title type='text'></title>
38
+ <content type='text'></content>
39
+ <gd:transparency value=''></gd:transparency>
40
+ <gd:eventStatus value=''></gd:eventStatus>
41
+ <gd:where valueString=''></gd:where>
42
+ <gd:when startTime='' endTime=''></gd:when>
43
+ </entry>
44
+ "
45
+
46
+ class AuthenticationFailed < StandardError; end #:nodoc: all
47
+
48
+ class NotAuthenticated < StandardError; end
49
+
50
+ class InvalidService < StandardError; end
51
+
52
+ class HTTPPostFailed < StandardError; end
53
+
54
+ class HTTPPutFailed < StandardError; end
55
+
56
+ class HTTPGetFailed < StandardError; end
57
+
58
+ class HTTPDeleteFailed < StandardError; end
59
+
60
+ class CalendarSaveFailed < StandardError; end
61
+
62
+ class EventSaveFailed < StandardError; end
63
+
64
+ class RecurrenceValueError < StandardError; end
65
+
66
+ #The ProxyInfo class contains information for configuring a proxy connection
67
+
68
+ class ProxyInfo
69
+ attr_accessor :address, :port, :username, :password
70
+ @address = nil
71
+ @port = nil
72
+ @username = nil
73
+ @password = nil
74
+
75
+ #The initialize function accepts four variables for configuring the ProxyInfo object.
76
+ #The proxy connection is initiated using the builtin Net::HTTP proxy support.
77
+
78
+ def initialize(address, port, username=nil, password=nil)
79
+ @address = address
80
+ @port = port
81
+ @username = username
82
+ @password = password
83
+ end
84
+ end
85
+
86
+ #The Base class includes the basic HTTP methods for sending and receiving
87
+ #messages from the Google Calendar API. You shouldn't have to use this class
88
+ #directly, rather access the functionality through the Service subclass.
89
+
90
+ class Base
91
+ AUTH_URL = "https://www.google.com/accounts/ClientLogin"
92
+ CALENDAR_LIST_FEED = "http://www.google.com/calendar/feeds/default/allcalendars/full"
93
+ @proxy_info = nil
94
+ @auth_token = nil
95
+ @debug = false
96
+
97
+ #Contains the ProxyInfo object for using a proxy server
98
+ attr_accessor :proxy_info
99
+
100
+ #If set to true, debug will dump all raw HTTP requests and responses
101
+ attr_accessor :debug
102
+
103
+ # Sends an HTTP POST request. The header should be a hash of name/value pairs.
104
+ # Returns the Net::HTTPResponse object on succces, or raises the appropriate
105
+ # error if a non 20x response code is received.
106
+ def send_post(url, content, header=nil)
107
+ header = auth_header(header)
108
+ ret = nil
109
+ location = URI.parse(url)
110
+ https = get_http_object(location)
111
+ puts "url = "+url if @debug
112
+ if location.scheme == 'https'
113
+ puts "SSL True" if @debug
114
+ https.use_ssl = true
115
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
116
+ end
117
+ puts "Starting post\nHeader: #{header}\nContent: #{content}" if @debug
118
+ https.start do |http|
119
+ ret = http.post(location.path, content, header)
120
+ end
121
+ while ret.is_a?(Net::HTTPRedirection)
122
+ puts "Redirect recieved, resending post" if @debug
123
+ https.start do |http|
124
+ ret = http.post(ret['location'], content, header)
125
+ end
126
+ end
127
+ if ret.is_a?(Net::HTTPSuccess)
128
+ puts "20x response recieved\nResponse: \n"+ret.read_body if @debug
129
+ return ret
130
+ else
131
+ puts "invalid response received: "+ret.code if @debug
132
+ raise HTTPPostFailed, ret.body
133
+ end
134
+ end
135
+
136
+ # Sends an HTTP PUT request. The header should be a hash of name/value pairs.
137
+ # Returns the Net::HTTPResponse object on succces, or raises the appropriate
138
+ # error if a non 20x response code is received.
139
+ def send_put(url, content, header=nil)
140
+ header = auth_header(header)
141
+ ret = nil
142
+ location = URI.parse(url)
143
+ https = get_http_object(location)
144
+ puts "url = "+url if @debug
145
+ if location.scheme == 'https'
146
+ puts "SSL True" if @debug
147
+ https.use_ssl = true
148
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
149
+ end
150
+ puts "Starting post\nHeader: #{header}\nContent: #{content}" if @debug
151
+ https.start do |http|
152
+ ret = http.put(location.path, content, header)
153
+ end
154
+ while ret.is_a?(Net::HTTPRedirection)
155
+ puts "Redirect recieved, resending post" if @debug
156
+ https.start do |http|
157
+ ret = http.put(ret['location'], content, header)
158
+ end
159
+ end
160
+ if ret.is_a?(Net::HTTPSuccess)
161
+ puts "20x response recieved\nResponse: \n"+ret.read_body if @debug
162
+ return ret
163
+ else
164
+ puts "invalid response received: "+ret.code if @debug
165
+ raise HTTPPutFailed, ret.body
166
+ end
167
+ end
168
+
169
+ # Sends an HTTP GET request. The header should be a hash of name/value pairs.
170
+ # Returns the Net::HTTPResponse object on succces, or raises the appropriate
171
+ # error if a non 20x response code is received.
172
+ def send_get(url, header = nil)
173
+ header = auth_header(header)
174
+ ret = nil
175
+ location = URI.parse(url)
176
+ http = get_http_object(location)
177
+ puts "url = "+url if @debug
178
+ if location.scheme == 'https'
179
+ puts "SSL True" if @debug
180
+ https.use_ssl = true
181
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
182
+ end
183
+ puts "Starting post\nHeader: #{header}\n" if @debug
184
+ http.start do |http|
185
+ ret = http.get(location.path, header)
186
+ end
187
+ while ret.is_a?(Net::HTTPRedirection)
188
+ puts "Redirect recieved, resending post" if @debug
189
+ http.start do |http|
190
+ ret = http.get(ret['location'], header)
191
+ end
192
+ end
193
+ if ret.is_a?(Net::HTTPSuccess)
194
+ puts "20x response recieved\nResponse: \n"+ret.read_body if @debug
195
+ return ret
196
+ else
197
+ puts "Redirect recieved, resending post" if @debug
198
+ raise HTTPGetFailed, ret.body
199
+ end
200
+ end
201
+
202
+ # Sends an HTTP DELETE request. The header should be a hash of name/value pairs.
203
+ # Returns the Net::HTTPResponse object on succces, or raises the appropriate
204
+ # error if a non 20x response code is received.
205
+ def send_delete(url, header = nil)
206
+ header = auth_header(header)
207
+ ret = nil
208
+ location = URI.parse(url)
209
+ https = get_http_object(location)
210
+ puts "url = "+url if @debug
211
+ if location.scheme == 'https'
212
+ puts "SSL True" if @debug
213
+ https.use_ssl = true
214
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
215
+ end
216
+ puts "Starting post\nHeader: #{header}\n" if @debug
217
+ https.start do |http|
218
+ ret = http.delete(location.path, header)
219
+ end
220
+ while ret.is_a?(Net::HTTPRedirection)
221
+ puts "Redirect recieved, resending post" if @debug
222
+ https.start do |http|
223
+ ret = http.delete(ret['location'], header)
224
+ end
225
+ end
226
+ if ret.is_a?(Net::HTTPSuccess)
227
+ puts "20x response recieved\nResponse: \n"+ret.read_body if @debug
228
+ return true
229
+ else
230
+ puts "invalid response received: "+ret.code if @debug
231
+ raise HTTPDeleteFailed, ret.body
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ def get_http_object(location)
238
+ if @proxy_info and @proxy_info.address
239
+ return Net::HTTP.new(location.host, location.port, @proxy_info.address, @proxy_info.port, @proxy_info.username, @proxy_info.password)
240
+ else
241
+ return Net::HTTP.new(location.host, location.port)
242
+ end
243
+ end
244
+
245
+ def auth_header(header)
246
+ if @auth_token
247
+ if header
248
+ header.merge!({'Authorization' => "GoogleLogin auth=#{@auth_token}", "GData-Version" => "2.1"})
249
+ else
250
+ header = {'Authorization' => "GoogleLogin auth=#{@auth_token}", "GData-Version" => "2.1"}
251
+ end
252
+ end
253
+ return header
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,297 @@
1
+ require 'gcal4ruby/event'
2
+
3
+ module GCal4Ruby
4
+ #The Calendar Class is the representation of a Google Calendar. Each user account
5
+ #can have multiple calendars. You must have an authenticated Service object before
6
+ #using the Calendar object.
7
+ #=Usage
8
+ #All usages assume a successfully authenticated Service.
9
+ #1. Create a new Calendar
10
+ # cal = Calendar.new(service)
11
+ #
12
+ #2. Find an existing Calendar
13
+ # cal = Calendar.find(service, "New Calendar", :first)
14
+ #
15
+ #3. Find all calendars containing the search term
16
+ # cal = Calendar.find(service, "Soccer Team")
17
+ #
18
+ #4. Find a calendar by ID
19
+ # cal = Calendar.find(service, id, :first)
20
+ #
21
+ #After a calendar object has been created or loaded, you can change any of the
22
+ #attributes like you would any other object. Be sure to save the calendar to write changes
23
+ #to the Google Calendar service.
24
+
25
+ class Calendar
26
+ CALENDAR_FEED = "http://www.google.com/calendar/feeds/default/owncalendars/full"
27
+
28
+ #The calendar title
29
+ attr_accessor :title
30
+
31
+ #A short description of the calendar
32
+ attr_accessor :summary
33
+
34
+ #The parent Service object passed on initialization
35
+ attr_reader :service
36
+
37
+ #The unique calendar id
38
+ attr_reader :id
39
+
40
+ #Boolean value indicating the calendar visibility
41
+ attr_accessor :hidden
42
+
43
+ #The calendar timezone[http://code.google.com/apis/calendar/docs/2.0/reference.html#gCaltimezone]
44
+ attr_accessor :timezone
45
+
46
+ #The calendar color. Must be one of these[http://code.google.com/apis/calendar/docs/2.0/reference.html#gCalcolor] values.
47
+ attr_accessor :color
48
+
49
+ #The calendar geo location, if any
50
+ attr_accessor :where
51
+
52
+ #A boolean value indicating whether the calendar appears by default when viewed online
53
+ attr_accessor :selected
54
+
55
+ #The event feed for the calendar
56
+ attr_reader :event_feed
57
+
58
+ #Returns true if the calendar exists on the Google Calendar system (i.e. was
59
+ #loaded or has been saved). Otherwise returns false.
60
+ def exists?
61
+ return @exists
62
+ end
63
+
64
+ #Returns true if the calendar is publically accessable, otherwise returns false.
65
+ def public?
66
+ return @public
67
+ end
68
+
69
+ #Returns an array of Event objects corresponding to each event in the calendar.
70
+ def events
71
+ events = []
72
+ ret = @service.send_get(@event_feed)
73
+ REXML::Document.new(ret.body).root.elements.each("entry"){}.map do |entry|
74
+ entry.attributes["xmlns:gCal"] = "http://schemas.google.com/gCal/2005"
75
+ entry.attributes["xmlns:gd"] = "http://schemas.google.com/g/2005"
76
+ entry.attributes["xmlns"] = "http://www.w3.org/2005/Atom"
77
+ e = Event.new(self)
78
+ if e.load(entry.to_s)
79
+ events << e
80
+ end
81
+ end
82
+ return events
83
+ end
84
+
85
+ #Set the calendar to public (p = true) or private (p = false). Publically viewable
86
+ #calendars can be accessed by anyone without having to log in to google calendar. See
87
+ #Calendar#to_iframe for options to display a public calendar in a webpage.
88
+ def public=(p)
89
+ if p
90
+ permissions = 'http://schemas.google.com/gCal/2005#read'
91
+ else
92
+ permissions = 'none'
93
+ end
94
+
95
+ #if p != @public
96
+ path = "http://www.google.com/calendar/feeds/#{@id}/acl/full/default"
97
+ request = REXML::Document.new(ACL_XML)
98
+ request.root.elements.each() do |ele|
99
+ if ele.name == 'role'
100
+ ele.attributes['value'] = permissions
101
+ end
102
+
103
+ end
104
+ if @service.send_put(path, request.to_s, {"Content-Type" => "application/atom+xml", "Content-Length" => request.length.to_s})
105
+ @public = p
106
+ return true
107
+ else
108
+ return false
109
+ end
110
+ #end
111
+ end
112
+
113
+ #Accepts a Service object. Returns the new Calendar if successful, otherwise raises the InvalidService
114
+ #error.
115
+ def initialize(service)
116
+ super()
117
+ if !service.is_a?(Service)
118
+ raise InvalidService
119
+ end
120
+ @xml = CALENDAR_XML
121
+ @service = service
122
+ @exists = false
123
+ @title = ""
124
+ @summary = ""
125
+ @public = false
126
+ @id = nil
127
+ @hidden = false
128
+ @timezone = "America/Los_Angeles"
129
+ @color = "#2952A3"
130
+ @where = ""
131
+ return true
132
+ end
133
+
134
+ #Deletes a calendar. If successful, returns true, otherwise false. If successful, the
135
+ #calendar object is cleared.
136
+ def delete
137
+ if @exists
138
+ if @service.send_delete(CALENDAR_FEED+"/"+@id)
139
+ @exists = false
140
+ @title = nil
141
+ @summary = nil
142
+ @public = false
143
+ @id = nil
144
+ @hidden = false
145
+ @timezone = nil
146
+ @color = nil
147
+ @where = nil
148
+ return true
149
+ else
150
+ return false
151
+ end
152
+ else
153
+ return false
154
+ end
155
+ end
156
+
157
+ #If the calendar does not exist, creates it, otherwise updates the calendar info. Returns
158
+ #true if the save is successful, otherwise false.
159
+ def save
160
+ if @exists
161
+ ret = service.send_put(@edit_feed, to_xml(), {'Content-Type' => 'application/atom+xml'})
162
+ else
163
+ ret = service.send_post(CALENDAR_FEED, to_xml(), {'Content-Type' => 'application/atom+xml'})
164
+ end
165
+ if !@exists
166
+ if load(ret.read_body)
167
+ return true
168
+ else
169
+ raise CalendarSaveFailed
170
+ end
171
+ end
172
+ return true
173
+ end
174
+
175
+ #Class method for querying the google service for specific calendars. The service parameter
176
+ #should be an appropriately authenticated Service. The term parameter can be any string. The
177
+ #scope parameter may be either :all to return an array of matches, or :first to return
178
+ #the first match as a Calendar object.
179
+ def self.find(service, query_term, scope = :all)
180
+ t = query_term.downcase
181
+ cals = service.calendars
182
+ ret = []
183
+ cals.each do |cal|
184
+ title = cal.title || ""
185
+ summary = cal.summary || ""
186
+ id = cal.id || ""
187
+ if title.downcase.match(t) or summary.downcase.match(t) or id.downcase.match(t)
188
+ if scope == :first
189
+ return cal
190
+ elsif scope == :all
191
+ ret << cal
192
+ end
193
+ end
194
+ end
195
+ ret
196
+ end
197
+
198
+ #Reloads the calendar objects information from the stored server version. Returns true
199
+ #if successful, otherwise returns false. Any information not saved will be overwritten.
200
+ def reload
201
+ if not @exists
202
+ return false
203
+ end
204
+ t = Calendar.find(service, @id, :first)
205
+ if t
206
+ load(t.xml)
207
+ else
208
+ return false
209
+ end
210
+ end
211
+
212
+ #Returns the xml representation of the Calenar.
213
+ def to_xml
214
+ xml = REXML::Document.new(@xml)
215
+ xml.root.elements.each(){}.map do |ele|
216
+ case ele.name
217
+ when "title"
218
+ ele.text = @title
219
+ when "summary"
220
+ ele.text = @summary
221
+ when "timezone"
222
+ ele.attributes["value"] = @timezone
223
+ when "hidden"
224
+ ele.attributes["value"] = @hidden
225
+ when "color"
226
+ ele.attributes["value"] = @color
227
+ when "selected"
228
+ ele.attributes["value"] = @selected
229
+ end
230
+ end
231
+ xml.to_s
232
+ end
233
+
234
+ #Loads the Calendar with returned data from Google Calendar feed. Returns true if successful.
235
+ def load(string)
236
+ @exists = true
237
+ @xml = string
238
+ xml = REXML::Document.new(string)
239
+ xml.root.elements.each(){}.map do |ele|
240
+ case ele.name
241
+ when "id"
242
+ @id = ele.text.gsub("http://www.google.com/calendar/feeds/default/calendars/", "")
243
+ when 'title'
244
+ @title = ele.text
245
+ when 'summary'
246
+ @summary = ele.text
247
+ when "color"
248
+ @color = ele.attributes['value']
249
+ when 'hidden'
250
+ @hidden = ele.attributes["value"] == "true" ? true : false
251
+ when 'timezone'
252
+ @timezone = ele.attributes["value"]
253
+ when "selected"
254
+ @selected = ele.attributes["value"] == "true" ? true : false
255
+ when "link"
256
+ if ele.attributes['rel'] == 'edit'
257
+ @edit_feed = ele.attributes['href']
258
+ end
259
+ end
260
+ end
261
+
262
+ @event_feed = "http://www.google.com/calendar/feeds/#{@id}/private/full"
263
+
264
+ puts "Getting ACL Feed" if @service.debug
265
+ ret = @service.send_get("http://www.google.com/calendar/feeds/#{@id}/acl/full/")
266
+ r = REXML::Document.new(ret.read_body)
267
+ r.root.elements.each("entry") do |ele|
268
+ ele.elements.each do |e|
269
+ #puts "e = "+e.to_s if @service.debug
270
+ #puts "previous element = "+e.previous_element.to_s if @service.debug
271
+ if e.name == 'role' and e.previous_element.name == 'scope' and e.previous_element.attributes['type'] == 'default'
272
+ if e.attributes['value'].match('#read')
273
+ @public = true
274
+ else
275
+ @public = false
276
+ end
277
+ end
278
+ end
279
+ end
280
+ return true
281
+ end
282
+
283
+ #Returns a HTML <iframe> tag displaying the calendar.
284
+ def to_iframe(height=300, width=400)
285
+
286
+ end
287
+
288
+ private
289
+ @xml
290
+ @exists = false
291
+ @public = false
292
+ @event_feed = ''
293
+ @edit_feed = ''
294
+
295
+ end
296
+
297
+ end