rtmapi 0.5
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.
- data/README +236 -0
- data/gpl.txt +339 -0
- data/lib/rtmapi.rb +597 -0
- data/test/data/test_get_method_exceptions.1.xml +2 -0
- data/test/data/test_get_method_exceptions.2.xml +2 -0
- data/test/data/test_get_method_exceptions.3.xml +2 -0
- data/test/data/test_rtm_auth_checkToken_bad.1.xml +2 -0
- data/test/data/test_rtm_auth_checkToken_good.1.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.1.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.2.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.3.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.4.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.5.xml +2 -0
- data/test/data/test_rtm_contacts_delete_and_add.6.xml +2 -0
- data/test/data/test_rtm_contacts_getList.1.xml +2 -0
- data/test/data/test_rtm_get_frob.1.xml +2 -0
- data/test/data/test_rtm_get_task_and_is_complete.1.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.1.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.2.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.3.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.4.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.5.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.6.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.7.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.8.xml +2 -0
- data/test/data/test_rtm_groups_add_and_delete.9.xml +2 -0
- data/test/data/test_rtm_groups_getList.1.xml +2 -0
- data/test/data/test_rtm_lists_getList.1.xml +2 -0
- data/test/data/test_rtm_lists_setName.1.xml +2 -0
- data/test/data/test_rtm_lists_setName.2.xml +2 -0
- data/test/data/test_rtm_lists_setName.3.xml +2 -0
- data/test/data/test_rtm_lists_setName.4.xml +2 -0
- data/test/data/test_rtm_lists_setName.5.xml +2 -0
- data/test/data/test_rtm_lists_setName.6.xml +2 -0
- data/test/data/test_rtm_reflection_getMethods.1.xml +2 -0
- data/test/data/test_rtm_settings_getList.1.xml +2 -0
- data/test/data/test_rtm_tasks_add.1.xml +2 -0
- data/test/data/test_rtm_tasks_add.2.xml +2 -0
- data/test/data/test_rtm_tasks_add.3.xml +2 -0
- data/test/data/test_rtm_tasks_getList.1.xml +2 -0
- data/test/data/test_rtm_tasks_getList_all.1.xml +2 -0
- data/test/data/test_rtm_tasks_getList_from_smartlist.1.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.1.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.2.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.3.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.4.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.5.xml +2 -0
- data/test/data/test_rtm_tasks_setDueDate.6.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.1.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.2.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.3.xml +2 -0
- data/test/data/test_rtm_tasks_setRecurrence.4.xml +2 -0
- data/test/data/test_rtm_test_echo.1.xml +2 -0
- data/test/data/test_rtm_time_parse.1.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.1.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.2.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.3.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.4.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.5.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_priorities.6.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.1.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.2.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.3.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.4.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.5.xml +2 -0
- data/test/data/test_rtm_timelines_and_transactions_with_tags.6.xml +2 -0
- data/test/data/test_rtm_timezones_getList.1.xml +2 -0
- data/test/data/test_rtm_user.1.xml +2 -0
- data/test/test-rtmapi.rb +506 -0
- metadata +131 -0
data/lib/rtmapi.rb
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
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
|
+
require 'uri'
|
|
21
|
+
require 'md5'
|
|
22
|
+
require 'cgi'
|
|
23
|
+
require 'net/http'
|
|
24
|
+
require 'date'
|
|
25
|
+
require 'time'
|
|
26
|
+
require 'parsedate'
|
|
27
|
+
require 'rubygems'
|
|
28
|
+
require 'xml/libxml'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#TODO: allow specifying whether retval should be indexed by rtm_id or list name for lists
|
|
32
|
+
|
|
33
|
+
class RememberTheMilk
|
|
34
|
+
RUBY_API_VERSION = '0.5'
|
|
35
|
+
# you can just put set these here so you don't have to pass them in with
|
|
36
|
+
# every constructor call
|
|
37
|
+
API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
|
38
|
+
API_SHARED_SECRET = 'xxxxxxxxxxxxxxxx'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Element = 0
|
|
42
|
+
CloseTag = 1
|
|
43
|
+
Tag = 2
|
|
44
|
+
Attributes = 3
|
|
45
|
+
#SelfContainedElement = 4
|
|
46
|
+
TextNode = 4
|
|
47
|
+
|
|
48
|
+
TagName = 0
|
|
49
|
+
TagHash = 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
attr_accessor :debug, :auth_token, :return_raw_response, :api_key, :shared_secret, :max_connection_attempts, :use_user_tz
|
|
53
|
+
|
|
54
|
+
def user
|
|
55
|
+
@user_info_cache[auth_token] ||= auth.checkToken.user
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def user_settings
|
|
59
|
+
@user_settings_cache[auth_token]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def time_to_user_tz( time )
|
|
63
|
+
return time unless(use_user_tz && auth_token && defined?(TZInfo::Timezone.get))
|
|
64
|
+
begin
|
|
65
|
+
unless defined?(@user_settings_cache[auth_token]) && defined?(@user_settings_cache[auth_token][:tz])
|
|
66
|
+
@user_settings_cache[auth_token] = settings.getList
|
|
67
|
+
@user_settings_cache[auth_token][:tz] = TZInfo::Timezone.get(@user_settings_cache[auth_token].timezone)
|
|
68
|
+
end
|
|
69
|
+
debug "returning time in local zone(%s/%s)", @user_settings_cache[auth_token].timezone, @user_settings_cache[auth_token][:tz]
|
|
70
|
+
@user_settings_cache[auth_token][:tz].utc_to_local(time)
|
|
71
|
+
rescue Exception => err
|
|
72
|
+
debug "unable to read local timezone for auth_token<%s>, ignoring timezone. err<%s>", auth_token, err
|
|
73
|
+
time
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def logout_user(auth_token)
|
|
78
|
+
@auth_token = nil if @auth_token == auth_token
|
|
79
|
+
@user_settings_cache.delete(auth_token)
|
|
80
|
+
@user_info_cache.delete(auth_token)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# TODO: test efficacy of using https://www.rememberthemilk.com/services/rest/
|
|
84
|
+
def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, endpoint = 'http://www.rememberthemilk.com/services/rest/' )
|
|
85
|
+
@max_connection_attempts = 3
|
|
86
|
+
@debug = false
|
|
87
|
+
@api_key = api_key
|
|
88
|
+
@shared_secret = shared_secret
|
|
89
|
+
@uri = URI.parse(endpoint)
|
|
90
|
+
@auth_token = nil
|
|
91
|
+
@return_raw_response = false
|
|
92
|
+
@use_user_tz = true
|
|
93
|
+
@user_settings_cache = {}
|
|
94
|
+
@user_info_cache = {}
|
|
95
|
+
@xml_parser = XML::Parser.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def version() RUBY_API_VERSION end
|
|
99
|
+
|
|
100
|
+
def debug(*args)
|
|
101
|
+
return unless @debug
|
|
102
|
+
if defined?(RAILS_DEFAULT_LOGGER)
|
|
103
|
+
RAILS_DEFAULT_LOGGER.warn( sprintf(*args) )
|
|
104
|
+
else
|
|
105
|
+
$stderr.puts(sprintf(*args))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def auth_url( perms = 'delete' )
|
|
110
|
+
auth_url = 'http://www.rememberthemilk.com/services/auth/'
|
|
111
|
+
args = { 'api_key' => @api_key, 'perms' => perms }
|
|
112
|
+
args['api_sig'] = sign_request(args)
|
|
113
|
+
return auth_url + '?' + args.keys.collect {|k| "#{k}=#{args[k]}"}.join('&')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# this is a little fragile. it assumes we are being invoked with RTM api calls
|
|
117
|
+
# (which are two levels deep)
|
|
118
|
+
# e.g.,
|
|
119
|
+
# rtm = RememberTheMilk.new
|
|
120
|
+
# data = rtm.reflection.getMethodInfo('method_name' => 'rtm.test.login')
|
|
121
|
+
# the above line gets turned into two calls, the first to this, which returns
|
|
122
|
+
# an RememberTheMilkAPINamespace object, which then gets *its* method_missing
|
|
123
|
+
# invoked with 'getMethodInfo' and the above args
|
|
124
|
+
# i.e.,
|
|
125
|
+
# rtm.foo.bar
|
|
126
|
+
# rtm.foo() => a
|
|
127
|
+
# a.bar
|
|
128
|
+
|
|
129
|
+
def method_missing( symbol, *args )
|
|
130
|
+
rtm_namespace = symbol.id2name
|
|
131
|
+
debug("method_missing called with namespace <%s>", rtm_namespace)
|
|
132
|
+
RememberTheMilkAPINamespace.new( rtm_namespace, self )
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def xml_node_to_hash( node, recursion_level = 0 )
|
|
136
|
+
result = xml_attributes_to_hash( node.properties )
|
|
137
|
+
if node.element? == false
|
|
138
|
+
result[node.name.to_sym] = node.content
|
|
139
|
+
else
|
|
140
|
+
node.each do |child|
|
|
141
|
+
name = child.name.to_sym
|
|
142
|
+
value = xml_node_to_hash( child, recursion_level+1 )
|
|
143
|
+
|
|
144
|
+
# if we have the same node name appear multiple times, we need to build up an array
|
|
145
|
+
# of the converted nodes
|
|
146
|
+
if !result.has_key?(name)
|
|
147
|
+
result[name] = value
|
|
148
|
+
elsif result[name].class != Array
|
|
149
|
+
result[name] = [result[name], value]
|
|
150
|
+
else
|
|
151
|
+
result[name] << value
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# top level nodes should be a hash no matter what
|
|
157
|
+
(recursion_level == 0 || result.values.size > 1) ? result : result.values[0]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def xml_attributes_to_hash( attributes, class_name = RememberTheMilkHash )
|
|
161
|
+
hash = class_name.send(:new)
|
|
162
|
+
attributes.each {|a| hash[a.name.to_sym] = a.value} if attributes.respond_to?(:each)
|
|
163
|
+
return hash
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def index_data_into_hash( data, key )
|
|
167
|
+
new_hash = RememberTheMilkHash.new
|
|
168
|
+
|
|
169
|
+
if data.class == Array
|
|
170
|
+
data.each {|datum| new_hash[datum[key]] = datum }
|
|
171
|
+
else
|
|
172
|
+
new_hash[data[key]] = data
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
new_hash
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def parse_response(response,method,args)
|
|
179
|
+
# groups -- an array of group obj
|
|
180
|
+
# group -- some attributes and a possible contacts array
|
|
181
|
+
# contacts -- an array of contact obj
|
|
182
|
+
# contact -- just attributes
|
|
183
|
+
# lists -- array of list obj
|
|
184
|
+
# list -- attributes and possible filter obj, and a set of taskseries objs?
|
|
185
|
+
# task sereies obj are always wrapped in a list. why?
|
|
186
|
+
# taskseries -- set of attributes, array of tags, an rrule, participants array of contacts, notes,
|
|
187
|
+
# and task. created and modified are time obj,
|
|
188
|
+
# task -- attributes, due/added are time obj
|
|
189
|
+
# note -- attributes and a body of text, with created and modified time obj
|
|
190
|
+
# time -- convert to a time obj
|
|
191
|
+
# timeline -- just has a body of text
|
|
192
|
+
return true unless response.keys.size > 1 # empty response (stat only)
|
|
193
|
+
|
|
194
|
+
rtm_transaction = nil
|
|
195
|
+
if response.has_key?(:transaction)
|
|
196
|
+
# debug("got back <%s> elements in my transaction", response[:transaction].keys.size)
|
|
197
|
+
# we just did a write operation, got back a transaction AND some data.
|
|
198
|
+
# Now, we will do some fanciness.
|
|
199
|
+
rtm_transaction = response[:transaction]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
response_types = response.keys - [:stat, :transaction]
|
|
203
|
+
|
|
204
|
+
if response.has_key?(:api_key) # echo call, we assume
|
|
205
|
+
response_type = :echo
|
|
206
|
+
data = response
|
|
207
|
+
elsif response_types.size > 1
|
|
208
|
+
error = RememberTheMilkAPIError.new({:code => "666", :msg=>"found more than one response type[#{response_types.join(',')}]"},method,args)
|
|
209
|
+
debug( "%s", error )
|
|
210
|
+
raise error
|
|
211
|
+
else
|
|
212
|
+
response_type = response_types[0] || :transaction
|
|
213
|
+
data = response[response_type]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
case response_type
|
|
218
|
+
when :auth
|
|
219
|
+
when :frob
|
|
220
|
+
when :echo
|
|
221
|
+
when :transaction
|
|
222
|
+
when :timeline
|
|
223
|
+
when :methods
|
|
224
|
+
when :settings
|
|
225
|
+
when :contact
|
|
226
|
+
when :group
|
|
227
|
+
# no op
|
|
228
|
+
|
|
229
|
+
when :tasks
|
|
230
|
+
new_hash = RememberTheMilkHash.new
|
|
231
|
+
if data.class == Array # a bunch of lists
|
|
232
|
+
data.each do |list|
|
|
233
|
+
if list.class == String # empty list, just an id, so we create a stub
|
|
234
|
+
new_list = RememberTheMilkHash.new
|
|
235
|
+
new_list[:id] = list
|
|
236
|
+
list = new_list
|
|
237
|
+
end
|
|
238
|
+
new_hash[list[:id]] = process_task_list( list[:id], list.arrayify_value(:taskseries) )
|
|
239
|
+
end
|
|
240
|
+
data = new_hash
|
|
241
|
+
elsif data.class == RememberTheMilkHash # only one list
|
|
242
|
+
data = process_task_list( data[:id], data[:taskseries] )
|
|
243
|
+
elsif data.class == NilClass # empty list
|
|
244
|
+
data = new_hash
|
|
245
|
+
else # who knows...
|
|
246
|
+
debug( "got a class of (%s) when processing tasks. passing it on through", data.class )
|
|
247
|
+
end
|
|
248
|
+
when :groups
|
|
249
|
+
# contacts expected to be array, so look at each group and fix it's contact
|
|
250
|
+
data = [data] unless data.class == Array # won't be array if there's only one group. normalize here
|
|
251
|
+
data.each do |datum|
|
|
252
|
+
datum.arrayify_value( :contacts )
|
|
253
|
+
end
|
|
254
|
+
data = index_data_into_hash( data, :id )
|
|
255
|
+
when :time
|
|
256
|
+
data = time_to_user_tz( Time.parse(data[:text]) )
|
|
257
|
+
when :timezones
|
|
258
|
+
data = index_data_into_hash( data, :name )
|
|
259
|
+
when :lists
|
|
260
|
+
data = index_data_into_hash( data, :id )
|
|
261
|
+
when :contacts
|
|
262
|
+
data = [data].compact unless data.class == Array
|
|
263
|
+
when :list
|
|
264
|
+
# rtm.tasks.add returns one of these, which looks like this:
|
|
265
|
+
# <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>
|
|
266
|
+
# rtm.lists.add also returns this, but it looks like this:
|
|
267
|
+
# <rsp stat='ok'><transaction id='978727001' undoable='0'/><list name='PersonalClone2' smart='0' id='761266' archived='0' deleted='0' position='0' locked='0'/></rsp>
|
|
268
|
+
# so we can look for a name attribute
|
|
269
|
+
if !data.has_key?(:name)
|
|
270
|
+
data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
|
|
271
|
+
data = data.values[0] if data.values.size == 1
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
throw "Unsupported reply type<#{response_type}>#{response.inspect}"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
if rtm_transaction
|
|
278
|
+
if !data.respond_to?(:keys)
|
|
279
|
+
new_hash = RememberTheMilkHash.new
|
|
280
|
+
new_hash[response_type] = data
|
|
281
|
+
data = new_hash
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if data.keys.size == 0
|
|
285
|
+
data = rtm_transaction
|
|
286
|
+
else
|
|
287
|
+
data[:rtm_transaction] = rtm_transaction if rtm_transaction
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
return data
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def process_task_list( list_id, list )
|
|
295
|
+
return {} unless list
|
|
296
|
+
tasks = RememberTheMilkHash.new
|
|
297
|
+
|
|
298
|
+
list.each do |taskseries_as_hash|
|
|
299
|
+
taskseries = RememberTheMilkTask.new.merge(taskseries_as_hash)
|
|
300
|
+
|
|
301
|
+
taskseries[:parent_list] = list_id # parent pointers are nice
|
|
302
|
+
taskseries[:tasks] = taskseries.arrayify_value(:task)
|
|
303
|
+
taskseries.arrayify_value(:tags)
|
|
304
|
+
taskseries.arrayify_value(:participants)
|
|
305
|
+
|
|
306
|
+
# TODO is there a ruby lib that speaks rrule?
|
|
307
|
+
taskseries[:recurrence] = nil
|
|
308
|
+
if taskseries[:rrule]
|
|
309
|
+
taskseries[:recurrence] = taskseries[:rrule]
|
|
310
|
+
taskseries[:recurrence][:rule] = taskseries[:rrule][:text]
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
taskseries[:completed] = nil
|
|
314
|
+
taskseries.tasks.each do |item|
|
|
315
|
+
if item.has_key?(:due) && item.due != ''
|
|
316
|
+
item.due = time_to_user_tz( Time.parse(item.due) )
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
if item.has_key?(:completed) && item.completed != '' && taskseries[:completed] == nil
|
|
320
|
+
taskseries[:completed] = true
|
|
321
|
+
else # once we set it to false, it can't get set to true
|
|
322
|
+
taskseries[:completed] = false
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# TODO: support past tasks?
|
|
327
|
+
tasks[taskseries[:id]] = taskseries
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
return tasks
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def call_api_method( method, args={} )
|
|
334
|
+
|
|
335
|
+
unless args.has_key?(:test_data)
|
|
336
|
+
args['method'] = "rtm.#{method}"
|
|
337
|
+
args['api_key'] = @api_key
|
|
338
|
+
args['auth_token'] ||= @auth_token if @auth_token
|
|
339
|
+
|
|
340
|
+
# make sure everything in our arguments is a string
|
|
341
|
+
args.each do |key,value|
|
|
342
|
+
key_s = key.to_s
|
|
343
|
+
args.delete(key) if key.class != String
|
|
344
|
+
args[key_s] = value.to_s
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
args['api_sig'] = sign_request(args)
|
|
348
|
+
|
|
349
|
+
debug( 'rtm.%s(%s)', method, args.inspect )
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
attempts_left = @max_connection_attempts
|
|
353
|
+
|
|
354
|
+
begin
|
|
355
|
+
if args.has_key?(:test_data)
|
|
356
|
+
@xml_parser.string = args[:test_data]
|
|
357
|
+
else
|
|
358
|
+
attempts_left -= 1
|
|
359
|
+
response = Net::HTTP.get_response(@uri.host, "#{@uri.path}?#{args.keys.collect {|k| "#{CGI::escape(k).gsub(/ /,'+')}=#{CGI::escape(args[k]).gsub(/ /,'+')}"}.join('&')}")
|
|
360
|
+
debug('RESPONSE code: %s\n%sEND RESPONSE\n', response.code, response.body)
|
|
361
|
+
@xml_parser.string = response.body
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
raw_data = @xml_parser.parse
|
|
365
|
+
data = xml_node_to_hash( raw_data.root )
|
|
366
|
+
|
|
367
|
+
if data[:stat] != 'ok'
|
|
368
|
+
debug( "processed into data<#{data.inspect}>")
|
|
369
|
+
error = RememberTheMilkAPIError.new(data[:err],method,args)
|
|
370
|
+
debug( "%s", error )
|
|
371
|
+
raise error
|
|
372
|
+
end
|
|
373
|
+
return return_raw_response ? @xml_parser.string : parse_response(data,method,args)
|
|
374
|
+
rescue XML::Parser::ParseError => err
|
|
375
|
+
debug("Unable to parse document.\nGot response:%s\nGot Error:\n", response.body, err.to_s)
|
|
376
|
+
raise err
|
|
377
|
+
rescue Timeout::Error => timeout
|
|
378
|
+
$stderr.puts "Timed out to<#{endpoint}>, trying #{attempts_left} more times"
|
|
379
|
+
if attempts_left > 0
|
|
380
|
+
retry
|
|
381
|
+
else
|
|
382
|
+
raise timeout
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def sign_request( args )
|
|
388
|
+
return MD5.md5(@shared_secret + args.sort.flatten.join).to_s
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
## a pretty crappy exception class, but it should be sufficient for bubbling
|
|
394
|
+
## up errors returned by the RTM API (website)
|
|
395
|
+
class RememberTheMilkAPIError < RuntimeError
|
|
396
|
+
attr_reader :response, :error_code, :error_message
|
|
397
|
+
|
|
398
|
+
def initialize(error, method, args_to_method)
|
|
399
|
+
@method_name = method
|
|
400
|
+
@args_to_method = args_to_method
|
|
401
|
+
@error_code = error[:code].to_i
|
|
402
|
+
@error_message = error[:msg]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def to_s
|
|
406
|
+
"Calling rtm.#{@method_name}(#{@args_to_method.inspect}) produced => <#{@error_code}>: #{@error_message}"
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
## this is just a helper class so that you can do things like
|
|
412
|
+
## rtm.test.echo. the method_missing in RememberTheMilkAPI returns one of
|
|
413
|
+
## these.
|
|
414
|
+
## this class is the "test" portion of the programming. its method_missing then
|
|
415
|
+
## get invoked with "echo" as the symbol. it has stored a reference to the original
|
|
416
|
+
## rtm object, so it can then invoke call_api_method
|
|
417
|
+
class RememberTheMilkAPINamespace
|
|
418
|
+
def initialize(namespace, rtm)
|
|
419
|
+
@namespace = namespace
|
|
420
|
+
@rtm = rtm
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def method_missing( symbol, *args )
|
|
424
|
+
method_name = symbol.id2name
|
|
425
|
+
@rtm.call_api_method( "#{@namespace}.#{method_name}", *args)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
## a standard hash with some helper methods
|
|
430
|
+
class RememberTheMilkHash < Hash
|
|
431
|
+
attr_accessor :rtm
|
|
432
|
+
|
|
433
|
+
@@strict_keys = true
|
|
434
|
+
def self.strict_keys=( value )
|
|
435
|
+
@@strict_keys = value
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def initialize(rtm_object = nil)
|
|
439
|
+
super
|
|
440
|
+
@rtm = rtm_object
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def id
|
|
444
|
+
rtm_id || object_id
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def rtm_id
|
|
448
|
+
self[:id]
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# guarantees that a given key corresponds to an array, even if it's an empty array
|
|
452
|
+
def arrayify_value( key )
|
|
453
|
+
if !self.has_key?(key)
|
|
454
|
+
self[key] = []
|
|
455
|
+
elsif self[key].class != Array
|
|
456
|
+
self[key] = [ self[key] ].compact
|
|
457
|
+
else
|
|
458
|
+
self[key]
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def method_missing( key, *args )
|
|
464
|
+
name = key.to_s
|
|
465
|
+
|
|
466
|
+
setter = false
|
|
467
|
+
if name[-1,1] == '='
|
|
468
|
+
name = name.chop
|
|
469
|
+
setter = true
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
if name == ""
|
|
473
|
+
name = "rtm_nil".to_sym
|
|
474
|
+
else
|
|
475
|
+
name = name.to_sym
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# TODO: should we allow the blind setting of values? (i.e., only do this test
|
|
480
|
+
# if setter==false )
|
|
481
|
+
raise "unknown hash key<#{name}> requested for #{self.inspect}" if @@strict_keys && !self.has_key?(name)
|
|
482
|
+
|
|
483
|
+
if setter
|
|
484
|
+
self[name] = *args
|
|
485
|
+
else
|
|
486
|
+
self[name]
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
## DateSet is to manage rrules
|
|
493
|
+
## this comes from the iCal ruby module as mentioned here:
|
|
494
|
+
## http://www.macdevcenter.com/pub/a/mac/2003/09/03/rubycocoa.html
|
|
495
|
+
|
|
496
|
+
class DateSet
|
|
497
|
+
|
|
498
|
+
def initialize(startDate, rule)
|
|
499
|
+
@startDate = startDate
|
|
500
|
+
@frequency = nil
|
|
501
|
+
@count = nil
|
|
502
|
+
@untilDate = nil
|
|
503
|
+
@byMonth = nil
|
|
504
|
+
@byDay = nil
|
|
505
|
+
@starts = nil
|
|
506
|
+
if not rule.nil? then
|
|
507
|
+
@starts = rule.every == 1 ? 'every' : 'after'
|
|
508
|
+
parseRecurrenceRule(rule.rule)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def parseRecurrenceRule(rule)
|
|
513
|
+
|
|
514
|
+
if rule =~ /FREQ=(.*?);/ then
|
|
515
|
+
@frequency = $1
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
if rule =~ /COUNT=(\d*)/ then
|
|
519
|
+
@count = $1.to_i
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
if rule =~ /UNTIL=(.*?)[;\r]/ then
|
|
523
|
+
@untilDate = DateParser.parse($1)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
if rule =~ /INTERVAL=(\d*)/ then
|
|
527
|
+
@interval = $1.to_i
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
if rule =~ /BYMONTH=(.*?);/ then
|
|
531
|
+
@byMonth = $1
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
if rule =~ /BYDAY=(.*?);/ then
|
|
535
|
+
@byDay = $1
|
|
536
|
+
#puts "byDay = #{@byDay}"
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def to_s
|
|
541
|
+
# after/every FREQ
|
|
542
|
+
puts "UNIMPLETEMENT"
|
|
543
|
+
# puts "#<DateSet: starts: #{@startDate.strftime("%m/%d/%Y")}, occurs: #{@frequency}, count: #{@count}, until: #{@untilDate}, byMonth: #{@byMonth}, byDay: #{@byDay}>"
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def includes?(date)
|
|
547
|
+
return true if date == @startDate
|
|
548
|
+
return false if @untilDate and date > @untilDate
|
|
549
|
+
|
|
550
|
+
case @frequency
|
|
551
|
+
when 'DAILY'
|
|
552
|
+
#if @untilDate then
|
|
553
|
+
# return (@startDate..@untilDate).include?(date)
|
|
554
|
+
#end
|
|
555
|
+
increment = @interval ? @interval : 1
|
|
556
|
+
d = @startDate
|
|
557
|
+
counter = 0
|
|
558
|
+
until d > date
|
|
559
|
+
|
|
560
|
+
if @count then
|
|
561
|
+
counter += 1
|
|
562
|
+
if counter >= @count
|
|
563
|
+
return false
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
d += (increment * SECONDS_PER_DAY)
|
|
568
|
+
if d.day == date.day and
|
|
569
|
+
d.year == date.year and
|
|
570
|
+
d.month == date.month then
|
|
571
|
+
puts "true for start: #{@startDate}, until: #{@untilDate}"
|
|
572
|
+
return true
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
when 'WEEKLY'
|
|
578
|
+
return true if @startDate.wday == date.wday
|
|
579
|
+
|
|
580
|
+
when 'MONTHLY'
|
|
581
|
+
|
|
582
|
+
when 'YEARLY'
|
|
583
|
+
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
false
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
attr_reader :frequency
|
|
590
|
+
attr_accessor :startDate
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class RememberTheMilkTask < RememberTheMilkHash
|
|
595
|
+
# The API is aware it's creating tasks. You may want to add semantics to a "task"
|
|
596
|
+
# elsewhere in your program. This gives you that flexibility
|
|
597
|
+
end
|