Class: RememberTheMilk

Inherits:
Object
  • Object
show all
Defined in:
lib/rtmapi.rb

Overview

TODO: allow specifying whether retval should be indexed by rtm_id or list name for lists

Constant Summary collapse

RUBY_API_VERSION =
'0.6'
API_KEY =

you can just put set these here so you don’t have to pass them in with every constructor call

'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
API_SHARED_SECRET =
'xxxxxxxxxxxxxxxx'
Element =
0
CloseTag =
1
Tag =
2
Attributes =
3
TextNode =

SelfContainedElement = 4

4
TagName =
0
TagHash =
1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key = API_KEY, shared_secret = API_SHARED_SECRET, endpoint = 'http://www.rememberthemilk.com/services/rest/') ⇒ RememberTheMilk

TODO: test efficacy of using www.rememberthemilk.com/services/rest/



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rtmapi.rb', line 88

def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, endpoint = 'http://www.rememberthemilk.com/services/rest/' )
  @max_connection_attempts = 3
  @debug = false
  @api_key = api_key
  @shared_secret = shared_secret
  @uri = URI.parse(endpoint)
  @auth_token = nil
  @return_raw_response = false
  @use_user_tz = true
  @user_settings_cache = {}
  @user_info_cache = {}
  @xml_parser = XML::Parser.new
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(symbol, *args) ⇒ Object

this is a little fragile. it assumes we are being invoked with RTM api calls (which are two levels deep) e.g., rtm = RememberTheMilk.new data = rtm.reflection.getMethodInfo(‘method_name’ => ‘rtm.test.login’)

the above line gets turned into two calls, the first to this, which returns
an RememberTheMilkAPINamespace object, which then gets *its* method_missing
invoked with 'getMethodInfo' and the above args 
i.e.,
 rtm.foo.bar
 rtm.foo() => a
 a.bar


133
134
135
136
137
# File 'lib/rtmapi.rb', line 133

def method_missing( symbol, *args )
  rtm_namespace = symbol.id2name
  debug("method_missing called with namespace <%s>", rtm_namespace)
  RememberTheMilkAPINamespace.new( rtm_namespace, self )
end

Instance Attribute Details

#api_keyObject

Returns the value of attribute api_key.



52
53
54
# File 'lib/rtmapi.rb', line 52

def api_key
  @api_key
end

#auth_tokenObject

Returns the value of attribute auth_token.



52
53
54
# File 'lib/rtmapi.rb', line 52

def auth_token
  @auth_token
end

#debug(*args) ⇒ Object

Returns the value of attribute debug.



52
53
54
# File 'lib/rtmapi.rb', line 52

def debug
  @debug
end

#max_connection_attemptsObject

Returns the value of attribute max_connection_attempts.



52
53
54
# File 'lib/rtmapi.rb', line 52

def max_connection_attempts
  @max_connection_attempts
end

#return_raw_responseObject

Returns the value of attribute return_raw_response.



52
53
54
# File 'lib/rtmapi.rb', line 52

def return_raw_response
  @return_raw_response
end

#shared_secretObject

Returns the value of attribute shared_secret.



52
53
54
# File 'lib/rtmapi.rb', line 52

def shared_secret
  @shared_secret
end

#use_user_tzObject

Returns the value of attribute use_user_tz.



52
53
54
# File 'lib/rtmapi.rb', line 52

def use_user_tz
  @use_user_tz
end

Instance Method Details

#auth_url(perms = 'delete') ⇒ Object



113
114
115
116
117
118
# File 'lib/rtmapi.rb', line 113

def auth_url( perms = 'delete' )
  auth_url = 'http://www.rememberthemilk.com/services/auth/'
  args = { 'api_key' => @api_key, 'perms' => perms }
  args['api_sig'] = sign_request(args)
  return auth_url + '?' + args.keys.collect {|k| "#{k}=#{args[k]}"}.join('&')
end

#call_api_method(method, args = {}) ⇒ Object



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/rtmapi.rb', line 337

def call_api_method( method, args={} )
  
  args['method'] = "rtm.#{method}"
  args['api_key'] = @api_key
  args['auth_token'] ||= @auth_token if @auth_token

  # make sure everything in our arguments is a string
  args.each do |key,value|
    key_s = key.to_s
    args.delete(key) if key.class != String
    args[key_s] = value.to_s
  end

  args['api_sig'] = sign_request(args)

  debug( 'rtm.%s(%s)', method, args.inspect )

  attempts_left = @max_connection_attempts
  
  begin
  if args.has_key?('test_data')
    @xml_parser.string = args['test_data']
  else
    attempts_left -= 1
    response = Net::HTTP.get_response(@uri.host, "#{@uri.path}?#{args.keys.collect {|k| "#{CGI::escape(k).gsub(/ /,'+')}=#{CGI::escape(args[k]).gsub(/ /,'+')}"}.join('&')}")
    debug('RESPONSE code: %s\n%sEND RESPONSE\n', response.code, response.body)
    @xml_parser.string = response.body
  end

    raw_data = @xml_parser.parse
    data = xml_node_to_hash( raw_data.root )
    debug( "processed into data<#{data.inspect}>")
    
    if data[:stat] != 'ok'
      error = RememberTheMilkAPIError.new(data[:err],method,args)
      debug( "%s", error )
      raise error
    end
    return return_raw_response ? @xml_parser.string : parse_response(data,method,args)
  rescue XML::Parser::ParseError => err
    debug("Unable to parse document.\nGot response:%s\nGot Error:\n", response.body, err.to_s)
    raise err
  rescue Timeout::Error => timeout
    $stderr.puts "Timed out to<#{endpoint}>, trying #{attempts_left} more times"
    if attempts_left > 0
      retry
    else
      raise timeout
    end
  end
end

#get_timelineObject



62
63
64
# File 'lib/rtmapi.rb', line 62

def get_timeline
  user[:timeline] ||= timelines.create
end

#index_data_into_hash(data, key) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
# File 'lib/rtmapi.rb', line 170

def index_data_into_hash( data, key )
  new_hash = RememberTheMilkHash.new

  if data.class == Array
    data.each {|datum| new_hash[datum[key]] = datum }
  else
    new_hash[data[key]] = data
  end

  new_hash
end

#logout_user(auth_token) ⇒ Object



81
82
83
84
85
# File 'lib/rtmapi.rb', line 81

def logout_user(auth_token)
  @auth_token = nil if @auth_token == auth_token
  @user_settings_cache.delete(auth_token)
  @user_info_cache.delete(auth_token)
end

#parse_response(response, method, args) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/rtmapi.rb', line 182

def parse_response(response,method,args)
# groups -- an array of group obj
# group -- some attributes and a possible contacts array
# contacts -- an array of contact obj
#  contact -- just attributes
# lists -- array of list obj
# list -- attributes and possible filter obj, and a set of taskseries objs?
#  task sereies obj are always wrapped in a list.  why?
# taskseries -- set of attributes, array of tags, an rrule, participants array of contacts, notes, 
# and task.  created and modified are time obj,
#  task -- attributes, due/added are time obj
# note -- attributes and a body of text, with created and modified time obj
# time -- convert to a time obj
# timeline -- just has a body of text
  return true unless response.keys.size > 1 # empty response (stat only)

  rtm_transaction = nil
  if response.has_key?(:transaction)
#      debug("got back <%s> elements in my transaction", response[:transaction].keys.size)
    # we just did a write operation, got back a transaction AND some data.  
    # Now, we will do some fanciness.
    rtm_transaction = response[:transaction]
  end

  response_types = response.keys - [:stat, :transaction]

  if response.has_key?(:api_key) # echo call, we assume
    response_type = :echo
    data = response
  elsif response_types.size > 1 
    error = RememberTheMilkAPIError.new({:code => "666", :msg=>"found more than one response type[#{response_types.join(',')}]"},method,args)
    debug( "%s", error )
    raise error
  else
    response_type = response_types[0] || :transaction
    data = response[response_type]
  end


  case response_type
  when :auth
  when :frob
  when :echo
  when :transaction
  when :timeline
  when :methods
  when :settings
  when :contact
  when :group
    # no op
    
  when :tasks
    new_hash = RememberTheMilkHash.new
    if data.class == Array                    # a bunch of lists
      data.each do |list| 
        if list.class == String  # empty list, just an id, so we create a stub
          new_list = RememberTheMilkHash.new
          new_list[:id] = list
          list = new_list
        end
        new_hash[list[:id]] = process_task_list( list[:id], list.arrayify_value(:taskseries) )
      end
      data = new_hash
    elsif data.class == RememberTheMilkHash  # only one list
      data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
    elsif data.class == NilClass || (data.class == String && data == args['list_id']) # empty list
      data = new_hash
    else                                      # who knows...  
      debug( "got a class of (%s [%s]) when processing tasks.  passing it on through", data.class, data )
    end
  when :groups
    # contacts expected to be array, so look at each group and fix it's contact
    data = [data] unless data.class == Array  # won't be array if there's only one group.  normalize here
    data.each do |datum| 
      datum.arrayify_value( :contacts )
    end
    data = index_data_into_hash( data, :id )
  when :time
    data = time_to_user_tz( Time.parse(data[:text]) )
  when :timezones
    data = index_data_into_hash( data, :name )
  when :lists
    data = index_data_into_hash( data, :id )
  when :contacts
    data = [data].compact unless data.class == Array
  when :list
    # rtm.tasks.add returns one of these, which looks like this:
    # <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>
    # rtm.lists.add also returns this, but it looks like this:
    # <rsp stat='ok'><transaction id='978727001' undoable='0'/><list name='PersonalClone2' smart='0' id='761266' archived='0' deleted='0' position='0' locked='0'/></rsp>
    # so we can look for a name attribute
    if !data.has_key?(:name)
      data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
      data = data.values[0] if data.values.size == 1
    end
  else
    throw "Unsupported reply type<#{response_type}>#{response.inspect}"
  end

  if rtm_transaction
    if !data.respond_to?(:keys)
      new_hash = RememberTheMilkHash.new
      new_hash[response_type] = data
      data = new_hash
    end
    
    if data.keys.size == 0
      data = rtm_transaction
    else
      data[:rtm_transaction] = rtm_transaction if rtm_transaction
    end
  end
  return data
end

#process_task_list(list_id, list) ⇒ Object



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/rtmapi.rb', line 298

def process_task_list( list_id, list )
  return {} unless list
  tasks = RememberTheMilkHash.new

  list.each do |taskseries_as_hash|
    taskseries = RememberTheMilkTask.new(self).merge(taskseries_as_hash)

    taskseries[:parent_list] = list_id  # parent pointers are nice
    taskseries[:tasks] = taskseries.arrayify_value(:task)
    taskseries.arrayify_value(:tags)
    taskseries.arrayify_value(:participants)
  
    # TODO is there a ruby lib that speaks rrule? 
    taskseries[:recurrence] = nil
    if taskseries[:rrule]
      taskseries[:recurrence] = taskseries[:rrule]
      taskseries[:recurrence][:rule] = taskseries[:rrule][:text]
    end

    taskseries[:completed] = nil
    taskseries.tasks.each do |item|
      if item.has_key?(:due) && item.due != ''
        item.due = time_to_user_tz( Time.parse(item.due) )
      end
      
      if item.has_key?(:completed) && item.completed != '' && taskseries[:completed] == nil
        taskseries[:completed] = true
      else  # once we set it to false, it can't get set to true
        taskseries[:completed] = false
      end
    end

    # TODO: support past tasks?
    tasks[taskseries[:id]] = taskseries
  end

  return tasks
end

#sign_request(args) ⇒ Object



389
390
391
# File 'lib/rtmapi.rb', line 389

def sign_request( args )
  return MD5.md5(@shared_secret + args.sort.flatten.join).to_s
end

#time_to_user_tz(time) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/rtmapi.rb', line 66

def time_to_user_tz( time )
  return time unless(@use_user_tz && @auth_token && defined?(TZInfo::Timezone))
  begin
    unless defined?(@user_settings_cache[auth_token]) && defined?(@user_settings_cache[auth_token][:tz])
      @user_settings_cache[auth_token] = settings.getList
      @user_settings_cache[auth_token][:tz] = TZInfo::Timezone.get(@user_settings_cache[auth_token].timezone)
    end
    debug "returning time in local zone(%s/%s)", @user_settings_cache[auth_token].timezone, @user_settings_cache[auth_token][:tz]
    @user_settings_cache[auth_token][:tz].utc_to_local(time)
  rescue Exception => err
    debug "unable to read local timezone for auth_token<%s>, ignoring timezone.  err<%s>", auth_token, err
    time
  end
end

#userObject



54
55
56
# File 'lib/rtmapi.rb', line 54

def user
  @user_info_cache[auth_token] ||= auth.checkToken.user
end

#user_settingsObject



58
59
60
# File 'lib/rtmapi.rb', line 58

def 
  @user_settings_cache[auth_token]
end

#versionObject



102
# File 'lib/rtmapi.rb', line 102

def version() RUBY_API_VERSION  end

#xml_attributes_to_hash(attributes, class_name = RememberTheMilkHash) ⇒ Object



164
165
166
167
168
# File 'lib/rtmapi.rb', line 164

def xml_attributes_to_hash( attributes, class_name = RememberTheMilkHash )
  hash = class_name.send(:new)
  attributes.each {|a| hash[a.name.to_sym] = a.value} if attributes.respond_to?(:each)
  return hash
end

#xml_node_to_hash(node, recursion_level = 0) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/rtmapi.rb', line 139

def xml_node_to_hash( node, recursion_level = 0 )
  result = xml_attributes_to_hash( node.properties )
  if node.element? == false
    result[node.name.to_sym] = node.content
  else
    node.each do |child| 
      name = child.name.to_sym
      value = xml_node_to_hash( child, recursion_level+1 )

      # if we have the same node name appear multiple times, we need to build up an array
      # of the converted nodes
      if !result.has_key?(name)
        result[name] = value
      elsif result[name].class != Array
        result[name] = [result[name], value]
      else
        result[name] << value
      end
    end
  end
  
  # top level nodes should be a hash no matter what
  (recursion_level == 0 || result.values.size > 1) ? result : result.values[0]
end