Class: RememberTheMilk
- Inherits:
-
Object
- Object
- RememberTheMilk
- 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
-
#api_key ⇒ Object
Returns the value of attribute api_key.
-
#auth_token ⇒ Object
Returns the value of attribute auth_token.
-
#debug(*args) ⇒ Object
Returns the value of attribute debug.
-
#max_connection_attempts ⇒ Object
Returns the value of attribute max_connection_attempts.
-
#return_raw_response ⇒ Object
Returns the value of attribute return_raw_response.
-
#shared_secret ⇒ Object
Returns the value of attribute shared_secret.
-
#use_user_tz ⇒ Object
Returns the value of attribute use_user_tz.
Instance Method Summary collapse
- #auth_url(perms = 'delete') ⇒ Object
- #call_api_method(method, args = {}) ⇒ Object
- #get_timeline ⇒ Object
- #index_data_into_hash(data, key) ⇒ Object
-
#initialize(api_key = API_KEY, shared_secret = API_SHARED_SECRET, endpoint = 'http://www.rememberthemilk.com/services/rest/') ⇒ RememberTheMilk
constructor
TODO: test efficacy of using www.rememberthemilk.com/services/rest/.
- #logout_user(auth_token) ⇒ Object
-
#method_missing(symbol, *args) ⇒ Object
this is a little fragile.
- #parse_response(response, method, args) ⇒ Object
- #process_task_list(list_id, list) ⇒ Object
- #sign_request(args) ⇒ Object
- #time_to_user_tz(time) ⇒ Object
- #user ⇒ Object
- #user_settings ⇒ Object
- #version ⇒ Object
- #xml_attributes_to_hash(attributes, class_name = RememberTheMilkHash) ⇒ Object
- #xml_node_to_hash(node, recursion_level = 0) ⇒ Object
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_key ⇒ Object
Returns the value of attribute api_key.
52 53 54 |
# File 'lib/rtmapi.rb', line 52 def api_key @api_key end |
#auth_token ⇒ Object
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_attempts ⇒ Object
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_response ⇒ Object
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_secret ⇒ Object
Returns the value of attribute shared_secret.
52 53 54 |
# File 'lib/rtmapi.rb', line 52 def shared_secret @shared_secret end |
#use_user_tz ⇒ Object
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_timeline ⇒ Object
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 |
#user ⇒ Object
54 55 56 |
# File 'lib/rtmapi.rb', line 54 def user @user_info_cache[auth_token] ||= auth.checkToken.user end |
#user_settings ⇒ Object
58 59 60 |
# File 'lib/rtmapi.rb', line 58 def user_settings @user_settings_cache[auth_token] end |
#version ⇒ Object
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 |