2

I am trying to write a python wrapper for selenium to send and receive messages on WhatsApp Web. However, I don't want people to have to scan the QR authentication code every single time they run the program.

From what I've read online, the session data is stored in indexeddb with the name "wawc" and syncs that data to local storage. I have written js scripts to extract the IndexedDB keys and values and to inject these into local storage and indexeddb on start up. These appear to work when I look at the Chrome devtools but when reloading the page, I get a logging out screen and then the IndexedDB gets cleared.

I've seen that some people get around this by specifying the user-data-dir argument in ChromeOptions but I'd rather do this manually as I'd prefer user data only to be saved for web.whatsapp.com, not any other sites.

extract_session.js:

function requestPromise(request) {
    return new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
}

async function openDatabase(dbName) {
    const request = indexedDB.open(dbName);
    return requestPromise(request);
}

async function getAllFromStore(db, storeName) {
    const transaction = db.transaction(storeName, "readonly");
    const objectStore = transaction.objectStore(storeName);
    return requestPromise(objectStore.getAll());
}

try {
    const db = await openDatabase(arguments[0]);
    const data = await getAllFromStore(db, arguments[1]);
    return JSON.stringify(data);
} catch (error) {
    console.log("Failed to extract session:", error)
    return null;
}

inject_session.js:

function requestPromise(request) {
    return new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
}

async function openDatabase(dbName) {
    const request = indexedDB.open(dbName);
    return requestPromise(request);
}

async function putInStore(db, storeName, data) {
    const transaction = db.transaction(storeName, "readwrite");
    const objectStore = transaction.objectStore(storeName);
    for (const item of data) {
        const request = objectStore.put(item);
        await requestPromise(request);
    }
}

try {
    const session = JSON.parse(arguments[2]);
    const db = await openDatabase(arguments[0]);
    await putInStore(db, arguments[1], session);
    localStorage.clear()
    for (const item of session) {
        localStorage.setItem(item.key, item.value);
    }
} catch (error) {
    console.error("Failed to inject session:", error);
}

Minimal code to recreate the problem:

from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By

import os

def _handle_await(parent, locator, timeout, poll_freq):
    try:
        return WebDriverWait(parent,
                             timeout,
                             poll_frequency=poll_freq
                             ).until(EC.presence_of_element_located(locator))
    except TimeoutException:
        return None

def await_element(parent, locator, timeout=0, poll_freq=0.05):

    if timeout == 0:
        while True:
            res = _handle_await(parent, locator, timeout, poll_freq)
            if res is not None:
                return res
    else:
        return _handle_await(parent, locator, timeout, poll_freq)

QR_CODE = (By.CSS_SELECTOR, "canvas[role='img']")
LOGGED_IN = (By.CSS_SELECTOR, "div[title='Chats']")

options = ChromeOptions()

driver = Chrome(options=options)
driver.get("https://web.whatsapp.com/")

await_element(driver, QR_CODE)

if os.path.isfile("session.wa"):

    js_code = open("inject_session.js", "r").read()
    with open("session.wa", "r", encoding="utf-8") as file:
        driver.execute_script(js_code, "wawc", "user", file.read())

    driver.refresh()
    input()

else:

    await_element(driver, LOGGED_IN)

    js_code = open("extract_session.js", "r").read()
    session_data = driver.execute_script(js_code, "wawc", "user")

    with open("session.wa", "w", encoding="utf-8") as file:
        file.write(str(session_data))

    input()

The first time the code is run, the session.wa file is created and looks fine. Once the code is run again, the local storage and IndexedDB appear to be updated correctly but once the page is reloaded, I get this screen:

Whatsapp Logout Image

And then I'm redirected to the main web.whatsapp.com screen with the QR code for logging in.

Update 1: I've begun to look into the execute_cdp_cmd method to interact with the CDP and set the IndexedDB and LocalStorage values before the webpage is loaded. This still results in a logout page which suggests that we are missing session data being saved somewhere else that must also be injected.

5
  • You're only saving localStorage, shouldn't you save the cookies aswel ?
    – 0stone0
    Commented Nov 7, 2024 at 14:57
  • 2
    From what I've seen, and read online cookies are irrelevant to how whatsapp web saves the session. When you clear the cookies and reload the page, the cookies are generated again and you stay logged in.
    – akrentz6
    Commented Nov 7, 2024 at 15:02
  • 2
    Ah oke, good way of testing that. Thinking out loud: Whatapp might see an other browser reference and sets the local data to invalid due to mismatching browser <-> data. You might just want to test it with a user-dir as explained here.
    – 0stone0
    Commented Nov 7, 2024 at 15:07
  • Yeah, thanks for the suggestion. I did implement a working version using the user-data-dir (it'll end up being less susceptible to breaking and bugs) but would love to also work out how I could go about saving the session this way as well just out of curiosity.
    – akrentz6
    Commented Nov 7, 2024 at 15:11
  • @redoc seems like that project has some trouble saving the session after the whatsaap web update from 2021. See this github issue.
    – 0stone0
    Commented Nov 11, 2024 at 15:32

1 Answer 1

2
+50

After wasting way to much time on this, I', placing my findings as an answer since it's too long for a comment.


There are 3 ways of saving user-data:

Type Selenium way
Cookies driver.get_cookies()
Local Storage driver.execute_script("return window.localStorage;")
Session Storage driver.execute_script("return window.sessionStorage;")

Saving those 3 after scanning the QR, and injecting them the second time still does not work.


So as suggested in my comment, it seems like Whatsapp sees that the session is not of the current user/browser and thus invalidates them.

To test this:

  1. I've created a new Chrome profile (Foo), logged into Whatsapp.
  2. Created a second Chrome profile (Bar)
  3. Copy all Chrome user-data from Foo to Bar's profile
  4. Opened Whatsapp as Bar, I'm being logged out.

Since it's quite impossible to find out what Whatsapp uses to invalidate the data, I dare to state my conclusion as:

It is not possible to keep a login on Whatsapp while moving between different sessions/profile/browsers.

The only way to achieve this is by providing a data-dir to the browser:

options.add_argument(f"--user-data-dir=whatsapp-data")
1
  • 2
    Thanks for the help! I'm not gonna accept this answer is the solution because I will be working on this problem in the future and trying to come up with a workaround for saving sessions but I've awarded you the bounty for all the work you put in.
    – akrentz6
    Commented Nov 14, 2024 at 16:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.