rtmapi 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README +236 -0
- data/gpl.txt +339 -0
- data/lib/rtmapi.rb +597 -0
- data/test/data/test_get_method_exceptions.1.xml +2 -0
- data/test/data/test_get_method_exceptions.2.xml +2 -0
- data/test/data/test_get_method_exceptions.3.xml +2 -0
- data/test/data/test_rtm_auth_checkToken_bad.1.xml +2 -0
- data/test/data/test_rtm_auth_checkToken_good.1.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.1.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.2.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.3.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.4.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.5.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.6.xml +2 -0
- data/test/data/test_rtm_contacts_getList.1.xml +2 -0
- data/test/data/test_rtm_get_frob.1.xml +2 -0
- data/test/data/test_rtm_get_task_and_is_complete.1.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.1.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.2.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.3.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.4.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.5.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.6.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.7.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.8.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.9.xml +2 -0
- data/test/data/test_rtm_groups_getList.1.xml +2 -0
- data/test/data/test_rtm_lists_getList.1.xml +2 -0
- data/test/data/test_rtm_lists_setName.1.xml +2 -0
- data/test/data/test_rtm_lists_setName.2.xml +2 -0
- data/test/data/test_rtm_lists_setName.3.xml +2 -0
- data/test/data/test_rtm_lists_setName.4.xml +2 -0
- data/test/data/test_rtm_lists_setName.5.xml +2 -0
- data/test/data/test_rtm_lists_setName.6.xml +2 -0
- data/test/data/test_rtm_reflection_getMethods.1.xml +2 -0
- data/test/data/test_rtm_settings_getList.1.xml +2 -0
- data/test/data/test_rtm_tasks_add.1.xml +2 -0
- data/test/data/test_rtm_tasks_add.2.xml +2 -0
- data/test/data/test_rtm_tasks_add.3.xml +2 -0
- data/test/data/test_rtm_tasks_getList.1.xml +2 -0
- data/test/data/test_rtm_tasks_getList_all.1.xml +2 -0
- data/test/data/test_rtm_tasks_getList_from_smartlist.1.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.1.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.2.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.3.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.4.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.5.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.6.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.1.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.2.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.3.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.4.xml +2 -0
- data/test/data/test_rtm_test_echo.1.xml +2 -0
- data/test/data/test_rtm_time_parse.1.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.1.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.2.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.3.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.4.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.5.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.6.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.1.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.2.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.3.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.4.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.5.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.6.xml +2 -0
- data/test/data/test_rtm_timezones_getList.1.xml +2 -0
- data/test/data/test_rtm_user.1.xml +2 -0
- data/test/test-rtmapi.rb +506 -0
- metadata +131 -0
data/lib/rtmapi.rb
ADDED
@@ -0,0 +1,597 @@
|
|
1
|
+
# This file is part of the RTM Ruby API Wrapper.
|
2
|
+
#
|
3
|
+
# The RTM Ruby API Wrapper is free software; you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as
|
5
|
+
# published by the Free Software Foundation; either version 2 of the
|
6
|
+
# License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# The RTM Ruby API Wrapper is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with the RTM Ruby API Wrapper; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
#
|
17
|
+
# (c) 2006, QuantumFoam.org, Inc.
|
18
|
+
|
19
|
+
|
20
|
+
require 'uri'
|
21
|
+
require 'md5'
|
22
|
+
require 'cgi'
|
23
|
+
require 'net/http'
|
24
|
+
require 'date'
|
25
|
+
require 'time'
|
26
|
+
require 'parsedate'
|
27
|
+
require 'rubygems'
|
28
|
+
require 'xml/libxml'
|
29
|
+
|
30
|
+
|
31
|
+
#TODO: allow specifying whether retval should be indexed by rtm_id or list name for lists
|
32
|
+
|
33
|
+
class RememberTheMilk
|
34
|
+
RUBY_API_VERSION = '0.5'
|
35
|
+
# you can just put set these here so you don't have to pass them in with
|
36
|
+
# every constructor call
|
37
|
+
API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
38
|
+
API_SHARED_SECRET = 'xxxxxxxxxxxxxxxx'
|
39
|
+
|
40
|
+
|
41
|
+
Element = 0
|
42
|
+
CloseTag = 1
|
43
|
+
Tag = 2
|
44
|
+
Attributes = 3
|
45
|
+
#SelfContainedElement = 4
|
46
|
+
TextNode = 4
|
47
|
+
|
48
|
+
TagName = 0
|
49
|
+
TagHash = 1
|
50
|
+
|
51
|
+
|
52
|
+
attr_accessor :debug, :auth_token, :return_raw_response, :api_key, :shared_secret, :max_connection_attempts, :use_user_tz
|
53
|
+
|
54
|
+
def user
|
55
|
+
@user_info_cache[auth_token] ||= auth.checkToken.user
|
56
|
+
end
|
57
|
+
|
58
|
+
def user_settings
|
59
|
+
@user_settings_cache[auth_token]
|
60
|
+
end
|
61
|
+
|
62
|
+
def time_to_user_tz( time )
|
63
|
+
return time unless(use_user_tz && auth_token && defined?(TZInfo::Timezone.get))
|
64
|
+
begin
|
65
|
+
unless defined?(@user_settings_cache[auth_token]) && defined?(@user_settings_cache[auth_token][:tz])
|
66
|
+
@user_settings_cache[auth_token] = settings.getList
|
67
|
+
@user_settings_cache[auth_token][:tz] = TZInfo::Timezone.get(@user_settings_cache[auth_token].timezone)
|
68
|
+
end
|
69
|
+
debug "returning time in local zone(%s/%s)", @user_settings_cache[auth_token].timezone, @user_settings_cache[auth_token][:tz]
|
70
|
+
@user_settings_cache[auth_token][:tz].utc_to_local(time)
|
71
|
+
rescue Exception => err
|
72
|
+
debug "unable to read local timezone for auth_token<%s>, ignoring timezone. err<%s>", auth_token, err
|
73
|
+
time
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def logout_user(auth_token)
|
78
|
+
@auth_token = nil if @auth_token == auth_token
|
79
|
+
@user_settings_cache.delete(auth_token)
|
80
|
+
@user_info_cache.delete(auth_token)
|
81
|
+
end
|
82
|
+
|
83
|
+
# TODO: test efficacy of using https://www.rememberthemilk.com/services/rest/
|
84
|
+
def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, endpoint = 'http://www.rememberthemilk.com/services/rest/' )
|
85
|
+
@max_connection_attempts = 3
|
86
|
+
@debug = false
|
87
|
+
@api_key = api_key
|
88
|
+
@shared_secret = shared_secret
|
89
|
+
@uri = URI.parse(endpoint)
|
90
|
+
@auth_token = nil
|
91
|
+
@return_raw_response = false
|
92
|
+
@use_user_tz = true
|
93
|
+
@user_settings_cache = {}
|
94
|
+
@user_info_cache = {}
|
95
|
+
@xml_parser = XML::Parser.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def version() RUBY_API_VERSION end
|
99
|
+
|
100
|
+
def debug(*args)
|
101
|
+
return unless @debug
|
102
|
+
if defined?(RAILS_DEFAULT_LOGGER)
|
103
|
+
RAILS_DEFAULT_LOGGER.warn( sprintf(*args) )
|
104
|
+
else
|
105
|
+
$stderr.puts(sprintf(*args))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def auth_url( perms = 'delete' )
|
110
|
+
auth_url = 'http://www.rememberthemilk.com/services/auth/'
|
111
|
+
args = { 'api_key' => @api_key, 'perms' => perms }
|
112
|
+
args['api_sig'] = sign_request(args)
|
113
|
+
return auth_url + '?' + args.keys.collect {|k| "#{k}=#{args[k]}"}.join('&')
|
114
|
+
end
|
115
|
+
|
116
|
+
# this is a little fragile. it assumes we are being invoked with RTM api calls
|
117
|
+
# (which are two levels deep)
|
118
|
+
# e.g.,
|
119
|
+
# rtm = RememberTheMilk.new
|
120
|
+
# data = rtm.reflection.getMethodInfo('method_name' => 'rtm.test.login')
|
121
|
+
# the above line gets turned into two calls, the first to this, which returns
|
122
|
+
# an RememberTheMilkAPINamespace object, which then gets *its* method_missing
|
123
|
+
# invoked with 'getMethodInfo' and the above args
|
124
|
+
# i.e.,
|
125
|
+
# rtm.foo.bar
|
126
|
+
# rtm.foo() => a
|
127
|
+
# a.bar
|
128
|
+
|
129
|
+
def method_missing( symbol, *args )
|
130
|
+
rtm_namespace = symbol.id2name
|
131
|
+
debug("method_missing called with namespace <%s>", rtm_namespace)
|
132
|
+
RememberTheMilkAPINamespace.new( rtm_namespace, self )
|
133
|
+
end
|
134
|
+
|
135
|
+
def xml_node_to_hash( node, recursion_level = 0 )
|
136
|
+
result = xml_attributes_to_hash( node.properties )
|
137
|
+
if node.element? == false
|
138
|
+
result[node.name.to_sym] = node.content
|
139
|
+
else
|
140
|
+
node.each do |child|
|
141
|
+
name = child.name.to_sym
|
142
|
+
value = xml_node_to_hash( child, recursion_level+1 )
|
143
|
+
|
144
|
+
# if we have the same node name appear multiple times, we need to build up an array
|
145
|
+
# of the converted nodes
|
146
|
+
if !result.has_key?(name)
|
147
|
+
result[name] = value
|
148
|
+
elsif result[name].class != Array
|
149
|
+
result[name] = [result[name], value]
|
150
|
+
else
|
151
|
+
result[name] << value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# top level nodes should be a hash no matter what
|
157
|
+
(recursion_level == 0 || result.values.size > 1) ? result : result.values[0]
|
158
|
+
end
|
159
|
+
|
160
|
+
def xml_attributes_to_hash( attributes, class_name = RememberTheMilkHash )
|
161
|
+
hash = class_name.send(:new)
|
162
|
+
attributes.each {|a| hash[a.name.to_sym] = a.value} if attributes.respond_to?(:each)
|
163
|
+
return hash
|
164
|
+
end
|
165
|
+
|
166
|
+
def index_data_into_hash( data, key )
|
167
|
+
new_hash = RememberTheMilkHash.new
|
168
|
+
|
169
|
+
if data.class == Array
|
170
|
+
data.each {|datum| new_hash[datum[key]] = datum }
|
171
|
+
else
|
172
|
+
new_hash[data[key]] = data
|
173
|
+
end
|
174
|
+
|
175
|
+
new_hash
|
176
|
+
end
|
177
|
+
|
178
|
+
def parse_response(response,method,args)
|
179
|
+
# groups -- an array of group obj
|
180
|
+
# group -- some attributes and a possible contacts array
|
181
|
+
# contacts -- an array of contact obj
|
182
|
+
# contact -- just attributes
|
183
|
+
# lists -- array of list obj
|
184
|
+
# list -- attributes and possible filter obj, and a set of taskseries objs?
|
185
|
+
# task sereies obj are always wrapped in a list. why?
|
186
|
+
# taskseries -- set of attributes, array of tags, an rrule, participants array of contacts, notes,
|
187
|
+
# and task. created and modified are time obj,
|
188
|
+
# task -- attributes, due/added are time obj
|
189
|
+
# note -- attributes and a body of text, with created and modified time obj
|
190
|
+
# time -- convert to a time obj
|
191
|
+
# timeline -- just has a body of text
|
192
|
+
return true unless response.keys.size > 1 # empty response (stat only)
|
193
|
+
|
194
|
+
rtm_transaction = nil
|
195
|
+
if response.has_key?(:transaction)
|
196
|
+
# debug("got back <%s> elements in my transaction", response[:transaction].keys.size)
|
197
|
+
# we just did a write operation, got back a transaction AND some data.
|
198
|
+
# Now, we will do some fanciness.
|
199
|
+
rtm_transaction = response[:transaction]
|
200
|
+
end
|
201
|
+
|
202
|
+
response_types = response.keys - [:stat, :transaction]
|
203
|
+
|
204
|
+
if response.has_key?(:api_key) # echo call, we assume
|
205
|
+
response_type = :echo
|
206
|
+
data = response
|
207
|
+
elsif response_types.size > 1
|
208
|
+
error = RememberTheMilkAPIError.new({:code => "666", :msg=>"found more than one response type[#{response_types.join(',')}]"},method,args)
|
209
|
+
debug( "%s", error )
|
210
|
+
raise error
|
211
|
+
else
|
212
|
+
response_type = response_types[0] || :transaction
|
213
|
+
data = response[response_type]
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
case response_type
|
218
|
+
when :auth
|
219
|
+
when :frob
|
220
|
+
when :echo
|
221
|
+
when :transaction
|
222
|
+
when :timeline
|
223
|
+
when :methods
|
224
|
+
when :settings
|
225
|
+
when :contact
|
226
|
+
when :group
|
227
|
+
# no op
|
228
|
+
|
229
|
+
when :tasks
|
230
|
+
new_hash = RememberTheMilkHash.new
|
231
|
+
if data.class == Array # a bunch of lists
|
232
|
+
data.each do |list|
|
233
|
+
if list.class == String # empty list, just an id, so we create a stub
|
234
|
+
new_list = RememberTheMilkHash.new
|
235
|
+
new_list[:id] = list
|
236
|
+
list = new_list
|
237
|
+
end
|
238
|
+
new_hash[list[:id]] = process_task_list( list[:id], list.arrayify_value(:taskseries) )
|
239
|
+
end
|
240
|
+
data = new_hash
|
241
|
+
elsif data.class == RememberTheMilkHash # only one list
|
242
|
+
data = process_task_list( data[:id], data[:taskseries] )
|
243
|
+
elsif data.class == NilClass # empty list
|
244
|
+
data = new_hash
|
245
|
+
else # who knows...
|
246
|
+
debug( "got a class of (%s) when processing tasks. passing it on through", data.class )
|
247
|
+
end
|
248
|
+
when :groups
|
249
|
+
# contacts expected to be array, so look at each group and fix it's contact
|
250
|
+
data = [data] unless data.class == Array # won't be array if there's only one group. normalize here
|
251
|
+
data.each do |datum|
|
252
|
+
datum.arrayify_value( :contacts )
|
253
|
+
end
|
254
|
+
data = index_data_into_hash( data, :id )
|
255
|
+
when :time
|
256
|
+
data = time_to_user_tz( Time.parse(data[:text]) )
|
257
|
+
when :timezones
|
258
|
+
data = index_data_into_hash( data, :name )
|
259
|
+
when :lists
|
260
|
+
data = index_data_into_hash( data, :id )
|
261
|
+
when :contacts
|
262
|
+
data = [data].compact unless data.class == Array
|
263
|
+
when :list
|
264
|
+
# rtm.tasks.add returns one of these, which looks like this:
|
265
|
+
# <rsp stat='ok'><transaction id='978920558' undoable='0'/><list id='761280'><taskseries name='Try out Remember The Milk' modified='2006-12-19T22:07:50Z' url='' id='1939553' created='2006-12-19T22:07:50Z' source='api'><tags/><participants/><notes/><task added='2006-12-19T22:07:50Z' completed='' postponed='0' priority='N' id='2688677' has_due_time='0' deleted='' estimate='' due=''/></taskseries></list></rsp>
|
266
|
+
# rtm.lists.add also returns this, but it looks like this:
|
267
|
+
# <rsp stat='ok'><transaction id='978727001' undoable='0'/><list name='PersonalClone2' smart='0' id='761266' archived='0' deleted='0' position='0' locked='0'/></rsp>
|
268
|
+
# so we can look for a name attribute
|
269
|
+
if !data.has_key?(:name)
|
270
|
+
data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
|
271
|
+
data = data.values[0] if data.values.size == 1
|
272
|
+
end
|
273
|
+
else
|
274
|
+
throw "Unsupported reply type<#{response_type}>#{response.inspect}"
|
275
|
+
end
|
276
|
+
|
277
|
+
if rtm_transaction
|
278
|
+
if !data.respond_to?(:keys)
|
279
|
+
new_hash = RememberTheMilkHash.new
|
280
|
+
new_hash[response_type] = data
|
281
|
+
data = new_hash
|
282
|
+
end
|
283
|
+
|
284
|
+
if data.keys.size == 0
|
285
|
+
data = rtm_transaction
|
286
|
+
else
|
287
|
+
data[:rtm_transaction] = rtm_transaction if rtm_transaction
|
288
|
+
end
|
289
|
+
end
|
290
|
+
return data
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
def process_task_list( list_id, list )
|
295
|
+
return {} unless list
|
296
|
+
tasks = RememberTheMilkHash.new
|
297
|
+
|
298
|
+
list.each do |taskseries_as_hash|
|
299
|
+
taskseries = RememberTheMilkTask.new.merge(taskseries_as_hash)
|
300
|
+
|
301
|
+
taskseries[:parent_list] = list_id # parent pointers are nice
|
302
|
+
taskseries[:tasks] = taskseries.arrayify_value(:task)
|
303
|
+
taskseries.arrayify_value(:tags)
|
304
|
+
taskseries.arrayify_value(:participants)
|
305
|
+
|
306
|
+
# TODO is there a ruby lib that speaks rrule?
|
307
|
+
taskseries[:recurrence] = nil
|
308
|
+
if taskseries[:rrule]
|
309
|
+
taskseries[:recurrence] = taskseries[:rrule]
|
310
|
+
taskseries[:recurrence][:rule] = taskseries[:rrule][:text]
|
311
|
+
end
|
312
|
+
|
313
|
+
taskseries[:completed] = nil
|
314
|
+
taskseries.tasks.each do |item|
|
315
|
+
if item.has_key?(:due) && item.due != ''
|
316
|
+
item.due = time_to_user_tz( Time.parse(item.due) )
|
317
|
+
end
|
318
|
+
|
319
|
+
if item.has_key?(:completed) && item.completed != '' && taskseries[:completed] == nil
|
320
|
+
taskseries[:completed] = true
|
321
|
+
else # once we set it to false, it can't get set to true
|
322
|
+
taskseries[:completed] = false
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# TODO: support past tasks?
|
327
|
+
tasks[taskseries[:id]] = taskseries
|
328
|
+
end
|
329
|
+
|
330
|
+
return tasks
|
331
|
+
end
|
332
|
+
|
333
|
+
def call_api_method( method, args={} )
|
334
|
+
|
335
|
+
unless args.has_key?(:test_data)
|
336
|
+
args['method'] = "rtm.#{method}"
|
337
|
+
args['api_key'] = @api_key
|
338
|
+
args['auth_token'] ||= @auth_token if @auth_token
|
339
|
+
|
340
|
+
# make sure everything in our arguments is a string
|
341
|
+
args.each do |key,value|
|
342
|
+
key_s = key.to_s
|
343
|
+
args.delete(key) if key.class != String
|
344
|
+
args[key_s] = value.to_s
|
345
|
+
end
|
346
|
+
|
347
|
+
args['api_sig'] = sign_request(args)
|
348
|
+
|
349
|
+
debug( 'rtm.%s(%s)', method, args.inspect )
|
350
|
+
end
|
351
|
+
|
352
|
+
attempts_left = @max_connection_attempts
|
353
|
+
|
354
|
+
begin
|
355
|
+
if args.has_key?(:test_data)
|
356
|
+
@xml_parser.string = args[:test_data]
|
357
|
+
else
|
358
|
+
attempts_left -= 1
|
359
|
+
response = Net::HTTP.get_response(@uri.host, "#{@uri.path}?#{args.keys.collect {|k| "#{CGI::escape(k).gsub(/ /,'+')}=#{CGI::escape(args[k]).gsub(/ /,'+')}"}.join('&')}")
|
360
|
+
debug('RESPONSE code: %s\n%sEND RESPONSE\n', response.code, response.body)
|
361
|
+
@xml_parser.string = response.body
|
362
|
+
end
|
363
|
+
|
364
|
+
raw_data = @xml_parser.parse
|
365
|
+
data = xml_node_to_hash( raw_data.root )
|
366
|
+
|
367
|
+
if data[:stat] != 'ok'
|
368
|
+
debug( "processed into data<#{data.inspect}>")
|
369
|
+
error = RememberTheMilkAPIError.new(data[:err],method,args)
|
370
|
+
debug( "%s", error )
|
371
|
+
raise error
|
372
|
+
end
|
373
|
+
return return_raw_response ? @xml_parser.string : parse_response(data,method,args)
|
374
|
+
rescue XML::Parser::ParseError => err
|
375
|
+
debug("Unable to parse document.\nGot response:%s\nGot Error:\n", response.body, err.to_s)
|
376
|
+
raise err
|
377
|
+
rescue Timeout::Error => timeout
|
378
|
+
$stderr.puts "Timed out to<#{endpoint}>, trying #{attempts_left} more times"
|
379
|
+
if attempts_left > 0
|
380
|
+
retry
|
381
|
+
else
|
382
|
+
raise timeout
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def sign_request( args )
|
388
|
+
return MD5.md5(@shared_secret + args.sort.flatten.join).to_s
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
## a pretty crappy exception class, but it should be sufficient for bubbling
|
394
|
+
## up errors returned by the RTM API (website)
|
395
|
+
class RememberTheMilkAPIError < RuntimeError
|
396
|
+
attr_reader :response, :error_code, :error_message
|
397
|
+
|
398
|
+
def initialize(error, method, args_to_method)
|
399
|
+
@method_name = method
|
400
|
+
@args_to_method = args_to_method
|
401
|
+
@error_code = error[:code].to_i
|
402
|
+
@error_message = error[:msg]
|
403
|
+
end
|
404
|
+
|
405
|
+
def to_s
|
406
|
+
"Calling rtm.#{@method_name}(#{@args_to_method.inspect}) produced => <#{@error_code}>: #{@error_message}"
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
|
411
|
+
## this is just a helper class so that you can do things like
|
412
|
+
## rtm.test.echo. the method_missing in RememberTheMilkAPI returns one of
|
413
|
+
## these.
|
414
|
+
## this class is the "test" portion of the programming. its method_missing then
|
415
|
+
## get invoked with "echo" as the symbol. it has stored a reference to the original
|
416
|
+
## rtm object, so it can then invoke call_api_method
|
417
|
+
class RememberTheMilkAPINamespace
|
418
|
+
def initialize(namespace, rtm)
|
419
|
+
@namespace = namespace
|
420
|
+
@rtm = rtm
|
421
|
+
end
|
422
|
+
|
423
|
+
def method_missing( symbol, *args )
|
424
|
+
method_name = symbol.id2name
|
425
|
+
@rtm.call_api_method( "#{@namespace}.#{method_name}", *args)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
## a standard hash with some helper methods
|
430
|
+
class RememberTheMilkHash < Hash
|
431
|
+
attr_accessor :rtm
|
432
|
+
|
433
|
+
@@strict_keys = true
|
434
|
+
def self.strict_keys=( value )
|
435
|
+
@@strict_keys = value
|
436
|
+
end
|
437
|
+
|
438
|
+
def initialize(rtm_object = nil)
|
439
|
+
super
|
440
|
+
@rtm = rtm_object
|
441
|
+
end
|
442
|
+
|
443
|
+
def id
|
444
|
+
rtm_id || object_id
|
445
|
+
end
|
446
|
+
|
447
|
+
def rtm_id
|
448
|
+
self[:id]
|
449
|
+
end
|
450
|
+
|
451
|
+
# guarantees that a given key corresponds to an array, even if it's an empty array
|
452
|
+
def arrayify_value( key )
|
453
|
+
if !self.has_key?(key)
|
454
|
+
self[key] = []
|
455
|
+
elsif self[key].class != Array
|
456
|
+
self[key] = [ self[key] ].compact
|
457
|
+
else
|
458
|
+
self[key]
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
|
463
|
+
def method_missing( key, *args )
|
464
|
+
name = key.to_s
|
465
|
+
|
466
|
+
setter = false
|
467
|
+
if name[-1,1] == '='
|
468
|
+
name = name.chop
|
469
|
+
setter = true
|
470
|
+
end
|
471
|
+
|
472
|
+
if name == ""
|
473
|
+
name = "rtm_nil".to_sym
|
474
|
+
else
|
475
|
+
name = name.to_sym
|
476
|
+
end
|
477
|
+
|
478
|
+
|
479
|
+
# TODO: should we allow the blind setting of values? (i.e., only do this test
|
480
|
+
# if setter==false )
|
481
|
+
raise "unknown hash key<#{name}> requested for #{self.inspect}" if @@strict_keys && !self.has_key?(name)
|
482
|
+
|
483
|
+
if setter
|
484
|
+
self[name] = *args
|
485
|
+
else
|
486
|
+
self[name]
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
|
492
|
+
## DateSet is to manage rrules
|
493
|
+
## this comes from the iCal ruby module as mentioned here:
|
494
|
+
## http://www.macdevcenter.com/pub/a/mac/2003/09/03/rubycocoa.html
|
495
|
+
|
496
|
+
class DateSet
|
497
|
+
|
498
|
+
def initialize(startDate, rule)
|
499
|
+
@startDate = startDate
|
500
|
+
@frequency = nil
|
501
|
+
@count = nil
|
502
|
+
@untilDate = nil
|
503
|
+
@byMonth = nil
|
504
|
+
@byDay = nil
|
505
|
+
@starts = nil
|
506
|
+
if not rule.nil? then
|
507
|
+
@starts = rule.every == 1 ? 'every' : 'after'
|
508
|
+
parseRecurrenceRule(rule.rule)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
def parseRecurrenceRule(rule)
|
513
|
+
|
514
|
+
if rule =~ /FREQ=(.*?);/ then
|
515
|
+
@frequency = $1
|
516
|
+
end
|
517
|
+
|
518
|
+
if rule =~ /COUNT=(\d*)/ then
|
519
|
+
@count = $1.to_i
|
520
|
+
end
|
521
|
+
|
522
|
+
if rule =~ /UNTIL=(.*?)[;\r]/ then
|
523
|
+
@untilDate = DateParser.parse($1)
|
524
|
+
end
|
525
|
+
|
526
|
+
if rule =~ /INTERVAL=(\d*)/ then
|
527
|
+
@interval = $1.to_i
|
528
|
+
end
|
529
|
+
|
530
|
+
if rule =~ /BYMONTH=(.*?);/ then
|
531
|
+
@byMonth = $1
|
532
|
+
end
|
533
|
+
|
534
|
+
if rule =~ /BYDAY=(.*?);/ then
|
535
|
+
@byDay = $1
|
536
|
+
#puts "byDay = #{@byDay}"
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
def to_s
|
541
|
+
# after/every FREQ
|
542
|
+
puts "UNIMPLETEMENT"
|
543
|
+
# puts "#<DateSet: starts: #{@startDate.strftime("%m/%d/%Y")}, occurs: #{@frequency}, count: #{@count}, until: #{@untilDate}, byMonth: #{@byMonth}, byDay: #{@byDay}>"
|
544
|
+
end
|
545
|
+
|
546
|
+
def includes?(date)
|
547
|
+
return true if date == @startDate
|
548
|
+
return false if @untilDate and date > @untilDate
|
549
|
+
|
550
|
+
case @frequency
|
551
|
+
when 'DAILY'
|
552
|
+
#if @untilDate then
|
553
|
+
# return (@startDate..@untilDate).include?(date)
|
554
|
+
#end
|
555
|
+
increment = @interval ? @interval : 1
|
556
|
+
d = @startDate
|
557
|
+
counter = 0
|
558
|
+
until d > date
|
559
|
+
|
560
|
+
if @count then
|
561
|
+
counter += 1
|
562
|
+
if counter >= @count
|
563
|
+
return false
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
d += (increment * SECONDS_PER_DAY)
|
568
|
+
if d.day == date.day and
|
569
|
+
d.year == date.year and
|
570
|
+
d.month == date.month then
|
571
|
+
puts "true for start: #{@startDate}, until: #{@untilDate}"
|
572
|
+
return true
|
573
|
+
end
|
574
|
+
|
575
|
+
end
|
576
|
+
|
577
|
+
when 'WEEKLY'
|
578
|
+
return true if @startDate.wday == date.wday
|
579
|
+
|
580
|
+
when 'MONTHLY'
|
581
|
+
|
582
|
+
when 'YEARLY'
|
583
|
+
|
584
|
+
end
|
585
|
+
|
586
|
+
false
|
587
|
+
end
|
588
|
+
|
589
|
+
attr_reader :frequency
|
590
|
+
attr_accessor :startDate
|
591
|
+
end
|
592
|
+
|
593
|
+
|
594
|
+
class RememberTheMilkTask < RememberTheMilkHash
|
595
|
+
# The API is aware it's creating tasks. You may want to add semantics to a "task"
|
596
|
+
# elsewhere in your program. This gives you that flexibility
|
597
|
+
end
|