thartmx 0.1.9

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,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
+ #