thartmx 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +344 -0
- data/README.rdoc +302 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/bin/rrtm +40 -0
- data/lib/thartmx.rb +268 -0
- data/lib/thartmx_lib.rb +744 -0
- data/test/helper.rb +10 -0
- data/test/test_thartm.rb +7 -0
- metadata +103 -0
data/lib/thartmx_lib.rb
ADDED
@@ -0,0 +1,744 @@
|
|
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
|
+
#Modified by thamayor, mail: thamayor at gmail dot com
|
21
|
+
#my private rtm key is inside this file..
|
22
|
+
|
23
|
+
#this file is intended to be used with my rtm command line interface
|
24
|
+
|
25
|
+
#TODO add yaml api check?
|
26
|
+
|
27
|
+
require 'uri'
|
28
|
+
if /^1\.9/ === RUBY_VERSION then
|
29
|
+
require 'digest/md5'
|
30
|
+
else
|
31
|
+
require 'md5'
|
32
|
+
require 'parsedate'
|
33
|
+
end
|
34
|
+
require 'cgi'
|
35
|
+
require 'net/http'
|
36
|
+
require 'date'
|
37
|
+
require 'time'
|
38
|
+
require 'rubygems'
|
39
|
+
require 'xml/libxml'
|
40
|
+
require 'tzinfo'
|
41
|
+
|
42
|
+
|
43
|
+
#TODO: allow specifying whether retval should be indexed by rtm_id or list name for lists
|
44
|
+
|
45
|
+
class ThaRememberTheMilk
|
46
|
+
|
47
|
+
RUBY_API_VERSION = '0.6'
|
48
|
+
# you can just put set these here so you don't have to pass them in with
|
49
|
+
# every constructor call
|
50
|
+
API_KEY = ''
|
51
|
+
API_SHARED_SECRET = ''
|
52
|
+
AUTH_TOKEN= ''
|
53
|
+
|
54
|
+
|
55
|
+
Element = 0
|
56
|
+
CloseTag = 1
|
57
|
+
Tag = 2
|
58
|
+
Attributes = 3
|
59
|
+
#SelfContainedElement = 4
|
60
|
+
TextNode = 4
|
61
|
+
|
62
|
+
TagName = 0
|
63
|
+
TagHash = 1
|
64
|
+
|
65
|
+
|
66
|
+
attr_accessor :debug, :auth_token, :return_raw_response, :api_key, :shared_secret, :max_connection_attempts, :use_user_tz
|
67
|
+
|
68
|
+
def user
|
69
|
+
@user_info_cache[auth_token] ||= auth.checkToken.user
|
70
|
+
end
|
71
|
+
|
72
|
+
def user_settings
|
73
|
+
@user_settings_cache[auth_token]
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_timeline
|
77
|
+
user[:timeline] ||= timelines.create
|
78
|
+
end
|
79
|
+
|
80
|
+
def time_to_user_tz( time )
|
81
|
+
return time unless(@use_user_tz && @auth_token && defined?(TZInfo::Timezone))
|
82
|
+
begin
|
83
|
+
unless defined?(@user_settings_cache[auth_token]) && defined?(@user_settings_cache[auth_token][:tz])
|
84
|
+
@user_settings_cache[auth_token] = settings.getList
|
85
|
+
@user_settings_cache[auth_token][:tz] = TZInfo::Timezone.get(@user_settings_cache[auth_token].timezone)
|
86
|
+
end
|
87
|
+
debug "returning time in local zone(%s/%s)", @user_settings_cache[auth_token].timezone, @user_settings_cache[auth_token][:tz]
|
88
|
+
@user_settings_cache[auth_token][:tz].utc_to_local(time)
|
89
|
+
rescue Exception => err
|
90
|
+
debug "unable to read local timezone for auth_token<%s>, ignoring timezone. err<%s>", auth_token, err
|
91
|
+
time
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def logout_user(auth_token)
|
96
|
+
@auth_token = nil if @auth_token == auth_token
|
97
|
+
@user_settings_cache.delete(auth_token)
|
98
|
+
@user_info_cache.delete(auth_token)
|
99
|
+
end
|
100
|
+
|
101
|
+
# TODO: test efficacy of using https://www.rememberthemilk.com/services/rest/
|
102
|
+
def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, auth_token = AUTH_TOKEN, endpoint = 'http://www.rememberthemilk.com/services/rest/')
|
103
|
+
@max_connection_attempts = 3
|
104
|
+
@debug = false
|
105
|
+
@api_key = api_key
|
106
|
+
@shared_secret = shared_secret
|
107
|
+
@uri = URI.parse(endpoint)
|
108
|
+
#@auth_token = nil
|
109
|
+
@auth_token = auth_token
|
110
|
+
@return_raw_response = false
|
111
|
+
@use_user_tz = true
|
112
|
+
@user_settings_cache = {}
|
113
|
+
@user_info_cache = {}
|
114
|
+
#@xml_parser = XML::Parser.new
|
115
|
+
@xml_parser = XML::Parser.new(XML::Parser::Context.new)
|
116
|
+
end
|
117
|
+
|
118
|
+
def version() RUBY_API_VERSION end
|
119
|
+
|
120
|
+
def debug(*args)
|
121
|
+
return unless @debug
|
122
|
+
if defined?(RAILS_DEFAULT_LOGGER)
|
123
|
+
RAILS_DEFAULT_LOGGER.warn( sprintf(*args) )
|
124
|
+
else
|
125
|
+
$stderr.puts(sprintf(*args))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def auth_url( perms = 'delete' )
|
130
|
+
auth_url = 'http://www.rememberthemilk.com/services/auth/'
|
131
|
+
args = { 'api_key' => @api_key, 'perms' => perms }
|
132
|
+
args['api_sig'] = sign_request(args)
|
133
|
+
return auth_url + '?' + args.keys.collect {|k| "#{k}=#{args[k]}"}.join('&')
|
134
|
+
end
|
135
|
+
|
136
|
+
# this is a little fragile. it assumes we are being invoked with RTM api calls
|
137
|
+
# (which are two levels deep)
|
138
|
+
# e.g.,
|
139
|
+
# rtm = RememberTheMilk.new
|
140
|
+
# data = rtm.reflection.getMethodInfo('method_name' => 'rtm.test.login')
|
141
|
+
# the above line gets turned into two calls, the first to this, which returns
|
142
|
+
# an RememberTheMilkAPINamespace object, which then gets *its* method_missing
|
143
|
+
# invoked with 'getMethodInfo' and the above args
|
144
|
+
# i.e.,
|
145
|
+
# rtm.foo.bar
|
146
|
+
# rtm.foo() => a
|
147
|
+
# a.bar
|
148
|
+
|
149
|
+
def method_missing( symbol, *args )
|
150
|
+
rtm_namespace = symbol.id2name
|
151
|
+
debug("method_missing called with namespace <%s>", rtm_namespace)
|
152
|
+
RememberTheMilkAPINamespace.new( rtm_namespace, self )
|
153
|
+
end
|
154
|
+
|
155
|
+
def xml_node_to_hash( node, recursion_level = 0 )
|
156
|
+
result = xml_attributes_to_hash( node.attributes )
|
157
|
+
if node.element? == false
|
158
|
+
result[node.name.to_sym] = node.content
|
159
|
+
else
|
160
|
+
node.each do |child|
|
161
|
+
name = child.name.to_sym
|
162
|
+
value = xml_node_to_hash( child, recursion_level+1 )
|
163
|
+
|
164
|
+
# if we have the same node name appear multiple times, we need to build up an array
|
165
|
+
# of the converted nodes
|
166
|
+
if !result.has_key?(name)
|
167
|
+
result[name] = value
|
168
|
+
elsif result[name].class != Array
|
169
|
+
result[name] = [result[name], value]
|
170
|
+
else
|
171
|
+
result[name] << value
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# top level nodes should be a hash no matter what
|
177
|
+
(recursion_level == 0 || result.values.size > 1) ? result : result.values[0]
|
178
|
+
end
|
179
|
+
|
180
|
+
def xml_attributes_to_hash( attributes, class_name = RememberTheMilkHash )
|
181
|
+
hash = class_name.send(:new)
|
182
|
+
attributes.each {|a| hash[a.name.to_sym] = a.value} if attributes.respond_to?(:each)
|
183
|
+
return hash
|
184
|
+
end
|
185
|
+
|
186
|
+
def index_data_into_hash( data, key )
|
187
|
+
new_hash = RememberTheMilkHash.new
|
188
|
+
|
189
|
+
if data.class == Array
|
190
|
+
data.each {|datum| new_hash[datum[key]] = datum }
|
191
|
+
else
|
192
|
+
new_hash[data[key]] = data
|
193
|
+
end
|
194
|
+
|
195
|
+
new_hash
|
196
|
+
end
|
197
|
+
|
198
|
+
def parse_response(response,method,args)
|
199
|
+
# groups -- an array of group obj
|
200
|
+
# group -- some attributes and a possible contacts array
|
201
|
+
# contacts -- an array of contact obj
|
202
|
+
# contact -- just attributes
|
203
|
+
# lists -- array of list obj
|
204
|
+
# list -- attributes and possible filter obj, and a set of taskseries objs?
|
205
|
+
# task sereies obj are always wrapped in a list. why?
|
206
|
+
# taskseries -- set of attributes, array of tags, an rrule, participants array of contacts, notes,
|
207
|
+
# and task. created and modified are time obj,
|
208
|
+
# task -- attributes, due/added are time obj
|
209
|
+
# note -- attributes and a body of text, with created and modified time obj
|
210
|
+
# time -- convert to a time obj
|
211
|
+
# timeline -- just has a body of text
|
212
|
+
return true unless response.keys.size > 1 # empty response (stat only)
|
213
|
+
|
214
|
+
rtm_transaction = nil
|
215
|
+
if response.has_key?(:transaction)
|
216
|
+
# debug("got back <%s> elements in my transaction", response[:transaction].keys.size)
|
217
|
+
# we just did a write operation, got back a transaction AND some data.
|
218
|
+
# Now, we will do some fanciness.
|
219
|
+
rtm_transaction = response[:transaction]
|
220
|
+
end
|
221
|
+
|
222
|
+
response_types = response.keys - [:stat, :transaction]
|
223
|
+
|
224
|
+
if response.has_key?(:api_key) # echo call, we assume
|
225
|
+
response_type = :echo
|
226
|
+
data = response
|
227
|
+
elsif response_types.size > 1
|
228
|
+
error = RememberTheMilkAPIError.new({:code => "666", :msg=>"found more than one response type[#{response_types.join(',')}]"},method,args)
|
229
|
+
debug( "%s", error )
|
230
|
+
raise error
|
231
|
+
else
|
232
|
+
response_type = response_types[0] || :transaction
|
233
|
+
|
234
|
+
data = response[response_type]
|
235
|
+
end
|
236
|
+
|
237
|
+
case response_type
|
238
|
+
when :auth
|
239
|
+
when :frob
|
240
|
+
when :echo
|
241
|
+
when :transaction
|
242
|
+
when :timeline
|
243
|
+
when :methods
|
244
|
+
when :settings
|
245
|
+
when :contact
|
246
|
+
when :group
|
247
|
+
# no op
|
248
|
+
|
249
|
+
when :tasks
|
250
|
+
data = data[:list]
|
251
|
+
new_hash = RememberTheMilkHash.new
|
252
|
+
if data.class == Array # a bunch of lists
|
253
|
+
data.each do |list|
|
254
|
+
if list.class == String # empty list, just an id, so we create a stub
|
255
|
+
new_list = RememberTheMilkHash.new
|
256
|
+
new_list[:id] = list
|
257
|
+
list = new_list
|
258
|
+
end
|
259
|
+
new_hash[list[:id]] = process_task_list( list[:id], list.arrayify_value(:taskseries) )
|
260
|
+
end
|
261
|
+
data = new_hash
|
262
|
+
elsif data.class == RememberTheMilkHash # only one list
|
263
|
+
#puts data.inspect
|
264
|
+
#puts data[:list][3][:taskseries].inspect
|
265
|
+
data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
|
266
|
+
elsif data.class == NilClass || (data.class == String && data == args['list_id']) # empty list
|
267
|
+
data = new_hash
|
268
|
+
else # who knows...
|
269
|
+
debug( "got a class of (%s [%s]) when processing tasks. passing it on through", data.class, data )
|
270
|
+
end
|
271
|
+
when :groups
|
272
|
+
# contacts expected to be array, so look at each group and fix it's contact
|
273
|
+
data = [data] unless data.class == Array # won't be array if there's only one group. normalize here
|
274
|
+
data.each do |datum|
|
275
|
+
datum.arrayify_value( :contacts )
|
276
|
+
end
|
277
|
+
data = index_data_into_hash( data, :id )
|
278
|
+
when :time
|
279
|
+
data = time_to_user_tz( Time.parse(data[:text]) )
|
280
|
+
when :timezones
|
281
|
+
data = index_data_into_hash( data, :name )
|
282
|
+
when :lists
|
283
|
+
data = index_data_into_hash( data, :id )
|
284
|
+
when :contacts
|
285
|
+
data = [data].compact unless data.class == Array
|
286
|
+
when :list
|
287
|
+
# rtm.tasks.add returns one of these, which looks like this:
|
288
|
+
# <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>
|
289
|
+
# rtm.lists.add also returns this, but it looks like this:
|
290
|
+
# <rsp stat='ok'><transaction id='978727001' undoable='0'/><list name='PersonalClone2' smart='0' id='761266' archived='0' deleted='0' position='0' locked='0'/></rsp>
|
291
|
+
# so we can look for a name attribute
|
292
|
+
if !data.has_key?(:name)
|
293
|
+
data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
|
294
|
+
data = data.values[0] if data.values.size == 1
|
295
|
+
end
|
296
|
+
else
|
297
|
+
throw "Unsupported reply type<#{response_type}>#{response.inspect}"
|
298
|
+
end
|
299
|
+
|
300
|
+
if rtm_transaction
|
301
|
+
if !data.respond_to?(:keys)
|
302
|
+
new_hash = RememberTheMilkHash.new
|
303
|
+
new_hash[response_type] = data
|
304
|
+
data = new_hash
|
305
|
+
end
|
306
|
+
|
307
|
+
if data.keys.size == 0
|
308
|
+
data = rtm_transaction
|
309
|
+
else
|
310
|
+
data[:rtm_transaction] = rtm_transaction if rtm_transaction
|
311
|
+
end
|
312
|
+
end
|
313
|
+
return data
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
def process_task_list( list_id, list )
|
318
|
+
return {} unless list
|
319
|
+
tasks = RememberTheMilkHash.new
|
320
|
+
list.each do |taskseries_as_hash|
|
321
|
+
taskseries = RememberTheMilkTask.new(self).merge(taskseries_as_hash)
|
322
|
+
|
323
|
+
taskseries[:parent_list] = list_id # parent pointers are nice
|
324
|
+
taskseries[:tasks] = taskseries.arrayify_value(:task)
|
325
|
+
taskseries.arrayify_value(:tags)
|
326
|
+
taskseries.arrayify_value(:participants)
|
327
|
+
|
328
|
+
# TODO is there a ruby lib that speaks rrule?
|
329
|
+
taskseries[:recurrence] = nil
|
330
|
+
if taskseries[:rrule]
|
331
|
+
taskseries[:recurrence] = taskseries[:rrule]
|
332
|
+
taskseries[:recurrence][:rule] = taskseries[:rrule][:text]
|
333
|
+
end
|
334
|
+
|
335
|
+
taskseries[:completed] = nil
|
336
|
+
taskseries.tasks.each do |item|
|
337
|
+
if item.has_key?(:due) && item.due != ''
|
338
|
+
item.due = time_to_user_tz( Time.parse(item.due) )
|
339
|
+
end
|
340
|
+
|
341
|
+
if item.has_key?(:completed) && item.completed != '' && taskseries[:completed] == nil
|
342
|
+
taskseries[:completed] = true
|
343
|
+
else # once we set it to false, it can't get set to true
|
344
|
+
taskseries[:completed] = false
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# TODO: support past tasks?
|
349
|
+
tasks[taskseries[:id]] = taskseries
|
350
|
+
end
|
351
|
+
|
352
|
+
return tasks
|
353
|
+
end
|
354
|
+
|
355
|
+
def call_api_method( method, args={} )
|
356
|
+
|
357
|
+
args['method'] = "rtm.#{method}"
|
358
|
+
args['api_key'] = @api_key
|
359
|
+
args['auth_token'] ||= @auth_token if @auth_token
|
360
|
+
|
361
|
+
# make sure everything in our arguments is a string
|
362
|
+
args.each do |key,value|
|
363
|
+
key_s = key.to_s
|
364
|
+
args.delete(key) if key.class != String
|
365
|
+
args[key_s] = value.to_s
|
366
|
+
end
|
367
|
+
|
368
|
+
args['api_sig'] = sign_request(args)
|
369
|
+
|
370
|
+
debug( 'rtm.%s(%s)', method, args.inspect )
|
371
|
+
|
372
|
+
attempts_left = @max_connection_attempts
|
373
|
+
|
374
|
+
begin
|
375
|
+
if args.has_key?('test_data')
|
376
|
+
@xml_parser.string = args['test_data']
|
377
|
+
else
|
378
|
+
attempts_left -= 1
|
379
|
+
response = Net::HTTP.get_response(@uri.host, "#{@uri.path}?#{args.keys.collect {|k| "#{CGI::escape(k).gsub(/ /,'+')}=#{CGI::escape(args[k]).gsub(/ /,'+')}"}.join('&')}")
|
380
|
+
debug('RESPONSE code: %s\n%sEND RESPONSE\n', response.code, response.body)
|
381
|
+
#puts response.body
|
382
|
+
#@xml_parser.string = response.body
|
383
|
+
@xml_parser= XML::Parser.string(response.body)
|
384
|
+
end
|
385
|
+
|
386
|
+
raw_data = @xml_parser.parse
|
387
|
+
data = xml_node_to_hash( raw_data.root )
|
388
|
+
#puts data.inspect
|
389
|
+
debug( "processed into data<#{data.inspect}>")
|
390
|
+
|
391
|
+
if data[:stat] != 'ok'
|
392
|
+
error = RememberTheMilkAPIError.new(data[:err],method,args)
|
393
|
+
debug( "%s", error )
|
394
|
+
raise error
|
395
|
+
end
|
396
|
+
#return return_raw_response ? @xml_parser.string : parse_response(data,method,args)
|
397
|
+
return parse_response(data,method,args)
|
398
|
+
#rescue XML::Parser::ParseError => err
|
399
|
+
# debug("Unable to parse document.\nGot response:%s\nGot Error:\n", response.body, err.to_s)
|
400
|
+
# raise err
|
401
|
+
rescue Timeout::Error => timeout
|
402
|
+
$stderr.puts "Timed out to<#{endpoint}>, trying #{attempts_left} more times"
|
403
|
+
if attempts_left > 0
|
404
|
+
retry
|
405
|
+
else
|
406
|
+
raise timeout
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def sign_request( args )
|
412
|
+
if /^1\.9/ === RUBY_VERSION then
|
413
|
+
return (Digest::MD5.new << @shared_secret + args.sort.flatten.join).to_s
|
414
|
+
else
|
415
|
+
return MD5.md5(@shared_secret + args.sort.flatten.join).to_s
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
## a pretty crappy exception class, but it should be sufficient for bubbling
|
422
|
+
## up errors returned by the RTM API (website)
|
423
|
+
class RememberTheMilkAPIError < RuntimeError
|
424
|
+
attr_reader :response, :error_code, :error_message
|
425
|
+
|
426
|
+
def initialize(error, method, args_to_method)
|
427
|
+
@method_name = method
|
428
|
+
@args_to_method = args_to_method
|
429
|
+
@error_code = error[:code].to_i
|
430
|
+
@error_message = error[:msg]
|
431
|
+
end
|
432
|
+
|
433
|
+
def to_s
|
434
|
+
"Calling rtm.#{@method_name}(#{@args_to_method.inspect}) produced => <#{@error_code}>: #{@error_message}"
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
## this is just a helper class so that you can do things like
|
440
|
+
## rtm.test.echo. the method_missing in RememberTheMilkAPI returns one of
|
441
|
+
## these.
|
442
|
+
## this class is the "test" portion of the programming. its method_missing then
|
443
|
+
## get invoked with "echo" as the symbol. it has stored a reference to the original
|
444
|
+
## rtm object, so it can then invoke call_api_method
|
445
|
+
class RememberTheMilkAPINamespace
|
446
|
+
def initialize(namespace, rtm)
|
447
|
+
@namespace = namespace
|
448
|
+
@rtm = rtm
|
449
|
+
end
|
450
|
+
|
451
|
+
def method_missing( symbol, *args )
|
452
|
+
method_name = symbol.id2name
|
453
|
+
@rtm.call_api_method( "#{@namespace}.#{method_name}", *args)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
## a standard hash with some helper methods
|
458
|
+
class RememberTheMilkHash < Hash
|
459
|
+
attr_accessor :rtm
|
460
|
+
|
461
|
+
@@strict_keys = true
|
462
|
+
def self.strict_keys=( value )
|
463
|
+
@@strict_keys = value
|
464
|
+
end
|
465
|
+
|
466
|
+
def initialize(rtm_object = nil)
|
467
|
+
super
|
468
|
+
@rtm = rtm_object
|
469
|
+
end
|
470
|
+
|
471
|
+
def id
|
472
|
+
rtm_id || object_id
|
473
|
+
end
|
474
|
+
|
475
|
+
def rtm_id
|
476
|
+
self[:id]
|
477
|
+
end
|
478
|
+
|
479
|
+
# guarantees that a given key corresponds to an array, even if it's an empty array
|
480
|
+
def arrayify_value( key )
|
481
|
+
if !self.has_key?(key)
|
482
|
+
self[key] = []
|
483
|
+
elsif self[key].class != Array
|
484
|
+
self[key] = [ self[key] ].compact
|
485
|
+
else
|
486
|
+
self[key]
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
|
491
|
+
def method_missing( key, *args )
|
492
|
+
name = key.to_s
|
493
|
+
|
494
|
+
setter = false
|
495
|
+
if name[-1,1] == '='
|
496
|
+
name = name.chop
|
497
|
+
setter = true
|
498
|
+
end
|
499
|
+
|
500
|
+
if name == ""
|
501
|
+
name = "rtm_nil".to_sym
|
502
|
+
else
|
503
|
+
name = name.to_sym
|
504
|
+
end
|
505
|
+
|
506
|
+
|
507
|
+
# TODO: should we allow the blind setting of values? (i.e., only do this test
|
508
|
+
# if setter==false )
|
509
|
+
raise "unknown hash key<#{name}> requested for #{self.inspect}" if @@strict_keys && !self.has_key?(name)
|
510
|
+
|
511
|
+
if setter
|
512
|
+
self[name] = *args
|
513
|
+
else
|
514
|
+
self[name]
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
|
520
|
+
## TODO -- better rrule support. start here with this code, commented out for now
|
521
|
+
## DateSet is to manage rrules
|
522
|
+
## this comes from the iCal ruby module as mentioned here:
|
523
|
+
## http://www.macdevcenter.com/pub/a/mac/2003/09/03/rubycocoa.html
|
524
|
+
|
525
|
+
# The API is aware it's creating tasks. You may want to add semantics to a "task"
|
526
|
+
# elsewhere in your program. This gives you that flexibility
|
527
|
+
# plus, we've added some helper methods
|
528
|
+
|
529
|
+
class RememberTheMilkTask < RememberTheMilkHash
|
530
|
+
attr_accessor :rtm
|
531
|
+
|
532
|
+
def timeline
|
533
|
+
@timeline ||= rtm.get_timeline # this caches timelines per user
|
534
|
+
end
|
535
|
+
|
536
|
+
def initialize( rtm_api_handle=nil )
|
537
|
+
super
|
538
|
+
@rtm = rtm_api_handle # keep track of this so we can do setters (see factory below)
|
539
|
+
end
|
540
|
+
|
541
|
+
def task() tasks[-1] end
|
542
|
+
def taskseries_id() self.has_key?(:taskseries_id) ? self[:taskseries_id] : rtm_id end
|
543
|
+
def task_id() self.has_key?(:task_id) ? self[:task_id] : task.rtm_id end
|
544
|
+
def list_id() parent_list end
|
545
|
+
def due() task.due end
|
546
|
+
|
547
|
+
def has_due?() due.class == Time end
|
548
|
+
def has_due_time?() task.has_due_time == '1' end
|
549
|
+
def complete?() task[:completed] != '' end
|
550
|
+
def to_s
|
551
|
+
a_parent_list = self[:parent_list] || '<Parent Not Set>'
|
552
|
+
a_taskseries_id = self[:taskseries_id] || self[:id] || '<No Taskseries Id>'
|
553
|
+
a_task_id = self[:task_id] || (self[:task] && self[:task].rtm_td) || '<No Task Id>'
|
554
|
+
a_name = self[:name] || '<Name Not Set>'
|
555
|
+
"#{a_parent_list}/#{a_taskseries_id}/#{a_task_id}: #{a_name}"
|
556
|
+
end
|
557
|
+
|
558
|
+
def due_display
|
559
|
+
if has_due?
|
560
|
+
if has_due_time?
|
561
|
+
due.strftime("%a %d %b %y at %I:%M%p")
|
562
|
+
else
|
563
|
+
due.strftime("%a %d %b %y")
|
564
|
+
end
|
565
|
+
else
|
566
|
+
'[no due date]'
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
@@BeginningOfEpoch = Time.parse("Jan 1 1904") # kludgey.. sure. life's a kludge. deal with it.
|
571
|
+
include Comparable
|
572
|
+
def <=>(other)
|
573
|
+
due = (has_key?(:tasks) && tasks.class == Array) ? task[:due] : nil
|
574
|
+
due = @@BeginningOfEpoch unless due.class == Time
|
575
|
+
other_due = (other.has_key?(:tasks) && other.tasks.class == Array) ? other.task[:due] : nil
|
576
|
+
other_due = @@BeginningOfEpoch unless other_due.class == Time
|
577
|
+
|
578
|
+
# sort based on priority, then due date, then name
|
579
|
+
# which is the rememberthemilk default
|
580
|
+
# if 0 was false in ruby, we could have done
|
581
|
+
# prio <=> other_due || due <=> other_due || self['name'].to_s <=> other['name'].to_s
|
582
|
+
# but it's not, so oh well....
|
583
|
+
prio = priority.to_i
|
584
|
+
prio += 666 if prio == 0 # prio of 0 is no priority which means it should show up below 1-3
|
585
|
+
other_prio = other.priority.to_i
|
586
|
+
other_prio += 666 if other_prio == 0
|
587
|
+
|
588
|
+
if prio != other_prio
|
589
|
+
return prio <=> other_prio
|
590
|
+
elsif due != other_due
|
591
|
+
return due <=> other_due
|
592
|
+
else
|
593
|
+
# TODO: should this be case insensitive?
|
594
|
+
return self[:name].to_s <=> other[:name].to_s
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# Factory Methods...
|
599
|
+
# these are for methods that take arguments and apply to the taskseries
|
600
|
+
# if you have RememberTheMilkTask called task, you might do:
|
601
|
+
# task.addTags( 'tag1, tag2, tag3' )
|
602
|
+
# task.setRecurrence # turns off all rrules
|
603
|
+
# task.complete # marks last task as complete
|
604
|
+
# task.setDueDate # unsets due date for last task
|
605
|
+
# task.setDueDate( nil, :task_id => task.tasks[0].id ) # unsets due date for first task in task array
|
606
|
+
# task.setDueDate( "tomorrow at 1pm", :parse => 1 ) # sets due date for last task to tomorrow at 1pm
|
607
|
+
[['addTags','tags'], ['setTags', 'tags'], ['removeTags', 'tags'], ['setName', 'name'],
|
608
|
+
['setRecurrence', 'repeat'], ['complete', ''], ['uncomplete', ''], ['setDueDate', 'due'],
|
609
|
+
['setPriority', 'priority'], ['movePriority', 'direction'], ['setEstimate', 'estimate'],
|
610
|
+
['setURL', 'url'], ['postpone', ''], ['delete', ''] ].each do |method_name, arg|
|
611
|
+
class_eval <<-RTM_METHOD
|
612
|
+
def #{method_name} ( value=nil, args={} )
|
613
|
+
if @rtm == nil
|
614
|
+
raise RememberTheMilkAPIError.new( :code => '667', :msg => "#{method_name} called without a handle to an rtm object [#{self.to_s}]" )
|
615
|
+
end
|
616
|
+
method_args = {}
|
617
|
+
method_args["#{arg}"] = value if "#{arg}" != '' && value
|
618
|
+
method_args[:timeline] = timeline
|
619
|
+
method_args[:list_id] = list_id
|
620
|
+
method_args[:taskseries_id] = taskseries_id
|
621
|
+
method_args[:task_id] = task_id
|
622
|
+
method_args.merge!( args )
|
623
|
+
@rtm.call_api_method( "tasks.#{method_name}", method_args ) # returns the modified task
|
624
|
+
end
|
625
|
+
RTM_METHOD
|
626
|
+
end
|
627
|
+
|
628
|
+
# We have to do this because moveTo takes a "from_list_id", not "list_id", so the above factory
|
629
|
+
# wouldn't work. sigh.
|
630
|
+
def moveTo( to_list_id, args = {} )
|
631
|
+
if @rtm == nil
|
632
|
+
raise RememberTheMilkAPIError.new( :code => '667', :msg => "moveTO called without a handle to an rtm object [#{self.to_s}]" )
|
633
|
+
end
|
634
|
+
method_args = {}
|
635
|
+
method_args[:timeline] = timeline
|
636
|
+
method_args[:from_list_id] = list_id
|
637
|
+
method_args[:to_list_id] = to_list_id
|
638
|
+
method_args[:taskseries_id] = taskseries_id
|
639
|
+
method_args[:task_id] = task_id
|
640
|
+
method_args.merge( args )
|
641
|
+
@rtm.call_api_method( :moveTo, method_args )
|
642
|
+
end
|
643
|
+
|
644
|
+
end
|
645
|
+
|
646
|
+
|
647
|
+
#
|
648
|
+
# class DateSet
|
649
|
+
#
|
650
|
+
# def initialize(startDate, rule)
|
651
|
+
# @startDate = startDate
|
652
|
+
# @frequency = nil
|
653
|
+
# @count = nil
|
654
|
+
# @untilDate = nil
|
655
|
+
# @byMonth = nil
|
656
|
+
# @byDay = nil
|
657
|
+
# @starts = nil
|
658
|
+
# if not rule.nil? then
|
659
|
+
# @starts = rule.every == 1 ? 'every' : 'after'
|
660
|
+
# parseRecurrenceRule(rule.rule)
|
661
|
+
# end
|
662
|
+
# end
|
663
|
+
#
|
664
|
+
# def parseRecurrenceRule(rule)
|
665
|
+
#
|
666
|
+
# if rule =~ /FREQ=(.*?);/ then
|
667
|
+
# @frequency = $1
|
668
|
+
# end
|
669
|
+
#
|
670
|
+
# if rule =~ /COUNT=(\d*)/ then
|
671
|
+
# @count = $1.to_i
|
672
|
+
# end
|
673
|
+
#
|
674
|
+
# if rule =~ /UNTIL=(.*?)[;\r]/ then
|
675
|
+
# @untilDate = DateParser.parse($1)
|
676
|
+
# end
|
677
|
+
#
|
678
|
+
# if rule =~ /INTERVAL=(\d*)/ then
|
679
|
+
# @interval = $1.to_i
|
680
|
+
# end
|
681
|
+
#
|
682
|
+
# if rule =~ /BYMONTH=(.*?);/ then
|
683
|
+
# @byMonth = $1
|
684
|
+
# end
|
685
|
+
#
|
686
|
+
# if rule =~ /BYDAY=(.*?);/ then
|
687
|
+
# @byDay = $1
|
688
|
+
# #puts "byDay = #{@byDay}"
|
689
|
+
# end
|
690
|
+
# end
|
691
|
+
#
|
692
|
+
# def to_s
|
693
|
+
# # after/every FREQ
|
694
|
+
# puts "UNIMPLETEMENT"
|
695
|
+
# # puts "#<DateSet: starts: #{@startDate.strftime("%m/%d/%Y")}, occurs: #{@frequency}, count: #{@count}, until: #{@untilDate}, byMonth: #{@byMonth}, byDay: #{@byDay}>"
|
696
|
+
# end
|
697
|
+
#
|
698
|
+
# def includes?(date)
|
699
|
+
# return true if date == @startDate
|
700
|
+
# return false if @untilDate and date > @untilDate
|
701
|
+
#
|
702
|
+
# case @frequency
|
703
|
+
# when 'DAILY'
|
704
|
+
# #if @untilDate then
|
705
|
+
# # return (@startDate..@untilDate).include?(date)
|
706
|
+
# #end
|
707
|
+
# increment = @interval ? @interval : 1
|
708
|
+
# d = @startDate
|
709
|
+
# counter = 0
|
710
|
+
# until d > date
|
711
|
+
#
|
712
|
+
# if @count then
|
713
|
+
# counter += 1
|
714
|
+
# if counter >= @count
|
715
|
+
# return false
|
716
|
+
# end
|
717
|
+
# end
|
718
|
+
#
|
719
|
+
# d += (increment * SECONDS_PER_DAY)
|
720
|
+
# if d.day == date.day and
|
721
|
+
# d.year == date.year and
|
722
|
+
# d.month == date.month then
|
723
|
+
# puts "true for start: #{@startDate}, until: #{@untilDate}"
|
724
|
+
# return true
|
725
|
+
# end
|
726
|
+
#
|
727
|
+
# end
|
728
|
+
#
|
729
|
+
# when 'WEEKLY'
|
730
|
+
# return true if @startDate.wday == date.wday
|
731
|
+
#
|
732
|
+
# when 'MONTHLY'
|
733
|
+
#
|
734
|
+
# when 'YEARLY'
|
735
|
+
#
|
736
|
+
# end
|
737
|
+
#
|
738
|
+
# false
|
739
|
+
# end
|
740
|
+
#
|
741
|
+
# attr_reader :frequency
|
742
|
+
# attr_accessor :startDate
|
743
|
+
# end
|
744
|
+
#
|