gcal4ruby 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/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