def __init__(self, cursor, path, daemon = False): QtGui.QWidget.__init__(self) self.ui = Ui_WhatsAppBrowser() self.ui.setupUi(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.cursor = cursor self.backup_path = path self.fname_contacts = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename="Contacts.sqlite", domaintype="AppDomain")) self.fname_chatstorage = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename="ChatStorage.sqlite", domaintype="AppDomain")) # check if files exist if (not os.path.isfile(self.fname_chatstorage)): raise Exception("WhatsApp database not found: \"%s\""%self.fname_chatstorage) if (daemon == False): self.populateUI() # signal-slot chats/msgs connection QtCore.QObject.connect(self.ui.chatsWidget, QtCore.SIGNAL("itemSelectionChanged()"), self.onChatsClick) self.ui.chatsWidget.setColumnHidden(0,True) self.ui.msgsWidget.setColumnHidden(0,True) # signal-slot connection: right click context menu on contacts table self.ui.contactsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.contactsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuContacts) # signal-slot connection: right click context menu on chats table self.ui.chatsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.chatsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuChats) # signal-slot connection: right click context menu on messages table self.ui.msgsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.msgsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuMsgs)
class WABrowserWidget(QtGui.QWidget): def __init__(self, cursor, path, daemon = False): QtGui.QWidget.__init__(self) self.ui = Ui_WhatsAppBrowser() self.ui.setupUi(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.cursor = cursor self.backup_path = path self.fname_contacts = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename="Contacts.sqlite", domaintype="AppDomain")) self.fname_chatstorage = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename="ChatStorage.sqlite", domaintype="AppDomain")) # check if files exist if (not os.path.isfile(self.fname_chatstorage)): raise Exception("WhatsApp database not found: \"%s\""%self.fname_chatstorage) if (daemon == False): self.populateUI() # signal-slot chats/msgs connection QtCore.QObject.connect(self.ui.chatsWidget, QtCore.SIGNAL("itemSelectionChanged()"), self.onChatsClick) self.ui.chatsWidget.setColumnHidden(0,True) self.ui.msgsWidget.setColumnHidden(0,True) # signal-slot connection: right click context menu on contacts table self.ui.contactsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.contactsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuContacts) # signal-slot connection: right click context menu on chats table self.ui.chatsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.chatsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuChats) # signal-slot connection: right click context menu on messages table self.ui.msgsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.msgsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuMsgs) ''' Populates the WhatsApp Browser widget ''' def populateUI(self): ###################################################### # CONTACTS SECTION # ###################################################### contacts = self.getContacts() self.ui.contactsWidget.setRowCount(len(contacts)) row = 0 for contact in contacts: id = contact[0] name = contact[1] phonenum = contact[2] text = contact[3] date = contact[4] newItem = QtGui.QTableWidgetItem(name) self.ui.contactsWidget.setItem(row, 0, newItem) newItem = QtGui.QTableWidgetItem(phonenum) self.ui.contactsWidget.setItem(row, 1, newItem) newItem = QtGui.QTableWidgetItem(text) self.ui.contactsWidget.setItem(row, 2, newItem) newItem = QtGui.QTableWidgetItem(str(date)) self.ui.contactsWidget.setItem(row, 3, newItem) row = row + 1 self.ui.contactsWidget.resizeColumnsToContents() self.ui.contactsWidget.resizeRowsToContents() ###################################################### # CHATS SECTION # ###################################################### chats = self.getChats() self.ui.chatsWidget.setRowCount(len(chats)) row = 0 for chat in chats: if hasattr(chat, 'Z_PK'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,chat.Z_PK) self.ui.chatsWidget.setItem(row, 0, newItem) if hasattr(chat, 'ZPARTNERNAME'): newItem = QtGui.QTableWidgetItem(chat.ZPARTNERNAME) self.ui.chatsWidget.setItem(row, 1, newItem) if hasattr(chat, 'ZCONTACTJID'): newItem = QtGui.QTableWidgetItem(chat.ZCONTACTJID) self.ui.chatsWidget.setItem(row, 2, newItem) if hasattr(chat, 'ZMESSAGECOUNTER'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,chat.ZMESSAGECOUNTER-1) self.ui.chatsWidget.setItem(row, 3, newItem) if hasattr(chat, 'ZUNREADCOUNT'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,chat.ZUNREADCOUNT) self.ui.chatsWidget.setItem(row, 4, newItem) if hasattr(chat, 'ZLASTMESSAGEDATE'): newItem = QtGui.QTableWidgetItem(str(self.formatDate(chat.ZLASTMESSAGEDATE))) self.ui.chatsWidget.setItem(row, 5, newItem) if hasattr(chat, 'ZGROUPINFO'): if chat.ZGROUPINFO is not None: for i in range(6): self.ui.chatsWidget.item(row,i).setBackground(QtCore.Qt.yellow) row = row + 1 self.ui.chatsWidget.resizeColumnsToContents() self.ui.chatsWidget.resizeRowsToContents() def formatDate(self, mactime): # if timestamp is not like "304966548", but like "306350664.792749", # then just use the numbers in front of the "." if mactime is None: return None mactime = int(Decimal(mactime)) date_time = datetime.fromtimestamp(mactime+11323*60*1440) return date_time ######################################################################## # DB QUERIES # ######################################################################## ''' Contacts information are stored into Contacts.sqlite (newer version of WhatsApp for iOS) or into ChatStorage.sqlite. ''' def getContacts(self): try: try: # opening database (1st attempt: Contacts.sqlite) << New Version self.tempdb = sqlite3.connect(self.fname_contacts) has_contacts_sqlite = True except: # opening database (2nd attempt: ChatStorage.sqlite) << Old Version self.tempdb = sqlite3.connect(self.fname_chatstorage) has_contacts_sqlite = False except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() self.tempdb.row_factory = sqlite3.Row self.tempcur = self.tempdb.cursor() contactsToReturn = [] if has_contacts_sqlite: # reading contacts from database # 1st step: ZWAPHONE table query = "SELECT * FROM ZWAPHONE" self.tempcur.execute(query) contacts = self.tempcur.fetchall() readCount = 0 for contact in contacts: id = contact['Z_PK'] contact_key = contact['ZCONTACT'] favorite_key = contact['ZFAVORITE'] status_key = contact['ZSTATUS'] phonenum = contact['ZPHONE'] # 2nd step: name from ZWACONTACT table query = "SELECT * FROM ZWACONTACT WHERE Z_PK=?;" self.tempcur.execute(query, [contact_key]) contact_entry = self.tempcur.fetchone() if contact_entry == None: name = "N/A" else: name = contact_entry['ZFULLNAME'] # 3rd step: status from ZWASTATUS table query = "SELECT * FROM ZWASTATUS WHERE Z_PK=?;" self.tempcur.execute(query, [status_key]) status_entry = self.tempcur.fetchone() if status_entry == None: text = "N/A" date = "N/A" else: text = status_entry['ZTEXT'] date = self.formatDate(status_entry['ZDATE']) contactsToReturn.append([id, name, phonenum, text, date]) else: # has_contacts_sqlite == False # reading contacts from database # 1st step: ZWAPHONE table query = "SELECT * FROM ZWAFAVORITE" self.tempcur.execute(query) contacts = self.tempcur.fetchall() for contact in contacts: id = contact['Z_PK'] status_key = contact['ZSTATUS'] name = contact['ZDISPLAYNAME'] phonenum = contact['ZPHONENUMBER'] # 2nd step: status from ZWASTATUS table query = "SELECT * FROM ZWASTATUS WHERE Z_PK=?;" self.tempcur.execute(query, [status_key]) status_entry = self.tempcur.fetchone() if status_entry == None: text = "N/A" date = "N/A" else: text = status_entry['ZSTATUSTEXT'] date = self.formatDate(status_entry['ZSTATUSDATE']) contactsToReturn.append([id, name, phonenum, text, date]) # closing database self.tempdb.close() return contactsToReturn ''' Chats information are stored into ChatStorage.sqlite. ''' def getChats(self): # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZWACHATSESSION table query = "SELECT * FROM ZWACHATSESSION" self.tempcur.execute(query) # fetches chats namedtuple chats = self.tempcur.fetchall() # closing database self.tempdb.close() # a namedtuple is returned return chats ''' Messages are stored into ChatStorage.sqlite. ''' def getMsgs(self, zchatsession): # refresh the window #(the selected row is highligthed and the chats table appears disabled) QtGui.QApplication.processEvents() # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZWAMESSAGE table query = "SELECT * FROM ZWAMESSAGE WHERE ZCHATSESSION=? ORDER BY ZMESSAGEDATE ASC;" self.tempcur.execute(query, [zchatsession]) messages = self.tempcur.fetchall() # closing database self.tempdb.close() # a namedtuple is returned return messages ''' Messages are stored into ChatStorage.sqlite. ''' def getMsgsThreaded(self, zchatsession): # progress window progress = QtGui.QProgressDialog("Querying the database ...", "Abort", 0, 0, self) progress.setWindowTitle("WhatsApp Browser ...") progress.setWindowModality(QtCore.Qt.WindowModal) progress.setMinimumDuration(0) progress.setCancelButton(None) progress.show() # ZWAMESSAGE table query = """ SELECT ZWAMESSAGE.*, ZWAGROUPMEMBER.ZCONTACTNAME, ZWAGROUPMEMBER.ZMEMBERJID FROM ZWAMESSAGE LEFT JOIN ZWAGROUPMEMBER ON ZWAGROUPMEMBER.Z_PK = ZWAMESSAGE.ZGROUPMEMBER WHERE ZWAMESSAGE.ZCHATSESSION = ? ORDER BY ZWAMESSAGE.ZMESSAGEDATE ASC;""" # call a thread to query the db showing a progress bar queryTh = ThreadedQuery(self.fname_chatstorage,query,[zchatsession]) queryTh.start() while queryTh.isAlive(): QtGui.QApplication.processEvents() progress.close() messages = queryTh.getResult() # a namedtuple is returned return messages ''' GroupMember info are stored into ChatStorage.sqlite. ''' def getGroupInfo(self, zpk): # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZWAGROUPMEMBER table query = "SELECT * FROM ZWAGROUPMEMBER WHERE Z_PK=?;" self.tempcur.execute(query, [zpk]) groupmember = self.tempcur.fetchone() # closing database self.tempdb.close() # a namedtuple is returned return groupmember ''' MediaItem info are stored into ChatStorage.sqlite. ''' def getMediaItem(self, zpk): # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZMEDIAITEM table query = "SELECT * FROM ZWAMEDIAITEM WHERE Z_PK=?;" self.tempcur.execute(query, [zpk]) media = self.tempcur.fetchone() # closing database self.tempdb.close() # a namedtuple is returned return media ######################################################################## # SLOTS # ######################################################################## def onChatsClick(self): # disable chats table (to disable click events while processing) self.ui.chatsWidget.setEnabled(False) # retrieving selected row self.ui.chatsWidget.setCurrentCell(self.ui.chatsWidget.currentRow(),0) currentSelectedItem = self.ui.chatsWidget.currentItem() if (currentSelectedItem): pass else: return ###################################################### # MESSAGES SECTION # ###################################################### zpk = int(currentSelectedItem.text()) #msgs = self.getMsgs(zpk) # <--- msgs = self.getMsgsThreaded(zpk) # <--- # re-select a visible column to allow the keyboard selection self.ui.chatsWidget.setCurrentCell(self.ui.chatsWidget.currentRow(),1) # erase previous messages and set new table lenght #self.ui.msgsWidget.clearContents() self.ui.msgsWidget.setSortingEnabled(False) self.ui.msgsWidget.setRowCount(len(msgs)) row = 0 for msg in msgs: fields = set(msg.keys()) if 'Z_PK' in fields: newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole, msg['Z_PK']) self.ui.msgsWidget.setItem(row, 0, newItem) if 'ZFROMJID' in fields: fromstring = "Me" if msg['ZFROMJID'] is not None: fromstring = msg['ZFROMJID'] newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,fromstring) self.ui.msgsWidget.setItem(row, 1, newItem) if 'ZMESSAGEDATE' in fields: newItem = QtGui.QTableWidgetItem(str(self.formatDate(msg['ZMESSAGEDATE']))) self.ui.msgsWidget.setItem(row, 2, newItem) if 'ZTEXT' in fields: newItem = QtGui.QTableWidgetItem(msg['ZTEXT']) self.ui.msgsWidget.setItem(row, 3, newItem) if 'ZMESSAGESTATUS' in fields: newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,msg['ZMESSAGESTATUS']) self.ui.msgsWidget.setItem(row, 5, newItem) if 'ZGROUPMEMBER' in fields and msg['ZGROUPMEMBER'] is not None: if msg['ZCONTACTNAME'] is not None: fromstring = msg['ZCONTACTNAME'] + " - " + msg['ZMEMBERJID'] else: fromstring = "N/A" newItem = QtGui.QTableWidgetItem(fromstring) self.ui.msgsWidget.setItem(row, 1, newItem) if 'ZMEDIAITEM' in fields: mediaItem = QtGui.QTableWidgetItem("") if msg['ZMEDIAITEM'] is not None: media = self.getMediaItem(msg['ZMEDIAITEM']) msgcontent = "" # VCARD info if (media.ZVCARDNAME and media.ZVCARDSTRING) is not None: msgcontent += ("VCARD\n" + media.ZVCARDNAME + "\n" + media.ZVCARDSTRING + "\n") # GPS info if media.ZLATITUDE != 0. or media.ZLONGITUDE != 0.: msgcontent += ("GPS\n" + "lat: " + str(media.ZLATITUDE) + "\nlong: " + str(media.ZLONGITUDE) + "\n") # VIDEO info if media.ZMOVIEDURATION != 0: msgcontent += ("VIDEO\n" + "duration: " + str(media.ZMOVIEDURATION) + " sec\n") # FILE info if media.ZFILESIZE != 0: msgcontent += ("FILE\n" + "size: " + str(media.ZFILESIZE) + " B\n") # set message content (3rd column) newItem = QtGui.QTableWidgetItem(msgcontent) self.ui.msgsWidget.setItem(row, 3, newItem) thumbRealFilename = "" mediaRealFilename = "" mediallocalfile = "" # THUMBNAIL if media.ZTHUMBNAILLOCALPATH is not None: thumblocalfilepath = media.ZTHUMBNAILLOCALPATH thumblocalpath = os.path.dirname(thumblocalfilepath) thumbllocalfile = os.path.basename(thumblocalfilepath) thumbRealFilename = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename=thumbllocalfile, path='Library/'+thumblocalpath, domaintype="AppDomain")) # ATTACHMENT if media.ZMEDIALOCALPATH is not None: medialocalfilepath = media.ZMEDIALOCALPATH mediallocalpath = os.path.dirname(medialocalfilepath) mediallocalfile = os.path.basename(medialocalfilepath) mediaRealFilename = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename=mediallocalfile, path='Library/'+mediallocalpath, domaintype="AppDomain")) # add a thumnail to the table view icon = None if thumbRealFilename != "": icon = QtGui.QIcon(thumbRealFilename) else: icon = QtGui.QIcon(mediaRealFilename) mediaItem = QtGui.QTableWidgetItem() mediaItem.setIcon(icon) # add info for attachment export (ctx menu) if mediallocalfile != "": mediaItem.setData(QtCore.Qt.UserRole, mediaRealFilename) mediaItem.setData(QtCore.Qt.UserRole+1, mediallocalfile) if media.ZLATITUDE != 0. or media.ZLONGITUDE != 0.: mediaItem.setData(QtCore.Qt.UserRole+2, media.ZLATITUDE) mediaItem.setData(QtCore.Qt.UserRole+3, media.ZLONGITUDE) self.ui.msgsWidget.setItem(row, 4, mediaItem) if 'ZISFROMME' in fields and msg['ZISFROMME'] is 1: for i in range(6): self.ui.msgsWidget.item(row,i).setBackground(QtCore.Qt.green) row = row + 1 self.ui.msgsWidget.setSortingEnabled(True) self.ui.msgsWidget.setIconSize(QtCore.QSize(150,150)) self.ui.msgsWidget.resizeColumnsToContents() self.ui.msgsWidget.setColumnWidth(4, 150) self.ui.msgsWidget.resizeRowsToContents() # re-enable chats table self.ui.chatsWidget.setEnabled(True) self.ui.chatsWidget.setFocus() ###################################################### # CTX MENU SECTION # ###################################################### def ctxMenuMsgs(self, pos): cell = self.ui.msgsWidget.itemAt(pos) self.link = cell.data(QtCore.Qt.UserRole) self.name = cell.data(QtCore.Qt.UserRole + 1) self.lat = cell.data(QtCore.Qt.UserRole + 2) self.long = cell.data(QtCore.Qt.UserRole + 3) menu = QtGui.QMenu() action1 = QtGui.QAction("Export table CSV", self) action1.triggered.connect(self.exportCSVmsgs) menu.addAction(action1) if self.link != None: menu.addSeparator() action1 = QtGui.QAction("Open attachment in standard viewer", self) action1.triggered.connect(self.openWithViewer) menu.addAction(action1) action1 = QtGui.QAction("Export attachment", self) action1.triggered.connect(self.exportSelectedFile) menu.addAction(action1) if (self.lat and self.long) is not None: menu.addSeparator() action1 = QtGui.QAction("Show GPS coordinates on Google Maps", self) action1.triggered.connect(self.openGPSBrowser) menu.addAction(action1) menu.exec_(self.ui.msgsWidget.mapToGlobal(pos)); def ctxMenuContacts(self, pos): menu = QtGui.QMenu() action1 = QtGui.QAction("Export table CSV", self) action1.triggered.connect(self.exportCSVcontacts) menu.addAction(action1) menu.exec_(self.ui.contactsWidget.mapToGlobal(pos)); def ctxMenuChats(self, pos): menu = QtGui.QMenu() action1 = QtGui.QAction("Export table CSV", self) action1.triggered.connect(self.exportCSVchats) menu.addAction(action1) menu.exec_(self.ui.chatsWidget.mapToGlobal(pos)); ##### ATTACHMENTS EXPORT FUNCTIONS ##### def openWithViewer(self): if sys.platform.startswith('linux'): subprocess.call(["xdg-open", self.link]) else: os.startfile(self.link) def exportSelectedFile(self): filename = QtGui.QFileDialog.getSaveFileName(self, "Export attachment", self.name) filename = filename[0] if (len(filename) == 0): return try: shutil.copy(self.link, filename) QtGui.QMessageBox.about(self, "Confirm", "Attachment saved as %s."%filename) except: QtGui.QMessageBox.about(self, "Error", "Error while saving attachment") def openGPSBrowser(self): coordinatesURL = "https://maps.google.com/?q=" + str(self.lat) + "," + str(self.long) webbrowser.open(coordinatesURL) ##### TABLES EXPORT FUNCTIONS ##### def exportCSVcontacts(self): self.exportCSVtable(self.ui.contactsWidget) def exportCSVchats(self): self.exportCSVtable(self.ui.chatsWidget) def exportCSVmsgs(self): self.exportCSVtable(self.ui.msgsWidget) def exportCSVtable(self, table): filename = QtGui.QFileDialog.getSaveFileName(self, "Export table", "table", ".csv") filename = filename[0] if (len(filename) == 0): return f = open(filename, 'w') # header tablerow='"' for c in range(table.columnCount()): hitem = table.horizontalHeaderItem(c) if hitem is not None: tablerow += unicode(hitem.text()).encode('utf8') tablerow += '","' tablerow=tablerow[:-2]+"\n" f.write(tablerow) # content tablerow='"' for r in range(table.rowCount()): for c in range(table.columnCount()): item = table.item(r,c) if item is not None: tablerow += unicode(item.text().replace('\n',' ')).encode('utf8') tablerow += '","' tablerow = tablerow[:-2] + "\n" f.write(tablerow) tablerow='"' f.close()
class WABrowserWidget(QtGui.QWidget): def __init__(self, cursor, path, daemon = False): QtGui.QWidget.__init__(self) self.ui = Ui_WhatsAppBrowser() self.ui.setupUi(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.cursor = cursor self.backup_path = path self.fname_contacts = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename="Contacts.sqlite", domaintype="AppDomain")) self.fname_chatstorage = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename="ChatStorage.sqlite", domaintype="AppDomain")) # check if files exist if (not os.path.isfile(self.fname_chatstorage)): raise Exception("WhatsApp database not found: \"%s\""%self.fname_chatstorage) if (daemon == False): self.populateUI() # signal-slot chats/msgs connection QtCore.QObject.connect(self.ui.chatsWidget, QtCore.SIGNAL("itemSelectionChanged()"), self.onChatsClick) self.ui.chatsWidget.setColumnHidden(0,True) self.ui.msgsWidget.setColumnHidden(0,True) # signal-slot connection: right click context menu on contacts table self.ui.contactsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.contactsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuContacts) # signal-slot connection: right click context menu on chats table self.ui.chatsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.chatsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuChats) # signal-slot connection: right click context menu on messages table self.ui.msgsWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect(self.ui.msgsWidget, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.ctxMenuMsgs) ''' Populates the WhatsApp Browser widget ''' def populateUI(self): ###################################################### # CONTACTS SECTION # ###################################################### contacts = self.getContacts() self.ui.contactsWidget.setRowCount(len(contacts)) row = 0 for contact in contacts: id = contact[0] name = contact[1] phonenum = contact[2] text = contact[3] date = contact[4] newItem = QtGui.QTableWidgetItem(name) self.ui.contactsWidget.setItem(row, 0, newItem) newItem = QtGui.QTableWidgetItem(phonenum) self.ui.contactsWidget.setItem(row, 1, newItem) newItem = QtGui.QTableWidgetItem(text) self.ui.contactsWidget.setItem(row, 2, newItem) newItem = QtGui.QTableWidgetItem(str(date)) self.ui.contactsWidget.setItem(row, 3, newItem) row = row + 1 self.ui.contactsWidget.resizeColumnsToContents() self.ui.contactsWidget.resizeRowsToContents() ###################################################### # CHATS SECTION # ###################################################### chats = self.getChats() self.ui.chatsWidget.setRowCount(len(chats)) row = 0 for chat in chats: if hasattr(chat, 'Z_PK'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,chat.Z_PK) self.ui.chatsWidget.setItem(row, 0, newItem) if hasattr(chat, 'ZPARTNERNAME'): newItem = QtGui.QTableWidgetItem(chat.ZPARTNERNAME) self.ui.chatsWidget.setItem(row, 1, newItem) if hasattr(chat, 'ZCONTACTJID'): newItem = QtGui.QTableWidgetItem(chat.ZCONTACTJID) self.ui.chatsWidget.setItem(row, 2, newItem) if hasattr(chat, 'ZMESSAGECOUNTER'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,chat.ZMESSAGECOUNTER-1) self.ui.chatsWidget.setItem(row, 3, newItem) if hasattr(chat, 'ZUNREADCOUNT'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,chat.ZUNREADCOUNT) self.ui.chatsWidget.setItem(row, 4, newItem) if hasattr(chat, 'ZLASTMESSAGEDATE'): newItem = QtGui.QTableWidgetItem(str(self.formatDate(chat.ZLASTMESSAGEDATE))) self.ui.chatsWidget.setItem(row, 5, newItem) if hasattr(chat, 'ZGROUPINFO'): if chat.ZGROUPINFO is not None: for i in range(6): self.ui.chatsWidget.item(row,i).setBackground(QtCore.Qt.yellow) row = row + 1 self.ui.chatsWidget.resizeColumnsToContents() self.ui.chatsWidget.resizeRowsToContents() def formatDate(self, mactime): # if timestamp is not like "304966548", but like "306350664.792749", # then just use the numbers in front of the "." mactime = str(mactime) if mactime.find(".") > -1: mactime = mactime[:mactime.find(".")] date_time = datetime.fromtimestamp(int(mactime)+11323*60*1440) return date_time ######################################################################## # DB QUERIES # ######################################################################## ''' Contacts information are stored into Contacts.sqlite (newer version of WhatsApp for iOS) or into ChatStorage.sqlite. ''' def getContacts(self): try: try: # opening database (1st attempt: Contacts.sqlite) << New Version self.tempdb = sqlite3.connect(self.fname_contacts) has_contacts_sqlite = True except: # opening database (2nd attempt: ChatStorage.sqlite) << Old Version self.tempdb = sqlite3.connect(self.fname_chatstorage) has_contacts_sqlite = False except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() self.tempdb.row_factory = sqlite3.Row self.tempcur = self.tempdb.cursor() contactsToReturn = [] if has_contacts_sqlite: # reading contacts from database # 1st step: ZWAPHONE table query = "SELECT * FROM ZWAPHONE" self.tempcur.execute(query) contacts = self.tempcur.fetchall() readCount = 0 for contact in contacts: id = contact['Z_PK'] contact_key = contact['ZCONTACT'] favorite_key = contact['ZFAVORITE'] status_key = contact['ZSTATUS'] phonenum = contact['ZPHONE'] # 2nd step: name from ZWACONTACT table query = "SELECT * FROM ZWACONTACT WHERE Z_PK=?;" self.tempcur.execute(query, [contact_key]) contact_entry = self.tempcur.fetchone() if contact_entry == None: name = "N/A" else: name = contact_entry['ZFULLNAME'] # 3rd step: status from ZWASTATUS table query = "SELECT * FROM ZWASTATUS WHERE Z_PK=?;" self.tempcur.execute(query, [status_key]) status_entry = self.tempcur.fetchone() if status_entry == None: text = "N/A" date = "N/A" else: text = status_entry['ZTEXT'] date = self.formatDate(status_entry['ZDATE']) contactsToReturn.append([id, name, phonenum, text, date]) else: # has_contacts_sqlite == False # reading contacts from database # 1st step: ZWAPHONE table query = "SELECT * FROM ZWAFAVORITE" self.tempcur.execute(query) contacts = self.tempcur.fetchall() for contact in contacts: id = contact['Z_PK'] status_key = contact['ZSTATUS'] name = contact['ZDISPLAYNAME'] phonenum = contact['ZPHONENUMBER'] # 2nd step: status from ZWASTATUS table query = "SELECT * FROM ZWASTATUS WHERE Z_PK=?;" self.tempcur.execute(query, [status_key]) status_entry = self.tempcur.fetchone() if status_entry == None: text = "N/A" date = "N/A" else: text = status_entry['ZSTATUSTEXT'] date = self.formatDate(status_entry['ZSTATUSDATE']) contactsToReturn.append([id, name, phonenum, text, date]) # closing database self.tempdb.close() return contactsToReturn ''' Chats information are stored into ChatStorage.sqlite. ''' def getChats(self): # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZWACHATSESSION table query = "SELECT * FROM ZWACHATSESSION" self.tempcur.execute(query) # fetches chats namedtuple chats = self.tempcur.fetchall() # closing database self.tempdb.close() # a namedtuple is returned return chats ''' Messages are stored into ChatStorage.sqlite. ''' def getMsgs(self, zchatsession): # refresh the window #(the selected row is highligthed and the chats table appears disabled) QtGui.QApplication.processEvents() # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZWAMESSAGE table query = "SELECT * FROM ZWAMESSAGE WHERE ZCHATSESSION=? ORDER BY ZMESSAGEDATE ASC;" self.tempcur.execute(query, [zchatsession]) messages = self.tempcur.fetchall() # closing database self.tempdb.close() # a namedtuple is returned return messages ''' Messages are stored into ChatStorage.sqlite. ''' def getMsgsThreaded(self, zchatsession): # progress window progress = QtGui.QProgressDialog("Querying the database ...", "Abort", 0, 0, self) progress.setWindowTitle("WhatsApp Browser ...") progress.setWindowModality(QtCore.Qt.WindowModal) progress.setMinimumDuration(0) progress.setCancelButton(None) progress.show() # ZWAMESSAGE table query = "SELECT * FROM ZWAMESSAGE WHERE ZCHATSESSION=? ORDER BY ZMESSAGEDATE ASC;" # call a thread to query the db showing a progress bar queryTh = ThreadedQuery(self.fname_chatstorage,query,[zchatsession]) queryTh.start() while queryTh.isAlive(): QtGui.QApplication.processEvents() progress.close() messages = queryTh.getResult() # a namedtuple is returned return messages ''' GroupMember info are stored into ChatStorage.sqlite. ''' def getGroupInfo(self, zpk): # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZWAGROUPMEMBER table query = "SELECT * FROM ZWAGROUPMEMBER WHERE Z_PK=?;" self.tempcur.execute(query, [zpk]) groupmember = self.tempcur.fetchone() # closing database self.tempdb.close() # a namedtuple is returned return groupmember ''' MediaItem info are stored into ChatStorage.sqlite. ''' def getMediaItem(self, zpk): # opens database (ChatStorage.sqlite) try: self.tempdb = sqlite3.connect(self.fname_chatstorage) except: print("\nUnexpected error: %s"%sys.exc_info()[1]) self.close() # query results are retrieved as a namedtuple # (this step must be before cursor instantiation) self.tempdb.row_factory = namedtuple_factory self.tempcur = self.tempdb.cursor() # ZMEDIAITEM table query = "SELECT * FROM ZWAMEDIAITEM WHERE Z_PK=?;" self.tempcur.execute(query, [zpk]) media = self.tempcur.fetchone() # closing database self.tempdb.close() # a namedtuple is returned return media ######################################################################## # SLOTS # ######################################################################## def onChatsClick(self): # disable chats table (to disable click events while processing) self.ui.chatsWidget.setEnabled(False) # retrieving selected row self.ui.chatsWidget.setCurrentCell(self.ui.chatsWidget.currentRow(),0) currentSelectedItem = self.ui.chatsWidget.currentItem() if (currentSelectedItem): pass else: return ###################################################### # MESSAGES SECTION # ###################################################### zpk = int(currentSelectedItem.text()) #msgs = self.getMsgs(zpk) # <--- msgs = self.getMsgsThreaded(zpk) # <--- # re-select a visible column to allow the keyboard selection self.ui.chatsWidget.setCurrentCell(self.ui.chatsWidget.currentRow(),1) # erase previous messages and set new table lenght #self.ui.msgsWidget.clearContents() self.ui.msgsWidget.setSortingEnabled(False) self.ui.msgsWidget.setRowCount(len(msgs)) row = 0 for msg in msgs: if hasattr(msg, 'Z_PK'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,msg.Z_PK) self.ui.msgsWidget.setItem(row, 0, newItem) if hasattr(msg, 'ZFROMJID'): fromstring = "Me" if msg.ZFROMJID is not None: fromstring = msg.ZFROMJID newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,fromstring) self.ui.msgsWidget.setItem(row, 1, newItem) if hasattr(msg, 'ZMESSAGEDATE'): newItem = QtGui.QTableWidgetItem(str(self.formatDate(msg.ZMESSAGEDATE))) self.ui.msgsWidget.setItem(row, 2, newItem) if hasattr(msg, 'ZTEXT'): newItem = QtGui.QTableWidgetItem(msg.ZTEXT) self.ui.msgsWidget.setItem(row, 3, newItem) if hasattr(msg, 'ZMESSAGESTATUS'): newItem = QtGui.QTableWidgetItem() newItem.setData(QtCore.Qt.DisplayRole,msg.ZMESSAGESTATUS) self.ui.msgsWidget.setItem(row, 5, newItem) if hasattr(msg, 'ZGROUPMEMBER'): if msg.ZGROUPMEMBER is not None: gmember = self.getGroupInfo(msg.ZGROUPMEMBER) fromstring = "" if gmember is not None: fromstring = gmember.ZCONTACTNAME + " - " + gmember.ZMEMBERJID else: fromstring = "N/A" newItem = QtGui.QTableWidgetItem(fromstring) self.ui.msgsWidget.setItem(row, 1, newItem) if hasattr(msg, 'ZMEDIAITEM'): mediaItem = QtGui.QTableWidgetItem("") if msg.ZMEDIAITEM is not None: media = self.getMediaItem(msg.ZMEDIAITEM) msgcontent = "" # VCARD info if (media.ZVCARDNAME and media.ZVCARDSTRING) is not None: msgcontent += ("VCARD\n" + media.ZVCARDNAME + "\n" + media.ZVCARDSTRING + "\n") # GPS info if media.ZLATITUDE != 0. or media.ZLONGITUDE != 0.: msgcontent += ("GPS\n" + "lat: " + str(media.ZLATITUDE) + "\nlong: " + str(media.ZLONGITUDE) + "\n") # VIDEO info if media.ZMOVIEDURATION != 0: msgcontent += ("VIDEO\n" + "duration: " + str(media.ZMOVIEDURATION) + " sec\n") # FILE info if media.ZFILESIZE != 0: msgcontent += ("FILE\n" + "size: " + str(media.ZFILESIZE) + " B\n") # set message content (3rd column) newItem = QtGui.QTableWidgetItem(msgcontent) self.ui.msgsWidget.setItem(row, 3, newItem) thumbRealFilename = "" mediaRealFilename = "" mediallocalfile = "" # THUMBNAIL if media.ZTHUMBNAILLOCALPATH is not None: thumblocalfilepath = media.ZTHUMBNAILLOCALPATH thumblocalpath = os.path.dirname(thumblocalfilepath) thumbllocalfile = os.path.basename(thumblocalfilepath) thumbRealFilename = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename=thumbllocalfile, path='Library/'+thumblocalpath, domaintype="AppDomain")) # ATTACHMENT if media.ZMEDIALOCALPATH is not None: medialocalfilepath = media.ZMEDIALOCALPATH mediallocalpath = os.path.dirname(medialocalfilepath) mediallocalfile = os.path.basename(medialocalfilepath) mediaRealFilename = os.path.join(self.backup_path, plugins_utils.realFileName(self.cursor, filename=mediallocalfile, path='Library/'+mediallocalpath, domaintype="AppDomain")) # add a thumnail to the table view icon = None if thumbRealFilename != "": icon = QtGui.QIcon(thumbRealFilename) else: icon = QtGui.QIcon(mediaRealFilename) mediaItem = QtGui.QTableWidgetItem() mediaItem.setIcon(icon) # add info for attachment export (ctx menu) if mediallocalfile != "": mediaItem.setData(QtCore.Qt.UserRole, mediaRealFilename) mediaItem.setData(QtCore.Qt.UserRole+1, mediallocalfile) if media.ZLATITUDE != 0. or media.ZLONGITUDE != 0.: mediaItem.setData(QtCore.Qt.UserRole+2, media.ZLATITUDE) mediaItem.setData(QtCore.Qt.UserRole+3, media.ZLONGITUDE) self.ui.msgsWidget.setItem(row, 4, mediaItem) if hasattr(msg, 'ZISFROMME'): if msg.ZISFROMME is 1: for i in range(6): self.ui.msgsWidget.item(row,i).setBackground(QtCore.Qt.green) row = row + 1 self.ui.msgsWidget.setSortingEnabled(True) self.ui.msgsWidget.setIconSize(QtCore.QSize(150,150)) self.ui.msgsWidget.resizeColumnsToContents() self.ui.msgsWidget.setColumnWidth(4, 150) self.ui.msgsWidget.resizeRowsToContents() # re-enable chats table self.ui.chatsWidget.setEnabled(True) self.ui.chatsWidget.setFocus() ###################################################### # CTX MENU SECTION # ###################################################### def ctxMenuMsgs(self, pos): cell = self.ui.msgsWidget.itemAt(pos) self.link = cell.data(QtCore.Qt.UserRole) self.name = cell.data(QtCore.Qt.UserRole + 1) self.lat = cell.data(QtCore.Qt.UserRole + 2) self.long = cell.data(QtCore.Qt.UserRole + 3) menu = QtGui.QMenu() action1 = QtGui.QAction("Export table CSV", self) action1.triggered.connect(self.exportCSVmsgs) menu.addAction(action1) if self.link != None: menu.addSeparator() action1 = QtGui.QAction("Open attachment in standard viewer", self) action1.triggered.connect(self.openWithViewer) menu.addAction(action1) action1 = QtGui.QAction("Export attachment", self) action1.triggered.connect(self.exportSelectedFile) menu.addAction(action1) if (self.lat and self.long) is not None: menu.addSeparator() action1 = QtGui.QAction("Show GPS coordinates on Google Maps", self) action1.triggered.connect(self.openGPSBrowser) menu.addAction(action1) menu.exec_(self.ui.msgsWidget.mapToGlobal(pos)); def ctxMenuContacts(self, pos): menu = QtGui.QMenu() action1 = QtGui.QAction("Export table CSV", self) action1.triggered.connect(self.exportCSVcontacts) menu.addAction(action1) menu.exec_(self.ui.contactsWidget.mapToGlobal(pos)); def ctxMenuChats(self, pos): menu = QtGui.QMenu() action1 = QtGui.QAction("Export table CSV", self) action1.triggered.connect(self.exportCSVchats) menu.addAction(action1) menu.exec_(self.ui.chatsWidget.mapToGlobal(pos)); ##### ATTACHMENTS EXPORT FUNCTIONS ##### def openWithViewer(self): if sys.platform.startswith('linux'): subprocess.call(["xdg-open", self.link]) else: os.startfile(self.link) def exportSelectedFile(self): filename = QtGui.QFileDialog.getSaveFileName(self, "Export attachment", self.name) filename = filename[0] if (len(filename) == 0): return try: shutil.copy(self.link, filename) QtGui.QMessageBox.about(self, "Confirm", "Attachment saved as %s."%filename) except: QtGui.QMessageBox.about(self, "Error", "Error while saving attachment") def openGPSBrowser(self): coordinatesURL = "https://maps.google.com/?q=" + str(self.lat) + "," + str(self.long) webbrowser.open(coordinatesURL) ##### TABLES EXPORT FUNCTIONS ##### def exportCSVcontacts(self): self.exportCSVtable(self.ui.contactsWidget) def exportCSVchats(self): self.exportCSVtable(self.ui.chatsWidget) def exportCSVmsgs(self): self.exportCSVtable(self.ui.msgsWidget) def exportCSVtable(self, table): filename = QtGui.QFileDialog.getSaveFileName(self, "Export table", "table", ".csv") filename = filename[0] if (len(filename) == 0): return f = open(filename, 'w') # header tablerow='"' for c in range(table.columnCount()): hitem = table.horizontalHeaderItem(c) if hitem is not None: tablerow += unicode(hitem.text()).encode('utf8') tablerow += '","' tablerow=tablerow[:-2]+"\n" f.write(tablerow) # content tablerow='"' for r in range(table.rowCount()): for c in range(table.columnCount()): item = table.item(r,c) if item is not None: tablerow += unicode(item.text().replace('\n',' ')).encode('utf8') tablerow += '","' tablerow = tablerow[:-2] + "\n" f.write(tablerow) tablerow='"' f.close()