thartm 0.0.15

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/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