-
Notifications
You must be signed in to change notification settings - Fork 0
/
messenger_analysis_interface.py
307 lines (276 loc) · 11.8 KB
/
messenger_analysis_interface.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
"""Main file for user interface. Will print a splashscreen in terminal, takefiles from users, merge if
neccessary, then take input on which stats to calculate using the FacebookChat class methods.
Can then send these stats to a seperate script where data visualisation will be calculated and exported to
desired file system location."""
# TODO: Implement top emoji, pdf reporting/graphing
import sys
import os
import shutil
import emoji
from typing import TextIO, Any
import numpy as np
import matplotlib.pyplot as plot
from matplotlib.backends.backend_pdf import PdfPages
from facebookchat import FacebookChat as FB
from message_merge import acquire_and_merge
from time import sleep
def main():
"""
The main function of the module. Currently prints the splashscreen
for 5 seconds, then clears terminal window and enters file select dialogue.
A mode select screen to acces future features will be implemented next
"""
splash_screen()
sleep(5)
os.system("cls" if sys.platform == "win32" else "clear")
chat = file_accept_dialogue()
os.system("cls" if sys.platform == "win32" else "clear")
while True:
print(f"The currently loaded chat is titled: {chat.title}")
print("Press ctrl + C at any time to exit..\n")
try:
mode = mode_select()
os.system("cls" if sys.platform == "win32" else "clear")
if mode == "1":
selected = export_stats((options_list(chat)))
command_line_stats_output(selected)
save_to_file = input(
"Do you want to save the output to a file? (y/n)\n>> "
)
if save_to_file.lower() == "y":
with open(f"Stats output for {chat.title} chat.txt", "w") as f:
# Redirect output to file
original_stdout = sys.stdout
sys.stdout = f
command_line_stats_output(selected)
# Restore original standard output
sys.stdout = original_stdout
print("Output saved to file.")
print("Input 1 to go back to mode select menu, or Ctrl + C to exit")
back = input(">> ")
if back == "1":
os.system("cls" if sys.platform == "win32" else "clear")
continue
elif mode == "2":
search_mode(chat)
save_to_file = input(
"Do you want to save the output to a file? (y/n)\n>> "
)
if save_to_file.lower() == "y":
with open(f"Search output for {chat.title} chat.txt", "w") as f:
# Redirect output to file
original_stdout = sys.stdout
sys.stdout = f
command_line_stats_output(selected)
# Restore original standard output
sys.stdout = original_stdout
print("Output saved to file.")
print("Input 1 to go back to mode select menu, or Ctrl + C to exit")
back = input(">> ")
if back == "1":
os.system("cls" if sys.platform == "win32" else "clear")
continue
elif mode == "3":
pass
except KeyboardInterrupt:
os.system("cls" if sys.platform == "win32" else "clear")
print("Thankyou for using this utility!")
sleep(3)
os.system("cls" if sys.platform == "win32" else "clear")
sys.exit()
def mode_select():
"""
Present a choice of modes to the user, mode choice will determine
which functionality the user has access to.
"""
mode = ""
while mode != "1" or mode != "2" or mode != "3":
print("Which function would you like to use first?")
print("Currently only option 1 and 2 work")
print("CLI Stats (1)\nSearch Mode (2)\nGraph Report Mode (3)")
mode = input(">> ")
if mode == "1" or mode == "2" or mode == "3":
return mode
else:
print("Please enter a valid selection.")
os.system("cls" if sys.platform == "win32" else "clear")
def splash_screen(border_char: str = "#"):
"""
Clear all current terminal display and display a splash screen
with application title and copyright info.
"""
DISPLAY_STR_1 = "FACEBOOK MESSENGER ANALYSIS UTILITY"
DISPLAY_STR_2 = "Copyright Shaun Ferris 2023"
# Clear terminal
os.system("cls" if sys.platform == "win32" else "clear")
# Get terminal dimensions
width, height = shutil.get_terminal_size()
# Calculate display string
head = (height - 4) // 2
lead_1 = (width - len(DISPLAY_STR_1)) // 2 - len(border_char)
lead_2 = (width - len(DISPLAY_STR_2)) // 2 - len(border_char)
display_str = f"{border_char * width}\n"
display_str += f'{border_char + " " * (width - 2) + border_char}\n' * head
display_str += (
f'{border_char + " " * lead_1 + DISPLAY_STR_1 + " " * lead_1 + border_char}\n'
)
display_str += (
f'{border_char + " " * lead_2 + DISPLAY_STR_2 + " " * lead_2 + border_char}\n'
)
display_str += f'{border_char + " " * (width - 2) + border_char}\n' * head
display_str += f"{border_char * width}"
print(display_str)
def instantiate(json: TextIO):
"""
Create a FacebookChat object from supplied file.
Should only ever recieve files post merge if merging is required.
Merging will be handled by the file accept dialogue function.
"""
try:
chat_object = FB(json)
except:
print("Please supply a valid .json Facebook Messenger log")
sys.exit()
return chat_object
def file_accept_dialogue():
"""
Presents the user with a choice of submitting one or more files.
If one file is submitted, it is instantiated, if more than one,
they are merged and then instantiated.
Returns the instantiated facebookchat object.
"""
print("Do you need to merge files for your chat log? (y / n)")
mode = input(">> ")
if mode.lower() == "y":
file_path = acquire_and_merge()
elif mode.lower() == "n":
print("Enter path to file: ")
file_path = input(">> ")
return instantiate(file_path)
def options_list(chat_object):
"""
Presents a list of options for desired stats which map to FacebookChat class methods,
takes user input for which stats to get and prepares a list of ready to go methods.
Args:
FacebookChat object passed from instantiate method
Returns:
A list of the desired methods to run which can then be invoked iteratively
"""
command_list = []
OPTIONS = {
1: chat_object.get_participants,
2: chat_object.get_number_days,
3: chat_object.total_interactions,
4: chat_object.number_of_texts,
5: chat_object.common_words,
6: chat_object.voice_calls_analysis,
7: chat_object.video_calls_analysis,
8: chat_object.av_txts_per_day,
9: chat_object.av_words_per_text,
}
OPTIONS_TO_PRINT = """
1: Chat participants, 2: Conversation length (days),
3: Total interactions, 4: Number of text messages,
5: Common words, 6: Voice call stats,
7: Video call stats, 8: Texts per day,
9: Words per text
"""
print(
f"Which statistics would you like from the supplied chat log? \n {OPTIONS_TO_PRINT}"
f"\n (Enter the option codes, eg: 174)"
)
chosen = str(input(">> "))
for i in range(10):
if str(i) in chosen:
command_list.append(OPTIONS[i])
return command_list
def export_stats(command_list: list) -> dict[str, Any]:
"""
Function to call the list of desired FacebookChat class methods provided
by the options list function, then export their results.
Returns a dict of calculated stats keyed with the __name__ of function called.
"""
exported_stats = {f.__name__: f() for f in command_list}
return exported_stats
def command_line_stats_output(exported_stats: dict):
"""
Main flow control loop for returning the desired stats as readable strings.
Takes the exported stats dict from the export stats command, and formats the
data into a string that corresponds to the function that produced it.
Prints these strings to the terminal.
"""
for func_name, output in exported_stats.items():
if func_name == "get_participants":
print(f"The chat participants were: ", end="")
# Allow for formatting of any number of participant names
for i in output:
if i != output[-1] and i != output[-2]:
print(f"{i}, ", end="")
elif i == output[-2]:
print(f"{i} ", end="")
else:
print(f"and {i}.")
elif func_name == "get_number_days":
print(f"The chat spanned {output} days.")
elif func_name == "total_interactions":
print(f"The chat encompassed {output} interactions.")
elif func_name == "number_of_texts":
for i in output[1]:
print(f"{i[0]} sent {i[1]} texts")
print(f"There were a total of {output[0]} texts sent.")
elif func_name == "common_words":
for party, word_list in output.items():
print(f"{party}'s most common words were: \n {word_list}")
elif func_name == "voice_calls_analysis":
for party, calls in output[0].items():
print(f"{party} initiated {calls} voice calls.")
print(f"There were a total of {output[1]} minutes of voice call.")
elif func_name == "video_calls_analysis":
for party, calls in output[0].items():
print(f"{party} initiated {calls} video calls.")
print(f"There were a total of {output[1]} minutes of video call.")
elif func_name == "av_txts_per_day":
print(f"On average, {output} text messages were sent every day.")
elif func_name == "av_words_per_text":
for party, words in output.items():
print(f"The average text sent by {party} contained {words} words.")
def search_mode(chat):
"""
Second function option for the main loop. Takes an instantiated facebookchat
object as argument.
Provides functionality for use count and original message containing searched terms.
"""
print("Enter your desired search term")
search_term = input(">> ")
search_results = chat.message_search(search_term)
print(f"Count of messages containing '{search_term}' for each participant:")
for participant, count in search_results.items():
print(f"{participant}: {count}")
show_messages = input(
f"Do you want to see messages containing '{search_term}'? (y/n) "
)
if show_messages.lower() == "y":
print(f"Messages containing '{search_term}':")
hit_msgs = chat.search_source(search_term)
for hit in hit_msgs:
for sender, msg in hit.items():
print(f"Message from: {sender}\n{msg}")
def graph_reporting(chat):
"""
Third function option for the main loop. Takes an instantiated facebookchat
object as argument. Returns a pdf of selected stats plotted as graphs.
"""
pass
def section_header(display_text: str = "Default", border_chr: str = "#"):
"""
Wraps a border made up of border_chrs around a display_text.
Returns the three strings that make up the bordered text.
"""
header_width = len(display_text) + 6
return (
border_chr * header_width + "\n",
f"{border_chr} {display_text} {border_chr}\n",
border_chr * header_width + "\n",
)
if __name__ == "__main__":
main()