Exploring Google Hangouts' Code

← Back to varun.ch

Google Hangouts is shutting down soon. Here's what they'll tell you if you try visiting hangouts.google.com:

Hangouts is being replaced by Google Chat

Your conversations from the last year are already in Chat, and older conversations will be available later

And an even scarier message on my school provided Google account:

⚠ Move to Google Chat now

Hangouts is shutting down soon. In preparation, some features won't work as expected. Your organisation has moved to Chat, where you can continue any previous conversations.

Google Chat is Google's brand new instant messenger. Hopefully they get it right this time.

As much as I love all the new communication features that Google Chat offers, I can't help but feel a little betrayed at the removal (non inclusion) of the little easter eggs. They've been around for so long, and I think they're a clever idea that adds some personality into an otherwise standard messaging app.

Being able to show a dog on your friend's screen with /corgis is fun, but some of the other commands are also even somewhat practical. /8ball would tell you what your future holds, and /roll would roll a 6 sided die. (alongside /rolld20 for a 20 sided die)

/corgis in Google Hangouts

Since Hangouts is shutting down soon, I decided to take a small dive into the minified JavaScript bundle to see what other easter eggs I could find.

Chrome DevTools

To start, I opened up Chrome DevTools on a Hangouts window, and I used ctrl + shift + F to search for "corgis" to find where the easter eggs were handled.

Chrome DevTools with a search for 'corgis'

This took me to a function that looks like it handles the image easter eggs like /corgis.

    Q2.prototype.ja = function(a) {
    var b = !!Math.floor(2 * Math.random());
    switch (a.vs.getId()) {
    case "corgis":
        R2(this, Mnb[Math.floor(Math.random() * Mnb.length)], !b, b);
        break;
    case "ponies":
        R2(this, P2[Math.floor(Math.random() * P2.length)], !b, b);
        break;
    case "pitchforks":
        this.da += Math.floor(20 * Math.random()) + 20;
        this.$.setInterval(50);
        this.$.start();
        break;
    case "ponystream":
        (this.ha = !this.ha) ? (this.aa.setInterval(400),
        this.aa.start()) : this.aa.stop();
        break;
    case "shydino":
        a = this.oa().ka(this.Ta.MJ),
        b = this.oa().ka(this.Ta.FN),
        null != a || null != b ? (kg(a),
        kg(b)) : (a = this.oa().Ka("IMG", {
            src: "//ssl.gstatic.com/chat/babble/ee/dh.png",
            className: "aN",
            id: this.Ta.MJ
        }),
        this.ka().appendChild(a),
        b = this.oa().Ka("IMG", {
            src: "//ssl.gstatic.com/chat/babble/ee/sd.png",
            className: "bN",
            id: this.Ta.FN
        }),
        this.ka().appendChild(b))
    }
}

This is pretty neat, as we get a little look behind the scenes of an easter egg. Unfortunately it seems like this is only for the image easter eggs, so let's take a look at the text based commands (like /8ball)

Searching for "8ball" brings us to a much longer block of code that looks like it handles a ton more commands. So many more than I ever expected there to be! 😀

    var Wnb = function(a) {
    return a.replace(RegExp("^/alpaca"), "\u0f3c\u00b4\u30fb\uff6a\u30fb`\u0f3d")
};
var Xnb = function(a) {
    return a.replace(RegExp("^/counterspell"), "\uff61\uff65*.\uff9f\u2606\u2501\u2501\u2282(`- \u00b4 \u2229)")
};
var Ynb = function(a) {
    return a.replace(RegExp("^/disapprove"), "\u0ca0_\u0ca0")
};
var Znb = "It's certain.;It's decidedly so.;Without a doubt.;Yes, definitely.;You can rely on it.;As I see it, yes.;Most likely.;Outlook good.;Yes.;Signs point to yes.;Reply hazy. Try again.;Ask again later.;Better not tell you now.;Cannot predict at the moment.;Concentrate and ask again.;Don't count on it.;My reply is no.;My sources say no.;Outlook not so good.;Very doubtful.".split(";")
  , $nb = function(a) {
    var b = Znb[Math.floor(Math.random() * Znb.length)];
    return RegExp("^/eightball|^/8ball").test(a) ? b : a
};
var aob = function(a) {
    return a.replace(RegExp("^/facepalm"), "(\uff0d\u2038\u10da)")
};
var bob = function(a) {
    return a.replace(RegExp("^/flowerbeam"), "(  \u30fb\u25e1\u30fb)\u3064\u2501\u2606\ud83c\udf38\ud83c\udf3a\ud83c\udf3c")
};
var cob = function(a) {
    return a.replace(RegExp("^/happy"), "\u1555( \u141b )\u1557")
};
var dob = function(a) {
    return a.replace(RegExp("^/idk"), "\u00af\\(\u00b0_o)/\u00af")
};
var eob = function(a) {
    return a.replace(RegExp("^/lgtm"), "\ud83d\udc4d \ud83d\udc4d \ud83d\udc4d")
};
var fob = function(a) {
    return a.replace(RegExp("^/lit"), "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25")
};
var gob = function(a) {
    return a.replace(RegExp("^/pokeball(throw)?"), "(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35\u25d3")
};
var Hlb = {
    "/roll barrel": "https://www.youtube.com/watch?v=cIx1NCvb5TU",
    "/roll out": "https://www.youtube.com/watch?v=4DquF9Mdbxc&t=10s",
    "/roll rick": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
};
var hob = function(a) {
    return a.replace(RegExp("^/sadsauce"), "\u0ca5_\u0ca5")
};
var iob = function(a) {
    return a.replace(RegExp("^/shrug(gie)?"), "\u00af\\_(\u30c4)_/\u00af")
};
var job = function(a) {
    return a.replace(RegExp("^/spicy"), "\ud83c\udf36\ufe0f \ud83c\udf36\ufe0f \ud83c\udf36\ufe0f")
};
var kob = function(a) {
    return a.replace(RegExp("^/success(kid)?"), "(\u2022\u0300\u1d17\u2022\u0301)\u0648 \u0311\u0311")
};
var lob = function(a) {
    return a.replace(RegExp("^/tableback"), "\u252c\u2500\u252c\ufeff \u30ce( \u309c-\u309c\u30ce)")
};
var mob = function(a) {
    return a.replace(RegExp("^/tableflip"), "(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35 \u253b\u2501\u253b")
};
var nob = function(a) {
    return a.replace(RegExp("^/this"), "\u261c(\uff9f\u30ee\uff9f\u261c)")
};
var oob = function(a) {
    return a.replace(RegExp("^/wizard(beam)?"), "(\u2229 ` -\u00b4)\u2283\u2501\u2501\u2606\uff9f.*\uff65\uff61\uff9f")
};
var pob = function(a) {
    return a.replace(RegExp("^/(yu|whyyou)no"), "\u10da(\u0ca0\u76ca\u0ca0\u10da)")
};

It uses regular expressions to match messages for different strings, and modify them appropriately. Most turn your message into a Lenny Face, or some other inline ASCII art. Something that caught my eye was a set of YouTube links and /roll x commands.

var Hlb = {
  "/roll barrel": "https://www.youtube.com/watch?v=cIx1NCvb5TU",
  "/roll out": "https://www.youtube.com/watch?v=4DquF9Mdbxc&t=10s",
  "/roll rick": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
};

/roll barrel results in a 10 hour remix of the "do a barrel roll" voice line from Star Fox

/roll out results in a video of the Transformers Cartoon, specifically set to start 10 seconds in. This one is interesting because it has just over 70 thousand views, and not a single comment mentions Google Hangouts.

/roll rick does just about what you would imagine it does.

With some more searching, I found some more text replacement commands.

zlb = function(a) {
    return a.replace(RegExp("^/algebraic"), "| ( \u2022\u25e1\u2022)|        (\u274d\u1d25\u274d\u028b)")
}
  , Alb = function(a) {
    return a.replace(RegExp("^/anjali"), "\ud83d\ude4f")
}
  , Blb = function(a) {
    return a.replace(RegExp("^/cling"), "\ud83d\udc28")
}
  , Clb = function(a) {
    var b = RegExp("^/coinflip");
    return .5 > Math.random() ? a.replace(b, "/me flipped a coin and got heads (\uff65\u03c9\uff65)\ufeed \u0311\u0311\u0f09\u2469") : a.replace(b, "/me flipped a coin and got tails (\uff65\u03c9\uff65)\ufeed \u0311\u0311\u0f09\u2469")
}
  , Dlb = function(a) {
    return a.replace(RegExp("^/deece"), "\ud83d\udc4c \ud83d\udc4c \ud83d\udc4c")
}
  , Elb = function(a) {
    return a.replace(RegExp("^/octodisco"), "\ud83c\udfb6\ud83d\udc19\ud83c\udfb6")
}
  , Flb = function(a) {
    return a.replace(RegExp("^/peacesign"), "\u270c\ufe0f")
}
  , Glb = function(a) {
    return a.replace(RegExp("^/puppyparty"), "\ud83d\udc15\ud83d\udc15\u200d\ud83e\uddba\ud83d\udc29\ud83d\udc15\ud83d\ude4c\ud83d\udc29\ud83e\uddae\ud83d\udc15\ud83d\udc29")
}

I wonder if anyone has stumbled across these by accident.

There's some more easter eggs too, for example /terminal removes the padding of some elements, and /bikeshed sets the chat background to a random colour.

Next, I decided to search for "corp.google.com" to see if anything exciting came up. corp.google.com is the domain name used for internal Google things. Unfortunately I did not find anything juicy, but I did find some messages relating to Google Fi/Google Voice.

  Lfb = function() {
    return T('<span>[Dogfood Placeholder Message] Google Voice integration with Hangouts is being deprecated. <a href="https://b.corp.google.com/issues/127369188" target="_blank">Learn more</a></span>')
}
  , Mfb = function() {
    return T('<span>[Dogfood Placeholder Message] Google Fi integration with Hangouts is being deprecated. <a href="https://b.corp.google.com/issues/127369188">Learn more</a></span>')
}

Dogfooding is practice where employees test out their own products. I've heard that it's used extensively at Google, and so [Dogfood Placeholder Message], is probably a disclaimer so that Google employees know that they're seeing a message that regular users won't.

The Learn More text links to b.corp.google.com. For me, it redirects to the Google corporate intranet (MOMA?) login (which looks like it hasn't been updated in a while). Googling "b.corp.google.com" reveals instances where other links have been shared (accidentally?), and based on the URLs, I think it's Google's internal Buganizer instance. So the b in b.corp.google.com probably stands for bugs/Buganizer.

Anyhow, this exploration was cool, and I found it interesting to take a peak behind the curtains of an app like Hangouts.