thartmx 0.1.9

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