thartm 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 tha
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ = thartm
2
+
3
+ Remember the milk command line interface
4
+
5
+ using rtmapi library
6
+ patched to work with the new version of ruby-libxml
7
+
8
+ To make the cli work you have to obtain an api key
9
+ and an api secret for remember the milk.
10
+
11
+ Ask them at:
12
+ http://www.rememberthemilk.com/services/api/keys.rtm
13
+
14
+ puts those keys in a .rtm file in your $HOME
15
+ the file is supposed to be in YAML format
16
+
17
+ example:
18
+ key: yourkey
19
+ secret: yoursecret
20
+ tz: your timezone (UTC, GMT etc..)
21
+
22
+ Than you have to authorize the app and obtain the authorization token
23
+ start thartm command line interface
24
+ and you'll be prompted for an url
25
+
26
+ The auth method could be better. I now.. give me some time :)
27
+
28
+
29
+
30
+ == Note on Patches/Pull Requests
31
+
32
+ * Fork the project.
33
+ * Make your feature addition or bug fix.
34
+ * Add tests for it. This is important so I don't break it in a
35
+ future version unintentionally.
36
+ * Commit, do not mess with rakefile, version, or history.
37
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
38
+ * Send me a pull request. Bonus points for topic branches.
39
+
40
+ == Copyright
41
+
42
+ Copyright (c) 2010 thamayor. See LICENSE for details.
43
+
44
+ Feel free to send me suggestions :)
45
+
46
+ Thanks again to the rtmapi guys, and sorry for my bad fixies :P
47
+
48
+ Mail me at: thamayor [at] gmail [dot] com
49
+
50
+
51
+
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "thartm"
8
+ gem.summary = %Q{rtmapi based remember the milk cli.}
9
+ gem.description = %Q{rtmapi fixed version with a simple cli added}
10
+ gem.email = "thamayor@gmail.com"
11
+ gem.homepage = "http://github.com/ghedamat/thartm"
12
+ gem.authors = ["tha"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", ]
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/test_*.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |test|
33
+ test.libs << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :test => :check_dependencies
44
+
45
+ task :default => :test
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "thartm #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.15
data/bin/rrtm ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/ruby
2
+ require File.dirname(__FILE__) + '/../lib/thartm.rb'
3
+
4
+ CONFIGFILE = File.join(File.expand_path(ENV['HOME']), '.rtm')
5
+ begin
6
+ @@config = YAML.load_file(CONFIGFILE)
7
+ rescue
8
+ raise "please create a .rtm file in your $HOME"
9
+ end
10
+
11
+
12
+ ENV['TZ'] = @@config['tz']
13
+
14
+ # validating token
15
+ unless @@config['token']
16
+ @rtm = ThaRememberTheMilk.new(@@config['key'],@@config['secret'])
17
+ puts "please authorize this program: open the following url and puts the frob value back here."
18
+ puts @rtm.auth_url
19
+ frob = gets
20
+
21
+ auth = @rtm.auth.getToken('frob' => frob.chomp)
22
+ token_file = File.open(CONFIGFILE,"+w")
23
+ token_file << "token: " + auth.token
24
+
25
+ puts "restart the program now"
26
+ exit
27
+ end
28
+
29
+
30
+ cli = CommandLineInterface.new(@@config['key'],@@config['secret'],@@config['token'])
31
+ if ARGV[0]
32
+ begin
33
+ cli.send ARGV[0]
34
+ rescue
35
+ puts "command #{ARGV[0]} is not available"
36
+ cli.help
37
+ end
38
+ else
39
+ cli.tasks
40
+ end
data/lib/thartm.rb ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'yaml'
5
+ require 'thartm_lib.rb'
6
+
7
+ class Rrtm
8
+
9
+ def initialize(key,secret,token)
10
+ #@rtm = ThaRememberTheMilk.new(@@config['key'],@@config['secret'],@@config['token'])
11
+ @rtm = ThaRememberTheMilk.new(key,secret,token)
12
+ @rtm.use_user_tz = true
13
+
14
+ # id of the all tasks list
15
+ @allTaskList = String.new
16
+
17
+ @lists = lists
18
+ @timeline = @rtm.timelines.create
19
+ end
20
+
21
+ def allTaskList
22
+ allTaskList = ''
23
+ @lists.each do |k,v|
24
+ if v[:name] == "All Tasks"
25
+ allTaskList = v[:id]
26
+ end
27
+ end
28
+ return allTaskList
29
+ end
30
+
31
+ def lists
32
+ lists = @rtm.lists.getList
33
+ end
34
+
35
+ def tasks(args = {})
36
+ tasks = @rtm.tasks.getList args
37
+ end
38
+
39
+ def tasksAllTaskList
40
+ t = tasks :list_id => allTaskList
41
+ end
42
+
43
+
44
+ def addTask(text, * args )
45
+ if args.length == 1
46
+ listname = args[0]
47
+ end
48
+
49
+ listid = 0
50
+ if listname
51
+ @lists.each do |k,v|
52
+ if v[:name].match(listname)
53
+ listid = v[:id]
54
+ end
55
+ end
56
+ end
57
+
58
+ if listid != 0
59
+ @rtm.tasks.add :timeline => @timeline, :name => text, :parse => '1', :list_id => listid
60
+ else
61
+ @rtm.tasks.add :timeline => @timeline, :name => text, :parse => '1'
62
+ end
63
+ end
64
+
65
+ def findTask(id)
66
+ tt = tasks
67
+ tt.each do |key,val|
68
+ val.each do |k,v|
69
+ return v if v[:id] == id
70
+ end
71
+ end
72
+ return nil
73
+ end
74
+
75
+ def completeTask(id)
76
+ v = findTask(id)
77
+ @rtm.tasks.complete :timeline => @timeline,:list_id =>v.list_id , :taskseries_id => v.taskseries_id, :task_id => v.task_id
78
+ end
79
+
80
+ def postponeTask(id)
81
+ v = findTask(id)
82
+ @rtm.tasks.postpone :timeline => @timeline,:list_id =>v.list_id , :taskseries_id => v.taskseries_id, :task_id => v.task_id
83
+ end
84
+
85
+ def renameTask(id,newname)
86
+ v = findTask(id)
87
+ @rtm.tasks.setName :timeline => @timeline,:list_id =>v.list_id , :taskseries_id => v.taskseries_id, :task_id => v.task_id, :name => newname
88
+ end
89
+
90
+ end
91
+
92
+ class CommandLineInterface
93
+
94
+ def initialize(key,secret,token)
95
+ @rtm = Rrtm.new(key,secret,token)
96
+ end
97
+
98
+ def tasks
99
+ t = Array.new
100
+ tasks = @rtm.tasksAllTaskList
101
+
102
+ tasks.each do |key,val|
103
+ val.each do |k,v|
104
+ t.push(v) unless v.complete? # do not add c ompleted tasks
105
+ end
106
+ end
107
+
108
+ # sorting by date (inverse order) and than by task name
109
+ t.sort! do |a,b|
110
+ if (a.has_due? and b.has_due?)
111
+ a.due <=> b.due
112
+ elsif a.has_due?
113
+ -1
114
+ elsif b.has_due?
115
+ 1
116
+ else
117
+ a[:name] <=> b[:name]
118
+ end
119
+ end
120
+
121
+ # compose string result
122
+ s = ''
123
+ t.each do |tt|
124
+ s += tt[:id] + ": " + tt[:name].to_s + " -- " + tt.due.to_s + "\n"
125
+ end
126
+ puts s
127
+ end
128
+
129
+ def add
130
+ @rtm.addTask(ARGV[1], ARGV[2] )
131
+ end
132
+
133
+ def lists
134
+ l = @rtm.lists
135
+
136
+ l.each do |k,v|
137
+ puts v[:name]
138
+ end
139
+ end
140
+
141
+ def complete
142
+ @rtm.completeTask(ARGV[1])
143
+ end
144
+
145
+ def postpone
146
+ @rtm.postpone(ARGV[1])
147
+ end
148
+
149
+ def first
150
+ t = Array.new
151
+ tasks = @rtm.tasksAllTaskList
152
+
153
+ tasks.each do |key,val|
154
+ val.each do |k,v|
155
+ t.push(v) unless v.complete? # do not add c ompleted tasks
156
+ end
157
+ end
158
+
159
+ # sorting by date (inverse order) and than by task name
160
+ t.sort! do |a,b|
161
+ if (a.has_due? and b.has_due?)
162
+ a.due <=> b.due
163
+ elsif a.has_due?
164
+ -1
165
+ elsif b.has_due?
166
+ 1
167
+ else
168
+ a[:name] <=> b[:name]
169
+ end
170
+ end
171
+
172
+ # compose string result
173
+ s = ''
174
+ tt = t[0]
175
+ s += tt[:name].to_s + " -- " + tt.due.to_s + "\n"
176
+ puts s
177
+ end
178
+
179
+ def help
180
+ s = ''
181
+ s += 'Rrtm: Tha remember the milk Command Line Usage
182
+ usage rrtm <command> <params>
183
+
184
+ help: print this help and exits
185
+ lists: show available tasks lists
186
+ tasks: show not completed tasks
187
+ add name [lists name]: adds a task to the lists
188
+ complete id: mark task with id "id" as completed
189
+ postpone id: postpone task by one day
190
+ first: show first uncompleted task
191
+ '
192
+ puts s
193
+ end
194
+
195
+
196
+ end
197
+
198
+ #TODO gestione priorita' tasks
199
+ #TODO sort by priority?
200
+
data/lib/thartm_lib.rb ADDED
@@ -0,0 +1,735 @@
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
+ require 'md5'
29
+ require 'cgi'
30
+ require 'net/http'
31
+ require 'date'
32
+ require 'time'
33
+ require 'parsedate'
34
+ require 'rubygems'
35
+ require 'xml/libxml'
36
+ require 'tzinfo'
37
+
38
+
39
+ #TODO: allow specifying whether retval should be indexed by rtm_id or list name for lists
40
+
41
+ class ThaRememberTheMilk
42
+ RUBY_API_VERSION = '0.6'
43
+ # you can just put set these here so you don't have to pass them in with
44
+ # every constructor call
45
+ API_KEY = ''
46
+ API_SHARED_SECRET = ''
47
+ AUTH_TOKEN= ''
48
+
49
+
50
+ Element = 0
51
+ CloseTag = 1
52
+ Tag = 2
53
+ Attributes = 3
54
+ #SelfContainedElement = 4
55
+ TextNode = 4
56
+
57
+ TagName = 0
58
+ TagHash = 1
59
+
60
+
61
+ attr_accessor :debug, :auth_token, :return_raw_response, :api_key, :shared_secret, :max_connection_attempts, :use_user_tz
62
+
63
+ def user
64
+ @user_info_cache[auth_token] ||= auth.checkToken.user
65
+ end
66
+
67
+ def user_settings
68
+ @user_settings_cache[auth_token]
69
+ end
70
+
71
+ def get_timeline
72
+ user[:timeline] ||= timelines.create
73
+ end
74
+
75
+ def time_to_user_tz( time )
76
+ return time unless(@use_user_tz && @auth_token && defined?(TZInfo::Timezone))
77
+ begin
78
+ unless defined?(@user_settings_cache[auth_token]) && defined?(@user_settings_cache[auth_token][:tz])
79
+ @user_settings_cache[auth_token] = settings.getList
80
+ @user_settings_cache[auth_token][:tz] = TZInfo::Timezone.get(@user_settings_cache[auth_token].timezone)
81
+ end
82
+ debug "returning time in local zone(%s/%s)", @user_settings_cache[auth_token].timezone, @user_settings_cache[auth_token][:tz]
83
+ @user_settings_cache[auth_token][:tz].utc_to_local(time)
84
+ rescue Exception => err
85
+ debug "unable to read local timezone for auth_token<%s>, ignoring timezone. err<%s>", auth_token, err
86
+ time
87
+ end
88
+ end
89
+
90
+ def logout_user(auth_token)
91
+ @auth_token = nil if @auth_token == auth_token
92
+ @user_settings_cache.delete(auth_token)
93
+ @user_info_cache.delete(auth_token)
94
+ end
95
+
96
+ # TODO: test efficacy of using https://www.rememberthemilk.com/services/rest/
97
+ def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, auth_token = AUTH_TOKEN, endpoint = 'http://www.rememberthemilk.com/services/rest/')
98
+ @max_connection_attempts = 3
99
+ @debug = false
100
+ @api_key = api_key
101
+ @shared_secret = shared_secret
102
+ @uri = URI.parse(endpoint)
103
+ #@auth_token = nil
104
+ @auth_token = auth_token
105
+ @return_raw_response = false
106
+ @use_user_tz = true
107
+ @user_settings_cache = {}
108
+ @user_info_cache = {}
109
+ #@xml_parser = XML::Parser.new
110
+ @xml_parser = XML::Parser.new(XML::Parser::Context.new)
111
+ end
112
+
113
+ def version() RUBY_API_VERSION end
114
+
115
+ def debug(*args)
116
+ return unless @debug
117
+ if defined?(RAILS_DEFAULT_LOGGER)
118
+ RAILS_DEFAULT_LOGGER.warn( sprintf(*args) )
119
+ else
120
+ $stderr.puts(sprintf(*args))
121
+ end
122
+ end
123
+
124
+ def auth_url( perms = 'delete' )
125
+ auth_url = 'http://www.rememberthemilk.com/services/auth/'
126
+ args = { 'api_key' => @api_key, 'perms' => perms }
127
+ args['api_sig'] = sign_request(args)
128
+ return auth_url + '?' + args.keys.collect {|k| "#{k}=#{args[k]}"}.join('&')
129
+ end
130
+
131
+ # this is a little fragile. it assumes we are being invoked with RTM api calls
132
+ # (which are two levels deep)
133
+ # e.g.,
134
+ # rtm = RememberTheMilk.new
135
+ # data = rtm.reflection.getMethodInfo('method_name' => 'rtm.test.login')
136
+ # the above line gets turned into two calls, the first to this, which returns
137
+ # an RememberTheMilkAPINamespace object, which then gets *its* method_missing
138
+ # invoked with 'getMethodInfo' and the above args
139
+ # i.e.,
140
+ # rtm.foo.bar
141
+ # rtm.foo() => a
142
+ # a.bar
143
+
144
+ def method_missing( symbol, *args )
145
+ rtm_namespace = symbol.id2name
146
+ debug("method_missing called with namespace <%s>", rtm_namespace)
147
+ RememberTheMilkAPINamespace.new( rtm_namespace, self )
148
+ end
149
+
150
+ def xml_node_to_hash( node, recursion_level = 0 )
151
+ result = xml_attributes_to_hash( node.attributes )
152
+ if node.element? == false
153
+ result[node.name.to_sym] = node.content
154
+ else
155
+ node.each do |child|
156
+ name = child.name.to_sym
157
+ value = xml_node_to_hash( child, recursion_level+1 )
158
+
159
+ # if we have the same node name appear multiple times, we need to build up an array
160
+ # of the converted nodes
161
+ if !result.has_key?(name)
162
+ result[name] = value
163
+ elsif result[name].class != Array
164
+ result[name] = [result[name], value]
165
+ else
166
+ result[name] << value
167
+ end
168
+ end
169
+ end
170
+
171
+ # top level nodes should be a hash no matter what
172
+ (recursion_level == 0 || result.values.size > 1) ? result : result.values[0]
173
+ end
174
+
175
+ def xml_attributes_to_hash( attributes, class_name = RememberTheMilkHash )
176
+ hash = class_name.send(:new)
177
+ attributes.each {|a| hash[a.name.to_sym] = a.value} if attributes.respond_to?(:each)
178
+ return hash
179
+ end
180
+
181
+ def index_data_into_hash( data, key )
182
+ new_hash = RememberTheMilkHash.new
183
+
184
+ if data.class == Array
185
+ data.each {|datum| new_hash[datum[key]] = datum }
186
+ else
187
+ new_hash[data[key]] = data
188
+ end
189
+
190
+ new_hash
191
+ end
192
+
193
+ def parse_response(response,method,args)
194
+ # groups -- an array of group obj
195
+ # group -- some attributes and a possible contacts array
196
+ # contacts -- an array of contact obj
197
+ # contact -- just attributes
198
+ # lists -- array of list obj
199
+ # list -- attributes and possible filter obj, and a set of taskseries objs?
200
+ # task sereies obj are always wrapped in a list. why?
201
+ # taskseries -- set of attributes, array of tags, an rrule, participants array of contacts, notes,
202
+ # and task. created and modified are time obj,
203
+ # task -- attributes, due/added are time obj
204
+ # note -- attributes and a body of text, with created and modified time obj
205
+ # time -- convert to a time obj
206
+ # timeline -- just has a body of text
207
+ return true unless response.keys.size > 1 # empty response (stat only)
208
+
209
+ rtm_transaction = nil
210
+ if response.has_key?(:transaction)
211
+ # debug("got back <%s> elements in my transaction", response[:transaction].keys.size)
212
+ # we just did a write operation, got back a transaction AND some data.
213
+ # Now, we will do some fanciness.
214
+ rtm_transaction = response[:transaction]
215
+ end
216
+
217
+ response_types = response.keys - [:stat, :transaction]
218
+
219
+ if response.has_key?(:api_key) # echo call, we assume
220
+ response_type = :echo
221
+ data = response
222
+ elsif response_types.size > 1
223
+ error = RememberTheMilkAPIError.new({:code => "666", :msg=>"found more than one response type[#{response_types.join(',')}]"},method,args)
224
+ debug( "%s", error )
225
+ raise error
226
+ else
227
+ response_type = response_types[0] || :transaction
228
+
229
+ data = response[response_type]
230
+ end
231
+
232
+ case response_type
233
+ when :auth
234
+ when :frob
235
+ when :echo
236
+ when :transaction
237
+ when :timeline
238
+ when :methods
239
+ when :settings
240
+ when :contact
241
+ when :group
242
+ # no op
243
+
244
+ when :tasks
245
+ data = data[:list]
246
+ new_hash = RememberTheMilkHash.new
247
+ if data.class == Array # a bunch of lists
248
+ data.each do |list|
249
+ if list.class == String # empty list, just an id, so we create a stub
250
+ new_list = RememberTheMilkHash.new
251
+ new_list[:id] = list
252
+ list = new_list
253
+ end
254
+ new_hash[list[:id]] = process_task_list( list[:id], list.arrayify_value(:taskseries) )
255
+ end
256
+ data = new_hash
257
+ elsif data.class == RememberTheMilkHash # only one list
258
+ #puts data.inspect
259
+ #puts data[:list][3][:taskseries].inspect
260
+ data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
261
+ elsif data.class == NilClass || (data.class == String && data == args['list_id']) # empty list
262
+ data = new_hash
263
+ else # who knows...
264
+ debug( "got a class of (%s [%s]) when processing tasks. passing it on through", data.class, data )
265
+ end
266
+ when :groups
267
+ # contacts expected to be array, so look at each group and fix it's contact
268
+ data = [data] unless data.class == Array # won't be array if there's only one group. normalize here
269
+ data.each do |datum|
270
+ datum.arrayify_value( :contacts )
271
+ end
272
+ data = index_data_into_hash( data, :id )
273
+ when :time
274
+ data = time_to_user_tz( Time.parse(data[:text]) )
275
+ when :timezones
276
+ data = index_data_into_hash( data, :name )
277
+ when :lists
278
+ data = index_data_into_hash( data, :id )
279
+ when :contacts
280
+ data = [data].compact unless data.class == Array
281
+ when :list
282
+ # rtm.tasks.add returns one of these, which looks like this:
283
+ # <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>
284
+ # rtm.lists.add also returns this, but it looks like this:
285
+ # <rsp stat='ok'><transaction id='978727001' undoable='0'/><list name='PersonalClone2' smart='0' id='761266' archived='0' deleted='0' position='0' locked='0'/></rsp>
286
+ # so we can look for a name attribute
287
+ if !data.has_key?(:name)
288
+ data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
289
+ data = data.values[0] if data.values.size == 1
290
+ end
291
+ else
292
+ throw "Unsupported reply type<#{response_type}>#{response.inspect}"
293
+ end
294
+
295
+ if rtm_transaction
296
+ if !data.respond_to?(:keys)
297
+ new_hash = RememberTheMilkHash.new
298
+ new_hash[response_type] = data
299
+ data = new_hash
300
+ end
301
+
302
+ if data.keys.size == 0
303
+ data = rtm_transaction
304
+ else
305
+ data[:rtm_transaction] = rtm_transaction if rtm_transaction
306
+ end
307
+ end
308
+ return data
309
+ end
310
+
311
+
312
+ def process_task_list( list_id, list )
313
+ return {} unless list
314
+ tasks = RememberTheMilkHash.new
315
+ list.each do |taskseries_as_hash|
316
+ taskseries = RememberTheMilkTask.new(self).merge(taskseries_as_hash)
317
+
318
+ taskseries[:parent_list] = list_id # parent pointers are nice
319
+ taskseries[:tasks] = taskseries.arrayify_value(:task)
320
+ taskseries.arrayify_value(:tags)
321
+ taskseries.arrayify_value(:participants)
322
+
323
+ # TODO is there a ruby lib that speaks rrule?
324
+ taskseries[:recurrence] = nil
325
+ if taskseries[:rrule]
326
+ taskseries[:recurrence] = taskseries[:rrule]
327
+ taskseries[:recurrence][:rule] = taskseries[:rrule][:text]
328
+ end
329
+
330
+ taskseries[:completed] = nil
331
+ taskseries.tasks.each do |item|
332
+ if item.has_key?(:due) && item.due != ''
333
+ item.due = time_to_user_tz( Time.parse(item.due) )
334
+ end
335
+
336
+ if item.has_key?(:completed) && item.completed != '' && taskseries[:completed] == nil
337
+ taskseries[:completed] = true
338
+ else # once we set it to false, it can't get set to true
339
+ taskseries[:completed] = false
340
+ end
341
+ end
342
+
343
+ # TODO: support past tasks?
344
+ tasks[taskseries[:id]] = taskseries
345
+ end
346
+
347
+ return tasks
348
+ end
349
+
350
+ def call_api_method( method, args={} )
351
+
352
+ args['method'] = "rtm.#{method}"
353
+ args['api_key'] = @api_key
354
+ args['auth_token'] ||= @auth_token if @auth_token
355
+
356
+ # make sure everything in our arguments is a string
357
+ args.each do |key,value|
358
+ key_s = key.to_s
359
+ args.delete(key) if key.class != String
360
+ args[key_s] = value.to_s
361
+ end
362
+
363
+ args['api_sig'] = sign_request(args)
364
+
365
+ debug( 'rtm.%s(%s)', method, args.inspect )
366
+
367
+ attempts_left = @max_connection_attempts
368
+
369
+ begin
370
+ if args.has_key?('test_data')
371
+ @xml_parser.string = args['test_data']
372
+ else
373
+ attempts_left -= 1
374
+ response = Net::HTTP.get_response(@uri.host, "#{@uri.path}?#{args.keys.collect {|k| "#{CGI::escape(k).gsub(/ /,'+')}=#{CGI::escape(args[k]).gsub(/ /,'+')}"}.join('&')}")
375
+ debug('RESPONSE code: %s\n%sEND RESPONSE\n', response.code, response.body)
376
+ #puts response.body
377
+ #@xml_parser.string = response.body
378
+ @xml_parser= XML::Parser.string(response.body)
379
+ end
380
+
381
+ raw_data = @xml_parser.parse
382
+ data = xml_node_to_hash( raw_data.root )
383
+ #puts data.inspect
384
+ debug( "processed into data<#{data.inspect}>")
385
+
386
+ if data[:stat] != 'ok'
387
+ error = RememberTheMilkAPIError.new(data[:err],method,args)
388
+ debug( "%s", error )
389
+ raise error
390
+ end
391
+ #return return_raw_response ? @xml_parser.string : parse_response(data,method,args)
392
+ return parse_response(data,method,args)
393
+ #rescue XML::Parser::ParseError => err
394
+ # debug("Unable to parse document.\nGot response:%s\nGot Error:\n", response.body, err.to_s)
395
+ # raise err
396
+ rescue Timeout::Error => timeout
397
+ $stderr.puts "Timed out to<#{endpoint}>, trying #{attempts_left} more times"
398
+ if attempts_left > 0
399
+ retry
400
+ else
401
+ raise timeout
402
+ end
403
+ end
404
+ end
405
+
406
+ def sign_request( args )
407
+ return MD5.md5(@shared_secret + args.sort.flatten.join).to_s
408
+ end
409
+ end
410
+
411
+
412
+ ## a pretty crappy exception class, but it should be sufficient for bubbling
413
+ ## up errors returned by the RTM API (website)
414
+ class RememberTheMilkAPIError < RuntimeError
415
+ attr_reader :response, :error_code, :error_message
416
+
417
+ def initialize(error, method, args_to_method)
418
+ @method_name = method
419
+ @args_to_method = args_to_method
420
+ @error_code = error[:code].to_i
421
+ @error_message = error[:msg]
422
+ end
423
+
424
+ def to_s
425
+ "Calling rtm.#{@method_name}(#{@args_to_method.inspect}) produced => <#{@error_code}>: #{@error_message}"
426
+ end
427
+ end
428
+
429
+
430
+ ## this is just a helper class so that you can do things like
431
+ ## rtm.test.echo. the method_missing in RememberTheMilkAPI returns one of
432
+ ## these.
433
+ ## this class is the "test" portion of the programming. its method_missing then
434
+ ## get invoked with "echo" as the symbol. it has stored a reference to the original
435
+ ## rtm object, so it can then invoke call_api_method
436
+ class RememberTheMilkAPINamespace
437
+ def initialize(namespace, rtm)
438
+ @namespace = namespace
439
+ @rtm = rtm
440
+ end
441
+
442
+ def method_missing( symbol, *args )
443
+ method_name = symbol.id2name
444
+ @rtm.call_api_method( "#{@namespace}.#{method_name}", *args)
445
+ end
446
+ end
447
+
448
+ ## a standard hash with some helper methods
449
+ class RememberTheMilkHash < Hash
450
+ attr_accessor :rtm
451
+
452
+ @@strict_keys = true
453
+ def self.strict_keys=( value )
454
+ @@strict_keys = value
455
+ end
456
+
457
+ def initialize(rtm_object = nil)
458
+ super
459
+ @rtm = rtm_object
460
+ end
461
+
462
+ def id
463
+ rtm_id || object_id
464
+ end
465
+
466
+ def rtm_id
467
+ self[:id]
468
+ end
469
+
470
+ # guarantees that a given key corresponds to an array, even if it's an empty array
471
+ def arrayify_value( key )
472
+ if !self.has_key?(key)
473
+ self[key] = []
474
+ elsif self[key].class != Array
475
+ self[key] = [ self[key] ].compact
476
+ else
477
+ self[key]
478
+ end
479
+ end
480
+
481
+
482
+ def method_missing( key, *args )
483
+ name = key.to_s
484
+
485
+ setter = false
486
+ if name[-1,1] == '='
487
+ name = name.chop
488
+ setter = true
489
+ end
490
+
491
+ if name == ""
492
+ name = "rtm_nil".to_sym
493
+ else
494
+ name = name.to_sym
495
+ end
496
+
497
+
498
+ # TODO: should we allow the blind setting of values? (i.e., only do this test
499
+ # if setter==false )
500
+ raise "unknown hash key<#{name}> requested for #{self.inspect}" if @@strict_keys && !self.has_key?(name)
501
+
502
+ if setter
503
+ self[name] = *args
504
+ else
505
+ self[name]
506
+ end
507
+ end
508
+ end
509
+
510
+
511
+ ## TODO -- better rrule support. start here with this code, commented out for now
512
+ ## DateSet is to manage rrules
513
+ ## this comes from the iCal ruby module as mentioned here:
514
+ ## http://www.macdevcenter.com/pub/a/mac/2003/09/03/rubycocoa.html
515
+
516
+ # The API is aware it's creating tasks. You may want to add semantics to a "task"
517
+ # elsewhere in your program. This gives you that flexibility
518
+ # plus, we've added some helper methods
519
+
520
+ class RememberTheMilkTask < RememberTheMilkHash
521
+ attr_accessor :rtm
522
+
523
+ def timeline
524
+ @timeline ||= rtm.get_timeline # this caches timelines per user
525
+ end
526
+
527
+ def initialize( rtm_api_handle=nil )
528
+ super
529
+ @rtm = rtm_api_handle # keep track of this so we can do setters (see factory below)
530
+ end
531
+
532
+ def task() tasks[-1] end
533
+ def taskseries_id() self.has_key?(:taskseries_id) ? self[:taskseries_id] : rtm_id end
534
+ def task_id() self.has_key?(:task_id) ? self[:task_id] : task.rtm_id end
535
+ def list_id() parent_list end
536
+ def due() task.due end
537
+
538
+ def has_due?() due.class == Time end
539
+ def has_due_time?() task.has_due_time == '1' end
540
+ def complete?() task[:completed] != '' end
541
+ def to_s
542
+ a_parent_list = self[:parent_list] || '<Parent Not Set>'
543
+ a_taskseries_id = self[:taskseries_id] || self[:id] || '<No Taskseries Id>'
544
+ a_task_id = self[:task_id] || (self[:task] && self[:task].rtm_td) || '<No Task Id>'
545
+ a_name = self[:name] || '<Name Not Set>'
546
+ "#{a_parent_list}/#{a_taskseries_id}/#{a_task_id}: #{a_name}"
547
+ end
548
+
549
+ def due_display
550
+ if has_due?
551
+ if has_due_time?
552
+ due.strftime("%a %d %b %y at %I:%M%p")
553
+ else
554
+ due.strftime("%a %d %b %y")
555
+ end
556
+ else
557
+ '[no due date]'
558
+ end
559
+ end
560
+
561
+ @@BeginningOfEpoch = Time.parse("Jan 1 1904") # kludgey.. sure. life's a kludge. deal with it.
562
+ include Comparable
563
+ def <=>(other)
564
+ due = (has_key?(:tasks) && tasks.class == Array) ? task[:due] : nil
565
+ due = @@BeginningOfEpoch unless due.class == Time
566
+ other_due = (other.has_key?(:tasks) && other.tasks.class == Array) ? other.task[:due] : nil
567
+ other_due = @@BeginningOfEpoch unless other_due.class == Time
568
+
569
+ # sort based on priority, then due date, then name
570
+ # which is the rememberthemilk default
571
+ # if 0 was false in ruby, we could have done
572
+ # prio <=> other_due || due <=> other_due || self['name'].to_s <=> other['name'].to_s
573
+ # but it's not, so oh well....
574
+ prio = priority.to_i
575
+ prio += 666 if prio == 0 # prio of 0 is no priority which means it should show up below 1-3
576
+ other_prio = other.priority.to_i
577
+ other_prio += 666 if other_prio == 0
578
+
579
+ if prio != other_prio
580
+ return prio <=> other_prio
581
+ elsif due != other_due
582
+ return due <=> other_due
583
+ else
584
+ # TODO: should this be case insensitive?
585
+ return self[:name].to_s <=> other[:name].to_s
586
+ end
587
+ end
588
+
589
+ # Factory Methods...
590
+ # these are for methods that take arguments and apply to the taskseries
591
+ # if you have RememberTheMilkTask called task, you might do:
592
+ # task.addTags( 'tag1, tag2, tag3' )
593
+ # task.setRecurrence # turns off all rrules
594
+ # task.complete # marks last task as complete
595
+ # task.setDueDate # unsets due date for last task
596
+ # task.setDueDate( nil, :task_id => task.tasks[0].id ) # unsets due date for first task in task array
597
+ # task.setDueDate( "tomorrow at 1pm", :parse => 1 ) # sets due date for last task to tomorrow at 1pm
598
+ [['addTags','tags'], ['setTags', 'tags'], ['removeTags', 'tags'], ['setName', 'name'],
599
+ ['setRecurrence', 'repeat'], ['complete', ''], ['uncomplete', ''], ['setDueDate', 'due'],
600
+ ['setPriority', 'priority'], ['movePriority', 'direction'], ['setEstimate', 'estimate'],
601
+ ['setURL', 'url'], ['postpone', ''], ['delete', ''] ].each do |method_name, arg|
602
+ class_eval <<-RTM_METHOD
603
+ def #{method_name} ( value=nil, args={} )
604
+ if @rtm == nil
605
+ raise RememberTheMilkAPIError.new( :code => '667', :msg => "#{method_name} called without a handle to an rtm object [#{self.to_s}]" )
606
+ end
607
+ method_args = {}
608
+ method_args["#{arg}"] = value if "#{arg}" != '' && value
609
+ method_args[:timeline] = timeline
610
+ method_args[:list_id] = list_id
611
+ method_args[:taskseries_id] = taskseries_id
612
+ method_args[:task_id] = task_id
613
+ method_args.merge!( args )
614
+ @rtm.call_api_method( "tasks.#{method_name}", method_args ) # returns the modified task
615
+ end
616
+ RTM_METHOD
617
+ end
618
+
619
+ # We have to do this because moveTo takes a "from_list_id", not "list_id", so the above factory
620
+ # wouldn't work. sigh.
621
+ def moveTo( to_list_id, args = {} )
622
+ if @rtm == nil
623
+ raise RememberTheMilkAPIError.new( :code => '667', :msg => "moveTO called without a handle to an rtm object [#{self.to_s}]" )
624
+ end
625
+ method_args = {}
626
+ method_args[:timeline] = timeline
627
+ method_args[:from_list_id] = list_id
628
+ method_args[:to_list_id] = to_list_id
629
+ method_args[:taskseries_id] = taskseries_id
630
+ method_args[:task_id] = task_id
631
+ method_args.merge( args )
632
+ @rtm.call_api_method( :moveTo, method_args )
633
+ end
634
+
635
+ end
636
+
637
+
638
+ #
639
+ # class DateSet
640
+ #
641
+ # def initialize(startDate, rule)
642
+ # @startDate = startDate
643
+ # @frequency = nil
644
+ # @count = nil
645
+ # @untilDate = nil
646
+ # @byMonth = nil
647
+ # @byDay = nil
648
+ # @starts = nil
649
+ # if not rule.nil? then
650
+ # @starts = rule.every == 1 ? 'every' : 'after'
651
+ # parseRecurrenceRule(rule.rule)
652
+ # end
653
+ # end
654
+ #
655
+ # def parseRecurrenceRule(rule)
656
+ #
657
+ # if rule =~ /FREQ=(.*?);/ then
658
+ # @frequency = $1
659
+ # end
660
+ #
661
+ # if rule =~ /COUNT=(\d*)/ then
662
+ # @count = $1.to_i
663
+ # end
664
+ #
665
+ # if rule =~ /UNTIL=(.*?)[;\r]/ then
666
+ # @untilDate = DateParser.parse($1)
667
+ # end
668
+ #
669
+ # if rule =~ /INTERVAL=(\d*)/ then
670
+ # @interval = $1.to_i
671
+ # end
672
+ #
673
+ # if rule =~ /BYMONTH=(.*?);/ then
674
+ # @byMonth = $1
675
+ # end
676
+ #
677
+ # if rule =~ /BYDAY=(.*?);/ then
678
+ # @byDay = $1
679
+ # #puts "byDay = #{@byDay}"
680
+ # end
681
+ # end
682
+ #
683
+ # def to_s
684
+ # # after/every FREQ
685
+ # puts "UNIMPLETEMENT"
686
+ # # puts "#<DateSet: starts: #{@startDate.strftime("%m/%d/%Y")}, occurs: #{@frequency}, count: #{@count}, until: #{@untilDate}, byMonth: #{@byMonth}, byDay: #{@byDay}>"
687
+ # end
688
+ #
689
+ # def includes?(date)
690
+ # return true if date == @startDate
691
+ # return false if @untilDate and date > @untilDate
692
+ #
693
+ # case @frequency
694
+ # when 'DAILY'
695
+ # #if @untilDate then
696
+ # # return (@startDate..@untilDate).include?(date)
697
+ # #end
698
+ # increment = @interval ? @interval : 1
699
+ # d = @startDate
700
+ # counter = 0
701
+ # until d > date
702
+ #
703
+ # if @count then
704
+ # counter += 1
705
+ # if counter >= @count
706
+ # return false
707
+ # end
708
+ # end
709
+ #
710
+ # d += (increment * SECONDS_PER_DAY)
711
+ # if d.day == date.day and
712
+ # d.year == date.year and
713
+ # d.month == date.month then
714
+ # puts "true for start: #{@startDate}, until: #{@untilDate}"
715
+ # return true
716
+ # end
717
+ #
718
+ # end
719
+ #
720
+ # when 'WEEKLY'
721
+ # return true if @startDate.wday == date.wday
722
+ #
723
+ # when 'MONTHLY'
724
+ #
725
+ # when 'YEARLY'
726
+ #
727
+ # end
728
+ #
729
+ # false
730
+ # end
731
+ #
732
+ # attr_reader :frequency
733
+ # attr_accessor :startDate
734
+ # end
735
+ #
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'thartm'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestThartm < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thartm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.15
5
+ platform: ruby
6
+ authors:
7
+ - tha
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-06-06 00:00:00 +02:00
13
+ default_executable: rrtm
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: thoughtbot-shoulda
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: rtmapi fixed version with a simple cli added
36
+ email: thamayor@gmail.com
37
+ executables:
38
+ - rrtm
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - LICENSE
46
+ - README.rdoc
47
+ - Rakefile
48
+ - VERSION
49
+ - bin/rrtm
50
+ - lib/thartm.rb
51
+ - lib/thartm_lib.rb
52
+ - test/helper.rb
53
+ - test/test_thartm.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/ghedamat/thartm
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project:
76
+ rubygems_version: 1.3.1
77
+ signing_key:
78
+ specification_version: 2
79
+ summary: rtmapi based remember the milk cli.
80
+ test_files:
81
+ - test/helper.rb
82
+ - test/test_thartm.rb