mess 0.0.0 → 0.0.2
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +17 -3
- data/README.md +15 -11
- data/exe/mess +83 -190
- data/lib/mess.rb +9 -3
- data/lib/mess/chat.rb +57 -48
- data/lib/mess/error.rb +3 -1
- data/lib/mess/facebook_information.rb +49 -0
- data/lib/mess/msg.rb +9 -0
- data/lib/mess/plot.rb +9 -21
- data/lib/mess/plotter.rb +55 -0
- data/lib/mess/{plot → plotter}/daily_msgs.rb +17 -6
- data/lib/mess/plotter/total_msgs.rb +34 -0
- data/lib/mess/plotter/word_frequency.rb +58 -0
- data/lib/mess/plotter/words_per_msg.rb +42 -0
- data/lib/mess/version.rb +1 -1
- data/mess.gemspec +2 -1
- data/pkg/mess-0.0.1.gem +0 -0
- metadata +27 -11
- data/lib/mess/plot/total_delay.rb +0 -37
- data/lib/mess/plot/total_lettres.rb +0 -22
- data/lib/mess/plot/total_msgs.rb +0 -21
- data/lib/mess/tree.rb +0 -78
- data/pkg/mess-0.0.0.gem +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d71090b50fd3a172418162b58191baa839adedbbc51b5608f63aaf8f162a95a4
|
4
|
+
data.tar.gz: b6b8c3df531c2cd6656c69039448229ce065d8a8c1c90ca0cfaf7601eb51cfe1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3ad3c5ae653e7fddfd09957476d7b5a3220ab252e4b0ff79c49116228c338a75d86e96b44ab15ba1ff66e17dc7857b274e9ceb9e7aa3d01c971b03b2b5f8f1e
|
7
|
+
data.tar.gz: e6b5e2e7056217c635744c15e9497dbc2d465e14b175403f6a549c308d77c1275bfcf6980053964836f0887036f27453a5cbe14e624757e3bc839cff3faf5043
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mess (0.0.
|
5
|
-
|
4
|
+
mess (0.0.1)
|
5
|
+
tty-option (~> 0.1)
|
6
|
+
tty-prompt (~> 0.23)
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
9
10
|
specs:
|
11
|
+
pastel (0.8.0)
|
12
|
+
tty-color (~> 0.5)
|
10
13
|
rake (12.3.3)
|
11
|
-
|
14
|
+
tty-color (0.6.0)
|
15
|
+
tty-cursor (0.7.1)
|
16
|
+
tty-option (0.1.0)
|
17
|
+
tty-prompt (0.23.0)
|
18
|
+
pastel (~> 0.8)
|
19
|
+
tty-reader (~> 0.8)
|
20
|
+
tty-reader (0.9.0)
|
21
|
+
tty-cursor (~> 0.7)
|
22
|
+
tty-screen (~> 0.8)
|
23
|
+
wisper (~> 2.0)
|
24
|
+
tty-screen (0.8.1)
|
25
|
+
wisper (2.0.1)
|
12
26
|
|
13
27
|
PLATFORMS
|
14
28
|
ruby
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# mess
|
2
2
|
|
3
|
-
Ruby tool for analysis of
|
3
|
+
Ruby tool for analysis of Facebook Messenger conversations in JSON format. Output is table-processor-friendly (like MS Excell / LibreOffice Calc). Independently of `mess` executable, an API is provided for more nuanced use.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
Ruby is required. You can download mess as a Ruby Gem:
|
@@ -22,14 +22,11 @@ Call `main` either directly or via link.
|
|
22
22
|
# print help
|
23
23
|
main --help
|
24
24
|
|
25
|
-
#
|
26
|
-
main
|
25
|
+
# interactive prompt for given archive
|
26
|
+
main my/facebook/information
|
27
27
|
|
28
28
|
# analysis specifying chat and plots
|
29
|
-
main
|
30
|
-
|
31
|
-
# list all available chats and plots for given archive
|
32
|
-
main --facebook my/facebook/archive --list --chat --plot
|
29
|
+
main my/facebook/information/chat --plot DailyMsgs
|
33
30
|
|
34
31
|
```
|
35
32
|
|
@@ -38,9 +35,9 @@ Mess provides class `Mess::Plot` to be inherited for custom plots; \
|
|
38
35
|
quick example (for more, see `mess/plots` dir):
|
39
36
|
```ruby
|
40
37
|
module Mess
|
41
|
-
class
|
38
|
+
class MyPlotter < Plotter
|
42
39
|
|
43
|
-
# this will make
|
40
|
+
# this will make MyPlotter "available"
|
44
41
|
describe "my plot for magic analysis"
|
45
42
|
|
46
43
|
def initialize chat # constructor
|
@@ -49,16 +46,23 @@ module Mess
|
|
49
46
|
end
|
50
47
|
|
51
48
|
def push msg # being called for every single message chronologically
|
52
|
-
if msg
|
49
|
+
if msg.body.include? "my"
|
53
50
|
# do stuff
|
54
51
|
else
|
55
52
|
# don't etc.
|
56
53
|
end
|
57
54
|
end
|
58
55
|
|
59
|
-
def
|
56
|
+
def finalize # called after all messages have been push-ed
|
60
57
|
# body # perhaps reformat the data and so on
|
61
58
|
end
|
59
|
+
|
60
|
+
def generate_plot # output a unified-format plot for further manipulation
|
61
|
+
p = Plot.new
|
62
|
+
p.data = @data
|
63
|
+
p.head = @myHead
|
64
|
+
return p
|
65
|
+
end
|
62
66
|
|
63
67
|
end
|
64
68
|
end
|
data/exe/mess
CHANGED
@@ -1,232 +1,125 @@
|
|
1
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require '
|
5
|
-
require '
|
4
|
+
require 'tty-option'
|
5
|
+
require 'tty-prompt'
|
6
6
|
require_relative '../lib/mess'
|
7
7
|
|
8
|
-
|
8
|
+
class Command
|
9
|
+
include TTY::Option
|
9
10
|
|
10
|
-
|
11
|
+
usage do
|
12
|
+
program 'mess'
|
13
|
+
command "\b"
|
14
|
+
desc 'analyze conversation'
|
11
15
|
|
12
|
-
|
13
|
-
opts = Slop.parse do |o|
|
16
|
+
end
|
14
17
|
|
15
|
-
|
18
|
+
argument :path do
|
19
|
+
required
|
20
|
+
desc 'inbox or chat directory path'
|
21
|
+
end
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
23
|
+
flag :help do
|
24
|
+
long '--help'
|
25
|
+
desc 'Print usage'
|
26
|
+
end
|
21
27
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
flag :archived do
|
29
|
+
short '-a'
|
30
|
+
long '--archived'
|
31
|
+
desc 'list archived conversations too'
|
32
|
+
end
|
26
33
|
|
27
|
-
|
34
|
+
flag :version do
|
35
|
+
long '--version'
|
36
|
+
desc 'print version'
|
37
|
+
end
|
28
38
|
|
29
|
-
|
39
|
+
option :plot do
|
40
|
+
short '-p'
|
41
|
+
long '--plot name'
|
42
|
+
desc 'plot to be generated'
|
43
|
+
end
|
30
44
|
|
31
|
-
|
45
|
+
option :output do
|
46
|
+
short '-o'
|
47
|
+
long '--output path'
|
48
|
+
desc 'output data path'
|
49
|
+
end
|
32
50
|
|
33
|
-
|
34
|
-
|
51
|
+
def run
|
52
|
+
if params[:help]
|
53
|
+
print help
|
54
|
+
exit
|
35
55
|
end
|
36
56
|
|
37
|
-
|
38
|
-
|
57
|
+
if params[:version]
|
58
|
+
puts Mess::VERSION
|
59
|
+
exit
|
39
60
|
end
|
40
61
|
|
41
|
-
|
42
|
-
|
43
|
-
p e.message
|
44
|
-
rescue Slop::UnknownOption => e
|
45
|
-
puts e
|
46
|
-
exit 1
|
47
|
-
end
|
48
|
-
|
49
|
-
target = opts&.arguments || Array.new
|
50
|
-
target << '.' if target.none?
|
51
|
-
|
52
|
-
if target.one?
|
53
|
-
target = target.first
|
54
|
-
else
|
55
|
-
$stderr.puts 'too many targets'
|
56
|
-
exit 1
|
57
|
-
end
|
58
|
-
|
59
|
-
def list_chats tree
|
60
|
-
puts "chats"
|
61
|
-
tree.chats.each_with_index do |chat, index|
|
62
|
-
index += 1
|
63
|
-
index = index.to_s.rjust(5)
|
64
|
-
puts "#{index} | #{chat.title}"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def list_plots plots
|
69
|
-
puts "plots"
|
70
|
-
index = 1
|
71
|
-
plots.available.each_pair do |name, desc|
|
72
|
-
puts "#{index.to_s.rjust(5)} | #{name.ljust(15)} -> #{desc}"
|
73
|
-
index += 1
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# listing mode
|
78
|
-
if opts.list?
|
79
|
-
|
80
|
-
# chats
|
81
|
-
if $listing.include? :chats
|
82
|
-
begin
|
83
|
-
tree = Mess::Tree.new(target)
|
84
|
-
puts
|
85
|
-
list_chats tree
|
86
|
-
rescue Mess::TreeInvalidError
|
87
|
-
$stderr.puts "not an archive: #{target}"
|
62
|
+
if params[:path].nil?
|
63
|
+
print help
|
88
64
|
exit 1
|
89
65
|
end
|
90
66
|
end
|
67
|
+
end
|
91
68
|
|
92
|
-
|
93
|
-
|
94
|
-
puts
|
95
|
-
list_plots Mess::Plot
|
96
|
-
end
|
97
|
-
|
98
|
-
# none
|
99
|
-
if $listing.none?
|
100
|
-
$stderr.puts 'no specification flag for listing'
|
101
|
-
exit 1
|
102
|
-
end
|
69
|
+
cmd = Command.new
|
70
|
+
cmd.parse.run
|
103
71
|
|
104
|
-
|
105
|
-
exit
|
72
|
+
prompt = TTY::Prompt.new
|
106
73
|
|
107
|
-
|
74
|
+
todo = nil
|
108
75
|
|
109
76
|
begin
|
110
|
-
|
111
|
-
# given directly chat
|
112
|
-
chat = Mess::Chat.new(target)
|
113
|
-
|
77
|
+
todo = Mess::Chat.new(cmd.params[:path])
|
114
78
|
rescue Mess::ChatInvalidError
|
115
|
-
|
116
|
-
# given tree
|
117
79
|
begin
|
118
|
-
|
119
|
-
rescue Mess::
|
120
|
-
|
80
|
+
todo = Mess::FacebookInformation.new(cmd.params[:path], cmd.params[:archived])
|
81
|
+
rescue Mess::FacebookInformationInvalidError
|
82
|
+
puts 'path is not a chat nor an inbox'
|
121
83
|
exit 1
|
122
84
|
end
|
85
|
+
end
|
123
86
|
|
124
|
-
|
125
|
-
input = opts[:chat].join
|
126
|
-
chat = nil
|
127
|
-
failed = false
|
128
|
-
loop do
|
129
|
-
|
130
|
-
# search for nick or index
|
131
|
-
chat = tree.chats.find { |c| c.title == input }
|
132
|
-
chat ||= tree.chats[input.to_i - 1] if input.to_i.to_s == input and (1..tree.chats.size).include? input.to_i
|
133
|
-
unless chat.nil?
|
134
|
-
puts if failed
|
135
|
-
break
|
136
|
-
end
|
137
|
-
|
138
|
-
# failed
|
139
|
-
puts
|
140
|
-
list_chats tree unless failed
|
141
|
-
failed = true
|
142
|
-
|
143
|
-
print 'select> '
|
144
|
-
input = $stdin.gets.chomp
|
87
|
+
begin
|
145
88
|
|
89
|
+
if todo.class == Mess::FacebookInformation
|
90
|
+
todo = prompt.select(
|
91
|
+
'Select chat:',
|
92
|
+
todo.chats.map { |c| [c.title, c] }.to_h
|
93
|
+
)
|
94
|
+
#todo = todo.chats.find { |c| c.title == temp }
|
146
95
|
end
|
147
96
|
|
148
|
-
|
97
|
+
plot = cmd.params[:plot]
|
149
98
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
input.map! do |p|
|
158
|
-
if p.to_i.to_s == p and (1..Mess::Plot.available.size).include? p.to_i
|
159
|
-
Mess::Plot.available.keys[p.to_i - 1]
|
160
|
-
else
|
161
|
-
p
|
162
|
-
end
|
99
|
+
if Mess::Plotter.available.keys.include? plot
|
100
|
+
plot = Mess.const_get(plot)
|
101
|
+
else
|
102
|
+
plot = prompt.select(
|
103
|
+
'Select plot:',
|
104
|
+
Mess::Plotter.available.keys.map { |p| [p, Mess.const_get(p)] }.to_h
|
105
|
+
)
|
163
106
|
end
|
164
107
|
|
165
|
-
|
166
|
-
if input.any? and input.all? { |p| Mess::Plot.available.include? p }
|
167
|
-
plot = Set.new(input)
|
168
|
-
puts if failed
|
169
|
-
break
|
170
|
-
end
|
108
|
+
cmd.params[:output] ||= prompt.ask('Output path:', default: "./mess-out-#{Time.now.to_i}.txt")
|
171
109
|
|
172
|
-
|
110
|
+
rescue TTY::Reader::InputInterrupt
|
173
111
|
puts
|
174
|
-
|
175
|
-
failed = true
|
176
|
-
print 'select> '
|
177
|
-
input = $stdin.gets.strip.split(/[, ]/).compact
|
178
|
-
|
112
|
+
exit 1
|
179
113
|
end
|
180
114
|
|
181
|
-
|
182
|
-
|
183
|
-
i = 1
|
184
|
-
plot.map! do |p|
|
185
|
-
$stdout.cursor_up 1
|
186
|
-
puts "preparing... #{i}/#{plot.size}"
|
187
|
-
Mess.const_get(p).new(chat)
|
188
|
-
end
|
189
|
-
$stdout.cursor_up 1
|
190
|
-
puts 'preparing... done'
|
191
|
-
|
192
|
-
# analyze
|
193
|
-
total = chat.count
|
194
|
-
pcapx = total / 100
|
195
|
-
check = 0
|
196
|
-
chat.msgs.each_with_index do |m, i|
|
197
|
-
if i >= check
|
198
|
-
$stdout.cursor_up 1
|
199
|
-
puts "analyzing... #{100 * i / total}% "
|
200
|
-
check += pcapx
|
201
|
-
end
|
202
|
-
plot.each do |p|
|
203
|
-
p.push m #rescue nil
|
204
|
-
end
|
205
|
-
end
|
206
|
-
$stdout.cursor_up 1
|
207
|
-
puts 'analyzing... done'
|
208
|
-
|
209
|
-
# review
|
210
|
-
puts
|
211
|
-
plot.each_with_index do |p, i|
|
212
|
-
$stdout.cursor_up 1
|
213
|
-
puts "reviewing... #{i + 1}/#{plot.size}"
|
214
|
-
p.review
|
215
|
-
end
|
216
|
-
$stdout.cursor_up 1
|
217
|
-
puts 'reviewing... done'
|
218
|
-
|
219
|
-
# export
|
220
|
-
$stdout.cursor_up 1
|
221
|
-
puts 'exporting...'
|
222
|
-
data = Hash.new
|
223
|
-
plot.each do |p|
|
224
|
-
data[p.class.name] = p.data
|
225
|
-
end
|
226
|
-
$stdout.cursor_up 1
|
115
|
+
p = plot.new(todo)
|
116
|
+
r = p.run
|
227
117
|
|
228
|
-
File.open(
|
229
|
-
|
118
|
+
File.open(cmd.params[:output], 'w') do |f|
|
119
|
+
([r.head] + r.data).each do |row|
|
120
|
+
row.each do |col|
|
121
|
+
f << col << "\t"
|
122
|
+
end
|
123
|
+
f << "\n"
|
124
|
+
end
|
230
125
|
end
|
231
|
-
|
232
|
-
puts 'exporting... done'
|
data/lib/mess.rb
CHANGED
@@ -1,15 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'set'
|
4
3
|
require 'json'
|
4
|
+
require 'csv'
|
5
5
|
require 'date'
|
6
|
-
|
6
|
+
require 'iconv'
|
7
7
|
|
8
8
|
module Mess
|
9
9
|
class Error < StandardError; end
|
10
10
|
# Your code goes here...
|
11
11
|
end
|
12
12
|
|
13
|
+
require_relative 'mess/version'
|
13
14
|
require_relative 'mess/error'
|
15
|
+
require_relative 'mess/msg'
|
16
|
+
require_relative 'mess/chat'
|
14
17
|
require_relative 'mess/plot'
|
15
|
-
|
18
|
+
require_relative 'mess/plotter'
|
19
|
+
require_relative 'mess/facebook_information'
|
20
|
+
|
21
|
+
Dir["#{__dir__}/mess/plotter/*.rb"].each { |f| require_relative f }
|
data/lib/mess/chat.rb
CHANGED
@@ -1,77 +1,86 @@
|
|
1
|
-
#
|
1
|
+
# frozen string_literal: true
|
2
|
+
|
3
|
+
# Mess::Chat
|
4
|
+
# class wrapping a single Messenger conversation (Group/Person)
|
5
|
+
# initializer takes single conversation directory path as an argument
|
6
|
+
# (contains text files, attachmed photos and other files etc.)
|
7
|
+
# chat.title -> get conversation tytle
|
8
|
+
# chat.size -> count messages
|
9
|
+
# chat.usrs -> participant list
|
10
|
+
# chat.msgs -> all messages
|
2
11
|
|
3
12
|
module Mess
|
4
13
|
class Chat
|
5
|
-
|
6
|
-
|
7
|
-
|
14
|
+
attr_reader :path, :title, :usrs, :msgs
|
15
|
+
|
8
16
|
def initialize path
|
9
|
-
@path = File.expand_path
|
17
|
+
@path = File.expand_path(path)
|
10
18
|
|
11
|
-
# make sure path
|
12
|
-
unless File.exists? @path
|
13
|
-
raise ChatInvalidError
|
14
|
-
end
|
19
|
+
# make sure the path is somewhat valid
|
20
|
+
raise ChatInvalidError unless File.exists? @path
|
15
21
|
|
16
|
-
#
|
17
|
-
if File.file? @path
|
18
|
-
@path = File.dirname @path
|
19
|
-
end
|
22
|
+
# maybe given a file within the directory instead? -> scope out
|
23
|
+
@path = File.dirname(@path) if File.file? @path
|
20
24
|
|
25
|
+
# prepare data buffers
|
21
26
|
@title = nil
|
22
27
|
@usrs = nil
|
23
28
|
@msgs = Array.new
|
24
29
|
|
25
|
-
#
|
26
|
-
|
27
|
-
raise ChatInvalidError unless
|
28
|
-
|
29
|
-
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
get
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
file = "#@path/message_#{total - i}.#{ext}"
|
42
|
-
get.call(file) rescue raise ChatInvalidError
|
30
|
+
# either .json or .html -> message_1.ext to find out the format
|
31
|
+
message_1 = Dir["#@path/message_1\\.*"]
|
32
|
+
raise ChatInvalidError unless message_1.one?
|
33
|
+
message_1 = message_1.first
|
34
|
+
extension = File.extname(message_1)
|
35
|
+
extension = extension[1..-1] # remove the dot in .ext
|
36
|
+
|
37
|
+
# reference the get_extension function
|
38
|
+
get = method("get_#{extension}")
|
39
|
+
|
40
|
+
# iterate through message_X.ext and use get method to parse the files
|
41
|
+
# do it chronologically -> descending X for messenger (reeee)
|
42
|
+
total = Dir["#@path/message_*\\.#{extension}"].size
|
43
|
+
total.downto(1) do |i|
|
44
|
+
file = "#@path/message_#{i}.#{extension}"
|
45
|
+
get.call(file) #rescue raise ChatInvalidError
|
43
46
|
end
|
44
47
|
|
45
|
-
@title = '*you*' if @title.empty?
|
46
|
-
@
|
47
|
-
|
48
|
+
@title = '*you*' if @title.nil? or @title.empty?
|
49
|
+
@msgs.sort_by!(&:time)
|
50
|
+
@usrs.sort!
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
53
|
+
def size
|
51
54
|
@msgs.size
|
52
55
|
end
|
53
56
|
|
54
|
-
def prepare
|
55
|
-
@msgs.sort! { |a, b| a['timestamp_ms'] <=> b['timestamp_ms'] }
|
56
|
-
end
|
57
|
-
|
58
|
-
def msgs
|
59
|
-
prepare unless @ready
|
60
|
-
@ready = true
|
61
|
-
@msgs
|
62
|
-
end
|
63
|
-
|
64
57
|
private
|
65
58
|
|
59
|
+
# parse json file
|
66
60
|
def get_json file
|
67
|
-
|
61
|
+
# get data
|
62
|
+
buffer = File.read(file)
|
63
|
+
buffer = JSON.parse(buffer)
|
64
|
+
|
65
|
+
# get metadata if not already done before (each file contains it)
|
68
66
|
@title ||= buffer['title']
|
69
67
|
@usrs ||= buffer['participants'].map{ |p| p['name'] }.sort
|
70
|
-
|
68
|
+
|
69
|
+
# append messages
|
70
|
+
buffer['messages'].each do |hash|
|
71
|
+
msg = Msg.new
|
72
|
+
msg.from = hash['sender_name']
|
73
|
+
msg.body = hash['content']
|
74
|
+
msg.time = hash['timestamp_ms']
|
75
|
+
msg.type = hash['type']
|
76
|
+
@msgs << msg
|
77
|
+
@usrs << msg.from unless @usrs.include? msg.from
|
78
|
+
end
|
71
79
|
end
|
72
80
|
|
81
|
+
# parse html file
|
73
82
|
def get_html file
|
83
|
+
raise ChatInvalidError
|
74
84
|
end
|
75
|
-
|
76
85
|
end
|
77
86
|
end
|
data/lib/mess/error.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mess
|
4
|
+
class FacebookInformation
|
5
|
+
attr_reader :root, :chats
|
6
|
+
|
7
|
+
def initialize path, archived_too = false
|
8
|
+
path = File.expand_path(path)
|
9
|
+
|
10
|
+
# find the closest thing to facebook information copy root
|
11
|
+
# while gradually ascending
|
12
|
+
begin
|
13
|
+
break unless File.exists? path
|
14
|
+
next unless is_root? path
|
15
|
+
@root = path
|
16
|
+
end until (path = File.dirname(path)) == '/'
|
17
|
+
|
18
|
+
raise FacebookInformationInvalidError if @root.nil?
|
19
|
+
|
20
|
+
@chats = Array.new
|
21
|
+
to_list = Array.new
|
22
|
+
to_list << "inbox"
|
23
|
+
to_list << "archived_threads" if archived_too
|
24
|
+
to_list.each do |i|
|
25
|
+
list_chats "#@root/#{i}"
|
26
|
+
end
|
27
|
+
@chats.sort_by!(&:title)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# checks if given path is a root of a facebook information copy
|
33
|
+
def is_root? path
|
34
|
+
[
|
35
|
+
'/',
|
36
|
+
'/inbox',
|
37
|
+
'/archived_threads',
|
38
|
+
].all? { |dir| File.directory?(path + dir) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def list_chats inbox_dir
|
42
|
+
Dir["#{inbox_dir}/*"].each do |chat_dir|
|
43
|
+
base = File.basename(chat_dir)
|
44
|
+
@chats << Chat.new(chat_dir) rescue nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
data/lib/mess/msg.rb
ADDED
data/lib/mess/plot.rb
CHANGED
@@ -1,29 +1,17 @@
|
|
1
1
|
module Mess
|
2
2
|
class Plot
|
3
|
+
attr_accessor :head, :data
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@@available
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.describe desc
|
11
|
-
@@available[name] = desc
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.name
|
15
|
-
self.to_s.match(/Mess::(?<name>\w+)$/)[:name]
|
16
|
-
end
|
17
|
-
|
18
|
-
attr_reader :data
|
19
|
-
|
20
|
-
def initialize chat
|
21
|
-
end
|
22
|
-
|
23
|
-
def push msg
|
5
|
+
def initialize
|
6
|
+
@head = Array.new
|
7
|
+
@data = Array.new
|
24
8
|
end
|
25
9
|
|
26
|
-
def
|
10
|
+
def export_data
|
11
|
+
puts @head.join("\t");
|
12
|
+
@data.each do |d|
|
13
|
+
puts d.join("\t");
|
14
|
+
end
|
27
15
|
end
|
28
16
|
|
29
17
|
end
|
data/lib/mess/plotter.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# base class for plotters
|
4
|
+
# initializer should take in Chat class to be analyzed
|
5
|
+
#
|
6
|
+
# method push is feeded all messages chronologically
|
7
|
+
# after pushing all messages, finalize is called to format data etc.
|
8
|
+
|
9
|
+
module Mess
|
10
|
+
class Plotter
|
11
|
+
|
12
|
+
# stores all available plotters
|
13
|
+
@@available = Hash.new
|
14
|
+
|
15
|
+
# lists all available plotters
|
16
|
+
def self.available
|
17
|
+
@@available
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :data
|
21
|
+
|
22
|
+
# needs to be described in order to be "centrally" available
|
23
|
+
# adds plotter class to available plotters with custom description
|
24
|
+
# name is derived from plotter class name
|
25
|
+
def self.describe desc
|
26
|
+
name = self.to_s.match(/Mess::(?<name>\w+)$/)[:name]
|
27
|
+
@@available[name] = desc
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize chat
|
31
|
+
@chat = chat
|
32
|
+
end
|
33
|
+
|
34
|
+
# should return Mess::Plot
|
35
|
+
def run
|
36
|
+
@chat.msgs.each { |msg| push(msg) }
|
37
|
+
finalize
|
38
|
+
generate_plot
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# called for every message in chronological order
|
44
|
+
def push msg
|
45
|
+
end
|
46
|
+
|
47
|
+
def finalize
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_plot
|
51
|
+
Plot.new
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Mess
|
4
|
-
class DailyMsgs <
|
4
|
+
class DailyMsgs < Plotter
|
5
5
|
|
6
6
|
describe 'daily messages by user'
|
7
7
|
|
8
8
|
def initialize chat
|
9
|
+
super
|
9
10
|
@data = Hash.new
|
10
|
-
@usrs = chat.usrs
|
11
|
+
@usrs = @chat.usrs
|
11
12
|
oldest = get_day(chat.msgs[0])
|
12
13
|
latest = get_day(chat.msgs[-1])
|
13
14
|
for day in (oldest..latest)
|
@@ -15,16 +16,26 @@ module Mess
|
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
19
|
+
private
|
20
|
+
|
21
|
+
def generate_plot
|
22
|
+
p = Plot.new
|
23
|
+
p.head << 'Date'
|
24
|
+
p.head += @usrs
|
25
|
+
@data.each do |date, msgs|
|
26
|
+
p.data << [date] + msgs
|
27
|
+
end
|
28
|
+
p
|
29
|
+
end
|
30
|
+
|
18
31
|
def push msg
|
19
|
-
usr = msg
|
32
|
+
usr = msg.from
|
20
33
|
day = get_day(msg).to_s
|
21
34
|
@data[day][@usrs.index(usr)] += 1
|
22
35
|
end
|
23
36
|
|
24
|
-
private
|
25
|
-
|
26
37
|
def get_day msg
|
27
|
-
unix = msg
|
38
|
+
unix = msg.time / 1000
|
28
39
|
Date.parse(Time.at(unix).to_s)
|
29
40
|
end
|
30
41
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mess
|
4
|
+
class TotalMsgs < Plotter
|
5
|
+
|
6
|
+
describe 'total messages by user'
|
7
|
+
|
8
|
+
def initialize chat
|
9
|
+
super
|
10
|
+
@data = Hash.new
|
11
|
+
chat.usrs.each do |u|
|
12
|
+
@data[u] = 0
|
13
|
+
puts u
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def push msg
|
20
|
+
unless @data.include? msg.from
|
21
|
+
puts msg.from
|
22
|
+
end
|
23
|
+
@data[msg.from] += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_plot
|
27
|
+
p = Plot.new
|
28
|
+
p.head = ['User', 'Total Messages']
|
29
|
+
p.data = @data.to_a
|
30
|
+
p
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mess
|
4
|
+
class WordFrequency < Plotter
|
5
|
+
|
6
|
+
describe 'most frequent words by user'
|
7
|
+
|
8
|
+
def initialize chat
|
9
|
+
super
|
10
|
+
@data = Hash.new
|
11
|
+
chat.usrs.each do |u|
|
12
|
+
@data[u] = Hash.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def generate_plot
|
19
|
+
p = Plot.new
|
20
|
+
p.head << 'Index'
|
21
|
+
@data.each_key do |usr|
|
22
|
+
p.head += ["Word - #{usr}", "Frequency - #{usr}"]
|
23
|
+
end
|
24
|
+
i = 0
|
25
|
+
loop do
|
26
|
+
line = [i + 1]
|
27
|
+
@data.each_value do |data|
|
28
|
+
buff = Array.new(2)
|
29
|
+
word = data.to_a[i]
|
30
|
+
buff = word unless word.nil?
|
31
|
+
line += buff
|
32
|
+
end
|
33
|
+
break if line.compact.size == 1
|
34
|
+
p.data << line
|
35
|
+
i += 1
|
36
|
+
end
|
37
|
+
p
|
38
|
+
end
|
39
|
+
|
40
|
+
def push msg
|
41
|
+
return if msg.body.nil?
|
42
|
+
words = msg.body.split(/[ \t\n]/)
|
43
|
+
words.each do |w|
|
44
|
+
w.strip!
|
45
|
+
next if w.empty?
|
46
|
+
w.downcase!
|
47
|
+
@data[msg.from][w] = @data[msg.from][w].to_i + 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def finalize
|
52
|
+
@data.each_key do |usr|
|
53
|
+
@data[usr] = @data[usr].sort_by { |key, value| -value }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mess
|
4
|
+
class WordsPerMessage < Plotter
|
5
|
+
|
6
|
+
describe 'average words per message by user'
|
7
|
+
|
8
|
+
def initialize chat
|
9
|
+
super
|
10
|
+
@data = Hash.new
|
11
|
+
@msgs = Hash.new
|
12
|
+
@words = Hash.new
|
13
|
+
chat.usrs.each do |u|
|
14
|
+
@data[u] = nil
|
15
|
+
@msgs[u] = 0
|
16
|
+
@words[u] = 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def generate_plot
|
23
|
+
p = Plot.new
|
24
|
+
p.head = ['User', 'Words per Message']
|
25
|
+
p.data = @data.to_a
|
26
|
+
p
|
27
|
+
end
|
28
|
+
|
29
|
+
def push msg
|
30
|
+
@msgs[msg.from] += 1
|
31
|
+
return if msg.body.class != String
|
32
|
+
@words[msg.from] += msg.body.strip.squeeze.count("\n\t ")
|
33
|
+
end
|
34
|
+
|
35
|
+
def finalize
|
36
|
+
@data.each_key do |u|
|
37
|
+
@data[u] = @words[u].to_f / @msgs[u]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/mess/version.rb
CHANGED
data/mess.gemspec
CHANGED
@@ -21,5 +21,6 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.add_dependency "
|
24
|
+
spec.add_dependency "tty-prompt", "~> 0.23"
|
25
|
+
spec.add_dependency "tty-option", "~> 0.1"
|
25
26
|
end
|
data/pkg/mess-0.0.1.gem
ADDED
Binary file
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mess
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- patztablook22
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: tty-prompt
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '0.23'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '0.23'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: tty-option
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
27
41
|
description: Analyze your archived Messenger chatlogs in JSON format!
|
28
42
|
email:
|
29
43
|
- patz@tuta.io
|
@@ -43,15 +57,17 @@ files:
|
|
43
57
|
- lib/mess.rb
|
44
58
|
- lib/mess/chat.rb
|
45
59
|
- lib/mess/error.rb
|
60
|
+
- lib/mess/facebook_information.rb
|
61
|
+
- lib/mess/msg.rb
|
46
62
|
- lib/mess/plot.rb
|
47
|
-
- lib/mess/
|
48
|
-
- lib/mess/
|
49
|
-
- lib/mess/
|
50
|
-
- lib/mess/
|
51
|
-
- lib/mess/
|
63
|
+
- lib/mess/plotter.rb
|
64
|
+
- lib/mess/plotter/daily_msgs.rb
|
65
|
+
- lib/mess/plotter/total_msgs.rb
|
66
|
+
- lib/mess/plotter/word_frequency.rb
|
67
|
+
- lib/mess/plotter/words_per_msg.rb
|
52
68
|
- lib/mess/version.rb
|
53
69
|
- mess.gemspec
|
54
|
-
- pkg/mess-0.0.
|
70
|
+
- pkg/mess-0.0.1.gem
|
55
71
|
homepage: https://github.com/patztablook22/mess
|
56
72
|
licenses:
|
57
73
|
- MIT
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Mess
|
4
|
-
class TotalDelay < Plot
|
5
|
-
|
6
|
-
describe 'total delay by user'
|
7
|
-
|
8
|
-
def initialize chat
|
9
|
-
@last = nil
|
10
|
-
@time = Hash.new
|
11
|
-
@data = Hash.new
|
12
|
-
chat.usrs.each do |u|
|
13
|
-
@data[u] = 0
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def push msg
|
18
|
-
|
19
|
-
usr = msg["sender_name"]
|
20
|
-
time = msg["timestamp_ms"]
|
21
|
-
|
22
|
-
if @time[usr].to_i < time
|
23
|
-
@time[usr] = time
|
24
|
-
end
|
25
|
-
|
26
|
-
if @last.nil? or @last == usr
|
27
|
-
@last ||= usr
|
28
|
-
return
|
29
|
-
end
|
30
|
-
|
31
|
-
@data[usr] += time - @time[@last]
|
32
|
-
@last = usr
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|
37
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Mess
|
4
|
-
class TotalLetters < Plot
|
5
|
-
|
6
|
-
describe 'total letters per user'
|
7
|
-
|
8
|
-
def initialize chat
|
9
|
-
@data = Hash.new
|
10
|
-
chat.usrs.each do |u|
|
11
|
-
@data[u] = 0
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def push msg
|
16
|
-
usr = msg['sender_name']
|
17
|
-
letters = msg['content'].to_s.length
|
18
|
-
@data[usr] += letters
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|
22
|
-
end
|
data/lib/mess/plot/total_msgs.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Mess
|
4
|
-
class TotalMsgs < Plot
|
5
|
-
|
6
|
-
describe 'total messages by user'
|
7
|
-
|
8
|
-
def initialize chat
|
9
|
-
@data = Hash.new
|
10
|
-
chat.usrs.each do |u|
|
11
|
-
@data[u] = 0
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def push msg
|
16
|
-
usr = msg['sender_name']
|
17
|
-
@data[usr] += 1
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
data/lib/mess/tree.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Mess
|
4
|
-
class Tree
|
5
|
-
|
6
|
-
attr_reader :root, :chats
|
7
|
-
|
8
|
-
def initialize path, flags = Hash.new
|
9
|
-
|
10
|
-
# find the root from given path
|
11
|
-
|
12
|
-
path = File.expand_path(path)
|
13
|
-
begin
|
14
|
-
|
15
|
-
unless File.exists? path
|
16
|
-
break
|
17
|
-
end
|
18
|
-
|
19
|
-
if check(path)
|
20
|
-
@root = path
|
21
|
-
break
|
22
|
-
else
|
23
|
-
next
|
24
|
-
end
|
25
|
-
|
26
|
-
end until (path = File.dirname(path)) == "/"
|
27
|
-
|
28
|
-
raise TreeInvalidError if @root.nil?
|
29
|
-
|
30
|
-
import flags[:archived]
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
def check root
|
35
|
-
[
|
36
|
-
'/',
|
37
|
-
'/inbox',
|
38
|
-
'/archived_threads',
|
39
|
-
].all? { |dir| File.directory?(root + dir) }
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def import archived
|
45
|
-
|
46
|
-
@chats = Array.new
|
47
|
-
|
48
|
-
box = Array.new
|
49
|
-
box << 'inbox'
|
50
|
-
box << 'archived_threads' if archived
|
51
|
-
|
52
|
-
# find all text chat dirs
|
53
|
-
box.each do |b|
|
54
|
-
Dir["#@root/#{b}/*"].each do |dir|
|
55
|
-
|
56
|
-
# exclude asset dirs (capitalized)
|
57
|
-
base = File.basename dir
|
58
|
-
next unless base.downcase == base
|
59
|
-
|
60
|
-
# list it
|
61
|
-
begin
|
62
|
-
@chats << Chat.new(dir)
|
63
|
-
rescue
|
64
|
-
end
|
65
|
-
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
@chats.sort! { |a, b| a.title <=> b.title }
|
70
|
-
|
71
|
-
end
|
72
|
-
|
73
|
-
def select i
|
74
|
-
Chat.new(@index[i.to_i].to_s)
|
75
|
-
end
|
76
|
-
|
77
|
-
end
|
78
|
-
end
|
data/pkg/mess-0.0.0.gem
DELETED
Binary file
|