gcalapi 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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