diff --git a/README.md b/README.md index ba9d008..ab90e17 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Whatsapp Parser Toolset ==== -Updated: July 2021 +Updated: May 2022 WhatsApp Messenger Version 2.21.9.14 @@ -18,15 +18,15 @@ Whapa toolset is divided in five tools: **Android** ====== -* **Whapa** (Whatsapp Parser) -* **Whacipher** (Whatsapp Encryption/Decryption) *** NEW Crypt14 *** +* **Whapa** (Whatsapp Parser)(Only working with old database, Working in Progress...) +* **Whacipher** (Whatsapp Encryption/Decryption) *** Not support Crypt15 *** * **Whagodri** (Whataspp Google Drive Extractor) -* **Whamerge** (Whatsapp Merger) +* **Whamerge** (Whatsapp Merger) (Only working with old database, Working in Progress...) * **Whachat** (Whatsapp Chat Exporter) **IPhone** ==== -* **Whacloud** (Whatsapp ICloud Extractor) NEW BETA TOOL FOR IPHONE +* **Whacloud** (Whatsapp ICloud Extractor) (Not working) * **Whachat** (Whatsapp Chat Exporter) @@ -153,11 +153,7 @@ Edit only the values of the./cfg/settings.cfg file # You can specify a list of celnumbr = BackupNumber1, BackupNumber2, ... celnumbr = -* If you request it, log in to your browser and then click here. https://accounts.google.com/b/0/DisplayUnlockCaptcha - - -If you want to use 2FA (Two Factor Authentication), you will have to go to the URL: https://myaccount.google.com/apppasswords Then select Application: Other. Write down: Whapa, and a password will be display, then you must write the password in your settings.cfg. -(Thanks to YuriCosta) +* New Method: Now you will have to login in the browser, this method is not valid for accounts without a phone number or alternative email associated to the account. WHACLOUD ===== diff --git a/cfg/settings.cfg b/cfg/settings.cfg index 8f80c27..45f6f0b 100644 --- a/cfg/settings.cfg +++ b/cfg/settings.cfg @@ -1,19 +1,21 @@ [report] -company = -record = -unit = -examiner = -notes = +company = "" +record = "" +unit = "" +examiner = "" +notes = "" [google-auth] gmail = alias@gmail.com # Optional. The account password or app password when using 2FA. -password = +password = yourpassword +# Optional. Login using the oauth cookie. +oauth = "" # Optional. The result of "adb shell settings get secure android_id". -android_id = 0000000000000000 +android_id = 0000000000000000 # Optional. Enter the backup country code + phonenumber be synchronized, otherwise it synchronizes all backups. # You can specify a list of celnumbr = BackupNumber1, BackupNumber2, ... -celnumbr = +celnumbr = "" [icloud-auth] icloud = alias@icloud.com diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index dde1eba..88088a5 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -2,6 +2,16 @@ Changelog ==== All notable changes to this project will be documented in this file. +May 2022 + + [+] whapa-gui.py v1.58 + [+] whacipher.py + [-] Fixed Decrypt crypt14 files. + [+] whagodri.py + [-] Fixed bug connecting with Google. + [-] Added No parallel downloads + [-] Added support for jpeg files with option "-si" + Nov 2021 [+] whapa-gui.py v1.56 @@ -11,7 +21,7 @@ Agu 2021 [+] whapa-gui.py v1.55 [+] whapa.py - [-] Fixed bug with setting file + [-] Fixed bug with settings file [+] whachat.py [-] New time formats diff --git a/doc/requirements.txt b/doc/requirements.txt index 7ff2928..682ba49 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,11 @@ -pycryptodome +requests colorama -configparser gpsoauth -requests==2.21.0 pyicloud pandas numpy +selenium +webdriver-manager +selenium-stealth +ConfigObj +pycryptodome \ No newline at end of file diff --git a/libs/chromedriver.exe b/libs/chromedriver.exe new file mode 100644 index 0000000..bab4b5f Binary files /dev/null and b/libs/chromedriver.exe differ diff --git a/libs/chromedriverLinux b/libs/chromedriverLinux new file mode 100644 index 0000000..f97e9a5 Binary files /dev/null and b/libs/chromedriverLinux differ diff --git a/libs/chromedriverM1 b/libs/chromedriverM1 new file mode 100644 index 0000000..bcc6da6 Binary files /dev/null and b/libs/chromedriverM1 differ diff --git a/libs/chromedriverMac b/libs/chromedriverMac new file mode 100644 index 0000000..34b252f Binary files /dev/null and b/libs/chromedriverMac differ diff --git a/libs/gpsoauth/__init__.py b/libs/gpsoauth/__init__.py new file mode 100644 index 0000000..388e750 --- /dev/null +++ b/libs/gpsoauth/__init__.py @@ -0,0 +1,238 @@ +"""A python client library for Google Play Services OAuth.""" +from __future__ import annotations + +from collections.abc import MutableMapping +from importlib.metadata import version +import ssl +from typing import Any, Iterable + +import requests +from urllib3.poolmanager import PoolManager # type: ignore + +from . import google + +__version__ = version(__package__) + +# The key is distirbuted with Google Play Services. +# This one is from version 7.3.29. +B64_KEY_7_3_29 = ( + b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" + b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" + b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" + b"6rmf5AAAAAwEAAQ==" +) + +ANDROID_KEY_7_3_29 = google.key_from_b64(B64_KEY_7_3_29) + +AUTH_URL = "https://android.clients.google.com/auth" +USER_AGENT = "gpsoauth/" + __version__ + +# Google is very picky about list of used ciphers. Changing this list most likely +# will cause BadAuthentication error. +CIPHERS = [ + "ECDHE+AESGCM", + "ECDHE+CHACHA20", + "DHE+AESGCM", + "DHE+CHACHA20", + "ECDH+AES", + "DH+AES", + "RSA+AESGCM", + "RSA+AES", + "!aNULL", + "!eNULL", + "!MD5", + "!DSS", +] + + +class SSLContext(ssl.SSLContext): + """SSLContext wrapper.""" + + def set_alpn_protocols(self, alpn_protocols: Iterable[str]) -> None: + """ + ALPN headers cause Google to return 403 Bad Authentication. + """ + + +class AuthHTTPAdapter(requests.adapters.HTTPAdapter): + """HTTPAdapter wrapper.""" + + def init_poolmanager(self, *args: Any, **kwargs: Any) -> None: + """ + Secure settings from ssl.create_default_context(), but without + ssl.OP_NO_TICKET which causes Google to return 403 Bad + Authentication. + """ + context = SSLContext() + context.set_ciphers(":".join(CIPHERS)) + context.options |= ssl.OP_NO_COMPRESSION + context.options |= ssl.OP_NO_SSLv2 + context.options |= ssl.OP_NO_SSLv3 + context.post_handshake_auth = True + context.verify_mode = ssl.CERT_REQUIRED + self.poolmanager = PoolManager(*args, ssl_context=context, **kwargs) + + +def _perform_auth_request( + data: dict[str, int | str | bytes], proxies: MutableMapping[str, str] | None = None +) -> dict[str, str]: + session = requests.session() + session.mount(AUTH_URL, AuthHTTPAdapter()) + if proxies: + session.proxies = proxies + + res = session.post(AUTH_URL, data, headers={"User-Agent": USER_AGENT}) + return google.parse_auth_response(res.text) + + +def perform_master_login( + email: str, + password: str, + android_id: str, + service: str = "ac2dm", + device_country: str = "us", + operator_country: str = "us", + lang: str = "en", + sdk_version: int = 17, + proxy: MutableMapping[str, str] | None = None, +) -> dict[str, str]: + """ + Perform a master login, which is what Android does when you first add + a Google account. + + Return a dict, eg:: + + { + 'Auth': '...', + 'Email': 'email@gmail.com', + 'GooglePlusUpgrade': '1', + 'LSID': '...', + 'PicasaUser': 'My Name', + 'RopRevision': '1', + 'RopText': ' ', + 'SID': '...', + 'Token': 'oauth2rt_1/...', + 'firstName': 'My', + 'lastName': 'Name', + 'services': 'hist,mail,googleme,...' + } + """ + + data: dict[str, int | str | bytes] = { + "accountType": "HOSTED_OR_GOOGLE", + "Email": email, + "has_permission": 1, + "add_account": 1, + "EncryptedPasswd": google.construct_signature( + email, password, ANDROID_KEY_7_3_29 + ), + "service": service, + "source": "android", + "androidId": android_id, + "device_country": device_country, + "operatorCountry": operator_country, + "lang": lang, + "sdk_version": sdk_version, + } + + return _perform_auth_request(data, proxy) + + +def perform_master_login_oauth( + email: str, + oauth_token: str, + android_id: str, + service: str = "ac2dm", + device_country: str = "us", + operator_country: str = "us", + lang: str = "en", + sdk_version: int = 28, + proxy: MutableMapping[str, str] | None = None, +) -> dict[str, str]: + """ + Perform a master login, which is what Android does when you first add + a Google account. + + Return a dict, eg:: + + { + 'Auth': '...', + 'Email': 'email@gmail.com', + 'GooglePlusUpgrade': '1', + 'LSID': '...', + 'PicasaUser': 'My Name', + 'RopRevision': '1', + 'RopText': ' ', + 'SID': '...', + 'Token': 'oauth2rt_1/...', + 'firstName': 'My', + 'lastName': 'Name', + 'services': 'hist,mail,googleme,...' + } + """ + + data: dict[str, int | str | bytes] = { + "lang": lang, + "google_play_services_version": 19629032, + "sdk_version": sdk_version, + "device_country": device_country, + "Email": email, + "service": service, + "get_accountid": 1, + "ACCESS_TOKEN": 1, + "callerPkg": "com.google.android.gms", + "add_account": 1, + "Token": oauth_token, + "callerSig": "38918a453d07199354f8b19af05ec6562ced5788", + } + + return _perform_auth_request(data, proxy) + + +def perform_oauth( + email: str, + master_token: str, + android_id: str, + service: str, + app: str, + client_sig: str, + device_country: str = "us", + operator_country: str = "us", + lang: str = "en", + sdk_version: int = 17, + proxy: MutableMapping[str, str] | None = None, +) -> dict[str, str]: + """ + Use a master token from master_login to perform OAuth to a specific Google service. + + Return a dict, eg:: + + { + 'Auth': '...', + 'LSID': '...', + 'SID': '..', + 'issueAdvice': 'auto', + 'services': 'hist,mail,googleme,...' + } + + To authenticate requests to this service, include a header + ``Authorization: GoogleLogin auth=res['Auth']``. + """ + + data: dict[str, int | str | bytes] = { + "accountType": "HOSTED_OR_GOOGLE", + "Email": email, + "has_permission": 1, + "EncryptedPasswd": master_token, + "service": service, + "source": "android", + "androidId": android_id, + "app": app, + "client_sig": client_sig, + "device_country": device_country, + "operatorCountry": operator_country, + "lang": lang, + "sdk_version": sdk_version, + } + + return _perform_auth_request(data, proxy) diff --git a/libs/gpsoauth/google.py b/libs/gpsoauth/google.py new file mode 100644 index 0000000..e4c4e8d --- /dev/null +++ b/libs/gpsoauth/google.py @@ -0,0 +1,62 @@ +"""Functions to work with Google authentication structures.""" +from __future__ import annotations + +import base64 +import hashlib + +from Cryptodome.Cipher import PKCS1_OAEP +from Cryptodome.PublicKey import RSA +from Cryptodome.PublicKey.RSA import RsaKey + +from .util import bytes_to_int, int_to_bytes + + +def key_from_b64(b64_key: bytes) -> RsaKey: + """Extract key from base64.""" + binary_key = base64.b64decode(b64_key) + + i = bytes_to_int(binary_key[:4]) + mod = bytes_to_int(binary_key[4 : 4 + i]) + + j = bytes_to_int(binary_key[i + 4 : i + 4 + 4]) + exponent = bytes_to_int(binary_key[i + 8 : i + 8 + j]) + + key = RSA.construct((mod, exponent)) + + return key + + +def key_to_struct(key: RsaKey) -> bytes: + """Convert key to struct.""" + mod = int_to_bytes(key.n) + exponent = int_to_bytes(key.e) + + return b"\x00\x00\x00\x80" + mod + b"\x00\x00\x00\x03" + exponent + + +def parse_auth_response(text: str) -> dict[str, str]: + """Parse received auth response.""" + response_data = {} + for line in text.split("\n"): + if not line: + continue + + key, _, val = line.partition("=") + response_data[key] = val + + return response_data + + +def construct_signature(email: str, password: str, key: RsaKey) -> bytes: + """Construct signature.""" + signature = bytearray(b"\x00") + + struct = key_to_struct(key) + signature.extend(hashlib.sha1(struct).digest()[:4]) + + cipher = PKCS1_OAEP.new(key) + encrypted_login = cipher.encrypt((email + "\x00" + password).encode("utf-8")) + + signature.extend(encrypted_login) + + return base64.urlsafe_b64encode(signature) diff --git a/libs/gpsoauth/util.py b/libs/gpsoauth/util.py new file mode 100644 index 0000000..776207c --- /dev/null +++ b/libs/gpsoauth/util.py @@ -0,0 +1,31 @@ +"""Utility functions.""" +import binascii + + +def bytes_to_int(bytes_seq: bytes) -> int: + """Convert bytes to int.""" + return int.from_bytes(bytes_seq, "big") + + +def int_to_bytes(num: int, pad_multiple: int = 1) -> bytes: + """Packs the num into a byte string 0 padded to a multiple of pad_multiple + bytes in size. 0 means no padding whatsoever, so that packing 0 result + in an empty string. The resulting byte string is the big-endian two's + complement representation of the passed in long.""" + + # source: http://stackoverflow.com/a/14527004/1231454 + + if num == 0: + return b"\0" * pad_multiple + if num < 0: + raise ValueError("Can only convert non-negative numbers.") + value = hex(num)[2:] + value = value.rstrip("L") + if len(value) & 1: + value = "0" + value + result = binascii.unhexlify(value) + if pad_multiple not in [0, 1]: + filled_so_far = len(result) % pad_multiple + if filled_so_far != 0: + result = b"\0" * (pad_multiple - filled_so_far) + result + return result diff --git a/libs/whacipher.py b/libs/whacipher.py index 3529696..d7a8244 100644 --- a/libs/whacipher.py +++ b/libs/whacipher.py @@ -63,9 +63,10 @@ def encrypt12(db_file, key_file, db_cript, output): print("[e] An error has ocurred encrypting '" + db_file + "' - ", e) -def decrypt14(db_file, key_file, path): +def decrypt14(db_file, key_file, path, offset): """ Function decrypt Crypt14 Database """ try: + print("Trying offset {}".format(offset)) if os.path.getsize(key_file) != 158: quit('[e] The specified input key file is invalid.') @@ -76,15 +77,17 @@ def decrypt14(db_file, key_file, path): with open(db_file, "rb") as fh: db_data = fh.read() - data = db_data[191:] + data = db_data[offset:] #191 iv = db_data[67:83] aes = AES.new(key, mode=AES.MODE_GCM, nonce=iv) with open(path, "wb") as fh: fh.write(zlib.decompress(aes.decrypt(data))) print("[-] " + db_file + " decrypted, '" + path + "' created") + return True except Exception as e: print("[e] An error has ocurred decrypting '" + db_file + "' - ", e) + return False def decrypt12(db_file, key_file, path): @@ -144,8 +147,9 @@ def decrypt12(db_file, key_file, path): decrypt12(args.file, args.decrypt, args.output) elif ".crypt14" == os.path.splitext(args.file)[1]: - decrypt14(args.file, args.decrypt, args.output) - + for offset in range(185, 195): + if decrypt14(args.file, args.decrypt, args.output, offset): + break else: print("[e] " + args.decrypt + " doesn't exist") else: @@ -160,7 +164,9 @@ def decrypt12(db_file, key_file, path): for crypt_file in files: if ".crypt14" == os.path.splitext(crypt_file)[1]: output = args.output + os.path.splitext(crypt_file)[0] - decrypt14(dir + crypt_file, args.decrypt, output) + for offset in range(185, 195): + if decrypt14(dir + crypt_file, args.decrypt, output, offset): + break elif ".crypt12" == os.path.splitext(crypt_file)[1]: output = args.output + os.path.splitext(crypt_file)[0] diff --git a/libs/whacloud.py b/libs/whacloud.py index 114ed78..988002e 100644 --- a/libs/whacloud.py +++ b/libs/whacloud.py @@ -59,21 +59,23 @@ def createSettingsFile(): with open(cfg_file, 'w') as cfg: cfg.write(dedent(""" [report] - company = - record = - unit = - examiner = - notes = + company = "" + record = "" + unit = "" + examiner = "" + notes = "" [google-auth] gmail = alias@gmail.com # Optional. The account password or app password when using 2FA. - password = + password = yourpassword + # Optional. Login using the oauth cookie. + oauth = "" # Optional. The result of "adb shell settings get secure android_id". - android_id = 0000000000000000 + android_id = 0000000000000000 # Optional. Enter the backup country code + phonenumber be synchronized, otherwise it synchronizes all backups. # You can specify a list of celnumbr = BackupNumber1, BackupNumber2, ... - celnumbr = + celnumbr = "" [icloud-auth] icloud = alias@icloud.com @@ -165,13 +167,35 @@ def getMultipleFilesThread(photo, local, now, lenfiles, threadName): def login(): """ Get access to Icloud """ + global click api = PyiCloudService(icloud, passw) - if api.requires_2sa: + if api.requires_2fa: + print("Two-factor authentication required.") + code = input("Enter the code you received of one of your approved devices: ") + result = api.validate_2fa_code(code) + print("Code validation result: %s" % result) + + if not result: + print("Failed to verify security code") + sys.exit(1) + + if not api.is_trusted_session: + print("Session is not trusted. Requesting trust...") + result = api.trust_session() + print("Session trust result %s" % result) + + if not result: + print("Failed to request trust. You will likely be prompted for the code again in the coming weeks") + elif api.requires_2sa: + import click print("Two-step authentication required. Your trusted devices are:") devices = api.trusted_devices for i, device in enumerate(devices): - print(" %s: %s" % (i, device.get('deviceName', "SMS to %s" % device.get('phoneNumber')))) + print( + " %s: %s" % (i, device.get('deviceName', + "SMS to %s" % device.get('phoneNumber'))) + ) device = click.prompt('Which device would you like to use?', default=0) device = devices[device] @@ -237,9 +261,14 @@ def getConfigs(): getMultipleFiles(api, files) elif args.list: + print(api.photos.all) + api.files.params['dsid'] = api.data['dsInfo']['dsid'] + print(api.files.dir()) + + """ for photo in api.photos.albums['WhatsApp']: print(photo, photo.filename) - + """ elif args.s_images: for entries in api.photos.albums['WhatsApp']: file_name, extension = splitext(entries.filename) diff --git a/libs/whagodri.py b/libs/whagodri.py index f6be5e8..fda9ef2 100644 --- a/libs/whagodri.py +++ b/libs/whagodri.py @@ -8,12 +8,17 @@ import queue import threading import time - -from configparser import ConfigParser +import subprocess +from configobj import ConfigObj from getpass import getpass from textwrap import dedent - from requests import Response +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from webdriver_manager.chrome import ChromeDriverManager +from selenium_stealth import stealth + total_size: int num_files: int @@ -32,34 +37,93 @@ class WaBackup: Provide access to WhatsApp backups stored in Google drive. """ - def __init__(self, gmail, password, android_id, celnumbr): - print("Requesting Google access...") - token = gpsoauth.perform_master_login(gmail, password, android_id) - if "Token" not in token: - error(token) - print("Granted\n") - print("Requesting authentication for Google Drive...") + def __init__(self, gmail, password, android_id, celnumbr, oauth_token): + if not oauth_token: + print("Requesting access to Google by account and password...") + token = gpsoauth.perform_master_login(email=gmail, password=password, android_id=android_id) + if token.get("Url"): + url = token.get("Url") + options = Options() + options.add_argument("--window-size=720,720") + #os.environ['GH_TOKEN'] = "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" + try: + if operating_system() == "Windows": + driver = webdriver.Chrome(service=Service("chromedriver.exe"), options=options) + elif operating_system() == "MacOs M1": + driver = webdriver.Chrome(service=Service("chromedriverM1"), options=options) + elif operating_system() == "MacOs": + driver = webdriver.Chrome(service=Service("chromedriverMac"), options=options) + else: + driver = webdriver.Chrome(service=Service("chromedriver"), options=options) + except: + driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) + + stealth(driver, + languages=["en-US", "en"], + vendor="Google Inc.", + platform="Win32", + webgl_vendor="Intel Inc.", + renderer="Intel Iris OpenGL Engine", + fix_hairline=True, + ) + + driver.get(url) + for remaining in range(30, -1, -1): + sys.stdout.write("\r") + sys.stdout.write("{:2d} seconds remaining to login to your Google Account".format(remaining)) + sys.stdout.flush() + time.sleep(1) + + sys.stdout.write("\nFinished!\n") + cookies = driver.get_cookies() + for cookie in cookies: + if cookie.get("name") == 'oauth_token': + oauth_token = cookie.get("value") + print("A valid token has been obtained.") + break + + driver.close() + if not oauth_token: + print("No valid token has been obtained.") + exit() + + print("Requesting access to Google by OAuth cookie...") + token = gpsoauth.perform_master_login_oauth(email=gmail, oauth_token=oauth_token, android_id=android_id) + if "Token" not in token: + error(token) + else: + print("Granted.") + print("Writing Token in your settings.cfg file...") + cfg_file = r'{}/cfg/settings.cfg'.format(whapa_path).replace("/", os.path.sep) + config = ConfigObj(cfg_file, interpolation=None) + config['google-auth']['oauth'] = token['Token'] + config.write() + oauth_token = token['Token'] + else: + if "Token" not in token: + error(token) - self.auth = gpsoauth.perform_oauth( + print("Requesting authentication for Google Drive...") + auth = gpsoauth.perform_oauth( gmail, - token["Token"], + oauth_token, android_id, "oauth2:https://www.googleapis.com/auth/drive.appdata", "com.whatsapp", "38a0f7d505fe18fec64fbf343ecaaaf310dbd799", ) - if "Auth" not in self.auth: - error(token) - print("Granted\n") + if "Auth" not in auth: + error(auth) + print("Granted.\n") global Auth, phone - Auth = self.auth + Auth = auth phone = celnumbr def get(self, path, params=None, **kwargs): response = requests.get( "https://backup.googleapis.com/v1/{}".format(path), - headers={"Authorization": "Bearer {}".format(self.auth["Auth"])}, + headers={"Authorization": "Bearer {}".format(Auth["Auth"])}, params=params, **kwargs, ) @@ -113,25 +177,27 @@ def help(): def createSettingsFile(): """ Function that creates the settings file """ - cfg_file = system_slash(r'{}/cfg/settings.cfg'.format(whapa_path)) + cfg_file = r'{}/cfg/settings.cfg'.format(whapa_path).replace("/", os.path.sep) with open(cfg_file, 'w') as cfg: cfg.write(dedent(""" [report] - company = - record = - unit = - examiner = - notes = + company = "" + record = "" + unit = "" + examiner = "" + notes = "" [google-auth] gmail = alias@gmail.com # Optional. The account password or app password when using 2FA. - password = + password = yourpassword + # Optional. Login using the oauth cookie. + oauth = "" # Optional. The result of "adb shell settings get secure android_id". - android_id = 0000000000000000 + android_id = 0000000000000000 # Optional. Enter the backup country code + phonenumber be synchronized, otherwise it synchronizes all backups. # You can specify a list of celnumbr = BackupNumber1, BackupNumber2, ... - celnumbr = + celnumbr = "" [icloud-auth] icloud = alias@icloud.com @@ -140,29 +206,30 @@ def createSettingsFile(): def getConfigs(): - config = ConfigParser(interpolation=None) - cfg_file = system_slash(r'{}/cfg/settings.cfg'.format(whapa_path)) - + cfg_file = r'{}/cfg/settings.cfg'.format(whapa_path).replace("/", os.path.sep) + config = ConfigObj(cfg_file, interpolation=None) try: - config.read(cfg_file) - gmail = config.get('google-auth', 'gmail') - password = config.get('google-auth', 'password', fallback="") - celnumbr = config.get('google-auth', 'celnumbr').lstrip('+0') + gmail = config['google-auth']['gmail'] + password = config['google-auth']['password'] + celnumbr = config['google-auth']['celnumbr'].lstrip('+0') + oauth_token = config['google-auth']['oauth'] + android_id = config['google-auth']['android_id'] if not password: try: password = getpass("Enter your password for {}: ".format(gmail)) except KeyboardInterrupt: quit('\nCancelled!') - android_id = config.get("google-auth", "android_id") return { "gmail": gmail, "password": password, "android_id": android_id, "celnumbr": celnumbr, + "oauth_token": oauth_token, } - except(ConfigParser.NoSectionError, ConfigParser.NoOptionError): + except Exception as e: + print(e) quit('The "{}" file is missing or corrupt!'.format(cfg_file)) @@ -175,35 +242,34 @@ def human_size(size): def backup_info(backup): - print("[i] Backup name : {}".format(backup["name"])) - print("[-] Whatsapp version: {}".format(json.loads(backup["metadata"])["versionOfAppWhenBackup"])) try: - print("[-] Backup protected: {}".format(json.loads(backup["metadata"])["passwordProtectedBackupEnabled"])) - except: - pass - - print("[-] Backup upload : {}".format(backup["updateTime"])) - print("[-] Backup size : {} Bytes {}".format(backup["sizeBytes"], human_size(int(backup["sizeBytes"])))) - print("[+] Backup metadata") - print(" [-] Backup Frequency : {} ".format(json.loads(backup["metadata"])["backupFrequency"])) - print(" [-] Backup Network Settings : {} ".format(json.loads(backup["metadata"])["backupNetworkSettings"])) - print(" [-] Backup Version : {} ".format(json.loads(backup["metadata"])["backupVersion"])) - print(" [-] Include Videos In Backup : {} ".format(json.loads(backup["metadata"])["includeVideosInBackup"])) - print(" [-] Num Of Photos : {}".format(json.loads(backup["metadata"])["numOfPhotos"])) - print(" [-] Num Of Media Files : {}".format(json.loads(backup["metadata"])["numOfMediaFiles"])) - print(" [-] Num Of Messages : {}".format(json.loads(backup["metadata"])["numOfMessages"])) - print(" [-] Video Size : {} Bytes {}".format(json.loads(backup["metadata"])["videoSize"], - human_size(int( - json.loads(backup["metadata"])["videoSize"])))) - print(" [-] Backup Size : {} Bytes {}".format(json.loads(backup["metadata"])["backupSize"], - human_size(int( - json.loads(backup["metadata"])["backupSize"])))) - print(" [-] Media Size : {} Bytes {}".format(json.loads(backup["metadata"])["mediaSize"], - human_size(int( - json.loads(backup["metadata"])["mediaSize"])))) - print(" [-] Chat DB Size : {} Bytes {}".format(json.loads(backup["metadata"])["chatdbSize"], - human_size(int( - json.loads(backup["metadata"])["chatdbSize"])))) + print("[i] Backup name : {}".format(backup["name"])) + print("[-] Whatsapp version: {}".format(json.loads(backup["metadata"]).get("versionOfAppWhenBackup"))) + print("[-] Backup protected: {}".format(json.loads(backup["metadata"]).get("passwordProtectedBackupEnabled"))) + print("[-] Backup upload : {}".format(backup["updateTime"])) + print("[-] Backup size : {} Bytes {}".format(backup["sizeBytes"], human_size(int(backup["sizeBytes"])))) + print("[+] Backup metadata") + print(" [-] Backup Frequency : {} ".format(json.loads(backup["metadata"]).get("backupFrequency"))) + print(" [-] Backup Network Settings : {} ".format(json.loads(backup["metadata"]).get("backupNetworkSettings"))) + print(" [-] Backup Version : {} ".format(json.loads(backup["metadata"]).get("backupVersion"))) + print(" [-] Include Videos In Backup : {} ".format(json.loads(backup["metadata"]).get("includeVideosInBackup"))) + print(" [-] Num Of Photos : {}".format(json.loads(backup["metadata"]).get("numOfPhotos"))) + print(" [-] Num Of Media Files : {}".format(json.loads(backup["metadata"]).get("numOfMediaFiles"))) + print(" [-] Num Of Messages : {}".format(json.loads(backup["metadata"]).get("numOfMessages"))) + print(" [-] Video Size : {} Bytes {}".format(json.loads(backup["metadata"]).get("videoSize"), + human_size(int( + json.loads(backup["metadata"]).get("videoSize"))))) + print(" [-] Backup Size : {} Bytes {}".format(json.loads(backup["metadata"]).get("backupSize"), + human_size(int( + json.loads(backup["metadata"]).get("backupSize"))))) + print(" [-] Media Size : {} Bytes {}".format(json.loads(backup["metadata"]).get("mediaSize"), + human_size(int( + json.loads(backup["metadata"]).get("mediaSize"))))) + print(" [-] Chat DB Size : {} Bytes {}".format(json.loads(backup["metadata"]).get("chatdbSize"), + human_size(int( + json.loads(backup["metadata"]).get("chatdbSize"))))) + except Exception as e: + print(e) def error(token): @@ -215,7 +281,13 @@ def error(token): print( "1. Check that your email and password are correct in the settings file.\n" "2. Your are using a old python version. Works >= 3.8.\n" - "3. Update requirements, use in a terminal: 'pip3 install --upgrade -r ./doc/requirements.txt' or 'pip install --upgrade -r ./doc/requirements.txt") + "3. Update requirements, use in a terminal: 'pip3 install --upgrade -r ./doc/requirements.txt' or 'pip install --upgrade -r ./doc/requirements.txt\n" + "4. Your OAuth token configured in the settings file may have expired. The token will be deleted and you will have to log in again.") + + cfg_file = r'{}/cfg/settings.cfg'.format(whapa_path).replace("/", os.path.sep) + config = ConfigObj(cfg_file, interpolation=None) + config['google-auth']['oauth'] = "" + config.write() elif "NeedsBrowser" in failed: print("\n Workaround\n-----------------") @@ -301,7 +373,7 @@ def get_multiple_files(drives, files_dict: dict, is_dry_run: bool): for entry, size in files_dict.items(): file_name = os.path.sep.join(entry.split("/")[3:]) - local_store = (output_folder + file_name).replace("/", os.path.sep) + local_store = (output_folder + "/" + file_name).replace("/", os.path.sep) workQueue.put( {'bearer': Auth["Auth"], 'url': entry, 'local': local_store, 'now': n, 'lenfiles': lenfiles, 'size': size}) n += 1 @@ -328,18 +400,13 @@ def get_multiple_files_with_out_threads(files_dict: dict, is_dry_run: bool): num_files = 0 for file_url, file_size in files_dict.items(): - file_name = os.path.sep.join(file_url.split("/")[3:]) - local_file_path = (output_folder + file_name).replace("/", os.path.sep) - + local_file_path = (output_folder + "/" + file_name).replace("/", os.path.sep) if os.path.isfile(local_file_path) and os.path.getsize(local_file_path) == file_size: - print(" [-] Number: {}/{} - {} : Already Exists".format(file_index, total_files, local_file_path)) else: - if is_dry_run: - print(" [-] Skipped (Dry Run): {}".format(local_file_path)) else: @@ -358,7 +425,6 @@ def get_multiple_files_with_out_threads(files_dict: dict, is_dry_run: bool): destination.write(chunk) print(" [-] Number: {}/{} - {} : Download Success".format(file_index, total_files, local_file_path)) - total_size += file_size num_files += 1 @@ -405,7 +471,6 @@ def get_multiple_files_thread(bearer: str, url: str, local: str, now: int, len_f if not os.path.isfile(local): if is_dry_run: - print(" [-] Skipped (Dry Run): {}".format(local)) else: @@ -415,7 +480,6 @@ def get_multiple_files_thread(bearer: str, url: str, local: str, now: int, len_f stream=True ) if response.status_code == 200: - os.makedirs(os.path.dirname(local), exist_ok=True) destination: io.BufferedWriter with open(local, "bw") as destination: @@ -432,14 +496,18 @@ def get_multiple_files_thread(bearer: str, url: str, local: str, now: int, len_f print(" [-] Number: {}/{} - {} => Skipped: {}".format(now, len_files, thread_name, local)) -def system_slash(string): - """ Change / or \ depend on the OS""" - - if sys.platform == "win32" or sys.platform == "win64" or sys.platform == "cygwin": - return string.replace("/", "\\") +def operating_system(): + """ Get the name of the OS """ + if sys.platform == "win32" or sys.platform == "cygwin": + return "Windows" + elif sys.platform == "Darwin": + if subprocess.check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).decode('utf-8') == "Apple M1\n": + return "MacOs M1" + else: + return "MacOs" else: - return string.replace("\\", "/") + return "Linux" # Initializing @@ -462,7 +530,7 @@ def system_slash(string): parser.add_argument("-dr", "--dry_run", help="Dry Run : No downloads", action="store_true") args = parser.parse_args() - cfg_file = system_slash(r'{}/cfg/settings.cfg'.format(whapa_path)) + cfg_file = r'{}/cfg/settings.cfg'.format(whapa_path).replace("/", os.path.sep) if not os.path.isfile(cfg_file): createSettingsFile() @@ -473,41 +541,36 @@ def system_slash(string): print("[i] Searching...\n") wa_backup = WaBackup(**getConfigs()) backups = wa_backup.backups() - - if args.info: - try: + try: + if args.info: for backup in backups: backup_info(backup) - except Exception as e: - print("[e] Error {}".format(e)) - - elif args.list: - for backup in backups: - num_files = 0 - total_size = 0 - print("[i] Backup name: {}".format(backup["name"])) - for file in wa_backup.backup_files(backup): - num_files += 1 - total_size += int(file["sizeBytes"]) - print(" [-] {}".format(file["name"])) + elif args.list: + for backup in backups: + num_files = 0 + total_size = 0 + print("[i] Backup name: {}".format(backup["name"])) + for file in wa_backup.backup_files(backup): + num_files += 1 + total_size += int(file["sizeBytes"]) + print(" [-] {}".format(file["name"])) - print("[i] {} files {}".format(num_files, human_size(total_size))) + print("[i] {} files {}".format(num_files, human_size(total_size))) - elif args.list_whatsapp: - for backup in backups: - num_files = 0 - total_size = 0 - print("[i] Backup name: {}".format(backup["name"])) - for file in wa_backup.backup_files(backup): - num_files += 1 - total_size += int(file["sizeBytes"]) - if os.path.sep.join(file["name"].split("/")[6:]) == "msgstore.db.crypt14": - print(" [-] {}".format(file["name"])) - print(" [-] Size {} {}".format(file["sizeBytes"], human_size((int(file["sizeBytes"]))))) + elif args.list_whatsapp: + for backup in backups: + num_files = 0 + total_size = 0 + print("[i] Backup name: {}".format(backup["name"])) + for file in wa_backup.backup_files(backup): + num_files += 1 + total_size += int(file["sizeBytes"]) + if os.path.sep.join(file["name"].split("/")[6:]) == "msgstore.db.crypt14": + print(" [-] {}".format(file["name"])) + print(" [-] Size {} {}".format(file["sizeBytes"], human_size((int(file["sizeBytes"]))))) - elif args.sync: - try: + elif args.sync: for backup in backups: num_files = 0 total_size = 0 @@ -530,128 +593,136 @@ def system_slash(string): print("\n[i] Backup {} omitted. Write a correct phone number in the setting file".format( number_backup)) - except Exception as e: - print("[e] Error {}".format(e)) + elif args.s_images: + for backup in backups: + num_files = 0 + total_size = 0 + number_backup = backup["name"].split("/")[3] + if (number_backup in phone) or (phone == ""): + filter_file: dict = {} + for file in wa_backup.backup_files(backup): + i = os.path.splitext(file["name"])[1] + if ("jpg" in i) or ("jpeg" in i) or ("png" in i): + filter_file[file["name"]] = int(file["sizeBytes"]) - elif args.s_images: - for backup in backups: - num_files = 0 - total_size = 0 - number_backup = backup["name"].split("/")[3] - if (number_backup in phone) or (phone == ""): - filter_file: dict = {} - for file in wa_backup.backup_files(backup): - i = os.path.splitext(file["name"])[1] - if "jpg" in i: - filter_file[file["name"]] = int(file["sizeBytes"]) + if args.no_parallel: + get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) + else: + get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + + print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, + human_size(total_size))) - if args.no_parallel: - get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) else: - get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + print("[i] Backup {} omitted".format(number_backup)) - print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, - human_size(total_size))) + elif args.s_videos: + for backup in backups: + num_files = 0 + total_size = 0 + number_backup = backup["name"].split("/")[3] + if (number_backup in phone) or (phone == ""): + filter_file: dict = {} + for file in wa_backup.backup_files(backup): + i = os.path.splitext(file["name"])[1] + if "mp4" in i: + filter_file[file["name"]] = int(file["sizeBytes"]) - else: - print("[i] Backup {} omitted".format(number_backup)) - - elif args.s_videos: - for backup in backups: - num_files = 0 - total_size = 0 - number_backup = backup["name"].split("/")[3] - if (number_backup in phone) or (phone == ""): - filter_file: dict = {} - for file in wa_backup.backup_files(backup): - i = os.path.splitext(file["name"])[1] - if "mp4" in i: - filter_file[file["name"]] = int(file["sizeBytes"]) + if args.no_parallel: + get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) + else: + get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + + print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, + human_size(total_size))) - if args.no_parallel: - get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) else: - get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + print("[i] Backup {} omitted".format(number_backup)) + + elif args.s_audios: + for backup in backups: + num_files = 0 + total_size = 0 + number_backup = backup["name"].split("/")[3] + if (number_backup in phone) or (phone == ""): + filter_file: dict = {} + for file in wa_backup.backup_files(backup): + i = os.path.splitext(file["name"])[1] + if ("mp3" in i) or ("opus" in i): + filter_file[file["name"]] = int(file["sizeBytes"]) - print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, - human_size(total_size))) + if args.no_parallel: + get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) + else: + get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) - else: - print("[i] Backup {} omitted".format(number_backup)) - - elif args.s_audios: - for backup in backups: - num_files = 0 - total_size = 0 - number_backup = backup["name"].split("/")[3] - if (number_backup in phone) or (phone == ""): - filter_file: dict = {} - for file in wa_backup.backup_files(backup): - i = os.path.splitext(file["name"])[1] - if ("mp3" in i) or ("opus" in i): - filter_file[file["name"]] = int(file["sizeBytes"]) + print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, + human_size(total_size))) - if args.no_parallel: - get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) else: - get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + print("[i] Backup {} omitted".format(number_backup)) - print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, - human_size(total_size))) + elif args.s_documents: + for backup in backups: + num_files = 0 + total_size = 0 + number_backup = backup["name"].split("/")[3] + if (number_backup in phone) or (phone == ""): + filter_file: dict = {} + for file in wa_backup.backup_files(backup): + i = os.path.splitext(file["name"])[1] + if file["name"].split("/")[6] == "WhatsApp Documents": + filter_file[file["name"]] = int(file["sizeBytes"]) - else: - print("[i] Backup {} omitted".format(number_backup)) - - elif args.s_documents: - for backup in backups: - num_files = 0 - total_size = 0 - number_backup = backup["name"].split("/")[3] - if (number_backup in phone) or (phone == ""): - filter_file: dict = {} - for file in wa_backup.backup_files(backup): - i = os.path.splitext(file["name"])[1] - if file["name"].split("/")[6] == "WhatsApp Documents": - filter_file[file["name"]] = int(file["sizeBytes"]) + if args.no_parallel: + get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) + else: + get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + + print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, + human_size(total_size))) - if args.no_parallel: - get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) else: - get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + print("[i] Backup {} omitted".format(number_backup)) - print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, - human_size(total_size))) + elif args.s_databases: + for backup in backups: + num_files = 0 + total_size = 0 + number_backup = backup["name"].split("/")[3] + if (number_backup in phone) or (phone == ""): + filter_file: dict = {} + for file in wa_backup.backup_files(backup): + i = os.path.splitext(file["name"])[1] + if "crypt" in i: + filter_file[file["name"]] = int(file["sizeBytes"]) - else: - print("[i] Backup {} omitted".format(number_backup)) - - elif args.s_databases: - for backup in backups: - num_files = 0 - total_size = 0 - number_backup = backup["name"].split("/")[3] - if (number_backup in phone) or (phone == ""): - filter_file: dict = {} - for file in wa_backup.backup_files(backup): - i = os.path.splitext(file["name"])[1] - if "crypt" in i: - filter_file[file["name"]] = int(file["sizeBytes"]) + if args.no_parallel: + get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) + else: + get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) - if args.no_parallel: - get_multiple_files_with_out_threads(filter_file, is_dry_run=args.dry_run) - else: - get_multiple_files(backup, filter_file, is_dry_run=args.dry_run) + print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, + human_size(total_size))) - print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, - human_size(total_size))) + else: + print("[i] Backup {} omitted".format(number_backup)) + + elif args.pull: + file = args.pull + output = args.output + print("[+] Backup name: {}".format(os.path.sep.join(file.split("/")[:4]))) + get_file(file, is_dry_run=args.dry_run) + print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, + human_size(total_size))) + except Exception as e: + if "401 Client Error" in str(e): + print("Unable to access the resource, your OAuth token configured in the settings file may have" + " expired.\nRemoving token....\nTry again, you will have to log in again.""") + cfg_file = r'{}/cfg/settings.cfg'.format(whapa_path).replace("/", os.path.sep) + config = ConfigObj(cfg_file, interpolation=None) + config['google-auth']['oauth'] = "" + config.write() - else: - print("[i] Backup {} omitted".format(number_backup)) - - elif args.pull: - file = args.pull - output = args.output - print("[+] Backup name: {}".format(os.path.sep.join(file.split("/")[:4]))) - get_file(file, is_dry_run=args.dry_run) - print("\n[i] {} files downloaded, total size {} Bytes {}".format(num_files, total_size, - human_size(total_size))) + else: + print("[e] Error {}".format(e)) diff --git a/libs/whamerge.py b/libs/whamerge.py index fab9e4a..aadba10 100644 --- a/libs/whamerge.py +++ b/libs/whamerge.py @@ -9,31 +9,46 @@ # Define global variable - +# New Table message message_columns = [ + '_id', 'chat_row_id','from_me','key_id','sender_jid_row_id','status','broadcast','recipient_count','participant_hash', + 'origination_flags', 'origin','timestamp','received_timestamp','receipt_server_timestamp','message_type', + 'text_data','starred','lookup_tables', 'sort_id','message_add_on_flags' + ] + +messages_columns = [ '_id', 'key_remote_jid','key_from_me','key_id','status','needs_push','data','timestamp','media_url','media_mime_type', 'media_wa_type','media_size','media_name','media_caption','media_hash','media_duration','origin','latitude', 'longitude','thumb_image','remote_resource','received_timestamp','send_timestamp','receipt_server_timestamp', 'receipt_device_timestamp','read_device_timestamp','played_device_timestamp','raw_data','recipient_count', 'participant_hash','starred','quoted_row_id','mentioned_jids','multicast_id','edit_version','media_enc_hash', - 'payment_transaction_id','forwarded' + 'payment_transaction_id','forwarded', 'preview_type', 'send_count', 'lookup_tables', 'future_message_type', + 'message_add_on_flags' ] + chatlist_columns = [ - '_id','key_remote_jid','message_table_id','subject','creation','last_read_message_table_id','last_read_receipt_sent_message_table_id', - 'archived','sort_timestamp','mod_tag','gen','my_messages','plaintext_disabled','last_message_table_id','unseen_message_count', - 'unseen_missed_calls_count','unseen_row_count','vcard_ui_dismissed','deleted_message_id','deleted_starred_message_id', - 'deleted_message_categories','change_number_notified_message_id','last_important_message_table_id','show_group_description' + '_id','jid_row_id','hidden','subject','created_timestamp','display_message_row_id','last_message_row_id', + 'last_read_message_row_id','last_read_receipt_sent_message_row_id','last_important_message_row_id','archived','sort_timestamp','mod_tag', + 'gen','spam_detection', 'unseen_earliest_message_received_time', 'unseen_message_count', 'unseen_missed_calls_count', + 'unseen_row_count','plaintext_disabled','vcard_ui_dismissed','change_number_notified_message_row_id','show_group_description', + 'ephemeral_expiration','last_read_ephemeral_message_row_id','ephemeral_setting_timestamp','unseen_important_message_count', + 'ephemeral_disappearing_messages_initiator', 'group_type', 'last_message_reaction_row_id', 'last_seen_message_reaction_row_id', + 'unseen_message_reaction_count', 'growth_lock_level', 'growth_lock_expiration_ts', 'last_read_message_sort_id', 'display_message_sort_id', + 'last_message_sort_id', 'last_read_receipt_sent_message_sort_id' ] + quote_columns = [ '_id', 'key_remote_jid','key_from_me','key_id','status','needs_push','data','timestamp','media_url','media_mime_type', 'media_wa_type','media_size','media_name','media_caption','media_hash','media_duration','origin','latitude', 'longitude','thumb_image','remote_resource','received_timestamp','send_timestamp','receipt_server_timestamp', 'receipt_device_timestamp','read_device_timestamp','played_device_timestamp','raw_data','recipient_count', 'participant_hash','starred','quoted_row_id','mentioned_jids','multicast_id','edit_version','media_enc_hash', - 'payment_transaction_id','forwarded' + 'payment_transaction_id','forwarded', 'preview_type', 'send_count', 'lookup_tables', 'future_message_type', + 'message_add_on_flags' ] + thumbnail_columns = [ - 'rowid','thumbnail','timestamp','key_remote_jid','key_from_me','key_id' + 'thumbnail','timestamp','key_remote_jid','key_from_me','key_id' ] @@ -50,6 +65,7 @@ def banner(): ------------------------ Whatsapp Merger ----------------------- """) + def help(): """ Function show help """ @@ -91,8 +107,8 @@ def merge(db_path, db_name): except Exception as e: print("[e] Error copying: ", e) - num_message_cols = len(message_columns) - str_message_cols = ",".join(message_columns[:num_message_cols]) + num_message_cols = len(messages_columns) + str_message_cols = ",".join(messages_columns[:num_message_cols]) total_message = 0 num_chatlist_cols = len(chatlist_columns) @@ -119,7 +135,7 @@ def merge(db_path, db_name): cursor_write.execute("SELECT _id FROM messages;") ids_message_write = cursor_write.fetchall() - cursor_write.execute("SELECT _id FROM chat_view;") + cursor_write.execute("SELECT _id FROM chat;") ids_chatlist_write = cursor_write.fetchall() cursor_write.execute("SELECT _id FROM messages_quotes;") @@ -136,7 +152,7 @@ def merge(db_path, db_name): cursor_read.execute("SELECT _id FROM messages;") ids_message_read = cursor_read.fetchall() - cursor_read.execute("SELECT _id FROM chat_view;") + cursor_read.execute("SELECT _id FROM chat;") ids_chatlist_read = cursor_read.fetchall() cursor_read.execute("SELECT _id FROM messages_quotes;") @@ -182,7 +198,7 @@ def merge(db_path, db_name): num_ids_chatlist_cols = len(ids_chatlist_insert) str_id_chatlist_cols = ",".join(ids_chatlist_insert[:num_ids_chatlist_cols]) - elements_chatlist_cursor = cursor_read.execute("SELECT " + str_chatlist_cols + " FROM chat_view WHERE _id IN (" + str_id_chatlist_cols + ");") + elements_chatlist_cursor = cursor_read.execute("SELECT " + str_chatlist_cols + " FROM chat WHERE _id IN (" + str_id_chatlist_cols + ");") elements_chatlist_insert = elements_chatlist_cursor.fetchall() num_ids_quote_cols = len(ids_quote_insert) @@ -198,12 +214,12 @@ def merge(db_path, db_name): # Insert the elements into the database try: for msg in elements_message_insert: - insert_query = "INSERT INTO messages(" + str_message_cols + ") VALUES (" + ','.join('?' for x in range(0, len(message_columns))) + ")" + insert_query = "INSERT INTO messages(" + str_message_cols + ") VALUES (" + ','.join('?' for x in range(0, len(messages_columns))) + ")" cursor_write.execute(insert_query, msg) output.commit() for msg in elements_chatlist_insert: - insert_query = "INSERT INTO chat_view(" + str_chatlist_cols + ") VALUES (" + ','.join('?' for x in range(0, len(chatlist_columns))) + ")" + insert_query = "INSERT INTO chat(" + str_chatlist_cols + ") VALUES (" + ','.join('?' for x in range(0, len(chatlist_columns))) + ")" cursor_write.execute(insert_query, msg) output.commit() @@ -259,8 +275,8 @@ def merge_win(db_path, db_name): except Exception as e: print("[e] Error copying: ", e) - num_message_cols = len(message_columns) - str_message_cols = ",".join(message_columns[:num_message_cols]) + num_message_cols = len(messages_columns) + str_message_cols = ",".join(messages_columns[:num_message_cols]) total_message = 0 num_chatlist_cols = len(chatlist_columns) @@ -287,7 +303,7 @@ def merge_win(db_path, db_name): cursor_write.execute("SELECT _id FROM messages;") ids_message_write = cursor_write.fetchall() - cursor_write.execute("SELECT _id FROM chat_view;") + cursor_write.execute("SELECT _id FROM chat;") ids_chatlist_write = cursor_write.fetchall() cursor_write.execute("SELECT _id FROM messages_quotes;") @@ -306,7 +322,7 @@ def merge_win(db_path, db_name): cursor_read.execute("SELECT _id FROM messages;") ids_message_read = cursor_read.fetchall() - cursor_read.execute("SELECT _id FROM chat_view;") + cursor_read.execute("SELECT _id FROM chat;") ids_chatlist_read = cursor_read.fetchall() cursor_read.execute("SELECT _id FROM messages_quotes;") @@ -356,7 +372,7 @@ def merge_win(db_path, db_name): num_ids_chatlist_cols = len(ids_chatlist_insert) str_id_chatlist_cols = ",".join(ids_chatlist_insert[:num_ids_chatlist_cols]) elements_chatlist_cursor = cursor_read.execute( - "SELECT " + str_chatlist_cols + " FROM chat_view WHERE _id IN (" + str_id_chatlist_cols + ");") + "SELECT " + str_chatlist_cols + " FROM chat WHERE _id IN (" + str_id_chatlist_cols + ");") elements_chatlist_insert = elements_chatlist_cursor.fetchall() num_ids_quote_cols = len(ids_quote_insert) @@ -375,12 +391,12 @@ def merge_win(db_path, db_name): try: for msg in elements_message_insert: insert_query = "INSERT INTO messages(" + str_message_cols + ") VALUES (" + ','.join( - '?' for x in range(0, len(message_columns))) + ")" + '?' for x in range(0, len(messages_columns))) + ")" cursor_write.execute(insert_query, msg) output.commit() for msg in elements_chatlist_insert: - insert_query = "INSERT INTO chat_view(" + str_chatlist_cols + ") VALUES (" + ','.join( + insert_query = "INSERT INTO chat(" + str_chatlist_cols + ") VALUES (" + ','.join( '?' for x in range(0, len(chatlist_columns))) + ")" cursor_write.execute(insert_query, msg) output.commit() diff --git a/libs/whapa.py b/libs/whapa.py index 4d19cce..011a0e9 100644 --- a/libs/whapa.py +++ b/libs/whapa.py @@ -20,7 +20,7 @@ report_var = "None" report_html = "" report_group = "" -version = "1.55" +version = "1.60" names_dict = {} # names wa.db color = {} # participants color current_color = "#5586e5" # default participant color diff --git a/whapa-gui.py b/whapa-gui.py index 1d3c777..fce0286 100644 --- a/whapa-gui.py +++ b/whapa-gui.py @@ -10,11 +10,21 @@ from tkinter import messagebox from tkinter import filedialog +try: + import requests +except ModuleNotFoundError: + print("You need requests module to run this script") + print("Use this: python -m pip install requests") + print("Or: pip install requests") + exit(1) + + + """ Global vars""" author = 'B16f00t' title = 'WhatsApp Parser Toolset' contact = "https://t.me/bigfoot_whapa" -version = '1.57' +version = '1.58' system = "" abs_path_file = os.path.abspath(__file__) # C:\Users\Desktop\whapa\whapa-gui.py whapa_path = os.path.split(abs_path_file)[0] # C:\Users\Desktop\whapa @@ -691,9 +701,11 @@ def __init__(self, img_folder, icons): self.entry_whachat_te.bind('', self.on_entry_click_out_whachat) self.entry_whachat_te.bind('', self.on_focusout_out_whachat) self.entry_whachat_te.config(fg='grey') - + print("Checking updates...") + self.update() self.root.mainloop() + def on_entry_click_whapa(self, event): """function that gets called whenever entry is clicked""" if self.entry_whapa_ts.get() == "dd-mm-yyyy HH:MM": @@ -794,7 +806,11 @@ def update(self): """ About dialog""" if system == "Linux": - exec = self.system_slash(r'python3 "{}/libs/update.py" {}'.format(whapa_path, version)) + try: + exec = self.system_slash(r'python3 "{}/libs/update.py" {}'.format(whapa_path, version)) + except: + exec = self.system_slash(r'python "{}/libs/update.py" {}'.format(whapa_path, version)) + else: exec = self.system_slash(r'python "{}/libs/update.py" {}'.format(whapa_path, version)) @@ -1444,29 +1460,31 @@ def requirements(self): with open(cfg_file, 'w') as cfg: cfg.write(dedent(""" [report] - company = - record = - unit = - examiner = - notes = + company = "" + record = "" + unit = "" + examiner = "" + notes = "" [google-auth] gmail = alias@gmail.com # Optional. The account password or app password when using 2FA. - password = + password = yourpassword + # Optional. Login using the oauth cookie. + oauth = "" # Optional. The result of "adb shell settings get secure android_id". - android_id = 0000000000000000 + android_id = 0000000000000000 # Optional. Enter the backup country code + phonenumber be synchronized, otherwise it synchronizes all backups. # You can specify a list of celnumbr = BackupNumber1, BackupNumber2, ... - celnumbr = - + celnumbr = "" + [icloud-auth] icloud = alias@icloud.com passw = yourpassword """).lstrip()) error_icon = False - img_folder = Whapa.system_slash("", "{}/images/".format(whapa_path)) + img_folder = Whapa.system_slash("{}/images/".format(whapa_path)) icons = (img_folder + "logo.png", img_folder + "whapa.png", img_folder + "about.png", @@ -1511,4 +1529,5 @@ def requirements(self): system = "Windows" else: system = "Linux" + Whapa(img_folder, icons)