gcalapi 0.0.3
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 +13 -0
- data/Rakefile +82 -0
- data/VERSION +1 -0
- data/example/mail2gcal.rb +101 -0
- data/example/ol2gcal.rb +55 -0
- data/html/classes/GoogleCalendar.html +281 -0
- data/html/classes/GoogleCalendar/AuthenticationFailed.html +111 -0
- data/html/classes/GoogleCalendar/Calendar.html +204 -0
- data/html/classes/GoogleCalendar/Event.html +511 -0
- data/html/classes/GoogleCalendar/EventDeleteFailed.html +111 -0
- data/html/classes/GoogleCalendar/EventInsertFailed.html +111 -0
- data/html/classes/GoogleCalendar/EventUpdateFailed.html +111 -0
- data/html/classes/GoogleCalendar/InvalidCalendarURL.html +111 -0
- data/html/classes/GoogleCalendar/Service.html +472 -0
- data/html/created.rid +1 -0
- data/html/files/README.html +117 -0
- data/html/files/lib/googlecalendar/calendar_rb.html +110 -0
- data/html/files/lib/googlecalendar/event_rb.html +110 -0
- data/html/files/lib/googlecalendar/service_rb.html +114 -0
- data/html/fr_class_index.html +35 -0
- data/html/fr_file_index.html +30 -0
- data/html/fr_method_index.html +49 -0
- data/html/index.html +24 -0
- data/html/rdoc-style.css +208 -0
- data/lib/googlecalendar/calendar.rb +117 -0
- data/lib/googlecalendar/event.rb +283 -0
- data/lib/googlecalendar/service.rb +193 -0
- data/test/00_service_test.rb +169 -0
- data/test/01_calendar_test.rb +52 -0
- data/test/02_event_test.rb +60 -0
- data/test/all.sh +2 -0
- data/test/base_unit.rb +54 -0
- data/test/each.sh +2 -0
- metadata +85 -0
@@ -0,0 +1,283 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
require "time"
|
3
|
+
require "nkf"
|
4
|
+
|
5
|
+
module GoogleCalendar
|
6
|
+
class EventInsertFailed < StandardError; end #:nodoc: all
|
7
|
+
class EventUpdateFailed < StandardError; end #:nodoc: all
|
8
|
+
class EventDeleteFailed < StandardError; end #:nodoc: all
|
9
|
+
|
10
|
+
#
|
11
|
+
# = Summary
|
12
|
+
# this class represents an event of a calendar.
|
13
|
+
#
|
14
|
+
# = How to use this class
|
15
|
+
#
|
16
|
+
# * MAIL: your gmail account.
|
17
|
+
# * PASS: password for MAIL.
|
18
|
+
# * FEED: a calendar's editable feed url.
|
19
|
+
# 1. click "Manage Calendars" in Google Calendar.
|
20
|
+
# 2. select a calendar you want to edit.
|
21
|
+
# 3. copy private address of XML.
|
22
|
+
# 4. change the address's end into "/private/full".
|
23
|
+
# If your calendar's private XML address is
|
24
|
+
# "http://www.google.com/calendar/feeds/XXX@group.calendar.google.com/private-aaaaaa/basic",
|
25
|
+
# the editable feed url is
|
26
|
+
# "http://www.google.com/calendar/feeds/XXX@group.calendar.google.com/private/full".
|
27
|
+
# 5. for detail, See http://code.google.com/apis/gdata/calendar.html#Visibility.
|
28
|
+
#
|
29
|
+
# == create new events
|
30
|
+
#
|
31
|
+
# cal = Calendar.new(Service.new(MAIL, PASS), FEED)
|
32
|
+
# event = cal.create_event
|
33
|
+
# event.title = "event title"
|
34
|
+
# event.desc = "event description"
|
35
|
+
# event.where = "event location"
|
36
|
+
# event.st = Time.mktime(2006, 9, 21, 01, 0, 0)
|
37
|
+
# event.en = Time.mktime(2006, 9, 21, 03, 0, 0)
|
38
|
+
# event.save!
|
39
|
+
#
|
40
|
+
# == udpate existing events
|
41
|
+
#
|
42
|
+
# cal = Calendar.new(Service.new(MAIL, PASS), FEED)
|
43
|
+
# event = cal.events[0]
|
44
|
+
# event.title = "event title"
|
45
|
+
# event.desc = "event description"
|
46
|
+
# event.where = "event location"
|
47
|
+
# event.st = Time.mktime(2006, 9, 21, 01, 0, 0)
|
48
|
+
# event.en = Time.mktime(2006, 9, 21, 03, 0, 0)
|
49
|
+
# event.save!
|
50
|
+
#
|
51
|
+
# == delete events
|
52
|
+
#
|
53
|
+
# cal = Calendar.new(Service.new(MAIL, PASS), FEED)
|
54
|
+
# event = cal.events[0]
|
55
|
+
# event.destroy!
|
56
|
+
#
|
57
|
+
# == create all day events.
|
58
|
+
#
|
59
|
+
# event = cal.create_event
|
60
|
+
# event.title = "1 days event"
|
61
|
+
# event.st = Time.mktime(2006, 9, 22)
|
62
|
+
# event.en = Time.mktime(2006, 9, 23)
|
63
|
+
# event.allday = true
|
64
|
+
# event.save!
|
65
|
+
#
|
66
|
+
# event = cal.create_event
|
67
|
+
# event.title = "2 days event"
|
68
|
+
# event.st = Time.mktime(2006, 9, 22)
|
69
|
+
# event.en = Time.mktime(2006, 9, 24)
|
70
|
+
# event.allday = true
|
71
|
+
# event.save!
|
72
|
+
#
|
73
|
+
# = TODO
|
74
|
+
#
|
75
|
+
# * this class doesn't support recurring event.
|
76
|
+
#
|
77
|
+
|
78
|
+
class Event
|
79
|
+
ATTRIBUTES_MAP = {
|
80
|
+
"title" => { "element" => "title"},
|
81
|
+
"desc" => { "element" => "content"},
|
82
|
+
"where" => { "element" => "gd:where", "attribute" => "valueString" },
|
83
|
+
"st" => { "element" => "gd:when", "attribute" => "startTime", "to_xml" => "time_to_str", "from_xml" => "str_to_time" },
|
84
|
+
"en" => { "element" => "gd:when", "attribute" => "endTime", "to_xml" => "time_to_str", "from_xml" => "str_to_time" }
|
85
|
+
}
|
86
|
+
|
87
|
+
SKELTON = <<XML
|
88
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
89
|
+
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gd='http://schemas.google.com/g/2005'>
|
90
|
+
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'></category>
|
91
|
+
<title type='text'></title>
|
92
|
+
<content type='text'></content>
|
93
|
+
<gd:transparency value='http://schemas.google.com/g/2005#event.opaque'></gd:transparency>
|
94
|
+
<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'></gd:eventStatus>
|
95
|
+
</entry>
|
96
|
+
XML
|
97
|
+
|
98
|
+
attr_accessor :allday, :feed, :srv, :status, :where, :title, :desc, :st, :en, :xml
|
99
|
+
|
100
|
+
def initialize()
|
101
|
+
@xml = nil
|
102
|
+
self.status = :new
|
103
|
+
end
|
104
|
+
|
105
|
+
# load xml into this instance
|
106
|
+
def load_xml(str)
|
107
|
+
@xml = REXML::Document.new(str.to_s)
|
108
|
+
xml_to_instance
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
# same as save! If failed, this method returns false.
|
113
|
+
def save
|
114
|
+
do_without_exception(:save!)
|
115
|
+
end
|
116
|
+
|
117
|
+
# save this event into google calendar server. If failed, this method throws an Exception.
|
118
|
+
def save!
|
119
|
+
ret = nil
|
120
|
+
case self.status
|
121
|
+
when :new
|
122
|
+
ret = @srv.insert(self.feed, self.to_s)
|
123
|
+
raise EventInsertFailed, ret.body unless ret.code == "201"
|
124
|
+
when :old
|
125
|
+
ret = @srv.update(self.feed, self.to_s)
|
126
|
+
raise EventUpdateFailed, ret.body unless ret.code == "200"
|
127
|
+
when :deleted
|
128
|
+
raise EventDeleteFailed, "already deleted"
|
129
|
+
else
|
130
|
+
raise StandardError, "invalid inner status"
|
131
|
+
end
|
132
|
+
load_xml(ret.body)
|
133
|
+
end
|
134
|
+
|
135
|
+
# same as destroy! If failed, this method returns false.
|
136
|
+
def destroy
|
137
|
+
do_without_exception(:destroy!)
|
138
|
+
end
|
139
|
+
|
140
|
+
# delete this event from google calendar server. If failed, this method throws an Exception.
|
141
|
+
def destroy!
|
142
|
+
ret = nil
|
143
|
+
if self.status == :old
|
144
|
+
ret = @srv.delete(self.feed)
|
145
|
+
raise EventDeleteFailed, "Not Deleted" unless ret.code == "200"
|
146
|
+
else
|
147
|
+
raise EventDeleteFailed, "Not Saved"
|
148
|
+
end
|
149
|
+
status = :deleted
|
150
|
+
end
|
151
|
+
|
152
|
+
# retuns this event's xml.
|
153
|
+
def to_s
|
154
|
+
@xml = REXML::Document.new(SKELTON) if self.status == :new
|
155
|
+
instance_to_xml
|
156
|
+
@xml.to_s
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def do_without_exception(method)
|
162
|
+
ret = true
|
163
|
+
begin
|
164
|
+
self.send(method)
|
165
|
+
rescue
|
166
|
+
ret = false
|
167
|
+
end
|
168
|
+
ret
|
169
|
+
end
|
170
|
+
|
171
|
+
# set xml data to attributes of an instance
|
172
|
+
def xml_to_instance
|
173
|
+
ATTRIBUTES_MAP.each do |name, hash|
|
174
|
+
elem = @xml.root.elements[hash["element"]]
|
175
|
+
unless elem.nil?
|
176
|
+
val = (hash.has_key?("attribute") ? elem.attributes[hash["attribute"]] : elem.text)
|
177
|
+
val = self.send(hash["from_xml"], val) if hash.has_key?("from_xml")
|
178
|
+
self.send(name+"=", val)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
self.status = :old
|
182
|
+
|
183
|
+
@xml.root.elements.each("link") do |link|
|
184
|
+
@feed = link.attributes["href"] if link.attributes["rel"] == "edit"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# set attributes of an instance into xml
|
189
|
+
def instance_to_xml
|
190
|
+
ATTRIBUTES_MAP.each do |name, hash|
|
191
|
+
elem = @xml.root.elements[hash["element"]]
|
192
|
+
elem = @xml.root.elements.add(hash["element"]) if elem.nil?
|
193
|
+
val = self.send(name)
|
194
|
+
val = self.send(hash["to_xml"], val) if hash.has_key?("to_xml")
|
195
|
+
if hash.has_key?("attribute")
|
196
|
+
elem.attributes[hash["attribute"]] = val
|
197
|
+
else
|
198
|
+
elem.text = val
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# == Allday Event Bugs
|
204
|
+
# When creating all day event, the format of gd:when startTime and gd:when endTime must
|
205
|
+
# be "yyyy-mm-ddZ" which represents UTC. otherwise the wrong data returns.
|
206
|
+
# below is the test result. I used 3 countries' calendar. US, UK, and Japan.
|
207
|
+
# And in each calendar, I created all day events in three types of date format.
|
208
|
+
# A) yyyy-mm-dd
|
209
|
+
# B) yyyy-mm-ddZ
|
210
|
+
# C) yyyy-mm-dd+(-)hh:mm
|
211
|
+
# only type B format always returns the correct data.
|
212
|
+
#
|
213
|
+
# 1) US calendar (all type is OK)
|
214
|
+
# A: input start => 2006-09-18, end => 2006-09-19
|
215
|
+
# output start => 2006-09-18, end => 2006-09-19
|
216
|
+
#
|
217
|
+
# B: input start => 2006-09-18Z,end => 2006-09-19Z
|
218
|
+
# output start => 2006-09-18, end => 2006-09-19
|
219
|
+
#
|
220
|
+
# C: input start => 2006-09-18-08:00,end => 2006-09-19-08:00
|
221
|
+
# output start => 2006-09-18, end => 2006-09-19
|
222
|
+
#
|
223
|
+
# 2) UK calenar (A returns wrong data. B and C is OK)
|
224
|
+
# A: input start => 2006-09-18, end => 2006-09-19
|
225
|
+
# output start => 2006-09-17, end => 2006-09-18
|
226
|
+
#
|
227
|
+
# B: input start => 2006-09-18Z,end => 2006-09-19Z
|
228
|
+
# output start => 2006-09-18, end => 2006-09-19
|
229
|
+
#
|
230
|
+
# C: input start => 2006-09-18-00:00,end => 2006-09-19-00:00
|
231
|
+
# output start => 2006-09-18, end => 2006-09-19
|
232
|
+
#
|
233
|
+
# 3) Japan calendar (A and C returns wrong data. only B is OK)
|
234
|
+
# A: input start => 2006-09-18, end => 2006-09-19
|
235
|
+
# output start => 2006-09-17, end => 2006-09-18
|
236
|
+
#
|
237
|
+
# B: input start => 2006-09-18Z,end => 2006-09-19Z
|
238
|
+
# output start => 2006-09-18, end => 2006-09-19
|
239
|
+
#
|
240
|
+
# C: input start => 2006-09-18+09:00,end => 2006-09-19+09:00
|
241
|
+
# output start => 2006-09-17, end => 2006-09-18
|
242
|
+
#
|
243
|
+
# convert String to Time
|
244
|
+
def str_to_time(st)
|
245
|
+
ret = nil
|
246
|
+
if st.is_a? Time then
|
247
|
+
ret = st
|
248
|
+
elsif st.is_a? String then
|
249
|
+
begin
|
250
|
+
self.allday = false
|
251
|
+
ret = Time.iso8601(st)
|
252
|
+
rescue
|
253
|
+
self.allday = true if st =~ /\d{4}-\d{2}-\d{2}/ # yyyy-mm-dd
|
254
|
+
ret = Time.parse(st)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
ret
|
258
|
+
end
|
259
|
+
|
260
|
+
# returns string represents date or datetime
|
261
|
+
def time_to_str(dt)
|
262
|
+
ret = nil
|
263
|
+
if dt.nil?
|
264
|
+
ret = ""
|
265
|
+
else
|
266
|
+
ret = dt.iso8601
|
267
|
+
ret[10..-1] = "Z" if self.allday # yyyy-mm-ddZ
|
268
|
+
end
|
269
|
+
ret
|
270
|
+
end
|
271
|
+
|
272
|
+
# convert string to numeric character reference
|
273
|
+
def num_char_ref(str)
|
274
|
+
str.to_s.split(//u).map do |c|
|
275
|
+
if /[[:alnum:][:space:][:punct:]]/.match(c) then
|
276
|
+
c
|
277
|
+
else
|
278
|
+
"&##{c.unpack('U*')[0]};"
|
279
|
+
end
|
280
|
+
end.join
|
281
|
+
end
|
282
|
+
end #class Event
|
283
|
+
end #module GoogleCalendar
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "uri"
|
3
|
+
require "net/http"
|
4
|
+
require "net/https"
|
5
|
+
require "open-uri"
|
6
|
+
require "nkf"
|
7
|
+
require "time"
|
8
|
+
|
9
|
+
Net::HTTP.version_1_2
|
10
|
+
|
11
|
+
module GoogleCalendar
|
12
|
+
|
13
|
+
class AuthenticationFailed < StandardError; end #:nodoc: all
|
14
|
+
|
15
|
+
#
|
16
|
+
# This class interacts with google calendar service.
|
17
|
+
#
|
18
|
+
class Service
|
19
|
+
# Server name to Authenticate
|
20
|
+
AUTH_SERVER = "www.google.com"
|
21
|
+
|
22
|
+
# Server Path to authenticate
|
23
|
+
AUTH_PATH = "/accounts/ClientLogin"
|
24
|
+
|
25
|
+
# URL to get calendar list
|
26
|
+
CALENDAR_LIST_PATH = "http://www.google.com/calendar/feeds/"
|
27
|
+
|
28
|
+
# proxy server address
|
29
|
+
@@proxy_addr = nil
|
30
|
+
def self.proxy_addr
|
31
|
+
@@proxy_addr
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.proxy_addr=(addr)
|
35
|
+
@@proxy_addr=addr
|
36
|
+
end
|
37
|
+
|
38
|
+
# proxy server port number
|
39
|
+
@@proxy_port = nil
|
40
|
+
def self.proxy_port
|
41
|
+
@@proxy_port
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.proxy_port=(port)
|
45
|
+
@@proxy_port = port
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(email, pass)
|
49
|
+
@email = email
|
50
|
+
@pass = pass
|
51
|
+
@session = nil
|
52
|
+
@cookie = nil
|
53
|
+
@auth = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# get the list of user's calendars and returns http response object
|
58
|
+
#
|
59
|
+
def calendar_list
|
60
|
+
auth unless @auth
|
61
|
+
uri = URI.parse(CALENDAR_LIST_PATH + @email)
|
62
|
+
do_get(uri, "Authorization" => "GoogleLogin auth=#{@auth}")
|
63
|
+
end
|
64
|
+
|
65
|
+
alias :calendars :calendar_list
|
66
|
+
|
67
|
+
#
|
68
|
+
# send query for events of a calendar and returns http response object.
|
69
|
+
# available condtions:
|
70
|
+
# :q => query string
|
71
|
+
# :max-results => max contents count. (default: 25)
|
72
|
+
# :start-index => 1-based index of the first result to be retrieved
|
73
|
+
# :orderby => the order of retrieved data.
|
74
|
+
# :published-min => Bounds on the entry publication date(oldest)
|
75
|
+
# :published-max => Bounds on the entry publication date(newest)
|
76
|
+
# :updated-min => Bounds on the entry update date(oldest)
|
77
|
+
# :updated-max => Bounds on the entry update date(newest)
|
78
|
+
# :author => Entry author
|
79
|
+
#
|
80
|
+
def query(cal_url, conditions)
|
81
|
+
auth unless @auth
|
82
|
+
uri = URI.parse(cal_url)
|
83
|
+
uri.query = conditions.map do |key, val|
|
84
|
+
"#{key}=#{URI.escape(val.kind_of?(Time) ? val.getutc.iso8601 : val.to_s)}"
|
85
|
+
end.join("&")
|
86
|
+
do_get(uri, "Authorization" => "GoogleLogin auth=#{@auth}")
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# delete an event.
|
91
|
+
#
|
92
|
+
def delete(feed)
|
93
|
+
auth unless @auth
|
94
|
+
uri = URI.parse(feed)
|
95
|
+
do_post(uri,
|
96
|
+
{"X-HTTP-Method-Override" => "DELETE",
|
97
|
+
"Authorization" => "GoogleLogin auth=#{@auth}"},
|
98
|
+
"DELETE " + uri.path)
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# insert an event
|
103
|
+
#
|
104
|
+
def insert(feed, event)
|
105
|
+
auth unless @auth
|
106
|
+
uri = URI.parse(feed)
|
107
|
+
do_post(uri,
|
108
|
+
{"Authorization" => "GoogleLogin auth=#{@auth}",
|
109
|
+
"Content-Type" => "application/atom+xml",
|
110
|
+
"Content-Length" => event.length.to_s}, event)
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# update an event.
|
115
|
+
#
|
116
|
+
def update(feed, event)
|
117
|
+
auth unless @auth
|
118
|
+
uri = URI.parse(feed)
|
119
|
+
do_post(uri,
|
120
|
+
{"X-HTTP-Method-Override" => "PUT",
|
121
|
+
"Authorization" => "GoogleLogin auth=#{@auth}",
|
122
|
+
"Content-Type" => "application/atom+xml",
|
123
|
+
"Content-Length" => event.length.to_s}, event)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# authencate
|
129
|
+
def auth
|
130
|
+
https = Net::HTTP.new(AUTH_SERVER, 443, @@proxy_addr, @@proxy_port)
|
131
|
+
https.use_ssl = true
|
132
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
133
|
+
head = {'Content-Type' => 'application/x-www-form-urlencoded'}
|
134
|
+
https.start do |w|
|
135
|
+
res = w.post(AUTH_PATH, "Email=#{@email}&Passwd=#{@pass}&source=company-app-1&service=cl", head)
|
136
|
+
if res.body =~ /Auth=(.+)/
|
137
|
+
@auth = $1
|
138
|
+
else
|
139
|
+
raise AuthenticationFailed
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def do_post(uri, header, content)
|
145
|
+
res = nil
|
146
|
+
try_http(uri, header, content) do |http,path,head,args|
|
147
|
+
cont = args[0]
|
148
|
+
res = http.post(path, cont, head)
|
149
|
+
end
|
150
|
+
res
|
151
|
+
end
|
152
|
+
|
153
|
+
def do_get(uri, header)
|
154
|
+
res = nil
|
155
|
+
try_http(uri, header) do |http,path,head|
|
156
|
+
res = http.get(path, head)
|
157
|
+
end
|
158
|
+
res
|
159
|
+
end
|
160
|
+
|
161
|
+
def try_http(uri, header, *args)
|
162
|
+
res = nil
|
163
|
+
Net::HTTP.start(uri.host, uri.port, @@proxy_addr, @@proxy_port) do |http|
|
164
|
+
header["Cookie"] = @cookie if @cookie
|
165
|
+
res = yield(http, path_with_authorized_query(uri), header, args)
|
166
|
+
if res.code == "302"
|
167
|
+
ck = sess = nil
|
168
|
+
ck = res["set-cookie"] if res.key?("set-cookie")
|
169
|
+
uri = URI.parse(res["location"]) if res.key?("location")
|
170
|
+
if uri && uri.query
|
171
|
+
qr = CGI.parse(uri.query)
|
172
|
+
sess = qr["gsessionid"][0] if qr.key?("gsessionid")
|
173
|
+
end
|
174
|
+
if ck && sess
|
175
|
+
header["Cookie"] = @cookie = ck
|
176
|
+
@session = sess
|
177
|
+
res = yield(http, path_with_authorized_query(uri), header, args)
|
178
|
+
else
|
179
|
+
p res
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
res
|
184
|
+
end
|
185
|
+
|
186
|
+
def path_with_authorized_query(uri)
|
187
|
+
query = CGI.parse(uri.query.nil? ? "" : uri.query)
|
188
|
+
query["gsessionid"] = [@session] if @session
|
189
|
+
qs = query.map do |k,v| "#{CGI.escape(k)}=#{CGI.escape(v[0])}" end.join("&")
|
190
|
+
qs.empty? ? uri.path : "#{uri.path}?#{qs}"
|
191
|
+
end
|
192
|
+
end # class Service
|
193
|
+
end # module
|