Class: ThaRememberTheMilk
- Inherits:
-
Object
- Object
- ThaRememberTheMilk
- Defined in:
- lib/thartmx_lib.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
''
- API_SHARED_SECRET =
''
- AUTH_TOKEN =
''
- 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, auth_token = AUTH_TOKEN, endpoint = 'http://www.rememberthemilk.com/services/rest/') ⇒ ThaRememberTheMilk
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, auth_token = AUTH_TOKEN, endpoint = 'http://www.rememberthemilk.com/services/rest/') ⇒ ThaRememberTheMilk
TODO: test efficacy of using www.rememberthemilk.com/services/rest/
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/thartmx_lib.rb', line 102 def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, auth_token = AUTH_TOKEN, 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 @auth_token = auth_token @return_raw_response = false @use_user_tz = true @user_settings_cache = {} @user_info_cache = {} #@xml_parser = XML::Parser.new @xml_parser = XML::Parser.new(XML::Parser::Context.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
149 150 151 152 153 |
# File 'lib/thartmx_lib.rb', line 149 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.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def api_key @api_key end |
#auth_token ⇒ Object
Returns the value of attribute auth_token.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def auth_token @auth_token end |
#debug(*args) ⇒ Object
Returns the value of attribute debug.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def debug @debug end |
#max_connection_attempts ⇒ Object
Returns the value of attribute max_connection_attempts.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def max_connection_attempts @max_connection_attempts end |
#return_raw_response ⇒ Object
Returns the value of attribute return_raw_response.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def return_raw_response @return_raw_response end |
#shared_secret ⇒ Object
Returns the value of attribute shared_secret.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def shared_secret @shared_secret end |
#use_user_tz ⇒ Object
Returns the value of attribute use_user_tz.
66 67 68 |
# File 'lib/thartmx_lib.rb', line 66 def use_user_tz @use_user_tz end |
Instance Method Details
#auth_url(perms = 'delete') ⇒ Object
129 130 131 132 133 134 |
# File 'lib/thartmx_lib.rb', line 129 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
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 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/thartmx_lib.rb', line 355 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) #puts response.body #@xml_parser.string = response.body @xml_parser= XML::Parser.string(response.body) end raw_data = @xml_parser.parse data = xml_node_to_hash( raw_data.root ) #puts data.inspect 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) return 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
76 77 78 |
# File 'lib/thartmx_lib.rb', line 76 def get_timeline user[:timeline] ||= timelines.create end |
#index_data_into_hash(data, key) ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/thartmx_lib.rb', line 186 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
95 96 97 98 99 |
# File 'lib/thartmx_lib.rb', line 95 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
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 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/thartmx_lib.rb', line 198 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 data = data[:list] 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 #puts data.inspect #puts data[:list][3][:taskseries].inspect 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
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/thartmx_lib.rb', line 317 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
411 412 413 414 415 416 417 |
# File 'lib/thartmx_lib.rb', line 411 def sign_request( args ) if /^1\.9/ === RUBY_VERSION then return (Digest::MD5.new << @shared_secret + args.sort.flatten.join).to_s else return MD5.md5(@shared_secret + args.sort.flatten.join).to_s end end |
#time_to_user_tz(time) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/thartmx_lib.rb', line 80 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
68 69 70 |
# File 'lib/thartmx_lib.rb', line 68 def user @user_info_cache[auth_token] ||= auth.checkToken.user end |
#user_settings ⇒ Object
72 73 74 |
# File 'lib/thartmx_lib.rb', line 72 def user_settings @user_settings_cache[auth_token] end |
#version ⇒ Object
118 |
# File 'lib/thartmx_lib.rb', line 118 def version() RUBY_API_VERSION end |
#xml_attributes_to_hash(attributes, class_name = RememberTheMilkHash) ⇒ Object
180 181 182 183 184 |
# File 'lib/thartmx_lib.rb', line 180 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
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/thartmx_lib.rb', line 155 def xml_node_to_hash( node, recursion_level = 0 ) result = xml_attributes_to_hash( node.attributes ) 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 |