The database viewer provides an interactive interface to browse decrypted WhatsApp databases, list chats, view messages, and explore contact information. It supports multiple database schemas and handles various WhatsApp database versions.
The _load_contacts() method reads contact names from the wa_contacts table:
def _load_contacts(self): try: self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='wa_contacts'" ) if self.cursor.fetchone(): # Check columns self.cursor.execute("PRAGMA table_info(wa_contacts)") cols = [c[1] for c in self.cursor.fetchall()] name_col = None if 'display_name' in cols: name_col = 'display_name' elif 'wa_name' in cols: name_col = 'wa_name' if name_col and 'jid' in cols: self.cursor.execute( f"SELECT jid, {name_col} FROM wa_contacts WHERE {name_col} IS NOT NULL" ) for row in self.cursor.fetchall(): jid, name = row[0], row[1] if name: self.contact_map[jid] = name except: pass
Contact mapping allows displaying friendly names instead of phone numbers or JIDs.
def list_chats(self, limit: int = 0) -> List[Dict[str, Any]]: # Check for 'chat' table self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='chat'" ) if not self.cursor.fetchone(): print_error("Table 'chat' not found. This might be an old database version.") return []
limit_clause = f"LIMIT {limit}" if limit > 0 else ""self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='jid'")has_jid_table = bool(self.cursor.fetchone())if has_jid_table: query = f""" SELECT c._id, j.user, j.server, c.subject, c.sort_timestamp FROM chat c LEFT JOIN jid j ON c.jid_row_id = j._id ORDER BY c.sort_timestamp DESC {limit_clause} """ self.cursor.execute(query) rows = self.cursor.fetchall() for row in rows: chat_id = row[0] user = row[1] if row[1] else "" server = row[2] if row[2] else "" subject = row[3] if row[3] else "" timestamp = row[4] jid = f"{user}@{server}" if user and server else "Unknown" chats.append({ 'id': chat_id, 'jid': jid, 'subject': subject, 'timestamp': timestamp })
Modern WhatsApp databases separate JID (Jabber ID) information into a dedicated table for normalization.
for row in rows: msg_id = row[0] text = row[1] ts = row[2] from_me = row[3] sender_row_id = row[4] sender_jid = "Unknown" if not from_me and has_jid_table and sender_row_id > 0: self.cursor.execute( "SELECT user, server FROM jid WHERE _id=?", (sender_row_id,) ) jid_res = self.cursor.fetchone() if jid_res: sender_jid = f"{jid_res[0]}@{jid_res[1]}" elif from_me: sender_jid = "Me" # Resolve contact name sender_name = sender_jid if sender_jid in self.contact_map: sender_name = self.contact_map[sender_jid]
The viewer resolves sender IDs to phone numbers/JIDs and then to contact names for better readability.
if not messages: self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='view' AND name='available_message_view'" ) if self.cursor.fetchone(): query = f""" SELECT text_data, timestamp, from_me FROM available_message_view WHERE chat_row_id = ? ORDER BY timestamp DESC {limit_clause} """ self.cursor.execute(query, (chat_id,))
if not messages: self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='messages'" ) if self.cursor.fetchone(): query = f""" SELECT data, timestamp, key_from_me, remote_resource FROM messages WHERE key_remote_jid = ? ORDER BY timestamp DESC {limit_clause} """ self.cursor.execute(query, (chat_jid,))
Multi-strategy querying ensures compatibility across WhatsApp versions from 2.12.x to latest.
The get_message_count() method provides database statistics:
def get_message_count(self) -> int: try: # Check for 'message' table self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='message'" ) if self.cursor.fetchone(): self.cursor.execute("SELECT COUNT(*) FROM message") res = self.cursor.fetchone() return res[0] if res else 0 # Check for 'messages' table (older) self.cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='messages'" ) if self.cursor.fetchone(): self.cursor.execute("SELECT COUNT(*) FROM messages") res = self.cursor.fetchone() return res[0] if res else 0 return 0 except sqlite3.Error: return 0
The viewer allows manual path entry or selection from discovered databases:
dbs = []if os.path.exists("backups"): for root, _, files in os.walk("backups"): for f in files: if f.endswith('.decrypted.db'): dbs.append(os.path.join(root, f))if not dbs: path = ui.ask("Enter DB path manually") if path and os.path.exists(path): self.decrypted_db_path = path else: returnelse: rows = [] for i, p in enumerate(dbs): t = "WhatsApp" if "messenger" in p.lower() else "Business" if "business" in p.lower() else "Unknown" rows.append([str(i+1), t, os.path.basename(p)]) ui.print_table("Decrypted Databases", ["#", "Type", "File"], rows)
The tool remembers the last loaded database and offers to continue with it.