TokiCLI 0.0.9 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +166 -20
- data/TokiCLI.gemspec +2 -2
- data/lib/API/dbapi.rb +451 -0
- data/lib/API/helpers.rb +151 -0
- data/lib/TokiCLI/app.rb +225 -106
- data/lib/TokiCLI/authorize.rb +6 -4
- data/lib/TokiCLI/export.rb +81 -0
- data/lib/TokiCLI/import.rb +122 -0
- data/lib/TokiCLI/scan.rb +19 -0
- data/lib/TokiCLI/version.rb +1 -1
- data/lib/TokiCLI/view.rb +100 -43
- metadata +10 -7
- data/lib/TokiCLI/module.rb +0 -224
data/lib/TokiCLI/module.rb
DELETED
@@ -1,224 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module TokiCLI
|
3
|
-
class Toki
|
4
|
-
|
5
|
-
CLIENT_ID = 'm6AccJFM56ENCn58Vde9cSg3uSpbvAAs'
|
6
|
-
CALLBACK_URL = 'http://aya.io/toki_cli/auth.html'
|
7
|
-
|
8
|
-
attr_reader :bundles
|
9
|
-
|
10
|
-
def initialize(token, channel_id)
|
11
|
-
@token = token
|
12
|
-
@channel_id = channel_id
|
13
|
-
if File.exist? "#{Dir.home}/.TokiCLI/apps.json"
|
14
|
-
@bundles = JSON.parse(File.read("#{Dir.home}/.TokiCLI/apps.json"))
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def get_bundle_ids
|
19
|
-
puts "Analyzing apps in /Applications\n\n"
|
20
|
-
require 'CFPropertyList'
|
21
|
-
files = get_plists "/Applications/*/Contents/*"
|
22
|
-
utils = get_plists "/Applications/Utilities/*/Contents/*"
|
23
|
-
home = get_plists "#{Dir.home}/Applications/*/Contents/*"
|
24
|
-
@infos = {}
|
25
|
-
get_bundles files
|
26
|
-
get_bundles utils
|
27
|
-
get_bundles home
|
28
|
-
@infos
|
29
|
-
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
def get_content(options)
|
34
|
-
if options[:adn]
|
35
|
-
get_adn_data.reverse.map {|obj| make_obj_from_adn(obj)}
|
36
|
-
else
|
37
|
-
get_db_data.map {|obj| make_obj_from_db(obj)}
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def get_app(app, synced)
|
42
|
-
name_regex = /#{app.downcase}/
|
43
|
-
app_data = {}
|
44
|
-
totals = {}
|
45
|
-
totals[:app] = 0
|
46
|
-
found = synced.select {|obj| obj.app[:name].downcase =~ name_regex}
|
47
|
-
found.each do |obj|
|
48
|
-
totals[:app] += obj.app[:total]
|
49
|
-
app_data[obj.msg[:id]] = {
|
50
|
-
from: Time.at(obj.app[:active_from]).strftime("%Y/%m/%d %Hh:%Mm:%Ss"),
|
51
|
-
to: Time.at(obj.app[:active_to]).strftime("%Y/%m/%d %Hh:%Mm:%Ss"),
|
52
|
-
duration: humanized_date(obj.app[:total]),
|
53
|
-
name: obj.app[:name],
|
54
|
-
raw_from: obj.app[:active_from]
|
55
|
-
}
|
56
|
-
end
|
57
|
-
app_data['total'] = humanized_date(totals[:app])
|
58
|
-
app_data
|
59
|
-
end
|
60
|
-
|
61
|
-
def all_data(synced)
|
62
|
-
sorted = get_sorted_totals(synced)
|
63
|
-
sorted.inject({}) do |bundles, obj|
|
64
|
-
bundles.merge(obj[0] => humanized_date(obj[1])) #name, total
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def top(synced, number = 5)
|
69
|
-
number = -number
|
70
|
-
sorted = get_sorted_totals(synced)
|
71
|
-
humanized = sorted.map {|n,t| [n, humanized_date(t)]}
|
72
|
-
humanized[number..-1]
|
73
|
-
end
|
74
|
-
|
75
|
-
def get_day(date, entries, options)
|
76
|
-
min_epoch = date.to_time.to_i
|
77
|
-
max_epoch = date.next_day.to_time.to_i
|
78
|
-
get_between(min_epoch, max_epoch, entries)
|
79
|
-
end
|
80
|
-
|
81
|
-
def get_range(day1, day2, entries, options)
|
82
|
-
min_epoch = day1.to_time.to_i
|
83
|
-
max_epoch = day2.next_day.to_time.to_i # between start of 1 and end of 2
|
84
|
-
get_between(min_epoch, max_epoch, entries)
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
def get_between(epoch1, epoch2, entries)
|
90
|
-
range = entries.select {|obj| obj.app[:active_from] > epoch1 && obj.app[:active_from] < epoch2}
|
91
|
-
day_totals = sort_totals get_totals(range)
|
92
|
-
day_totals.map {|n,t| [n, humanized_date(t)]}
|
93
|
-
end
|
94
|
-
|
95
|
-
def get_sorted_totals(synced)
|
96
|
-
sort_totals(get_totals(synced))
|
97
|
-
end
|
98
|
-
|
99
|
-
def humanized_date(epoch)
|
100
|
-
human = epoch_to_human(epoch)
|
101
|
-
"#{human[:hours]}h #{human[:minutes]}m #{human[:seconds]}s"
|
102
|
-
end
|
103
|
-
|
104
|
-
def sort_totals(totals)
|
105
|
-
totals.sort_by { |k,v| v }
|
106
|
-
end
|
107
|
-
|
108
|
-
def get_totals(synced)
|
109
|
-
synced.inject({}) do |result, obj|
|
110
|
-
result.merge(obj.app[:name] => app_total(obj, result))
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def app_total(obj, totals)
|
115
|
-
totals[obj.app[:name]].nil? ? obj.app[:total] : totals[obj.app[:name]] + obj.app[:total]
|
116
|
-
end
|
117
|
-
|
118
|
-
def epoch_to_human(epoch)
|
119
|
-
hours = epoch / 3600
|
120
|
-
minutes = (epoch / 60 - hours * 60)
|
121
|
-
seconds = (epoch - (minutes * 60 + hours * 3600))
|
122
|
-
{hours: hours, minutes: minutes, seconds: seconds}
|
123
|
-
end
|
124
|
-
|
125
|
-
def get_adn_data
|
126
|
-
channels = ADNChannels::GetMessages.new(@token)
|
127
|
-
channels.get_messages(@channel_id)
|
128
|
-
end
|
129
|
-
|
130
|
-
def get_db_data
|
131
|
-
db = open_db
|
132
|
-
data = db.execute("SELECT * FROM KKAppActivity")
|
133
|
-
db.close
|
134
|
-
data
|
135
|
-
end
|
136
|
-
|
137
|
-
def open_db
|
138
|
-
toki_path = "#{Dir.home}/.TokiCLI"
|
139
|
-
db_path = "#{Dir.home}/Library/Containers/us.kkob.Toki/Data/Documents/toki_data.sqlite3"
|
140
|
-
abort(Status.no_db) unless File.exist? db_path
|
141
|
-
FileUtils.mkdir_p(toki_path) unless Dir.exist?(toki_path)
|
142
|
-
FileUtils.copy(db_path, "#{toki_path}/toki_data.sqlite3.bak")
|
143
|
-
Amalgalite::Database.new(db_path)
|
144
|
-
end
|
145
|
-
|
146
|
-
def make_obj_from_adn(obj)
|
147
|
-
msg = {}
|
148
|
-
usr = {}
|
149
|
-
app = {}
|
150
|
-
d = obj['annotations'][0]['value']['d']
|
151
|
-
msg[:id] = obj['id']
|
152
|
-
msg[:thread] = obj['thread_id']
|
153
|
-
msg[:date] = parsed_date obj['created_at']
|
154
|
-
usr[:username] = obj['user']['username']
|
155
|
-
usr[:name] = obj['user']['name']
|
156
|
-
usr[:id] = obj['user']['id']
|
157
|
-
app[:id] = d['id']
|
158
|
-
app[:uuid] = d['UUID']
|
159
|
-
app[:name] = make_app_name [nil, d['bundleIdentifier']]
|
160
|
-
app[:active_to] = d['activeTo']
|
161
|
-
app[:active_from] = d['activeFrom']
|
162
|
-
app[:total] = d['totalSeconds']
|
163
|
-
OpenStruct.new(app: app, msg: msg, usr: usr)
|
164
|
-
end
|
165
|
-
|
166
|
-
def make_obj_from_db(obj)
|
167
|
-
# content is an array:
|
168
|
-
# id (INTEGER) 0, bundleIdentifier (VARCHAR) 1, activeFrom (INTEGER) 2, activeTo (INTEGER) 3, totalSeconds (INTEGER) 4, UUID (VARCHAR) 5, synced (INTEGER) 6, availableToSync (INTEGER) 7
|
169
|
-
msg = {}
|
170
|
-
usr = {}
|
171
|
-
app = {}
|
172
|
-
msg[:id] = obj[0]
|
173
|
-
msg[:thread] = msg[:id]
|
174
|
-
msg[:date] = Time.now
|
175
|
-
usr[:username] = ENV['USERNAME']
|
176
|
-
usr[:name] = usr[:username]
|
177
|
-
usr[:id] = obj[5]
|
178
|
-
app[:id] = msg[:id]
|
179
|
-
app[:uuid] = obj[5]
|
180
|
-
app[:name] = make_app_name obj
|
181
|
-
app[:active_to] = obj[3]
|
182
|
-
app[:active_from] = obj[2]
|
183
|
-
app[:total] = obj[4]
|
184
|
-
OpenStruct.new(app: app, msg: msg, usr: usr)
|
185
|
-
end
|
186
|
-
|
187
|
-
def make_app_name obj
|
188
|
-
if @bundles[obj[1]]
|
189
|
-
@bundles[obj[1]]
|
190
|
-
else
|
191
|
-
obj[1]
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
def parsed_date(string)
|
196
|
-
"#{string[0...10]} #{string[11...19]}"
|
197
|
-
end
|
198
|
-
|
199
|
-
def get_plists path
|
200
|
-
Dir.glob(path).select {|f| (File.split f).last == 'Info.plist'}
|
201
|
-
end
|
202
|
-
|
203
|
-
def get_bundles plists
|
204
|
-
plists.each do |obj|
|
205
|
-
puts "Analyzing #{obj} ...\n"
|
206
|
-
begin
|
207
|
-
pl = CFPropertyList::List.new(:file => obj)
|
208
|
-
rescue CFFormatError
|
209
|
-
puts "Unable to read the file, skipping...\n"
|
210
|
-
next
|
211
|
-
end
|
212
|
-
data = CFPropertyList.native_types(pl.value)
|
213
|
-
name = data['CFBundleName']
|
214
|
-
bundle_id = data['CFBundleIdentifier']
|
215
|
-
if name.nil?
|
216
|
-
name = data['CFBundleExecutable']
|
217
|
-
end
|
218
|
-
next if name.nil?
|
219
|
-
next if bundle_id.nil? || bundle_id.empty?
|
220
|
-
@infos[bundle_id] = name
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|