diff --git a/Whatsapp_Chat_Exporter/__main__.py b/Whatsapp_Chat_Exporter/__main__.py index 16329f8..df8967c 100644 --- a/Whatsapp_Chat_Exporter/__main__.py +++ b/Whatsapp_Chat_Exporter/__main__.py @@ -295,6 +295,9 @@ def main(): else: print("Unsupported date format. See https://wts.knugi.dev/docs?dest=date") exit(1) + if args.filter_chat_include is not None and args.filter_chat_exclude is not None: + print("Chat inclusion and exclusion filters cannot be used together.") + exit(1) if args.filter_chat_include is not None: for chat in args.filter_chat_include: if not chat.isnumeric(): diff --git a/Whatsapp_Chat_Exporter/extract.py b/Whatsapp_Chat_Exporter/extract.py index d670eec..e42af43 100644 --- a/Whatsapp_Chat_Exporter/extract.py +++ b/Whatsapp_Chat_Exporter/extract.py @@ -9,7 +9,7 @@ from mimetypes import MimeTypes from hashlib import sha256 from base64 import b64decode, b64encode from Whatsapp_Chat_Exporter.data_model import ChatStore, Message -from Whatsapp_Chat_Exporter.utility import MAX_SIZE, ROW_SIZE, DbType, determine_metadata, get_status_location +from Whatsapp_Chat_Exporter.utility import MAX_SIZE, ROW_SIZE, DbType, determine_metadata, get_chat_condition, get_status_location from Whatsapp_Chat_Exporter.utility import rendering, Crypt, Device, get_file_name, setup_template from Whatsapp_Chat_Exporter.utility import brute_force_offset, CRYPT14_OFFSETS, JidType @@ -173,8 +173,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): FROM messages WHERE 1=1 {f'AND timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"messages.key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"messages.key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "messages.key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "messages.key_remote_jid")}""") except sqlite3.OperationalError: c.execute(f"""SELECT count() @@ -185,8 +185,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): ON jid._id = chat.jid_row_id WHERE 1=1 {f'AND timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"jid.raw_string LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"jid.raw_string NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "jid.raw_string")} + {get_chat_condition(filter_chat[1], False, "jid.raw_string")}""") total_row_number = c.fetchone()[0] print(f"Processing messages...(0/{total_row_number})", end="\r") @@ -242,8 +242,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): ON receipt_user.message_row_id = messages._id WHERE messages.key_remote_jid <> '-1' {f'AND messages.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"messages.key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"messages.key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "messages.key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "messages.key_remote_jid")} GROUP BY messages._id ORDER BY messages.timestamp ASC;""" ) @@ -310,8 +310,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): ON receipt_user.message_row_id = message._id WHERE key_remote_jid <> '-1' {f'AND message.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "key_remote_jid")} GROUP BY message._id;""" ) except Exception as e: @@ -486,8 +486,8 @@ def media(db, data, media_folder, range, filter_chat): ON message_media.message_row_id = messages._id WHERE 1=1 {f'AND messages.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"messages.key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"messages.key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "messages.key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "messages.key_remote_jid")}""") except sqlite3.OperationalError: c.execute(f"""SELECT count() FROM message_media @@ -499,8 +499,8 @@ def media(db, data, media_folder, range, filter_chat): ON jid._id = chat.jid_row_id WHERE 1=1 {f'AND message.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"jid.raw_string LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"jid.raw_string NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "jid.raw_string")} + {get_chat_condition(filter_chat[1], False, "jid.raw_string")}""") total_row_number = c.fetchone()[0] print(f"\nProcessing media...(0/{total_row_number})", end="\r") i = 0 @@ -522,8 +522,8 @@ def media(db, data, media_folder, range, filter_chat): ON messages.key_remote_jid = jid.raw_string WHERE jid.type <> 7 {f'AND messages.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"messages.key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"messages.key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "messages.key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "messages.key_remote_jid")} ORDER BY messages.key_remote_jid ASC""" ) except sqlite3.OperationalError: @@ -546,8 +546,8 @@ def media(db, data, media_folder, range, filter_chat): ON message_media.file_hash = media_hash_thumbnail.media_hash WHERE jid.type <> 7 {f'AND message.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "key_remote_jid")} ORDER BY jid.raw_string ASC""" ) content = c.fetchone() @@ -609,8 +609,8 @@ def vcard(db, data, media_folder, range, filter_chat): ON messages_vcards.message_row_id = messages._id WHERE 1=1 {f'AND messages.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"messages.key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"messages.key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "messages.key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "messages.key_remote_jid")} ORDER BY messages.key_remote_jid ASC;""" ) except sqlite3.OperationalError: @@ -627,8 +627,8 @@ def vcard(db, data, media_folder, range, filter_chat): ON jid._id = chat.jid_row_id WHERE 1=1 {f'AND message.timestamp {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"key_remote_jid LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"key_remote_jid NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "key_remote_jid")} + {get_chat_condition(filter_chat[1], False, "key_remote_jid")} ORDER BY message.chat_row_id ASC;""" ) @@ -664,8 +664,8 @@ def calls(db, data, timezone_offset, filter_chat): LEFT JOIN chat ON call_log.jid_row_id = chat.jid_row_id WHERE 1=1 - {'AND (' + ' OR '.join(f"jid.raw_string LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' OR '.join(f"jid.raw_string NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "jid.raw_string")} + {get_chat_condition(filter_chat[1], False, "jid.raw_string")}""") total_row_number = c.fetchone()[0] if total_row_number == 0: return @@ -686,8 +686,8 @@ def calls(db, data, timezone_offset, filter_chat): LEFT JOIN chat ON call_log.jid_row_id = chat.jid_row_id WHERE 1=1 - {'AND (' + ' OR '.join(f"jid.raw_string LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"jid.raw_string NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""" + {get_chat_condition(filter_chat[0], True, "jid.raw_string")} + {get_chat_condition(filter_chat[1], False, "jid.raw_string")}""" ) chat = ChatStore(Device.ANDROID, "WhatsApp Calls") content = c.fetchone() diff --git a/Whatsapp_Chat_Exporter/extract_iphone.py b/Whatsapp_Chat_Exporter/extract_iphone.py index 0e124f3..c3e847f 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone.py +++ b/Whatsapp_Chat_Exporter/extract_iphone.py @@ -5,7 +5,7 @@ from glob import glob from pathlib import Path from mimetypes import MimeTypes from Whatsapp_Chat_Exporter.data_model import ChatStore, Message -from Whatsapp_Chat_Exporter.utility import APPLE_TIME, Device +from Whatsapp_Chat_Exporter.utility import APPLE_TIME, Device, get_chat_condition def contacts(db, data): @@ -30,8 +30,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): c.execute(f"""SELECT count() FROM ZWACHATSESSION WHERE 1=1 - {'AND (' + ' OR '.join(f"ZWACHATSESSION.ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZWACHATSESSION.ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "ZWACHATSESSION.ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZWACHATSESSION.ZCONTACTJID")}""") total_row_number = c.fetchone()[0] print(f"Processing contacts...({total_row_number})") @@ -43,8 +43,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): LEFT JOIN ZWAPROFILEPUSHNAME ON ZWACHATSESSION.ZCONTACTJID = ZWAPROFILEPUSHNAME.ZJID WHERE 1=1 - {'AND (' + ' OR '.join(f"ZWACHATSESSION.ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZWACHATSESSION.ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''};""" + {get_chat_condition(filter_chat[0], True, "ZWACHATSESSION.ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZWACHATSESSION.ZCONTACTJID")};""" ) content = c.fetchone() while content is not None: @@ -78,8 +78,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): ON ZWAMESSAGE.ZCHATSESSION = ZWACHATSESSION.Z_PK WHERE 1=1 {f'AND ZMESSAGEDATE {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"ZWACHATSESSION.ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZWACHATSESSION.ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''}""") + {get_chat_condition(filter_chat[0], True, "ZWACHATSESSION.ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZWACHATSESSION.ZCONTACTJID")}""") total_row_number = c.fetchone()[0] print(f"Processing messages...(0/{total_row_number})", end="\r") c.execute(f"""SELECT ZCONTACTJID, @@ -101,8 +101,8 @@ def messages(db, data, media_folder, timezone_offset, range, filter_chat): ON ZWAMESSAGE.ZCHATSESSION = ZWACHATSESSION.Z_PK WHERE 1=1 {f'AND ZMESSAGEDATE {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZCONTACTJID")} ORDER BY ZMESSAGEDATE ASC;""") i = 0 content = c.fetchone() @@ -216,8 +216,8 @@ def media(db, data, media_folder, range, filter_chat): ON ZWAMESSAGE.ZCHATSESSION = ZWACHATSESSION.Z_PK WHERE 1=1 {f'AND ZMESSAGEDATE {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"ZWACHATSESSION.ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZWACHATSESSION.ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "ZWACHATSESSION.ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZWACHATSESSION.ZCONTACTJID")} """) total_row_number = c.fetchone()[0] print(f"\nProcessing media...(0/{total_row_number})", end="\r") @@ -236,8 +236,8 @@ def media(db, data, media_folder, range, filter_chat): ON ZWAMESSAGE.ZCHATSESSION = ZWACHATSESSION.Z_PK WHERE ZMEDIALOCALPATH IS NOT NULL {f'AND ZWAMESSAGE.ZMESSAGEDATE {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''} + {get_chat_condition(filter_chat[0], True, "ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZCONTACTJID")} ORDER BY ZCONTACTJID ASC""") content = c.fetchone() mime = MimeTypes() @@ -297,8 +297,8 @@ def vcard(db, data, media_folder, range, filter_chat): ON ZWAMESSAGE.ZCHATSESSION = ZWACHATSESSION.Z_PK WHERE 1=1 {f'AND ZWAMESSAGE.ZMESSAGEDATE {range}' if range is not None else ''} - {'AND (' + ' OR '.join(f"ZCONTACTJID LIKE '%{chat}%'" for chat in filter_chat[0]) + ')' if filter_chat[0] is not None else ''} - {'AND (' + ' AND '.join(f"ZCONTACTJID NOT LIKE '%{chat}%'" for chat in filter_chat[1]) + ')' if filter_chat[1] is not None else ''};""") + {get_chat_condition(filter_chat[0], True, "ZCONTACTJID")} + {get_chat_condition(filter_chat[1], False, "ZCONTACTJID")};""") contents = c.fetchall() total_row_number = len(contents) print(f"\nProcessing vCards...(0/{total_row_number})", end="\r") diff --git a/Whatsapp_Chat_Exporter/utility.py b/Whatsapp_Chat_Exporter/utility.py index 9e6cf94..ba2787b 100644 --- a/Whatsapp_Chat_Exporter/utility.py +++ b/Whatsapp_Chat_Exporter/utility.py @@ -156,6 +156,16 @@ def get_file_name(contact: str, chat: ChatStore): return "".join(x for x in file_name if x.isalnum() or x in "- "), name +def get_chat_condition(filter, include, column): + if filter is not None: + if include: + return f'''AND ({' OR '.join(f"{column} LIKE '%{chat}%'" for chat in filter)})''' + else: + return f'''AND ({' AND '.join(f"{column} NOT LIKE '%{chat}%'" for chat in filter)})''' + else: + return "" + + # Android Specific CRYPT14_OFFSETS = ( {"iv": 67, "db": 191},