diff --git a/.gitignore b/.gitignore index b23925f..a0aa52d 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,10 @@ ENV/ env.bak/ venv.bak/ +# IntelliJ files +/.idea +/*.iml + # Spyder project settings .spyderproject .spyproject @@ -142,8 +146,9 @@ cython_debug/ .DS_Store .vscode output/ -wa.db -msgstore.db +**/wa.db +**/msgstore.db TODO.md test_locally.py -tests/unit/tmp/* \ No newline at end of file +tests/unit/tmp/* +/whatsapp_backup \ No newline at end of file diff --git a/main.py b/main.py index 4a53e47..4c417e9 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,9 @@ chats_to_txt_raw, ) +CALL_LOGS_DIR = "/call_logs" +CHAT_DIR = "/chats" + def create_db_connection(file_path: str) -> Tuple[sqlite3.Connection, sqlite3.Cursor]: """Create a database connection and return it. @@ -80,7 +83,7 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: "--backup_specific_or_all_chat_call", "-e", nargs="*", - default="all", + default=["all"], help="Phone numbers (format: XXXXXXXXXXXX) of the chats and/or call logs that you want to extract from the database", ) args = ap.parse_args() @@ -95,20 +98,20 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) if args.backup_output_style == "raw_txt": - if args.backup_specific_or_all_chat_call == "all": + if args.backup_specific_or_all_chat_call == ["all"]: if "chats" in args.backup_strategy: - output_chat_directory = args.parsed_backup_output_dir + "/chats" + output_chat_directory = args.parsed_backup_output_dir + CHAT_DIR if not os.path.exists(output_chat_directory): os.makedirs(output_chat_directory) chats = chat_builder.build_all_chats(msgdb_cursor, wadb_cursor) for chat in tqdm(chats): chats_to_txt_raw( chat=chat, - dir=output_chat_directory, + folder=output_chat_directory, ) if "call_logs" in args.backup_strategy: output_call_logs_directory = ( - args.parsed_backup_output_dir + "/call_logs" + args.parsed_backup_output_dir + CALL_LOGS_DIR ) if not os.path.exists(output_call_logs_directory): os.makedirs(output_call_logs_directory) @@ -118,12 +121,12 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: for call_log in tqdm(call_logs): if call_log.calls: call_logs_to_txt_raw( - call_log=call_log, dir=output_call_logs_directory + call_log=call_log, folder=output_call_logs_directory ) else: for ph_no in tqdm(args.backup_specific_or_all_chat_call): if "chats" in args.backup_strategy: - output_chat_directory = args.parsed_backup_output_dir + "/chats" + output_chat_directory = args.parsed_backup_output_dir + CHAT_DIR if not os.path.exists(output_chat_directory): os.makedirs(output_chat_directory) chat = chat_builder.build_chat_for_given_id_or_phone_number( @@ -131,11 +134,11 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) chats_to_txt_raw( chat=chat, - dir=output_chat_directory, + folder=output_chat_directory, ) if "call_logs" in args.backup_strategy: output_call_logs_directory = ( - args.parsed_backup_output_dir + "/call_logs" + args.parsed_backup_output_dir + CALL_LOGS_DIR ) if not os.path.exists(output_call_logs_directory): os.makedirs(output_call_logs_directory) @@ -146,24 +149,24 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) if call_log.calls: call_logs_to_txt_raw( - call_log=call_log, dir=output_call_logs_directory + call_log=call_log, folder=output_call_logs_directory ) elif args.backup_output_style == "formatted_txt": - if args.backup_specific_or_all_chat_call == "all": + if args.backup_specific_or_all_chat_call == ["all"]: if "chats" in args.backup_strategy: - output_chat_directory = args.parsed_backup_output_dir + "/chats" + output_chat_directory = args.parsed_backup_output_dir + CHAT_DIR if not os.path.exists(output_chat_directory): os.makedirs(output_chat_directory) chats = chat_builder.build_all_chats(msgdb_cursor, wadb_cursor) for chat in tqdm(chats): chats_to_txt_formatted( chat=chat, - dir=output_chat_directory, + folder=output_chat_directory, ) if "call_logs" in args.backup_strategy: output_call_logs_directory = ( - args.parsed_backup_output_dir + "/call_logs" + args.parsed_backup_output_dir + CALL_LOGS_DIR ) if not os.path.exists(output_call_logs_directory): os.makedirs(output_call_logs_directory) @@ -173,12 +176,12 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: for call_log in tqdm(call_logs): if call_log.calls: call_logs_to_txt_formatted( - call_log=call_log, dir=output_call_logs_directory + call_log=call_log, folder=output_call_logs_directory ) else: for ph_no in tqdm(args.backup_specific_or_all_chat_call): if "chats" in args.backup_strategy: - output_chat_directory = args.parsed_backup_output_dir + "/chats" + output_chat_directory = args.parsed_backup_output_dir + CHAT_DIR if not os.path.exists(output_chat_directory): os.makedirs(output_chat_directory) chat = chat_builder.build_chat_for_given_id_or_phone_number( @@ -186,11 +189,11 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) chats_to_txt_formatted( chat=chat, - dir=output_chat_directory, + folder=output_chat_directory, ) if "call_logs" in args.backup_strategy: output_call_logs_directory = ( - args.parsed_backup_output_dir + "/call_logs" + args.parsed_backup_output_dir + CALL_LOGS_DIR ) if not os.path.exists(output_call_logs_directory): os.makedirs(output_call_logs_directory) @@ -201,24 +204,24 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) if call_log.calls: call_logs_to_txt_formatted( - call_log=call_log, dir=output_call_logs_directory + call_log=call_log, folder=output_call_logs_directory ) elif args.backup_output_style == "json": - if args.backup_specific_or_all_chat_call == "all": + if args.backup_specific_or_all_chat_call == ["all"]: if "chats" in args.backup_strategy: - output_chat_directory = args.parsed_backup_output_dir + "/chats" + output_chat_directory = args.parsed_backup_output_dir + CHAT_DIR if not os.path.exists(output_chat_directory): os.makedirs(output_chat_directory) chats = chat_builder.build_all_chats(msgdb_cursor, wadb_cursor) for chat in tqdm(chats): chats_to_json( chat=chat, - dir=output_chat_directory, + folder=output_chat_directory, ) if "call_logs" in args.backup_strategy: output_call_logs_directory = ( - args.parsed_backup_output_dir + "/call_logs" + args.parsed_backup_output_dir + CALL_LOGS_DIR ) if not os.path.exists(output_call_logs_directory): os.makedirs(output_call_logs_directory) @@ -228,12 +231,12 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: for call_log in tqdm(call_logs): if call_log.calls: call_logs_to_json( - call_log=call_log, dir=output_call_logs_directory + call_log=call_log, folder=output_call_logs_directory ) else: for ph_no in tqdm(args.backup_specific_or_all_chat_call): if "chats" in args.backup_strategy: - output_chat_directory = args.parsed_backup_output_dir + "/chats" + output_chat_directory = args.parsed_backup_output_dir + CHAT_DIR if not os.path.exists(output_chat_directory): os.makedirs(output_chat_directory) chat = chat_builder.build_chat_for_given_id_or_phone_number( @@ -241,11 +244,11 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) chats_to_json( chat=chat, - dir=output_chat_directory, + folder=output_chat_directory, ) if "call_logs" in args.backup_strategy: output_call_logs_directory = ( - args.parsed_backup_output_dir + "/call_logs" + args.parsed_backup_output_dir + CALL_LOGS_DIR ) if not os.path.exists(output_call_logs_directory): os.makedirs(output_call_logs_directory) @@ -256,11 +259,11 @@ def close_db_connections(databases: List[sqlite3.Connection]) -> None: ) if call_log.calls: call_logs_to_json( - call_log=call_log, dir=output_call_logs_directory + call_log=call_log, folder=output_call_logs_directory ) else: close_db_connections([msgdb, wadb]) - raise Exception("Invalid 'chat formatting' requested") + raise AssertionError("Invalid 'chat formatting' requested") close_db_connections([msgdb, wadb]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e213a65 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +tqdm +attrs \ No newline at end of file diff --git a/src/call_log_extractor/builder.py b/src/call_log_extractor/builder.py index 2f7d9c4..eb82b5c 100644 --- a/src/call_log_extractor/builder.py +++ b/src/call_log_extractor/builder.py @@ -24,7 +24,7 @@ def build_call_for_given_id( if call_details: return Call(**call_details) else: - None + return None def build_call_log_for_given_id_or_phone_number( @@ -53,7 +53,7 @@ def build_call_log_for_given_id_or_phone_number( msgdb_cursor=msgdb_cursor, phone_number=phone_number ) else: - raise Exception("'jid_row_id' and 'phone_number' both cannot be None") + raise AssertionError("'jid_row_id' and 'phone_number' cannot both be None") dm_or_group = contact_resolver( wadb_cursor=wadb_cursor, raw_string_jid=raw_string_jid @@ -61,10 +61,8 @@ def build_call_log_for_given_id_or_phone_number( call_log["caller_id"] = Contact(raw_string_jid=raw_string_jid, **dm_or_group) query = f"""SELECT call_log._id FROM 'call_log' WHERE call_log.jid_row_id={call_log.get("jid_row_id")}""" - exec = msgdb_cursor.execute(query) - res_query = list(chain.from_iterable(exec.fetchall())) - if res_query is None: - return None + execution = msgdb_cursor.execute(query) + res_query = list(chain.from_iterable(execution.fetchall())) call_log["calls"] = [ build_call_for_given_id(msgdb_cursor, call_row_id) for call_row_id in sorted(res_query) @@ -86,10 +84,8 @@ def build_all_call_logs( A generator of CallLog objects """ query = "SELECT jid._id FROM 'jid'" - exec = msgdb_cursor.execute(query) - res_query = list(chain.from_iterable(exec.fetchall())) - if res_query is None: - return None + execution = msgdb_cursor.execute(query) + res_query = list(chain.from_iterable(execution.fetchall())) return ( build_call_log_for_given_id_or_phone_number( diff --git a/src/call_log_extractor/resolver.py b/src/call_log_extractor/resolver.py index ec82244..05470b9 100644 --- a/src/call_log_extractor/resolver.py +++ b/src/call_log_extractor/resolver.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Tuple, Union -def call_resolver(msgdb_cursor: sqlite3.Cursor, call_row_id: int) -> Dict[str, Any]: +def call_resolver(msgdb_cursor: sqlite3.Cursor, call_row_id: int) -> Dict[str, Any] | None: """Fetch call data for a given call_row_id from the msgdb. Args: @@ -16,11 +16,11 @@ def call_resolver(msgdb_cursor: sqlite3.Cursor, call_row_id: int) -> Dict[str, A SELECT call_log._id as call_row_id, call_log.from_me, call_log.timestamp, call_log.video_call, call_log.duration, call_log.call_result FROM 'call_log' WHERE call_log._id={call_row_id}""" - exec = msgdb_cursor.execute(msgdb_query) - res_query = exec.fetchone() + execution = msgdb_cursor.execute(msgdb_query) + res_query = execution.fetchone() if res_query is None: return None - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) return res @@ -51,15 +51,15 @@ def call_jid_resolver( FROM 'jid' WHERE jid.raw_string LIKE '%{phone_number}@%'""" else: - raise Exception("'jid_row_id' and 'phone_number' both cannot be None") + raise AssertionError("'jid_row_id' and 'phone_number' both cannot be None") - exec = msgdb_cursor.execute(msgdb_query) - res_query = exec.fetchone() + execution = msgdb_cursor.execute(msgdb_query) + res_query = execution.fetchone() if res_query is None: res_query = [ None, None, ] # Need some better logic to resolve when we don't have a contact in msgdb.db - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) raw_string_jid = res.pop("raw_string_jid") return res, raw_string_jid diff --git a/src/chat_extractor/builder.py b/src/chat_extractor/builder.py index e1a4efb..53325d2 100644 --- a/src/chat_extractor/builder.py +++ b/src/chat_extractor/builder.py @@ -84,7 +84,7 @@ def build_chat_for_given_id_or_phone_number( msgdb_cursor=msgdb_cursor, phone_number=phone_number ) else: - raise Exception("'chat_row_id' and 'phone_number' both cannot be None") + raise AssertionError("'chat_row_id' and 'phone_number' both cannot be None") dm_or_group = contact_resolver( wadb_cursor=wadb_cursor, raw_string_jid=raw_string_jid @@ -97,10 +97,8 @@ def build_chat_for_given_id_or_phone_number( ) query = f"""SELECT message._id FROM 'message' WHERE message.chat_row_id={chat.get("chat_id")}""" - exec = msgdb_cursor.execute(query) - res_query = list(chain.from_iterable(exec.fetchall())) - if res_query is None: - return None + execution = msgdb_cursor.execute(query) + res_query = list(chain.from_iterable(execution.fetchall())) chat["messages"] = [ build_message_for_given_id(msgdb_cursor, wadb_cursor, message_id) for message_id in res_query @@ -125,10 +123,8 @@ def build_all_chats( A generator of Chat objects. """ query = "SELECT chat._id FROM 'chat'" - exec = msgdb_cursor.execute(query) - res_query = list(chain.from_iterable(exec.fetchall())) - if res_query is None: - return None + execution = msgdb_cursor.execute(query) + res_query = list(chain.from_iterable(execution.fetchall())) return ( build_chat_for_given_id_or_phone_number( diff --git a/src/chat_extractor/resolver.py b/src/chat_extractor/resolver.py index 885be43..e713598 100644 --- a/src/chat_extractor/resolver.py +++ b/src/chat_extractor/resolver.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Tuple, Union -def media_resolver(msgdb_cursor: sqlite3.Cursor, message_row_id: int) -> Dict[str, Any]: +def media_resolver(msgdb_cursor: sqlite3.Cursor, message_row_id: int) -> Dict[str, Any] | None: """Fetch media related data for a given message_id from the msgdb. Args: @@ -13,17 +13,17 @@ def media_resolver(msgdb_cursor: sqlite3.Cursor, message_row_id: int) -> Dict[st Dict[str, Any]: Dictionary containing 'message_id', 'media_job_uuid', 'file_path' and 'mime_type' keys. """ query = f"SELECT message_media.message_row_id as message_id, message_media.media_job_uuid, message_media.file_path, message_media.mime_type FROM message_media WHERE message_media.message_row_id='{message_row_id}'" - exec = msgdb_cursor.execute(query) - res_query = exec.fetchone() + execution = msgdb_cursor.execute(query) + res_query = execution.fetchone() if res_query is None: return None - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) return res def geo_position_resolver( msgdb_cursor: sqlite3.Cursor, message_row_id: int -) -> Dict[str, Any]: +) -> Dict[str, Any] | None: """Fetch geo-position related data for a given message_id from the msgdb. Args: @@ -34,11 +34,11 @@ def geo_position_resolver( Dict[str, Any]: Dictionary containing 'message_id', 'latitude' and 'longitude' keys. """ query = f"SELECT message_location.message_row_id as message_id, message_location.latitude, message_location.longitude FROM message_location WHERE message_location.message_row_id='{message_row_id}'" - exec = msgdb_cursor.execute(query) - res_query = exec.fetchone() + execution = msgdb_cursor.execute(query) + res_query = execution.fetchone() if res_query is None: return None - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) return res @@ -63,14 +63,14 @@ def message_resolver( WHERE message._id={message_row_id} """ - exec = msgdb_cursor.execute(query) - res_query = exec.fetchone() + execution = msgdb_cursor.execute(query) + res_query = execution.fetchone() if res_query is None: res_query = [ None, None, ] - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) raw_string_jid = res.pop("raw_string_jid") return res, raw_string_jid @@ -104,15 +104,15 @@ def chat_resolver( JOIN 'jid' ON chat.jid_row_id=jid._id WHERE jid.raw_string LIKE '%{phone_number}@%'""" else: - raise Exception("'chat_row_id' and 'phone_number' both cannot be None") + raise AssertionError("'chat_row_id' and 'phone_number' both cannot be None") - exec = msgdb_cursor.execute(msgdb_query) - res_query = exec.fetchone() + execution = msgdb_cursor.execute(msgdb_query) + res_query = execution.fetchone() if res_query is None: res_query = [ None, None, ] # Need some better logic to resolve when we don't have a contact in msgdb.db - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) raw_string_jid = res.pop("raw_string_jid") return res, raw_string_jid diff --git a/src/common.py b/src/common.py index 613ba9f..565d76f 100644 --- a/src/common.py +++ b/src/common.py @@ -17,14 +17,14 @@ def contact_resolver( query = f""" SELECT wa_contacts.display_name as name, wa_contacts.number FROM 'wa_contacts' WHERE wa_contacts.jid="{raw_string_jid}" """ - exec = wadb_cursor.execute(query) - res_query = exec.fetchone() + execution = wadb_cursor.execute(query) + res_query = execution.fetchone() if res_query is None: res_query = [ None, None, ] # Need some better logic to resolve when we don't have a contact in wa.db - res = dict(zip([col[0] for col in exec.description], res_query)) + res = dict(zip([col[0] for col in execution.description], res_query)) if res.get("name"): if "/" in res["name"]: res["name"] = res.get("name").replace("/", "_") diff --git a/src/exports/to_json.py b/src/exports/to_json.py index bd79cea..1d739a0 100644 --- a/src/exports/to_json.py +++ b/src/exports/to_json.py @@ -5,7 +5,7 @@ from ..models import CallLog, Chat, Contact, GroupName -def chats_to_json(chat: Chat, dir: str) -> None: +def chats_to_json(chat: Chat, folder: str) -> None: """Store chat as a JSON file. It takes a chat object and a directory, and writes a json file to the directory with the chat's @@ -13,7 +13,7 @@ def chats_to_json(chat: Chat, dir: str) -> None: Args: chat (Chat): Chat - the chat object to be converted to JSON - dir (str): The directory to save the chats to. + folder (str): The directory to save the chats to. Returns: None: Creates .json file of the chat in the given directory @@ -28,11 +28,11 @@ def chats_to_json(chat: Chat, dir: str) -> None: else: chat_title_details = "" - with open(f"{dir}/{chat_title_details}.json", "w", encoding="utf8") as file: + with open(f"{folder}/{chat_title_details}.json", "w", encoding="utf8") as file: json.dump(asdict(chat), file, sort_keys=True, indent=4, ensure_ascii=False) -def call_logs_to_json(call_log: CallLog, dir: str) -> None: +def call_logs_to_json(call_log: CallLog, folder: str) -> None: """Store call logs as a JSON file. It takes a `CallLog` object and a directory path, and writes a JSON file to the directory with the @@ -40,7 +40,7 @@ def call_logs_to_json(call_log: CallLog, dir: str) -> None: Args: call_log (CallLog): CallLog - The call log object to be converted to JSON. - dir (str): The directory where the JSON files will be saved. + folder (str): The directory where the JSON files will be saved. Returns: None: Creates .json file of the chat in the given directory @@ -50,5 +50,5 @@ def call_logs_to_json(call_log: CallLog, dir: str) -> None: else: caller_id_details = f"+{call_log.caller_id.raw_string_jid.split('@')[0]}" - with open(f"{dir}/{caller_id_details}.json", "w", encoding="utf8") as file: + with open(f"{folder}/{caller_id_details}.json", "w", encoding="utf8") as file: json.dump(asdict(call_log), file, sort_keys=True, indent=4, ensure_ascii=False) diff --git a/src/exports/to_txt.py b/src/exports/to_txt.py index 74c405a..4121ad3 100644 --- a/src/exports/to_txt.py +++ b/src/exports/to_txt.py @@ -4,12 +4,12 @@ from ..models import CallLog, Chat, Contact, GroupName, Message -def chats_to_txt_raw(chat: Chat, dir: str) -> None: +def chats_to_txt_raw(chat: Chat, folder: str) -> None: """Store chat messages in a text file without formatting. Args: chat (Chat): Chat to be formatted. - dir (str): Directory to write the formatted chat. + folder (str): Directory to write the formatted chat. Returns: None: Creates .txt file of the chat in the given directory @@ -26,40 +26,40 @@ def chats_to_txt_raw(chat: Chat, dir: str) -> None: chat_title_details = "" messages = "\n".join([str(message) for message in chat.messages]) - with open(f"{dir}/{chat_title_details}-raw.txt", "w", encoding="utf-8") as file: + with open(f"{folder}/{chat_title_details}-raw.txt", "w", encoding="utf-8") as file: file.write(f"{chat_title_details}\n\n{messages}") -def chats_to_txt_formatted(chat: Chat, dir: str) -> None: +def chats_to_txt_formatted(chat: Chat, folder: str) -> None: """Format chat messages in a readable format and store them as a text file. Args: chat (Chat): Chat to be formatted. - dir (str): Directory to write the formatted chat. + folder (str): Directory to write the formatted chat. Returns: None: Creates .txt file of the chat in the given directory """ message_list = [] - def resolve_sender_name(message: Message) -> str: + def resolve_sender_name(msg: Message) -> str: """Utility function to extract 'sender_name' from a given message. Args: - message (Message): Message from which we want to extract sender_name. + msg (Message): Message from which we want to extract sender_name. Returns: str: sender_name """ - if message.from_me: + if msg.from_me: sender_name = "Me" else: sender_name = ( - message.sender_contact.name - if message.sender_contact.name is not None - else message.sender_contact.raw_string_jid[ - : message.sender_contact.raw_string_jid.index("@") - ] + msg.sender_contact.name + if msg.sender_contact.name is not None + else msg.sender_contact.raw_string_jid[ + : msg.sender_contact.raw_string_jid.index("@") + ] ) return sender_name @@ -90,7 +90,7 @@ def find_reply( # If there is no data or media or reply_to, we can assume that the message was about change in chat settings. message_str = f"[{date_time}] 'Change in the chat settings'" else: - sender_name = resolve_sender_name(message=message) + sender_name = resolve_sender_name(msg=message) message_str = ( f"[{date_time}]: {sender_name} - {message.text_data}" @@ -102,7 +102,7 @@ def find_reply( if message.reply_to: orig_message = next( find_reply( - lambda x: message.reply_to == x.key_id, + lambda x, msg=message: msg.reply_to == x.key_id, chat.messages[:idx], ), None, @@ -146,16 +146,16 @@ def find_reply( chat_title_details = "" messages = "\n".join(message_list) - with open(f"{dir}/{chat_title_details}.txt", "w", encoding="utf-8") as file: + with open(f"{folder}/{chat_title_details}.txt", "w", encoding="utf-8") as file: file.write(f"{chat_title_details}\n\n{messages}") -def call_logs_to_txt_raw(call_log: CallLog, dir: str) -> None: +def call_logs_to_txt_raw(call_log: CallLog, folder: str) -> None: """Store call logs in a text file without formatting. Args: call_log (CallLog): CallLog to be formatted. - dir (str): Directory to write the formatted call log. + folder (str): Directory to write the formatted call log. Returns: None: Creates .txt file of the call log in the given directory. @@ -166,16 +166,16 @@ def call_logs_to_txt_raw(call_log: CallLog, dir: str) -> None: caller_id_details = f"+{call_log.caller_id.raw_string_jid.split('@')[0]}" call_logs = "\n".join([str(call) for call in call_log.calls]) - with open(f"{dir}/{caller_id_details}-raw.txt", "w", encoding="utf-8") as file: + with open(f"{folder}/{caller_id_details}-raw.txt", "w", encoding="utf-8") as file: file.write(f"{caller_id_details}\n\n{call_logs}") -def call_logs_to_txt_formatted(call_log: CallLog, dir: str) -> None: +def call_logs_to_txt_formatted(call_log: CallLog, folder: str) -> None: """Format call logs in a readable format and store them as a text file. Args: call_log (CallLog): CallLog to be formatted. - dir (str): Directory to write the formatted call log. + folder (str): Directory to write the formatted call log. Returns: None: Creates .txt file of the call log in the given directory. @@ -229,5 +229,5 @@ def seconds_to_hms(duration_in_sec: int) -> str: call_log_list.append(call_log_str) call_logs = "\n".join(call_log_list) - with open(f"{dir}/{caller_id_details}.txt", "w", encoding="utf-8") as file: + with open(f"{folder}/{caller_id_details}.txt", "w", encoding="utf-8") as file: file.write(f"{caller_id_details}\n\n{call_logs}") diff --git a/src/models.py b/src/models.py index cb1e130..b0467d9 100644 --- a/src/models.py +++ b/src/models.py @@ -1,4 +1,3 @@ -from datetime import datetime from typing import List, Optional, Union from attrs import define @@ -45,21 +44,21 @@ class GeoPosition(object): class Message(object): message_id: int # Message ID. Resolved from `message._id`. key_id: str # Key ID. Resolved from `message.key_id`. - chat_id: str # Which chat does this message belong to. Resolved from `message.chat_row_id`. + chat_id: int # Which chat does this message belong to. Resolved from `message.chat_row_id`. from_me: int # Whether this message is sent by me or not. Resolved from `message.from_me -> bool`. sender_contact: Optional[Contact] - timestamp: datetime # When was this message sent. Resolved from `message.received_timestamp`. + timestamp: int # When was this message sent. Resolved from `message.received_timestamp`. text_data: Optional[ str ] # The actual text message. Resolved from `message.text_data`. media: Optional[Media] geo_position: Optional[GeoPosition] - reply_to: str # If a reply, it is a reply to which message. Resolved from `message._id -> message_quoted.message_row_id -> message_quoted.key_id` + reply_to: str| None # If a reply, it is a reply to which message. Resolved from `message._id -> message_quoted.message_row_id -> message_quoted.key_id` @define class Chat(object): - chat_id: str # Chat ID. Resolved from `chat._id`. + chat_id: int # Chat ID. Resolved from `chat._id`. chat_title: Optional[Union[Contact, GroupName]] # Chat title. messages: List[Optional[Message]] @@ -68,7 +67,7 @@ class Chat(object): class Call(object): call_row_id: int # Call row ID. Resolved from `call_log._id`. from_me: int # Whether this call was made by me or not. Resolved from `call_log.from_me -> bool`. - timestamp: datetime # When was this call made. Resolved from `call_log.timestamp`. + timestamp: int # When was this call made. Resolved from `call_log.timestamp`. video_call: int # Whether this call was a video call or not. Resolved from `call_log.video_call -> bool`. duration: int # Duration of the call. Resolved from `call_log.duration`. call_result: int diff --git a/tests/unit/test_call_log_resolver.py b/tests/unit/test_call_log_resolver.py index a1668eb..4d8d829 100644 --- a/tests/unit/test_call_log_resolver.py +++ b/tests/unit/test_call_log_resolver.py @@ -71,7 +71,7 @@ def test_call_jid_resolver_with_phone_number(): msgdb = sqlite3.connect("tests/unit/data/test_msgstore.db") msgdb_cursor = msgdb.cursor() - phone_numbers = [979017585714, 899167416177, 885402477365] + phone_numbers = ["979017585714", "899167416177", "885402477365"] for phone_number, expected_result in zip(phone_numbers, expected_results): assert ( resolver.call_jid_resolver(msgdb_cursor, phone_number=phone_number) diff --git a/tests/unit/test_exports_to_json.py b/tests/unit/test_exports_to_json.py index 7c9b56c..3a5bbd1 100644 --- a/tests/unit/test_exports_to_json.py +++ b/tests/unit/test_exports_to_json.py @@ -75,7 +75,7 @@ def test_export_chats_to_json(tmp_path): test_chat_dir = tmp_path / "chats" test_chat_dir.mkdir() - to_json.chats_to_json(chat=test_chat, dir=f"{test_chat_dir}") + to_json.chats_to_json(chat=test_chat, folder=f"{test_chat_dir}") with open( f"{test_chat_dir}/+{test_chat.chat_title.raw_string_jid.split('@')[0]}.json", @@ -126,7 +126,7 @@ def test_export_call_logs_to_json(tmp_path): test_call_log_dir = tmp_path / "call_logs" test_call_log_dir.mkdir() - to_json.call_logs_to_json(call_log=test_call_log, dir=f"{test_call_log_dir}") + to_json.call_logs_to_json(call_log=test_call_log, folder=f"{test_call_log_dir}") with open( f"{test_call_log_dir}/{test_call_log.caller_id.name} ({test_call_log.caller_id.number}).json", diff --git a/tests/unit/test_exports_to_txt.py b/tests/unit/test_exports_to_txt.py index 988b152..7fcd811 100644 --- a/tests/unit/test_exports_to_txt.py +++ b/tests/unit/test_exports_to_txt.py @@ -88,7 +88,7 @@ def test_export_chats_to_txt_raw(tmp_path): test_chat_dir = tmp_path / "chats" test_chat_dir.mkdir() - to_txt.chats_to_txt_raw(chat=test_chat, dir=f"{test_chat_dir}") + to_txt.chats_to_txt_raw(chat=test_chat, folder=f"{test_chat_dir}") with open( f"{test_chat_dir}/{test_chat.chat_title.name} ({test_chat.chat_title.number})-raw.txt" ) as f: @@ -162,7 +162,7 @@ def test_export_chats_to_txt_formatted(tmp_path): test_chat_dir = tmp_path / "chats" test_chat_dir.mkdir() - to_txt.chats_to_txt_formatted(chat=test_chat, dir=f"{test_chat_dir}") + to_txt.chats_to_txt_formatted(chat=test_chat, folder=f"{test_chat_dir}") with open(f"{test_chat_dir}/{test_chat.chat_title.name}.txt") as f: assert f.read() == expected_export_chats_to_txt_formatted_result @@ -198,7 +198,7 @@ def test_export_call_logs_to_txt_raw(tmp_path): test_call_log_dir = tmp_path / "call_logs" test_call_log_dir.mkdir() - to_txt.call_logs_to_txt_raw(call_log=test_call_log, dir=f"{test_call_log_dir}") + to_txt.call_logs_to_txt_raw(call_log=test_call_log, folder=f"{test_call_log_dir}") with open( f"{test_call_log_dir}/{test_call_log.caller_id.name} ({test_call_log.caller_id.number})-raw.txt" ) as f: @@ -237,7 +237,7 @@ def test_export_call_logs_to_txt_formatted(tmp_path): test_call_log_dir = tmp_path / "call_logs" test_call_log_dir.mkdir() to_txt.call_logs_to_txt_formatted( - call_log=test_call_log, dir=f"{test_call_log_dir}" + call_log=test_call_log, folder=f"{test_call_log_dir}" ) with open( f"{test_call_log_dir}/{test_call_log.caller_id.name} ({test_call_log.caller_id.number}).txt"