ZeroMessenger: Improving Facebook Zero's messaging functionality

Djezzy is an Algerian mobile network operator. Djezzy provides a zero-rated, text-only version of Facebook: Facebook Zero (0.facebook.com) or 0FB for short.

Some students (like myself) are practically poor and cannot afford real internet access so they end up relying on this service. What I'm presenting here is my attempt at making Facebook Zero a better shit.

Turning something like this: Facebook Zero screenshot

... into something like this: ZeroMessenger screenshot

Disclaimer: This drafty post is a super simplistic explanation of how I wrote an incomplete project that used a discontinued service. Still, I wanted to publish it since it might be useful to others...

Idea

After "studying" (i.e. using) Facebook Zero for over a year, I realized that the website is very predictable and has a RESTful-like "structure".

The idea is simple: If I can manipulate only texts then that's what I'm gonna do:

We treat Facebook Zero as if it were merely a messy database and an intermediate to exchange data.
So, to send a photo (or any file for that matter), first, convert it to text (base64) and send it as a text message.
On the other end of the wire, the recipient should convert it back to binary and view it.

Gathering data

As I have already intimated, 0FB pages are so predictable that a few document.querySelector lines allow us to obtain the necessary information to work with.

Profiles

These are the most important information we need: id, name, username, hasGreenDot (which signifies the user is active).

name is easily obtained using this simple statement:

const name = document.querySelector('#objects_container strong').innerText // 'Djalil Dreamski'

username, phoneNumber, and gender...

// Okay, suppose we are on a profile's info page (e.g. https://0.facebook.com/zuck?v=info)
// We can use these two lines to get the user's gender:
const rawGender = document.querySelector(`#root [title="Gender"]`).innerText.trim() // 'Gender\nMale'
const gender = gender.split('\n')[1].toLowerCase() // 'male'
 
// The above two lines can be used to get other (*useful*) pieces of information, like the `username`
// so let's turn it into a more general and less error-prone function:
function getAttr(attr, lowerCase = true) {
  const $container = document.querySelector(`#root [title="${attr}"]`)
  if ($container) {
      let text = $container.innerText.trim()
      if (lowerCase) text = text.toLowerCase()
      const val = text.split('\n')[1]
      return val
  } else {
      return ''
  }
}
 
// Now we can use it like this:
const username = getAttr('Facebook') // '/zuck'
const phoneNumber = getAttr('Mobile') // ''

id

0FB profile As far as I know, Facebook assigns an id (*FBID*) to each of its objects (profiles, groups, posts, messages, etc.). In every 'message-able' profile ("page" or "user") webpage, there exists a 'Message' button (a link, actually). We can use this link to get the profile's id. We can either look for a link whose text content consists of "Message", or whose URL starts with a specific prefix. I chose the latter approach:
// Supposing we're on a user's page, and that this user has a 'Message' button/link
const linkPrefix = 'https://0.facebook.com/messages/thread/'
const messageLink = Array.from(document.querySelectorAll('a')).find(a => a.href.startsWith(linkPrefix))
const [, id] = messageLink.match(/thread\/(\d+)/)
// If we were on 0.facebook.com/zuck, 'id' would be '4'

My id

My profile in Italian

We assume I'm already logged in. To get my id, we go to my profile page (/profile.php) and extract it from the "Registo de atividade" ("Activity Log") link.
We basically repeat the same work we did earlier with id but this time the link has this pattern: https://0.facebook.com/<MY_ID>/allactivity.
Note: Many pieces of code in my app are currently language-specific (only English works for now).

hasGreenDot was a little tricky at first as I couldn't just use a simple CSS selector that identifies it: Apparently some parts of Facebook Zero pages get automatically minified/uglified so some classes get renamed randomly (e.g. 'cg', 'rt', etc.).
One thing was sure: If the current page contains a greenDot, there will be a class in the 'style' tag, whose body contains nothing but this rule: {color:#6ba93e;}.

// We could use the above information to do this:
const styleHtml = document.querySelector('style').innerHTML
const hasGreenDot = styleHtml.includes('{color:#6ba93e;}')
 
// But since we will be using that approach in other places (and for other purposes),
// we actually use a method that retrieves the class's name if it exists.
function getClassName(rule) {
  const styleHtml = document.querySelector('style').innerHTML
  const escapedRule = rule.replace(/[{}().]/g, '\\$&') // This should do.
  const rRule = RegExp('\\.(\\w+?)' + escapedRule)
  const [, className] = styleHtml.match(rRule) || []
  return className || ''
}
 
// The following may be an empty string or (probably) a two-character name
// const greenDotClassName = getClassName('{color:#6ba93e;}')
// const hasGreenDot = !!greenDotClassName

Finally, we create a "public" function that gathers all of that information using the above snippets and then returns it.
This function will be attached to the ZeroWorker namespace (The purpose of this will be shown later).

// ZeroWorker is declared globally as `const ZeroWorker = {};`
ZeroWorker.getProfileInfo = function getProfileInfo() {
  // ...
  return { id, myId, name, username, hasGreenDot, gender }
}

Conversations

0FB Chat, direct, active

We can see a chat page's markup like this (at least this is what I recall):

<div> <!-- HEADER -->
  <strong>Name!</strong>
  <span>greenDot?</span>
  <span>statusText (e.g. "Active a few seconds ago")?</span>
  <a>Link to chat group info?</a>
</div>
<div id="messageGroup">
  <a id="see_older">See Older Messages</a>
  <div>
    <div>MESSAGE_BLOCK_1</div>
    <div>MESSAGE_BLOCK_2</div>
  </div>
  <a id="see_newer">See Newer Messages</a>
</div>

0FB Chat, group, blocked

These attributes can be used to automatically update the conversation by fetching new messages and deduplicating repeated ones (for technical reasons, duplicates can appear when "scrolling" up and down).

As with profiles, using the mentioned specifications, we write a function that collects the needed data and returns it:

ZeroWorker.getChat = function getChat() {
  // ...
  return { name, messages, hasGreenDot, statusText, groupInfoLink, seeOlderLink, seeNewerLink }
}

As for sending messages, we use a simple function that performs the actions a user normally does: Fill in a message and then click send (submit).

Again, we attach this method to the ZeroWorker namespace.

// This code should be self-explanatory.
ZeroWorker.sendText = function sendText(str) {
  const $form = document.querySelector('#composer_form')
  const $input = $form.querySelector('textarea')
  $input.value = str
  $form.submit()
}

Parts

Basically, it consists of three Promise-based parts: Messenger, Master, and Worker (in addition to "Broker").

ZeroMessenger's complete UML class diagram ZeroMessenger.uml exported as image using WhiteStarUML.

ZeroWorker

A ZeroWorker (presented in the Gathering Data section) runs on iframes that are opened by Master. (ZeroWorker's scripts get automatically injected in 0FB iframes thanks to Chrome Extension API).

A Worker listens for orders, executes them, and finally sends a response to Master. ZeroWorkers and Masters communicate via cross-document messaging (opens in a new tab).

The following code shows how jobs are handled. This is the reason we have been attaching everything to the ZeroWorker namespace: To dynamically access needed functions.

window.addEventListener('message', ZeroWorker.onOrder, false)
 
ZeroWorker.onOrder = function onOrder(event) {
  const job = event.data
  const response = ZeroWorker[job.fn](...job.args)
  // Add some useful 'metadata' that Messenger uses to keep its data consistent and up-to-date
  response._pageDate = Date.now()
  response._pageLink = window.location.toString()
  event.source.postMessage({job, response}, '*')
}

ZeroMaster

Actually just Master.

It spawns ZeroWorkers (i.e. iframes), sends orders (jobs) to them, then listens for responses.

A Master kill()s the Worker he spawned when they lose their raison d'être (i.e. the job is done).

Also, Master deals with actions that make the page reload (for instance, sending messages) and handles time-outing requests (happens often on shitty cell connections like mine).

const username = 'dreamski21'
const job = {fn: 'getProfileInfo', url: `https://0.facebook.com/${username}?v=info`}
const master = new Master(job)
master.getResponse().then( (res) => {
  console.info(`${res.name} is ${res.hasGreenDot? 'online' : 'offline'}`)
})
// Probably outputs: "Djalil Dreamski is offline"

As for how it works:

class Master {
  // ...
 
  _launch() {
    return new Promise( (resolve, reject) => {
      const onMessage = (event) => {
        const data = event.data;
        if (data && data.job && data.job.id === this.job.id) {
          // Perfect, this is the event we were listening for.
          removeListener()
          if (data.response && !data.response.error) {
            resolve(data.response)
          } else {
            const err = (data.response && data.response.error) || 'Response err'
            reject({error: err})
          }
        }
      }
 
      const removeListener = () => window.removeEventListener('message', onMessage, false)
 
      // Start listening and then tell ZeroWorker to do the job
      window.addEventListener('message', onMessage, false)
      this._iframe.contentWindow.postMessage(this.job, '*')
    })
  }
 
}

ZeroMessenger

ZeroMessenger is the interface that interacts directly with the user.

Again, it abstracts ZeroMaster and ZeroWorker by providing dedicated classes and methods. For example:

class Message {
  // ...
 
  /**
   * Sends a text message.
   *
   * @param {string} id - Conversation id
   * @param {string} text
   * @returns {Promise<object>} "Zero Response" from ZeroWorker 
   */
  static async send(id, text) {
    // url = `https://0.facebook.com/messages/read?fbid=${id}&show_delete_message_button=1
    const url = Conversation.getChatLink(id)
    const job = {url, fn: 'sendText', args: [text], reloads: true}
    const res = await (new Master(job)).getResponse()
    return res
  }
}

And so on, we write classes Profile, Conversation, and sub-classes as shown in the UML class diagrams above.

These classes open different pages/links to do different things. For example, to get a user's info, you open their profile info page then invoke Worker (by specifying getProfileInfo as its job) to read and send that info to you.

Messenger contains all the other parts/classes and eases interaction between them. For instance, to distinguish between my id and another user's, Profile.getTheirId(url) needs to know my id which is stored in Messenger.moi.id.

As for dynamically updating the content, ZeroMessenger periodically checks Facebook Zero, the same way a user refreshes pages every few seconds. Really, the goal of this project was to mimic the user's actions + add photos.


By this point, I have created a good enough API for working with Facebook Zero; the rest is just a basic chat/instant-messaging app.

Once upon a time when nwjs used to be called node-webkit and when Object.observe was not deprecated, I wrote an APK Manager with a reactive view by observing 'data objects' and updating the DOM when changes occur. It was a fun and interesting project to work on... However, this time I decided to stop reinventing the wheel and use VueJS to handle reactivity, so I can focus on the app's logic.

ZeroBroker

This is actually my original idea (opens in a new tab): A "proxy bot" to send and receive binary data using only texts.

It's inspired by the TCP protocol and it works like this:

The bot logs in using my account and starts watching for incoming messages (including those I send to myself).

I didn't finish it but I made Zerofy which lets you do half of the job "manually" (sending), while the other half is done automatically (receiving).

Technicalities and Regrets


Data is half processed by Worker and the rest is handled by Messenger/Master. That's confusing. Only one of them should take the responsibility and do most of the work (preferably Messenger while Workers only gather raw data and "obey orders").


The way ZeroMessenger works is similar to crawlers (which Facebook tries to prevent), this necessitates that we mimic a browser. I could use libraries to grab pages (using axios), parse them and extract relevant info (using cheerio), compose requests and send them. This solution would be independent of the browser and work on Node, the possibilities would be limitless...

That wasn't what I did. Wanting to keep it simple and having used Google Chrome's Extension API before, I decided to use iframes and inject scripts into them. This is a bad approach since it's costly (needlessly rendering pages and loading images) and gives less control (like catching network errors and redirects and stuff).

Conclusions

Acknowledgments

I'd like to thank my friend Wanis R. for the help he provided (beta reading, beta testing, allowing me to use his real internet sometimes, etc.) and for his continuous support and encouragement.

2018-11 Update: Djezzy's Facebook Zero and Wikipedia no longer work. Sad.