# WhatsApp Chat Viewer # # This script reads a WhatsApp ChatStorage.sqlite database and associated media files # to generate a browsable HTML archive of chat conversations. # # Author: Gemini # Date: September 7, 2025 # Version: 1.3 - Improved name resolution to avoid displaying encoded strings. import sqlite3 import os import argparse import html from datetime import datetime, timedelta import shutil # WhatsApp's epoch starts on 2001-01-01 00:00:00 (Core Data timestamp) WHATSAPP_EPOCH = datetime(2001, 1, 1) def convert_whatsapp_timestamp(ts): """Converts WhatsApp's Core Data timestamp to a human-readable string.""" if not ts: return "" try: # Timestamps are seconds since the WhatsApp epoch dt = WHATSAPP_EPOCH + timedelta(seconds=ts) return dt.strftime('%Y-%m-%d %H:%M:%S') except (ValueError, TypeError): return "Invalid date" def get_media_tag(media_path, output_dir): """Generates the appropriate HTML tag for a given media file and copies it.""" if not media_path: return "" # Path in the DB is often relative like 'Media/WhatsApp Images/IMG-...' test_path = os.path.join(output_dir, 'Message', media_path) full_media_path = '' if not os.path.exists(test_path): return f'
Media not found: {html.escape(test_path)}
' full_media_path = os.path.join('Message', media_path) # remove ./ in the beginning if present full_media_path = full_media_path.lstrip('./') ext = os.path.splitext(media_path)[1].lower() if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: return f'Image' elif ext in ['.mp4', '.mov', '.webm']: return f'' elif ext in ['.mp3', '.ogg', '.opus', '.m4a']: return f'' else: return f'View Media: {os.path.basename(media_path)}' def generate_html_chat(db_path, media_path, output_dir, chat_id, chat_name, is_group, contact_jid): """Generates an HTML file for a single chat session.""" conn = sqlite3.connect(db_path) cursor = conn.cursor() # Updated query to fetch more potential name fields (like ZFIRSTNAME) to find the best one. query = """ SELECT m.ZISFROMME, m.ZTEXT, m.ZMESSAGEDATE, m.ZFROMJID, g.ZCONTACTNAME AS GroupMemberContactName, cs.ZPARTNERNAME AS ChatPartnerName, p.ZPUSHNAME AS ProfilePushName, mi.ZMEDIALOCALPATH, cs.ZCONTACTJID AS ChatJID FROM ZWAMESSAGE m LEFT JOIN ZWAGROUPMEMBER g ON m.ZGROUPMEMBER = g.Z_PK LEFT JOIN ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK LEFT JOIN ZWAPROFILEPUSHNAME p ON m.ZFROMJID = p.ZJID LEFT JOIN ZWAMEDIAITEM mi ON m.ZMEDIAITEM = mi.Z_PK WHERE m.ZCHATSESSION = ? ORDER BY m.ZMESSAGEDATE ASC; """ cursor.execute(query, (chat_id,)) messages = cursor.fetchall() conn.close() if not messages: print(f"No messages found for chat: {chat_name}") return # Sanitize contact_jid for a unique and safe filename if contact_jid: safe_filename = "".join(c if c.isalnum() else "_" for c in contact_jid) else: # Fallback to chat_id if contact_jid is not available safe_filename = str(chat_id) chats_dir = os.path.join(output_dir, "chats") os.makedirs(chats_dir, exist_ok=True) html_filename = os.path.join(chats_dir, f"{safe_filename}.html") with open(html_filename, 'w', encoding='utf-8') as f: f.write(f""" Chat with {html.escape(chat_name)}
{html.escape(chat_name)}
{contact_jid}
""") # Write messages for is_from_me, text, timestamp, from_jid, group_member_contact_name, chat_partner_name, profile_push_name, media_local_path, contact_jid in messages: msg_class = "sent" if is_from_me else "received" f.write(f'
') # Determine and display the sender's name for incoming messages if not is_from_me: # Prioritize group member contact name for group chats if is_group: # Try names in order of preference, avoiding encoded-looking strings potential_names = [ group_member_contact_name, profile_push_name, from_jid, chat_partner_name, ] # Filter out None values and strings that look like they're encoded valid_names = [name for name in potential_names if name and not ( name.startswith('CK') and any(c.isupper() for c in name[2:]) and '=' in name )] sender_name = next((name for name in valid_names), "Unknown") else: # For individual chats, prefer partner name or push name sender_name = chat_partner_name or profile_push_name or from_jid or "Unknown" f.write(f'
{html.escape(str(sender_name))}
') if text: # Replace newline characters with
tags for proper display escaped_text = html.escape(text) f.write(f'
{escaped_text.replace(chr(10), "
")}
') if media_local_path: # print("Media path:", media_local_path) f.write(get_media_tag(media_local_path, output_dir)) f.write(f'
{convert_whatsapp_timestamp(timestamp)}
') f.write('
') f.write("""
""") print(f"Successfully generated HTML for: {chat_name}") # Step: iPhone backup manifest.db processing def process_iphone_backup(backup_path, output_dir): """ Processes the iPhone backup manifest.db, extracts WhatsApp shared files, and recreates the file structure in output_dir. """ manifest_db_path = os.path.join(backup_path, 'Manifest.db') if not os.path.exists(manifest_db_path): print(f"Manifest.db not found in backup path: {manifest_db_path}") return # Connect to manifest.db and extract WhatsApp shared files conn = sqlite3.connect(manifest_db_path) cursor = conn.cursor() cursor.execute("SELECT fileID, domain, relativePath FROM Files WHERE domain = ?", ('AppDomainGroup-group.net.whatsapp.WhatsApp.shared',)) files = cursor.fetchall() print(f"Found {len(files)} WhatsApp shared files in manifest.db.") # Prepare to recreate file structure for fileID, domain, relativePath in files: src_file = os.path.join(backup_path, fileID[:2], fileID) dest_file = os.path.join(output_dir, relativePath) os.makedirs(os.path.dirname(dest_file), exist_ok=True) if os.path.exists(src_file): if not os.path.exists(dest_file): try: shutil.copy2(src_file, dest_file) except Exception as e: print(f"Error copying {src_file} to {dest_file}: {e}") else: print(f"Source file missing: {src_file}") def main(): parser = argparse.ArgumentParser(description="WhatsApp Chat Exporter") parser.add_argument("--output", default="_html_export", help="Directory to save the HTML files.") parser.add_argument("--backup-path", default=None, help="Path to iPhone backup directory (for manifest.db processing)") args = parser.parse_args() if args.backup_path: process_iphone_backup(args.backup_path, args.output) # Use backup paths for archive creation db_path = os.path.join(args.output, "ChatStorage.sqlite") media_path = os.path.join(args.output, "Message/") else: parser.add_argument("db_path", help="Path to the ChatStorage.sqlite file.") parser.add_argument("media_path", help="Path to the root 'Media' directory.") args = parser.parse_args() db_path = args.db_path media_path = args.media_path if not os.path.exists(db_path): print(f"Error: Database file not found at '{db_path}'") return if not os.path.exists(media_path): print(f"Error: Media directory not found at '{media_path}'") return os.makedirs(args.output, exist_ok=True) conn = sqlite3.connect(db_path) cursor = conn.cursor() # Get all chats, joining with ZWAPROFILEPUSHNAME and using COALESCE to get the best possible name. cursor.execute(""" SELECT cs.Z_PK, COALESCE(p.ZPUSHNAME, cs.ZPARTNERNAME) AS ChatName, cs.ZCONTACTJID, cs.ZMESSAGECOUNTER, MIN(m.ZMESSAGEDATE) as FirstMessageDate, MAX(m.ZMESSAGEDATE) as LastMessageDate, COALESCE(gi.ZPICTUREPATH, pic.ZPATH) AS AvatarPath FROM ZWACHATSESSION cs LEFT JOIN ZWAPROFILEPUSHNAME p ON cs.ZCONTACTJID = p.ZJID LEFT JOIN ZWAMESSAGE m ON cs.Z_PK = m.ZCHATSESSION LEFT JOIN ZWAGROUPINFO gi ON cs.ZGROUPINFO = gi.Z_PK LEFT JOIN ZWAPROFILEPICTUREITEM pic ON cs.ZCONTACTJID = pic.ZJID WHERE cs.ZCONTACTJID NOT LIKE '%@status' GROUP BY cs.Z_PK, ChatName, cs.ZCONTACTJID, cs.ZMESSAGECOUNTER, AvatarPath ORDER BY LastMessageDate DESC NULLS LAST, ChatName """) chats = cursor.fetchall() conn.close() print(f"Found {len(chats)} chats to export.") index_path = os.path.join(args.output, "whatsapp-chats.html") with open(index_path, 'w', encoding='utf-8') as index_f: index_f.write(f""" WhatsApp Chat Export

WhatsApp Chat Export

Exported on {datetime.now().strftime('%Y-%m-%d %H:%M')}
") # Create a simple redirect index.html redirect_index = os.path.join(args.output, "index.html") with open(redirect_index, 'w', encoding='utf-8') as f: f.write(f""" Redirecting to WhatsApp Chats...

Redirecting to WhatsApp Chats...

""") print(f"\nExport complete!") print(f"View your chats by opening either of these files in your browser:") print(f" • {os.path.abspath(index_path)}") print(f" • {os.path.abspath(redirect_index)}") if __name__ == "__main__": main()