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.
@@ -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