TokiCLI 0.0.9 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ # encoding: utf-8
2
+ module TokiCLI
3
+ class Helpers
4
+
5
+ attr_accessor :table, :bundles, :bundles_file, :home_path, :toki_path, :db_path
6
+ attr_reader :db
7
+
8
+ def initialize
9
+ @home_path = Dir.home
10
+ @toki_path = "#{@home_path}/.TokiCLI"
11
+ FileUtils.mkdir_p(@toki_path) unless Dir.exist?(@toki_path)
12
+ @db_path = "#{@home_path}/Library/Containers/us.kkob.Toki/Data/Documents/toki_data.sqlite3"
13
+ if File.exist? @db_path
14
+ FileUtils.copy @db_path, "#{@toki_path}/toki_data.sqlite3.bak"
15
+ @db = Amalgalite::Database.new @db_path
16
+ else
17
+ raise 'No DB'
18
+ end
19
+ @table = 'KKAppActivity'
20
+ @bundles_file = "#{@toki_path}/apps.json"
21
+ @bundles = if File.exist? @bundles_file
22
+ JSON.parse(File.read @bundles_file)
23
+ else
24
+ {}
25
+ end
26
+ end
27
+
28
+
29
+ # Returns the app name if exists
30
+ def find_app_name bundle_id
31
+ if @bundles[bundle_id]
32
+ @bundles[bundle_id]
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ def humanized_date epoch
39
+ human = seconds_to_time epoch
40
+ "#{human[:hours]}h #{human[:minutes]}m #{human[:seconds]}s"
41
+ end
42
+
43
+ def seconds_to_time epoch
44
+ begin
45
+ hours = epoch / 3600
46
+ minutes = (epoch / 60 - hours * 60)
47
+ seconds = (epoch - (minutes * 60 + hours * 3600))
48
+ {hours: hours, minutes: minutes, seconds: seconds}
49
+ rescue Exception => e
50
+ oops e
51
+ end
52
+ end
53
+
54
+ def oops error
55
+ puts "\nOOPS! A gremlin crashed the app.\n\n"
56
+ puts "STACK: #{caller}\n\n"
57
+ abort "GREMLIN: #{error}\n\n"
58
+ end
59
+
60
+ def epoch_to_date epoch
61
+ Time.at(epoch).to_time
62
+ end
63
+
64
+ def sec_to_time sec
65
+ seconds_to_time sec
66
+ end
67
+
68
+ # Scans for names from bundle ids, saves the file and returns its content
69
+ def scan params = {}
70
+ bundle_ids = get_bundle_ids params
71
+ f = File.new "#{@toki_path}/apps.json", "w"
72
+ f.write bundle_ids.to_json
73
+ f.close
74
+ bundle_ids
75
+ end
76
+
77
+ # Scan for names from bundle ids
78
+ def get_bundle_ids params = {}
79
+ @infos = {}
80
+ get_bundles(get_plists("/Applications/*/Contents/*"), params)
81
+ get_bundles(get_plists("/Applications/Utilities/*/Contents/*"), params)
82
+ get_bundles(get_plists("#{@home_path}/Applications/*/Contents/*"), params)
83
+ @infos
84
+ end
85
+
86
+ def get_bundle_from_name name
87
+ (@bundles).each do |k,v|
88
+ if v.downcase =~ /#{name.downcase}/
89
+ return [v, k]
90
+ end
91
+ end
92
+ []
93
+ end
94
+
95
+ def meta_nodata
96
+ {
97
+ code: 404,
98
+ error: { message: 'No data returned from the database' }
99
+ }
100
+ end
101
+
102
+ def json_nodata
103
+ {
104
+ meta: meta_nodata,
105
+ data: {}
106
+ }.to_json
107
+ end
108
+
109
+ def tracked_bundles apps
110
+ tracked = JSON.parse(apps)
111
+ list = []
112
+ tracked['data']['apps'].each {|v| list << v['bundle']}
113
+ list
114
+ end
115
+
116
+ private
117
+
118
+ def get_plists path
119
+ Dir.glob(path).select {|f| (File.split f).last == 'Info.plist'}
120
+ end
121
+
122
+ def get_bundles plists, params
123
+ plists.each do |obj|
124
+ puts "Analyzing #{obj} ...\n" if params[:verbose] == true
125
+ begin
126
+ pl = CFPropertyList::List.new(:file => obj)
127
+ rescue CFFormatError
128
+ bundles_error if params[:verbose] == true
129
+ next
130
+ rescue NoMethodError
131
+ bundles_error if params[:verbose] == true
132
+ next
133
+ end
134
+ data = CFPropertyList.native_types(pl.value)
135
+ name = data['CFBundleName']
136
+ bundle_id = data['CFBundleIdentifier']
137
+ if name.nil?
138
+ name = data['CFBundleExecutable']
139
+ end
140
+ next if name.nil?
141
+ next if bundle_id.nil? || bundle_id.empty?
142
+ @infos[bundle_id] = name
143
+ end
144
+ end
145
+
146
+ def bundles_error
147
+ puts "Unable to read the file, skipping...\n"
148
+ end
149
+
150
+ end
151
+ end
@@ -2,161 +2,288 @@
2
2
  module TokiCLI
3
3
  class App < Thor
4
4
  package_name "TokiCLI"
5
- %w{get_messages search_messages get_channels module status view authorize}.each {|r| require_relative "#{r}"}
5
+ %w{get_messages search_messages get_channels status view authorize export import scan ../API/dbapi}.each {|r| require_relative "#{r}"}
6
+
7
+ desc "auth", "App.net login"
8
+ def auth
9
+ begin
10
+ toki = DBAPI.new
11
+ adn = Authorize.new(toki.helpers.toki_path)
12
+ adn.authorize
13
+ rescue Interrupt
14
+ puts Status.canceled
15
+ exit
16
+ end
17
+ end
6
18
 
7
19
  desc "scan", "Scan /Applications to resolve app names"
8
20
  def scan
9
- toki = create_toki(options)
10
- bundle_ids = toki.get_bundle_ids
11
- path = "#{Dir.home}/.TokiCLI"
12
- Dir.mkdir path unless Dir.exist? path
13
- f = File.new "#{path}/apps.json", "w"
14
- f.write bundle_ids.to_json
15
- f.close
16
- puts "\nDone.\n\n"
17
- end
18
-
19
- desc "total", "Shows the total usage of all apps"
20
- option :adn, aliases: '-a', type: :boolean, desc: 'Select ADN channel as source (toki total -a)'
21
+ scanner = Scan.new DBAPI.new
22
+ scanner.scan({verbose: true})
23
+ end
24
+
25
+ desc "total", "Total usage of all apps"
21
26
  option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
22
27
  option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
23
28
  def total
24
- toki = create_toki(options)
25
- apps = get_total(toki, options)
26
- clear
27
- View.new.total_table(apps)
28
- export(options, apps, "toki-total-#{Time.now.to_s[0..9]}") if (options[:json] || options[:csv])
29
+ toki, view = DBAPI.new, View.new
30
+ apps = JSON.parse toki.apps_total
31
+ view.total_table apps
32
+ if (options[:json] || options[:csv])
33
+ export = Export.new toki, view
34
+ export.apps_total options, apps, 'total'
35
+ end
29
36
  end
30
37
 
31
- desc "top", "Shows your most used apps"
32
- option :adn, aliases: '-a', type: :boolean, desc: 'Select ADN channel as source (toki top -a)'
33
- option :number, aliases: '-n', type: :numeric, desc: 'Specify the number of apps (toki top -n 10)'
38
+ desc "top", "Most used apps"
39
+ option :number, aliases: '-n', type: :numeric, desc: 'Specify the number of apps'
34
40
  option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
35
41
  option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
36
42
  def top
37
- toki, entries = init(options)
38
- clear
39
- hits = toki.top(entries, options[:number] || 5)
40
- View.new.hits_table(hits.reverse)
41
- export(options, hits, "toki-top-#{Time.now.to_s[0..9]}") if (options[:json] || options[:csv])
43
+ toki, view = DBAPI.new, View.new
44
+ apps = JSON.parse toki.apps_top(options[:number] || 5)
45
+ view.total_table apps
46
+ if (options[:json] || options[:csv])
47
+ export = Export.new toki, view
48
+ export.apps_total options, apps, 'top'
49
+ end
42
50
  end
43
51
 
44
- desc "auth", "Input your App.net token for authorization"
45
- def auth
52
+ desc "day DATE", "All apps used on a specific day"
53
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
54
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
55
+ def day(*args)
56
+ abort(Status.specify_day) if args.empty?
46
57
  begin
47
- ADNAuthorize::Authorize.new.authorize
48
- rescue Interrupt
49
- puts Status.canceled
50
- exit
58
+ DateTime.strptime(args[0], '%Y-%m-%d')
59
+ rescue ArgumentError
60
+ abort(Status.specify_day)
61
+ end
62
+ toki, view = DBAPI.new, View.new
63
+ apps = JSON.parse toki.apps_day args[0]
64
+ view.total_table apps
65
+ if (options[:json] || options[:csv])
66
+ export = Export.new toki, view
67
+ export.apps_total options, apps, "day-#{args[0]}"
51
68
  end
52
69
  end
53
70
 
54
- desc "log APP", "Shows the complete log for an app"
55
- option :adn, aliases: '-a', type: :boolean, desc: 'Select ADN channel as source (toki log -a)'
71
+ desc "since DATE", "All apps used since a specific day"
56
72
  option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
57
73
  option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
58
- def log(*args)
59
- abort(Status.specify_name) if args.empty?
60
- asked = args[0]
61
- toki, entries = init(options)
62
- app_data = toki.get_app(asked, entries)
63
- View.new.app_table(asked, app_data)
64
- export(options, app_data, "toki-log-#{asked}-#{Time.now.to_s[0..9]}") if (options[:json] || options[:csv])
74
+ def since *args
75
+ abort(Status.specify_day) if args.empty?
76
+ begin
77
+ DateTime.strptime args[0], '%Y-%m-%d'
78
+ rescue ArgumentError
79
+ abort Status.specify_day
80
+ end
81
+ toki, view = DBAPI.new, View.new
82
+ apps = JSON.parse toki.apps_since args[0]
83
+ view.total_table apps
84
+ if (options[:json] || options[:csv])
85
+ export = Export.new toki, view
86
+ export.apps_total options, apps, "since-#{args[0]}"
87
+ end
65
88
  end
66
89
 
67
- desc "day DATE", "Shows all apps used on a specific day (toki day 2014-04-19)"
68
- option :adn, aliases: '-a', type: :boolean, desc: 'Select ADN channel as source (toki day 2014-04-19 -a)'
90
+ desc "before DATE", "All apps used before a specific day"
69
91
  option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
70
92
  option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
71
- def day(*args)
93
+ def before *args
72
94
  abort(Status.specify_day) if args.empty?
73
95
  begin
74
- date = DateTime.strptime(args[0], '%Y-%m-%d')
96
+ DateTime.strptime args[0], '%Y-%m-%d'
75
97
  rescue ArgumentError
76
- abort(Status.specify_day)
98
+ abort Status.specify_day
99
+ end
100
+ toki, view = DBAPI.new, View.new
101
+ apps = JSON.parse toki.apps_before args[0]
102
+ view.total_table apps
103
+ if (options[:json] || options[:csv])
104
+ export = Export.new toki, view
105
+ export.apps_total options, apps, "before-#{args[0]}"
77
106
  end
78
- toki, entries = init(options)
79
- day_data = toki.get_day(date, entries, options)
80
- clear
81
- View.new.day_table(args[0], day_data)
82
- export(options, day_data, "toki-day-#{args[0]}") if (options[:json] || options[:csv])
83
107
  end
84
108
 
85
- desc "range DAY1 DAY2", "Shows all apps used between day 1 and day 2 (toki range 2014-04-16 2014-04-18)"
86
- option :adn, aliases: '-a', type: :boolean, desc: 'Select ADN channel as source (toki range 2014-04-16 2014-04-18 -a)'
109
+ desc "range DATE1 DATE2", "All apps used between two specific days"
87
110
  option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
88
111
  option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
89
112
  def range(*args)
90
113
  abort(Status.specify_range) if args.empty? || args.length != 2
91
114
  begin
92
- day1 = DateTime.strptime(args[0], '%Y-%m-%d')
93
- day2 = DateTime.strptime(args[1], '%Y-%m-%d')
115
+ DateTime.strptime(args[0], '%Y-%m-%d')
116
+ DateTime.strptime(args[1], '%Y-%m-%d')
94
117
  rescue ArgumentError
95
118
  abort(Status.specify_range)
96
119
  end
97
- toki, entries = init(options)
98
- range_data = toki.get_range(day1, day2, entries, options)
99
- clear
100
- View.new.range_table(args[0], args[1], range_data)
101
- export(options, range_data, "toki-range-#{args[0]}_#{args[1]}") if (options[:json] || options[:csv])
120
+ toki, view = DBAPI.new, View.new
121
+ apps = JSON.parse toki.apps_range args[0], args[1]
122
+ view.total_table apps
123
+ if (options[:json] || options[:csv])
124
+ export = Export.new toki, view
125
+ export.apps_total options, apps, "range-#{args[0]}_#{args[1]}"
126
+ end
102
127
  end
103
128
 
104
- private
105
-
106
- def export(options, data, filename)
107
- case
108
- when options[:json]
109
- name = Dir.home + "/temp/toki/#{filename}.json"
110
- File.write(name, data.to_h.to_json)
111
- when options[:csv]
112
- name = Dir.home + "/temp/toki/#{filename}.csv"
113
- CSV.open(name, "wb") do |csv|
114
- data.to_a.each {|line| csv << line}
115
- end
129
+ desc "log APP", "Complete log for an app"
130
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
131
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
132
+ option :bundle, aliases: '-b', type: :boolean, desc: "Specify the complete bundle identifier (in case the app name resolve doesn't work)"
133
+ def log(*args)
134
+ abort(Status.specify_name) if args.empty?
135
+ asked = args[0]
136
+ toki = DBAPI.new
137
+ view = View.new
138
+ log = if options[:bundle]
139
+ JSON.parse toki.bundle_log asked
116
140
  else
117
- exit
141
+ JSON.parse toki.name_log asked
142
+ end
143
+ view.app_table asked, log
144
+ if (options[:json] || options[:csv])
145
+ export = Export.new toki, view
146
+ export.log options, log, "log-#{log['data']['bundle'].gsub('.', '_')}"
118
147
  end
119
- puts "File converted and exported to #{name}\n\n"
120
148
  end
121
149
 
122
- def init(options)
123
- toki = create_toki(options)
124
- return toki, toki.get_content(options)
150
+ desc "app APP", "Total tracked time for an app"
151
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
152
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
153
+ option :bundle, aliases: '-b', type: :boolean, desc: "Specify the complete bundle identifier (in case the app name resolve doesn't work)"
154
+ def app *args
155
+ abort(Status.specify_name) if args.empty?
156
+ asked = args[0]
157
+ toki = DBAPI.new
158
+ view = View.new
159
+ app = if options[:bundle]
160
+ JSON.parse toki.bundle_total asked
161
+ else
162
+ JSON.parse toki.name_total asked
163
+ end
164
+ view.total_table app
165
+ if (options[:json] || options[:csv])
166
+ export = Export.new toki, view
167
+ export.app_total options, app, "app-#{app['data']['bundle'].gsub('.', '_')}"
168
+ end
125
169
  end
126
170
 
127
- def create_toki(options)
128
- options[:adn] ? total_adn : total_db
171
+ desc "app_before APP DATE", "Total tracked time for an app before a specific day"
172
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
173
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
174
+ option :bundle, aliases: '-b', type: :boolean, desc: "Specify the complete bundle identifier (in case the app name resolve doesn't work)"
175
+ def app_before *args
176
+ abort('Error') if args.empty? #TODO: better and more complete
177
+ asked = args[0]
178
+ date = args[1]
179
+ begin
180
+ DateTime.strptime args[1], '%Y-%m-%d'
181
+ rescue ArgumentError
182
+ abort Status.specify_day
183
+ end
184
+ toki = DBAPI.new
185
+ view = View.new
186
+ app = if options[:bundle]
187
+ JSON.parse toki.bundle_total_before asked, date
188
+ else
189
+ JSON.parse toki.name_total_before asked, date
190
+ end
191
+ view.total_table app
192
+ if (options[:json] || options[:csv])
193
+ export = Export.new toki, view
194
+ export.app_total options, app, "app_before-#{app['data']['bundle'].gsub('.', '_')}_#{args[1]}"
195
+ end
129
196
  end
130
197
 
131
- def total_adn
132
- clear
133
- puts Status.get_all
134
- init_toki_adn
198
+ desc "app_since APP DATE", "Total tracked time for an app since a specific day"
199
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
200
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
201
+ option :bundle, aliases: '-b', type: :boolean, desc: "Specify the complete bundle identifier (in case the app name resolve doesn't work)"
202
+ def app_since *args
203
+ abort('Error') if args.empty? #TODO: better and more complete
204
+ asked = args[0]
205
+ date = args[1]
206
+ begin
207
+ DateTime.strptime args[1], '%Y-%m-%d'
208
+ rescue ArgumentError
209
+ abort Status.specify_day
210
+ end
211
+ toki = DBAPI.new
212
+ view = View.new
213
+ app = if options[:bundle]
214
+ JSON.parse toki.bundle_total_since asked, date
215
+ else
216
+ JSON.parse toki.name_total_since asked, date
217
+ end
218
+ view.total_table app
219
+ if (options[:json] || options[:csv])
220
+ export = Export.new toki, view
221
+ export.app_total options, app, "app_since-#{app['data']['bundle'].gsub('.', '_')}_#{date}"
222
+ end
135
223
  end
136
224
 
137
- def total_db
138
- clear
139
- #puts "getting data from db\n"
140
- init_toki_db
225
+ desc "app_day APP DATE", "Total tracked time for an app on a specific day"
226
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
227
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
228
+ option :bundle, aliases: '-b', type: :boolean, desc: "Specify the complete bundle identifier (in case the app name resolve doesn't work)"
229
+ def app_day *args
230
+ abort('Error') if args.empty? #TODO: better and more complete
231
+ asked = args[0]
232
+ date = args[1]
233
+ begin
234
+ DateTime.strptime args[1], '%Y-%m-%d'
235
+ rescue ArgumentError
236
+ abort Status.specify_day
237
+ end
238
+ toki = DBAPI.new
239
+ view = View.new
240
+ app = if options[:bundle]
241
+ JSON.parse toki.bundle_total_day asked, date
242
+ else
243
+ JSON.parse toki.name_total_day asked, date
244
+ end
245
+ view.total_table app
246
+ if (options[:json] || options[:csv])
247
+ export = Export.new toki, view
248
+ export.app_total options, app, "app_day-#{app['data']['bundle'].gsub('.', '_')}_#{date}"
249
+ end
141
250
  end
142
251
 
143
- def get_total(toki, options)
252
+ desc "app_range APP DATE1 DATE2", "total tracked time for an app between two specific days"
253
+ option :json, aliases: '-j', type: :boolean, desc: 'Export the results as a JSON file'
254
+ option :csv, aliases: '-c', type: :boolean, desc: 'Export the results as a CSV file'
255
+ option :bundle, aliases: '-b', type: :boolean, desc: "Specify the complete bundle identifier (in case the app name resolve doesn't work)"
256
+ def app_range *args
257
+ abort('Error') if args.empty? || args.length != 3
144
258
  begin
145
- get_all(toki, options)
146
- rescue Interrupt
147
- abort Status.canceled
148
- rescue => e
149
- abort Status.error(e)
259
+ DateTime.strptime(args[1], '%Y-%m-%d')
260
+ DateTime.strptime(args[2], '%Y-%m-%d')
261
+ rescue ArgumentError
262
+ abort(Status.specify_range)
263
+ end
264
+ toki = DBAPI.new
265
+ view = View.new
266
+ app = if options[:bundle]
267
+ JSON.parse toki.bundle_total_range args[0], args[1], args[2]
268
+ else
269
+ JSON.parse toki.name_total_range args[0], args[1], args[2]
270
+ end
271
+ view.total_table app
272
+ if (options[:json] || options[:csv])
273
+ export = Export.new toki, view
274
+ export.app_total options, app, "app_range-#{app['data']['bundle'].gsub('.', '_')}_#{args[1]}_#{args[2]}"
150
275
  end
151
276
  end
152
277
 
153
- def init_toki_db
154
- TokiCLI::Toki.new(nil, nil)
278
+ desc "restore", "Restore data from App.net and rebuild the Toki.app database"
279
+ def restore
280
+ toki_api = DBAPI.new
281
+ view = View.new
282
+ import = Import.new toki_api, view
283
+ import.restore get_token, get_channel_id
155
284
  end
156
285
 
157
- def init_toki_adn(channel_id = get_channel_id)
158
- TokiCLI::Toki.new("#{get_token}", channel_id)
159
- end
286
+ private
160
287
 
161
288
  def get_token
162
289
  filepath = Dir.home + '/.TokiCLI/config.json'
@@ -182,13 +309,5 @@ module TokiCLI
182
309
  abort Status.no_channel
183
310
  end
184
311
 
185
- def clear
186
- puts "\e[H\e[2J"
187
- end
188
-
189
- def get_all(t, options)
190
- content = t.get_content(options)
191
- t.all_data(content)
192
- end
193
312
  end
194
313
  end