TokiCLI 0.0.9 → 0.1.1

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.
@@ -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