WhatsApp Web

Ahmed Alsuwaidi edited this page Jun 12, 2018 · 1 revision

Overview

The bot interfaces with WhatsApp through the WhatsApp Web page, the main goal when developing the bot was to avoid processing the HTML. I have seen many repositories which used MutationObserver, this was unreliable for the following reasons:

  • Only messages on a selected chat would be processed.
  • Doesn’t provide a method to send messages, you would have to use JavaScript to emulate clicks.

With that in mind I started looking at the JavaScript side of WhatsApp Web, the loaded files are created with webpack which obfuscated a lot of the code. During the earlier stages of the bot, a Store object was exposed as a global variable. This object was the main point of interaction with WhatsApp. Now this object is not being exposed globally and it has to be found when the bot is started.

JIDs, Groups, Contacts

WhatsApp uses JIDs to lookup both groups and contacts, the format of the JID is the following:

Contacts

The phone number with the area code without the 00 + @c.us, for example if you are using a UK number this will be your JID:

447123123123@c.us

Groups

For groups the JID format is the phone number of the creator + the timestamp of creation(unix format) + @g.us, an example of a group created by the same UK number above would be:

447123123123-1528791228@g.us

Store

The Store object groups together functionalities under a common nested object. Below is a sample structure:

  • Store.Chat
  • Store.Msg
  • Store.GroupMetadata
  • Store.Contact
  • Store.Wap

Under each of the objects is a models attribute which is an array of the individual objects. For example, Store.Chat.models is an array of all the Chats in WhatsApp Web. This Store object is where the bot interfaces with WhatsApp Web.

It is fairly straightforward to understand how to interact with the Store, and it is done in inject.js. Additionally, here are some examples:

Listening for incoming messages

Store.Msg.models.push = function(message) {
    Array.prototype.push.call(this, message);
    this.onPush(message);
};
Store.Msg.models.onPush = function(message) {
    console.log(message);
}
/*
Modify the Store.Msg.models.push() function and add a call to the onPush function, while keeping Array.prototype.push.call(). 

This will call the onPush() function each time a new message arrives.
*/

Sending a message

Store.Chat.find(jid).then(function(chat) {
    chat.markComposing();
    chat.sendMessage(body);
});
/*
First we find the Chat object by searching with the jid (group or individual), this will 
return an individual chat object that has a sendMessage() function which takes the text as an argument.
*/

Webpack and finding the Store

One of the side effects of using Webpack to optimise JavaScript is that it obfuscates the code. The Store object can no longer be accessed globally, and it has to be dynamically found while the bot is starting up. The majority of the code that does this is from this stackoverflow answer. Specifically the following code snippets:

// Returns promise that resolves to all installed modules
function getAllModules() {
  return new Promise((resolve) => {
    const id = _.uniqueId('fakeModule_');
    window['webpackJsonp'](
      [],
      {[id]: function(module, exports, __webpack_require__) {
        resolve(__webpack_require__.c);
      }},
      [id]
    );
  });
}

// Get module by ID found from the function above
function _requireById(id) {
	return webpackJsonp([], null, [id]);
}

Once the modules are retrieved by the getAllModules() function, the following code is used to determine the IDs for the Store and other relevant functions:

// Module IDs
var createFromData_id = 0;
var prepareRawMedia_id = 0;
var store_id = 0;
var Store = {};

var modules = getAllModules()._value;

// Automatically locate modules
for (var key in modules) {
	if (modules[key].exports) {
		if (modules[key].exports.createFromData) {
			createFromData_id = modules[key].id.replace(/"/g, '"');
		}
		if (modules[key].exports.prepRawMedia) {
			prepareRawMedia_id = modules[key].id.replace(/"/g, '"');
		}
		if (modules[key].exports.default) {
			if (modules[key].exports.default.Wap) {
				store_id = modules[key].id.replace(/"/g, '"');
			}
		}
	}
}

Once the IDs are found, the Store can be defined as follows:

Store = _requireById(store_id).default;
console.log(Store);

This is all done through inject.js.

Sending Media

Sending media messages through the bot is done through a workaround and requires a separate section for explanation. One of the security features of browsers is that JavaScript cannot directly access files on the computer, it first has to be opened by the User through an HTML form before it can be accessed. This prevents the bot from being able to simply call a function like sendMedia() and passing in the path of the file as an argument. As a workaround, we can use XMLHttpRequest to load a file from a URL as a Blob and sending that instead.

Another issue is the cross origin policy that prevents requests for resources from a different domain, this is solved in the bot by performing a series of XMLHTTPRequests to move the file through different contexts. This process will be explained in detail below: (For some background on the context, have a look at Chrome-Extension-Design

Contexts

Code running in the context of the WhatsApp Web Page (inject.js) is unable to access resources from external URLs, however, the code running in the extension can access them.

The first step in the process is in the helper.js file. You can call the sendMedia function with the target URL and other relevant arguments, this will download the media as a blob using an XMLHttpRequest (domains have to be whitelisted in manifest.json). After the file is downloaded, it has to be transferred to inject.js so it can be sent, however, we cannot send a blob using chrome extension message passing. Instead we use createObjectURL() which will return a URL for the given blob.

This data URL is under the context of the extension, this means that inject.js still cannot access it as it is not in the same context. To bypass this we first send that URL to the content_script.js which runs in the WhatsApp Web context, but can still load URLs from the extension. In the content script, the URL is accessed again via an XMLHttpRequest to retrieve the blob, and createObjectURL() is called again. However, this time the resulting URL is in the context of the WhatsApp Web page, it is passed back to the extension which then passes it to inject.js to download and send.

Creating the message

Once the URL of the blob is sent to inject.js, the send_media() function is called, the blob is downloaded and converted into a File object by adding the filename, filetype, and lastModified fields (Blobs and Files are similar and differ by a few fields). Once the File object of the media is created, it is prepared for sending using two functions retrieved from webpack, the functions are:

var createFromDataClass = _requireById(createFromData_id)["default"];
var prepareRawMediaClass = _requireById(prepareRawMedia_id).prepRawMedia;

var temp = createFromDataClass.createFromData(file, file.type);
var rawMedia = prepareRawMediaClass(temp, {});
var target = _.filter(Store.Msg.models, (msg) => {
    return msg.id.id === msg_id;
})[0];
var textPortion = {
    caption: caption,
    mentionedJidList: [],
    quotedMsg: target
};
rawMedia.sendToChat(chat, textPortion);
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.