# HG changeset patch
# User Patrick Cloke <clokep@gmail.com>
# Date 1454799750 18000
#      Sat Feb 06 18:02:30 2016 -0500
# Node ID af4f2f54a315a1ca6faca285f0e3ac091622b3cc
# Parent  83785a57c3a64710edbe57ecdd7c80b66e5f5168
Start of Facebook code.

diff --git a/chat/chat-prefs.js b/chat/chat-prefs.js
--- a/chat/chat-prefs.js
+++ b/chat/chat-prefs.js
@@ -78,19 +78,17 @@ pref("messenger.status.userDisplayName",
 pref("chat.irc.defaultQuitMessage", "");
 // If this is true, requestRooomInfo will return LIST results when it is
 // called automatically by the awesometab. Otherwise, requestRoomInfo will
 // only do so when explicitly requested by the user, e.g. via the /list command.
 pref("chat.irc.automaticList", true);
 // Whether to enable or disable message carbons protocol (XEP-0280).
 pref("chat.xmpp.messageCarbons", true);
 // Disable Skype until it can be tested further.
-pref("chat.prpls.prpl-skype.disable", true);
-// Disable Facebook as the XMPP gateway no longer exists.
-pref("chat.prpls.prpl-facebook.disable", true);
+//pref("chat.prpls.prpl-skype.disable", true);
 // Disable Yahoo Messenger as legacy Yahoo was shut down.
 pref("chat.prpls.prpl-yahoo.disable", true);
 // Whether to disable SRV lookups that use the system DNS library.
 pref("chat.dns.srv.disable", false);
 // Disable JavaScript in browser requests.
 pref("chat.browserRequest.disableJavascript", false);
 
 // loglevel is the minimum severity level that a libpurple message
diff --git a/chat/modules/ArrayBufferUtils.jsm b/chat/modules/ArrayBufferUtils.jsm
--- a/chat/modules/ArrayBufferUtils.jsm
+++ b/chat/modules/ArrayBufferUtils.jsm
@@ -4,17 +4,17 @@
 
 /*
  * JavaScript ArrayBuffers are missing a variety of useful methods which are
  * provided by this module.
  */
 
 this.EXPORTED_SYMBOLS = ["copyBytes", "ArrayBufferToBytes",
   "BytesToArrayBuffer", "StringToBytes", "StringToArrayBuffer",
-  "ArrayBufferToString", "ArrayBufferToHexString"];
+  "ArrayBufferToString", "ArrayBufferToHexString", "HexStringToArrayBuffer"];
 
 /*
  * aTarget / aSource are ArrayBuffers.
  *
  * Note that this is very similar to ArrayBuffer.slice except that it allows
  * for an offset in the target as well as the source.
  */
 function copyBytes(aTarget, aSource, aTargetOffset = 0, aSourceOffset = 0,
@@ -49,8 +49,14 @@ function StringToArrayBuffer(aString) {
 
 function ArrayBufferToString(aData) {
   return Array.from(new Uint8Array(aData)).map(b => String.fromCharCode(b)).join("");
 }
 
 function ArrayBufferToHexString(aData) {
   return "0x" + Array.from(new Uint8Array(aData)).map(b => ("0" + b.toString(16)).slice(-2)).join(" ");
 }
+
+function HexStringToArrayBuffer(aString) {
+  return BytesToArrayBuffer(
+    aString.replace(/^0x/, "").split(" ").map(c => parseInt(c, 16))
+  );
+}
diff --git a/chat/modules/test/test_ArrayBufferUtils.js b/chat/modules/test/test_ArrayBufferUtils.js
--- a/chat/modules/test/test_ArrayBufferUtils.js
+++ b/chat/modules/test/test_ArrayBufferUtils.js
@@ -2,17 +2,18 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource:///modules/ArrayBufferUtils.jsm");
 
 function do_check_arraybuffer_eq(a, b) {
   let viewA = new Uint8Array(a);
   let viewB = new Uint8Array(b);
 
-  let res = a.byteLength == b.byteLength;
+  do_check_eq(a.byteLength, b.byteLength);
+  let res = true;
   for (let i = 0; i < viewA.byteLength; ++i)
     res = res && viewA[i] == viewB[i];
 
   do_check_true(res);
 }
 
 function do_check_array_eq(a, b) {
   let res = a.length == b.length;
@@ -84,17 +85,34 @@ function test_ArrayBufferToHexString() {
   view.setUint8(3, 0x11);
 
   let str = ArrayBufferToHexString(buf);
   do_check_eq(str, "0x00 10 01 11");
 
   run_next_test();
 }
 
+function test_HexStringToArrayBuffer() {
+  let str = "0x00 10 01 11";
+  let buf = HexStringToArrayBuffer(str);
+
+  let expectedBuf = new ArrayBuffer(4);
+  let view = new DataView(expectedBuf);
+  view.setUint8(0, 0x00);
+  view.setUint8(1, 0x10);
+  view.setUint8(2, 0x01);
+  view.setUint8(3, 0x11);
+
+  do_check_arraybuffer_eq(buf, expectedBuf);
+
+  run_next_test();
+}
+
 function run_test() {
   add_test(test_ArrayBufferToBytes);
   add_test(test_BytesToArrayBuffer);
   add_test(test_StringToBytes);
   add_test(test_ArrayBufferToString);
   add_test(test_ArrayBufferToHexString);
+  add_test(test_HexStringToArrayBuffer);
 
   run_next_test();
 }
diff --git a/chat/protocols/facebook/facebook-mqtt.jsm b/chat/protocols/facebook/facebook-mqtt.jsm
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/facebook-mqtt.jsm
@@ -0,0 +1,474 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+const { Loader, Require, Module } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/ArrayBufferUtils.jsm");
+Cu.import("resource:///modules/socket.jsm");
+Cu.import("resource:///modules/thrift/Buffer.jsm");
+
+Cu.importGlobalProperties(["TextEncoder", "TextDecoder"]);
+
+this.EXPORTED_SYMBOLS = ["MqttSocket"];
+
+// Set-up loading so require works properly in CommonJS modules.
+let loader = Loader({
+  // Point all loading to the thrift subdirectory of modules.
+  paths: {
+    '': 'resource:///modules/thrift/',
+    './': 'resource:///modules/thrift/',
+  },
+
+  // Use the proper file name for the Int64 module.
+  resolve: function(id, requirer) {
+    if (id == "node-int64")
+      return "Int64";
+    return id;
+  },
+
+  // Include the Buffer global.
+  globals: {
+    Buffer: Buffer
+  }
+})
+
+let requirer = Module('thrift-module', '');
+let require = Require(loader, requirer);
+
+const TCompactProtocol = require('compact_protocol.js');
+const { User, Connect, UserPresence, Presence } = require('facebook_types.js');
+
+// Constants used in MQTT.
+const FB_MQTT_HOST = "mqtt.facebook.com";
+const FB_MQTT_PORT = 443;
+
+function Transport(aData='') {
+  if (typeof aData != "string")
+    throw "Attempting to create a Transport with an unsupported type: " + typeof aData;
+
+  this.buf = aData;
+  this.offset = 0;
+}
+Transport.prototype = {
+  write: function(arg, encoding) {
+    // A String was passed in.
+    if (typeof arg == "string" && encoding == "utf8")
+      this.buf += arg;
+    else if (arg instanceof Buffer)
+      this.buf += ArrayBufferToString(arg);
+    else
+      throw new Error('write called without a string/Buffer argument: ' + arg);
+  },
+
+  read: this.readString,
+  readString: function(aSize) {
+    let result = this.buf.slice(this.offset, this.offset + aSize);
+    this.offset += aSize;
+    return result;
+  },
+  readByte: function() {
+    return this.buf.charCodeAt(this.offset++);
+  }
+}
+
+/*
+ * Based on gzipCompressString from
+ * devtools/client/netmonitor/test/sjs_content-type-test-server.sjs.
+ *
+ * Modified to return a Promise and to use deflate instead of gzip.
+ */
+function deflateString(aString) {
+  // Oddly nsIStreamListener seems to have no error condition, so aReject is
+  // unused.
+  return new Promise(function(aResolve, aReject) {
+
+    // Create an observer that just calls our callback.
+    let observer = {
+      onStreamComplete: function(aLoader, aContext, aStatus, aLength, aResult) {
+        let buffer = String.fromCharCode.apply(this, aResult);
+        aResolve(buffer);
+      }
+    };
+
+    // Convert the string to a stream.
+    let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+                         .createInstance(Ci.nsIStringInputStream);
+    stringStream.data = aString;
+
+    // Instantiate a service and lister with the callback.
+    let service = Cc["@mozilla.org/streamConverters;1"]
+                    .getService(Ci.nsIStreamConverterService);
+    let listener = Cc["@mozilla.org/network/stream-loader;1"]
+                     .createInstance(Ci.nsIStreamLoader);
+    listener.init(observer);
+
+    // Create a converter and start the conversion.
+    let converter = service.asyncConvertData("uncompressed", "deflate",
+                                             listener, null);
+    converter.onStartRequest(null, null);
+    converter.onDataAvailable(null, null, stringStream, 0, aString.length);
+    converter.onStopRequest(null, null, null);
+  });
+}
+
+function MqttPacket(aType, aFlags=0x00) {
+  this.type = aType;
+  this.flags = aFlags;
+  this.headers = [];
+  this.payload = "";
+
+  this._encoder = new TextEncoder('utf8');
+  this._decoder = new TextDecoder('utf8');
+}
+// See Table 2.1 of MQTT version 3.1.1.
+MqttPacket.TYPES = {
+  RESERVED0: 0,
+  CONNECT: 1,
+  CONNACK: 2,
+  PUBLISH: 3,
+  PUBACK: 4,
+  PUBREC: 5,
+  PUBREL: 6,
+  PUBCOMP: 7,
+  SUBSCRIBE: 8,
+  SUBACK: 9,
+  UNSUBSCRIBE: 10,
+  UNSUBACK: 11,
+  PINGREQ: 12,
+  PINGRESP: 13,
+  DISCONNECT: 14,
+  RESERVED15: 15
+};
+// Does not match section 3.1.2.1 of MQTT version 3.1.1.
+MqttPacket.PROTOCOL_NAME = "MQTToT";
+// Does not match section 3.1.2.2 of MQTT version 3.1.1.
+MqttPacket.PROTOCOL_LEVEL = 0x03;
+MqttPacket.FLAGS = {
+  CONNECT: {
+    USERNAME: 1 << 7,
+    PASSWORD: 1 << 6,
+    WILL_RETAIN: 1 << 5,
+    WILL_QoS0: 0 << 3, // QoS level can be 0, 1, or 2.
+    WILL_QoS1: 1 << 3, // QoS level can be 0, 1, or 2.
+    WILL_QoS2: 2 << 3, // QoS level can be 0, 1, or 2.
+    WILL_FLAG: 1 << 2,
+    CLEAN_SESSION: 1 << 1,
+    RESERVED: 0,
+  }
+};
+// Map a packet type to the variable fields it expects.
+MqttPacket.FIELDS = {
+  // Connect is really only used for testing.
+  [MqttPacket.TYPES.CONNECT]: [
+    ["Protocol Name", 0],
+    ["Protocol Level", 1],
+    ["Keep Alive", 2],
+  ],
+  [MqttPacket.TYPES.CONNACK]: [
+    ["Connect Acknowledge Flags", 1],
+    ["Connect Return code", 1],
+  ]
+};
+MqttPacket.prototype = {
+  /*
+   * Convert the MqttPacket into an ArrayBuffer for sending over a socket.
+   */
+  toArrayBuffer: function() {
+    // First calculate the length of the packet.
+    let length = this._calculateLength(); // The type/flags.
+
+    // Calculate the number of bytes necessary to represent the length.
+    let bytes = this._remainingLength(length);
+
+    // Generate an ArrayBuffer with the proper length.
+    let buffer = new ArrayBuffer(length + bytes.length + 1);
+    let view = new DataView(buffer);
+
+    // The fixed header, see section 3.2 of MQTT version 3.1.1.
+    view.setUint8(0, (this.type & 0x0F) << 4 | (this.flags & 0x0F));
+    // Set the length.
+    for (let i = 0; i < bytes.length; ++i)
+      view.setUint8(i + 1, bytes[i]);
+
+    // Set the variable length fields.
+    let offset = 1 + bytes.length;
+    for (let header of this.headers) {
+      if (typeof header.value == "string") {
+        // Two byte length.
+        length = byteLength(header.value);
+        let encodedStr = this._encoder.encode(header.value);
+
+        view.setUint16(offset, encodedStr.byteLength, false);
+        offset += 2;
+
+        // Set the actual string.
+        copyBytes(buffer, encodedStr, offset);
+        offset += length;
+      }
+      else if (typeof header.value == "number") {
+        // Go from the MSB to the LSB (i.e. big-endian).
+        for (let i = header.count - 1; i >= 0; --i) {
+            view.setUint8(offset, (header.value >> (8 * i)) & 0xFF);
+            ++offset;
+        }
+        length += header.count;
+      }
+      // Note that we already know that each header is a valid type because
+      // _calculateLength checks.
+    }
+
+    // Finally set the payload.
+    let payload = StringToArrayBuffer(this.payload);
+    copyBytes(buffer, payload, offset);
+
+    return buffer;
+  },
+
+  /*
+   * Fill in the fields of this MqttPacket based on an ArrayBuffer.
+   *
+   * Returns the amount of aBuf parsed.
+   */
+  fromArrayBuffer: function(aBuf) {
+    let view = new DataView(aBuf);
+    // Need at least two bytes!
+    if (view.byteLength < 2)
+        return 0;
+
+    // The fixed header, see section 3.2 of MQTT version 3.1.1.
+    let offset = 0;
+    let data = view.getUint8(offset++);
+    this.type = (data >> 4) & 0x0F;
+    this.flags = data & 0x0F;
+
+    // Calculate the remaining length, see section 2.2.3 of MQTT version 3.1.1.
+    let multiplier = 1;
+    let length = 0;
+    let encodedByte;
+    do {
+      // Ensure there are enough bytes.
+      if (offset >= view.byteLength)
+        return 0;
+
+      encodedByte = view.getUint8(offset++);
+
+      length += (encodedByte & 127) * multiplier;
+
+      multiplier <<= 7;
+
+      if (multiplier > Math.pow(128, 3))
+        throw "Malformed Remaining Length";
+
+    } while ((encodedByte & 128) != 0);
+
+    // Calculate the total length.
+    length += offset;
+
+    // Ensure there's enough bytes, otherwise we need to wait for more.
+    if (length > view.byteLength)
+      return 0;
+
+    if (!MqttPacket.FIELDS.hasOwnProperty(this.type))
+      throw "Cannot parse fields for unknown packet type: " + this.type;
+
+    for (field of MqttPacket.FIELDS[this.type]) {
+      switch (field[1]) {
+        case 0: // string
+          let length = view.getUint16(offset);
+          offset += 2;
+
+          // Get the actual string.
+          let decodedStr = this._decoder.decode(new Uint8Array(aBuf, offset, length));
+          offset += length;
+          this.addHeaderField(field[0], decodedStr);
+          break;
+        case 1:
+          this.addHeaderField(field[0], view.getUint8(offset++));
+          break;
+        case 2:
+          this.addHeaderField(field[0], view.getUint16(offset));
+          offset += 2;
+          break;
+        default:
+          throw "Cannot parse field '" + field[0] + "' with invalid length: " + field[1];
+      }
+    }
+
+    // Anything left must be the payload.
+    this.payload = ArrayBufferToString(new Uint8Array(aBuf, offset));
+
+    // The amount of data parsed.
+    return length;
+  },
+
+  /*
+   * Return a set of bytes for the encoded remaining length as specified in
+   * section 2.2.3 of MQTT version 3.1.1.
+   */
+  _remainingLength: function(aLength) {
+    let bytes = [];
+    do {
+      let encodedByte = aLength % 128;
+      aLength >>>= 7;
+
+      // If there are more data to encode, set the top bit of this byte.
+      if (aLength > 0)
+        encodedByte |= 128
+
+      bytes.push(encodedByte);
+    } while (aLength > 0);
+
+    // Verify that the length of bytes is <= 4.
+    if (bytes.length > 4)
+      throw "Attempting to serialize too large of an MQTT message: " + aLength;
+
+    return bytes;
+  },
+
+  /*
+   * Calculate the 'remaining length' after the fixed header.
+   */
+  _calculateLength: function() {
+    let length = 0;
+
+    // Each variable header field length.
+    for (let header of this.headers) {
+      if (typeof header.value == "string")
+        // Two byte length + UTF-8 byte length.
+        length += 2 + byteLength(header.value);
+      else if (typeof header.value == "number")
+        length += header.count;
+      else
+        throw "Invalid value type for MQTT."
+    }
+
+    // Now add the length of the payload.
+    length += this.payload.length;
+
+    return length;
+  },
+
+  /*
+   * Add a field to the variable length header.
+   *
+   * Inputs are:
+   *  aName   Used for debugging.
+   *  aValue  The value of the header field
+   *  aCount  (Optional) The number of bytes for the header field. Used when the
+   *          number of bytes is not obvious from aValue. Defaults to 1.
+   */
+  addHeaderField: function(aName, aValue, aCount=1) {
+    this.headers.push({name: aName, value: aValue, count: aCount});
+  },
+
+  toString: function() {
+    // Try to convert the type to a human readable string.
+    let str_type = this.type;
+    for (let type in MqttPacket.TYPES) {
+      if (this.type == MqttPacket.TYPES[type]) {
+        str_type = type;
+        break;
+      }
+    }
+
+    let str = "Type: " + str_type + " (Flags: 0x" +
+              ("0" + this.flags.toString(16)).slice(-2) + ")\n";
+
+    // Add each header.
+    for (let header of this.headers)
+      str += header.name + ": " + header.value + "\n";
+
+    if (this.payload) {
+      str += "Payload: 0x";
+      for (let i = 0; i < this.payload.length; ++i)
+        str += ("0" + this.payload.charCodeAt(i).toString(16)).slice(-2) + " ";
+      str += "\n";
+    }
+
+    return str;
+  }
+}
+
+function MqttSocket(aAccount) {
+    this._account = aAccount;
+}
+MqttSocket.prototype = {
+  __proto__: Socket,
+  binaryMode: true,
+  connectTimeout: 90, // Failure to connect after 90 seconds
+  //readWriteTimeout: 300, // Failure when no data for 5 minutes
+
+  kTimeBeforePing: 60, // Send a ping every 60 seconds
+  kTimeAfterPingBeforeDisconnect: 90, // Wait 90 seconds for a response
+
+  connect: function() {
+    // Open an TLS socket to the MQTT server.
+    Socket.connect.call(this, FB_MQTT_HOST, FB_MQTT_PORT, ["ssl"]);
+  },
+
+  onConnection: Task.async(function *() {
+    // Send the initial payload / set-up the MQTT connection.
+
+    // Create the thrift object.
+    let user = new User({
+      user_id: this._account._userId,
+      visible: true, // TODO Get from imStatusInfo
+      device_id: this._account._deviceId,
+      mqtt_id: this._account._mqttId
+    });
+    let thrift = new Connect({
+      client_id: this._account._clientId,
+      user: user,
+      token: this._account._token
+    });
+
+    // Write to a byte-stream.
+    let transport = new Transport();
+    let protocol = new TCompactProtocol(transport);
+    thrift.write(protocol);
+
+    // Zip the thrift object.
+    let bytes = yield deflateString(transport.buf);
+
+    // Create the MQTT CONNECT packet.
+    let connect = new MqttPacket(MqttPacket.TYPES.CONNECT);
+    connect.addHeaderField("Protocol Name", MqttPacket.PROTOCOL_NAME);
+    connect.addHeaderField("Protocol Level", MqttPacket.PROTOCOL_LEVEL);
+    let flags = MqttPacket.FLAGS.CONNECT.USERNAME |
+      MqttPacket.FLAGS.CONNECT.PASSWORD |
+      MqttPacket.FLAGS.CONNECT.WILL_QoS1 |
+      MqttPacket.FLAGS.CONNECT.CLEAN_SESSION;
+    connect.addHeaderField("Connect flags", flags);
+    connect.addHeaderField("Keep Alive", this.kTimeBeforePing, 2);
+    // Add the payload.
+    connect.payload = bytes;
+
+    // Send it!
+    this.sendPacket(connect);
+  }),
+
+  onBinaryDataReceived: function(aData) {
+    let packet = new MqttPacket();
+    let consumed = packet.fromArrayBuffer(aData);
+
+    if (consumed) {
+      // Do someting with the packet.
+      this.DEBUG(packet.toString());
+    }
+
+    return consumed;
+  },
+
+  sendPacket: function(aPacket) {
+    this.DEBUG(ArrayBufferToHexString(aPacket.toArrayBuffer()))
+    this.sendBinaryData(aPacket.toArrayBuffer(), aPacket.toString());
+  },
+
+  get DEBUG() { return this._account.DEBUG; },
+  get LOG() { return this._account.LOG; },
+  get WARN() { return this._account.WARN; },
+  get ERROR() { return this._account.ERROR; }
+}
diff --git a/chat/protocols/facebook/facebook.js b/chat/protocols/facebook/facebook.js
--- a/chat/protocols/facebook/facebook.js
+++ b/chat/protocols/facebook/facebook.js
@@ -1,36 +1,279 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var {interfaces: Ci, utils: Cu} = Components;
-
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+ 
+Cu.import("resource://gre/modules/Http.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/facebook-mqtt.jsm");
+Cu.import("resource:///modules/imServices.jsm");
 Cu.import("resource:///modules/imXPCOMUtils.jsm");
 Cu.import("resource:///modules/jsProtoHelper.jsm");
-
+ 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
   l10nHelper("chrome://chat/locale/facebook.properties")
 );
+XPCOMUtils.defineLazyServiceGetter(this, "UuidGenerator",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
 
-function FacebookAccount(aProtoInstance, aImAccount) {
-  this._init(aProtoInstance, aImAccount);
+// Hostnames for the Facebook APIs.
+const FB_API_B_HOST = "https://b-api.facebook.com";
+const FB_GRAPH_HOST = "https://graph.facebook.com";
+
+// From Facebook Messenger for Android.
+const FB_API_KEY = "256002347743983";
+const FB_API_SECRET = "374e60f8b9bb6b8cbb30f78030438895";
+
+// Miscellaneous constants.
+const FB_API_CONTACTS_COUNT = "500";
+
+// TODO Stolen from xmpp-authmechs.jsm, abstract this somewhere.
+// md5 function adapted from netwerk/test/unit/test_authentication.js
+// If aUTF8 is true, aString will be treated as an UTF8 encoded string,
+// otherwise it can contain binary data.
+function md5(aString, aUTF8) {
+  let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+  ch.init(ch.MD5);
+
+  let data;
+  if (aUTF8) {
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+    data = converter.convertToByteArray(aString);
+  }
+  else {
+    data = Object.keys(aString).map(i => aString.charCodeAt(i));
+  }
+
+  ch.update(data, data.length);
+  return ch.finish(false);
 }
-FacebookAccount.prototype = {
-  __proto__: GenericAccountPrototype,
+function md5hex(aString) {
+  let hash = md5(aString);
+  function toHexString(charCode) { return ("0" + charCode.toString(16)).slice(-2); }
+  return Object.keys(hash).map(i => toHexString(hash.charCodeAt(i))).join("");
+}
 
-  connect: function() {
-    this.WARN("As Facebook deprecated its XMPP gateway, it is currently not " +
-              "possible to connect to Facebook Chat. See bug 1141674.");
-    this.reportDisconnecting(Ci.prplIAccount.ERROR_OTHER_ERROR,
-                             _("facebook.disabled"));
-    this.reportDisconnected();
+// TODO Move this into Http.jsm.
+function HttpRequestPromise(aUrl, aOptions) {
+  return new Promise(function(aResolve, aReject) {
+    // TODO This modifies the aOptions object and will leak back to the caller.
+    aOptions['onLoad'] = aResolve;
+    aOptions['onError'] = aReject;
+
+    httpRequest(aUrl, aOptions)
+  });
+}
+
+function SmsPrompt(aAccount, aName) {
+  this._init(aAccount, aName);
+
+  this.promise = new Promise((aResolve, aReject) => {
+    this.resolve = aResolve;
+    this.reject = aReject;
+  })
+}
+SmsPrompt.prototype = {
+  __proto__: GenericConvIMPrototype,
+
+  sendMsg: function(aMsg) {
+    this.resolve(aMsg.trim());
   }
 };
 
+function FacebookAccountBuddy(aAccount, aData) {
+  let name = aData['structured_name']['text'];
+  this._data = aData;
+  this._init(aAccount, null, Services.tags.defaultTag, name);
+}
+FacebookAccountBuddy.prototype = {
+  __proto__: GenericAccountBuddyPrototype,
+
+  get normalizedName() { return this._data['represented_profile']['id']; }
+
+  // TODO Implement tooltips.
+};
+ 
+ function FacebookAccount(aProtoInstance, aImAccount) {
+   this._init(aProtoInstance, aImAccount);
+ }
+ FacebookAccount.prototype = {
+  __proto__: GenericAccountPrototype,
+ 
+  connect: function() {
+  connect: Task.async(function* () {
+    // If no MQTT client ID has been created, generate a random one.
+    if (!this.prefs.prefHasUserValue("clientId")) {
+      // 20 random alphanumeric characters.
+      const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+      let clientId = "";
+      for (let i = 0; i < 20; ++i)
+        clientId += chars.charAt(Math.floor(Math.random() * chars.length));
+      this.prefs.setCharPref("clientId", clientId);
+    }
+    this._clientId = this.prefs.getCharPref("clientId");
+
+    // If no device ID has been created, generate a random one.
+    if (!this.prefs.prefHasUserValue("deviceId")) {
+      let deviceId = UuidGenerator.generateUUID().toString().slice(1, -1);
+      this.prefs.setCharPref("deviceId", deviceId);
+    }
+    this._deviceId = this.prefs.getCharPref("deviceId");
+
+    // If no MQTT ID exists, generate a random one.
+    if (!this.prefs.prefHasUserValue("mqttId")) {
+      // A random 32-bit integer.
+      this._mqttId = this.prefs.setCharPref("mqttId",
+                                            Math.floor(Math.random() * 65536));
+    }
+    this._mqttId = this.prefs.getCharPref("mqttId");
+
+    // If a MQTT synchronization token exists, load it.
+    if (this.prefs.prefHasUserValue("syncToken"))
+      this._syncToken = this.prefs.getCharPref("syncToken");
+    else
+      this._syncToken = null;
+    
+    // Load previous OAuth information if available.
+    if (this.prefs.prefHasUserValue("accessToken"))
+      this._token = this.prefs.getCharPref("accessToken")
+    if (this.prefs.prefHasUserValue("userId"))
+      this._userId = this.prefs.getCharPref("userId")
+
+    // If OAuth information was not available, load it.
+    if (!this._token || !this._userId)
+      yield this._authenticate()
+
+    //// Fetch all of the user's contacts.
+    //this.reportConnecting("Fetching contacts");
+    //data = yield this._graph_request(
+    //  "FetchContactsFullQuery", "10153856456271729",
+    //  {"0": ["user"], "1": FB_API_CONTACTS_COUNT})
+    //
+    //// Iterate through the returned contacts and create the account buddies.
+    //for (let contactData of data['viewer']['messenger_contacts']['nodes']) {
+    //  // Skip contacts who we're not friends with, e.g. ourself.
+    //  if (contactData['represented_profile']['friendship_status'] != 'ARE_FRIENDS')
+    //    continue;
+    //
+    //    let id = contactData['represented_profile']['id'];
+    //
+    //    // In order to be backward compatible with old accounts that used the
+    //    // XMPP transport.
+    //    id = '-' + id + '@chat.facebook.com';
+    //
+    //  let buddy = new FacebookAccountBuddy(this, contactData);
+    //  Services.contacts.accountBuddyAdded(buddy);
+    //}
+
+    // TODO Handle multiple pages of contacts.
+    // TODO Remove buddies who you are no longer friends with?
+    // TODO Set timer to sync contacts every 5 - 30 minutes.
+
+    // Finally, start the MQTT connection.
+    this._connection = new MqttSocket(this);
+    this._connection.connect();
+  }),
+
+  _request: function(aUrl, aApiName, aApiMethod, aParams=[]) {
+    // Include the OAuth token if it was already received.
+    let headers = []
+    if (this._token)
+      headers.push(["Authorization", "OAuth " + this._token]);
+
+    let params = [
+        ['fb_api_req_friendly_name', aApiName],
+        ['method', aApiMethod],
+        ['api_key', FB_API_KEY],
+        ['device_id', this._deviceId],
+        ['format', 'json'],
+        ['locale', 'en_US']
+    ].concat(aParams);
+
+    // Calculate a signature (MD5 of the sorted key/value pairs).
+    let data = params.sort((a, b) => a[0] > b[0])
+                     .map(p => p[0] + '=' + p[1]).join('');
+    // And append the secret key.
+    data += FB_API_SECRET
+    params.push(['sig', md5hex(data)])
+
+    let account = this;
+    return HttpRequestPromise(aUrl, {
+      postData: params,
+      headers: headers
+    }).then(
+      (aResponse, aXhr) => {
+        this.LOG("Response receieved for request: " + aUrl + "\n" + aResponse);
+        return JSON.parse(aResponse);
+      },
+      (aError, aResponse, aXhr) => {
+        this.ERROR("Error received: " + aError + "\nRequest: " + aUrl + "\n" +
+                   JSON.stringify(params) + "\nResponse: " + aResponse);
+        return aError;
+      }
+    )
+  },
+
+  /*
+   * Make a request to the Facebook graph API. Note that aApiName and aQueryId
+   * match in a one-to-one fashion.
+   */
+  _graph_request: function(aApiName, aQueryId, aObj) {
+    let url = FB_GRAPH_HOST + "/graphql";
+    let apiMethod = "get";
+    let params = [
+      ['query_id', aQueryId],
+      ['query_params', JSON.stringify(aObj)]
+    ];
+
+    return this._request(url, aApiName, apiMethod, params)
+  },
+
+  // Handle the Facebook authentication dance.
+  _authenticate: Task.async(function* () {
+    this.reportConnecting("Authenticating");
+    let data = yield this._request(
+      FB_API_B_HOST + "/method/auth.login", 'authenticate', 'auth.login',
+      [['email', this.name], ['password', this.imAccount.password]]);
+    // Error code 406 is login approvals (aka 2FA).
+    if (data["error_code"] == 406) {
+      // Prompt the user for an SMS code.
+      let conv = new SmsPrompt(this, "2FA prompt");
+      // Pretend the account is connected so conversations can send messages.
+      this.reportConnected();
+      conv.writeMessage(null, "Please enter your two factor code.",
+                        {system: true});
+      let password = yield conv.promise;
+
+      // Do the same request but use the SMS code as the password.
+      this.reportConnecting("Authenticating");
+      data = yield this._request(
+        FB_API_B_HOST + "/method/auth.login", 'authenticate', 'auth.login',
+        [['email', this.name], ['password', password]]);
+    }
+    // TODO Error code 401 is wrong password.
+
+    // No error code.
+    // This returns the session_key, uid, secret and access_token and
+    // machine_id. Store the access token and user ID.
+    this._userId = data['uid'];
+    this._token = data['access_token'];
+
+    // Save this data for subsequent sessions.
+    this.prefs.setCharPref("userId", this._userId);
+    this.prefs.setCharPref("accessToken", this._token);
+  })
+};
+ 
 function FacebookProtocol() {}
 FacebookProtocol.prototype = {
   __proto__: GenericProtocolPrototype,
   get normalizedName() { return "facebook"; },
   get name() { return _("facebook.chat.name"); },
   get iconBaseURI() { return "chrome://prpl-facebook/skin/"; },
   getAccount: function(aImAccount) { return new FacebookAccount(this, aImAccount); },
   classID: Components.ID("{1d1d0bc5-610c-472f-b2cb-4b89857d80dc}")
diff --git a/chat/protocols/facebook/moz.build b/chat/protocols/facebook/moz.build
--- a/chat/protocols/facebook/moz.build
+++ b/chat/protocols/facebook/moz.build
@@ -1,11 +1,21 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
+
+DIRS += [
+    'thrift',
+]
+
 EXTRA_COMPONENTS += [
     'facebook.js',
     'facebook.manifest',
 ]
 
+EXTRA_JS_MODULES += [
+    'facebook-mqtt.jsm',
+]
+
 JAR_MANIFESTS += ['jar.mn']
diff --git a/chat/protocols/facebook/test/test_FacebookThrift.js b/chat/protocols/facebook/test/test_FacebookThrift.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/test/test_FacebookThrift.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+const { Loader, Require, Module } = Components.utils.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+Components.utils.import("resource:///modules/thrift/Buffer.jsm");
+
+var mqtt = {};
+Services.scriptloader.loadSubScript("resource:///modules/facebook-mqtt.jsm", mqtt);
+
+// Set-up loading so require works properly in CommonJS modules.
+let loader = Loader({
+  // Point all loading to the thrift subdirectory of modules.
+  paths: {
+    '': 'resource:///modules/thrift/',
+    './': 'resource:///modules/thrift/',
+  },
+
+  // Use the proper file name for the Int64 module.
+  resolve: function(id, requirer) {
+    if (id == "node-int64")
+      return "Int64";
+    return id;
+  },
+
+  // Include the Buffer global.
+  globals: {
+    Buffer: Buffer
+  }
+})
+
+let requirer = Module('thrift-module', '');
+let require = Require(loader, requirer);
+
+const TCompactProtocol = require('compact_protocol.js');
+const { User, Connect, UserPresence, Presence } = require('facebook_types.js');
+
+/* Convert a byte string to hex. */
+function toHex(aStr) {
+  let hex = "";
+  for (let i = 0; i < aStr.length; ++i)
+    hex += ("0" + aStr.charCodeAt(i).toString(16)).slice(-2) + " ";
+  return hex.trim();
+}
+
+/* Convert a hex string to bytes. */
+function toBinary(aHex) {
+  let data = aHex.split(' ');
+  for (let i = 0; i < data.length; ++i)
+      data[i] = String.fromCharCode(parseInt(data[i], 16));
+  return data.join('');
+}
+
+function run_test() {
+  add_test(testWrite);
+  add_test(testRead);
+  add_test(testDeflate);
+
+  run_next_test();
+}
+
+const EXPECTED_THRIFT = "18 14 4b 49 7a 30 30 34 7a 76 36 55 38 50 69 61 61 38 65 52 66 41 3c 16 d6 b0 84 05 18 00 16 2e 16 34 15 02 11 11 18 24 37 66 33 38 33 31 36 32 2d 64 37 39 62 2d 34 66 30 61 2d 39 30 37 64 2d 62 64 31 33 36 63 38 34 33 38 36 61 11 15 02 15 00 16 ca be e9 e0 23 29 05 00 18 c8 01 5a 4d 62 62 4b 74 50 7a 77 6c 73 42 71 34 37 46 75 47 53 79 51 46 55 42 61 62 68 4e 52 6b 4d 4b 64 57 56 70 72 6a 5a 66 34 6b 56 43 76 58 75 56 4e 58 72 33 47 51 74 6f 6a 35 4a 38 32 52 34 4a 6c 43 6d 38 75 72 46 64 56 76 4e 31 66 72 43 35 76 56 39 37 56 6c 39 71 33 6b 34 35 32 6a 74 46 36 63 42 7a 54 33 41 67 31 35 49 35 6c 6b 56 64 6a 34 45 63 73 6a 46 6c 61 41 4a 75 4b 35 68 75 34 69 77 67 6e 51 67 53 37 46 48 39 44 47 36 4b 47 69 45 49 6e 47 34 51 67 45 37 39 45 68 72 64 32 73 76 79 4e 64 68 74 79 70 41 6e 62 32 55 6c 72 52 77 65 4b 39 68 70 6c 77 70 6b 33 69 48 45 78 53 65 69 49 79 32 4c 00";
+const EXPECTED_COMPRESSED = '78 9c 05 c1 4b 4e 83 40 00 00 50 34 e9 01 9a 58 c2 5e 37 2e 30 c0 0c 0c 93 b8 a1 15 28 c5 92 42 ed d8 74 07 9d f2 1d 2b a5 7c 84 b5 07 f2 04 ae 8d 07 70 ed d6 5b f8 9e 70 e5 3a 83 24 c1 a1 d5 36 fa 2a 0b 43 fd 10 c4 c6 3d ff f3 f1 3e 12 38 fe 8e 87 93 cb f1 58 b8 41 31 d0 81 ac 29 22 45 38 12 61 2c 85 22 96 10 15 23 2a 03 6d af 43 a0 6b e1 78 72 39 e1 f8 ef cf bf df eb db 11 27 7c 5d ec 96 51 e4 d6 ab a1 63 e7 e9 09 22 ab b1 d7 bd 6f 6d a6 61 94 7a 41 b1 74 e9 33 29 ab 7c 17 c3 82 cc da 6d 43 bc 6d 05 6c bf 7e cd d5 85 ae 04 70 c1 66 2f 7a 53 59 94 b4 9e 1c 57 33 b5 25 18 11 86 4f a0 80 aa 92 d7 96 b6 9f 0e 4f c0 48 64 d5 51 59 41 68 0e cd fd 39 b7 58 68 2c 1a 57 4d 1b 98 75 c9 d1 4f d6 c8 9a e3 07 5b 73 ed cc 74 8e 36 f4 13 13 61 33 ad a8 72 6e 7b 8f a6 75 5f 1a c7 48 d9 b0 2a e8 0e 2e 4e 4b d6 95 05 c8 e6 e6 db fa 90 39 bd f2 c8 fd 03 26 77 5b fd'
+const USER_ID = "5278763";
+const VISIBLE = true;
+const DEVICE_ID = "7f383162-d79b-4f0a-907d-bd136c84386a";
+const MQTT_ID = "4799147941";
+const CLIENT_ID = "KIz004zv6U8Piaa8eRfA";
+const TOKEN = "ZMbbKtPzwlsBq47FuGSyQFUBabhNRkMKdWVprjZf4kVCvXuVNXr3GQtoj5J82R4JlCm8urFdVvN1frC5vV97Vl9q3k452jtF6cBzT3Ag15I5lkVdj4EcsjFlaAJuK5hu4iwgnQgS7FH9DG6KGiEInG4QgE79Ehrd2svyNdhtypAnb2UlrRweK9hplwpk3iHExSeiIy2L";
+
+function testWrite() {
+  // Create the thrift object.
+  let user = new User({
+    user_id: USER_ID,
+    visible: VISIBLE,
+    device_id: DEVICE_ID,
+    mqtt_id: MQTT_ID
+  });
+  let thrift = new Connect({
+    client_id: CLIENT_ID,
+    user: user,
+    token: TOKEN
+  });
+
+  // Write to a byte-stream.
+  let transport = new mqtt.Transport();
+  let protocol = new TCompactProtocol(transport);
+  thrift.write(protocol);
+
+  // Ensure it matches the expected hex data.
+  equal(EXPECTED_THRIFT, toHex(transport.buf));
+
+  run_next_test();
+}
+
+function testRead() {
+  // Set up the transport with the data.
+  let transport = new mqtt.Transport(toBinary(EXPECTED_THRIFT));
+  let protocol = new TCompactProtocol(transport);
+
+  // Create a new object and read it from the byte stream.
+  let thrift = new Connect();
+  thrift.read(protocol);
+
+  // Assert the results.
+  equal(thrift.client_id, CLIENT_ID);
+  equal(thrift.user.user_id.toString(), USER_ID);
+  equal(thrift.user.visible, VISIBLE);
+  equal(thrift.user.device_id, DEVICE_ID);
+  equal(thrift.user.mqtt_id.toString(), MQTT_ID);
+  equal(thrift.token, TOKEN);
+
+  run_next_test();
+}
+
+function testDeflate() {
+  mqtt.deflateString(toBinary(EXPECTED_THRIFT)).then(function(aResult) {
+    equal(toHex(aResult), EXPECTED_COMPRESSED);
+
+    run_next_test();
+  }).catch(function(aErr) {
+    ok(false)
+
+    run_next_test();
+  });
+}
diff --git a/chat/protocols/facebook/test/test_MqttMessage.js b/chat/protocols/facebook/test/test_MqttMessage.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/test/test_MqttMessage.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource:///modules/ArrayBufferUtils.jsm");
+var mqtt = {};
+Services.scriptloader.loadSubScript("resource:///modules/facebook-mqtt.jsm", mqtt);
+
+function run_test() {
+  add_test(testPacketLength);
+  add_test(testRemainingLength);
+  add_test(testPacketToArrayBuffer);
+  add_test(testPacketFromArrayBuffer);
+  add_test(testPacketString);
+
+  run_next_test();
+}
+
+/*
+ * Ensure that calculating the length of a message works correctly.
+ */
+function testPacketLength() {
+  let msg = new mqtt.MqttPacket(mqtt.MqttPacket.TYPES.CONNECT);
+
+  equal(msg._calculateLength(), 0);
+
+  // Add a string.
+  msg.addHeaderField("Protocol Name", mqtt.MqttPacket.PROTOCOL_NAME);
+  equal(msg.headers.length, 1);
+  equal(msg._calculateLength(), 8);
+
+  // Add a 1 byte int.
+  msg.addHeaderField("Protocol Level", mqtt.MqttPacket.PROTOCOL_LEVEL);
+  equal(msg.headers.length, 2);
+  equal(msg._calculateLength(), 9);
+
+  // Add a 2 byte int.
+  msg.addHeaderField("Keep Alive", 10, 2);
+  equal(msg.headers.length, 3);
+  equal(msg._calculateLength(), 11);
+
+  // Add a payload.
+  msg.payload = "test"
+  equal(msg.headers.length, 3);
+  equal(msg._calculateLength(), 15);
+
+  run_next_test();
+}
+
+/*
+ * Ensure the remaining length is calculate correctly.
+ */
+function testRemainingLength() {
+  let msg = new mqtt.MqttPacket(mqtt.MqttPacket.TYPES.CONNECT);
+
+  // Examples from section 2.2.3 of MQTT version 3.1.1.
+  equal_array(msg._remainingLength(64), [64]);
+  equal_array(msg._remainingLength(321), [193, 2]);
+
+  // Test the bounds of each point.
+  equal_array(msg._remainingLength(0), [0x00]);
+  equal_array(msg._remainingLength(127), [0x7F]);
+  equal_array(msg._remainingLength(128), [0x80, 0x01]);
+  equal_array(msg._remainingLength(16383), [0xFF, 0x7F]);
+  equal_array(msg._remainingLength(16384), [0x80, 0x80, 0x01]);
+  equal_array(msg._remainingLength(2097151), [0xFF, 0xFF, 0x7F]);
+  equal_array(msg._remainingLength(2097152), [0x80, 0x80, 0x80, 0x01]);
+  equal_array(msg._remainingLength(268435455), [0xFF, 0xFF, 0xFF, 0x7F]);
+
+  // This generates a message that is > 4 bytes in length.
+  Assert.throws(function() {
+    msg._remainingLength(268435456);
+  });
+
+  run_next_test();
+}
+
+/*
+ * Test that a message can be serialized to an ArrayBuffer correctly.
+ */
+function testPacketToArrayBuffer() {
+  let msg = new mqtt.MqttPacket(mqtt.MqttPacket.TYPES.CONNECT);
+
+  let buffer = ArrayBufferToHexString(msg.toArrayBuffer());
+
+  // Connect packet with no flags and 0 length.
+  let EXPECTED_BUFFER = "0x10 00";
+  equal(buffer, EXPECTED_BUFFER);
+
+  // Add a string.
+  msg.addHeaderField("Protocol Name", mqtt.MqttPacket.PROTOCOL_NAME);
+  buffer = ArrayBufferToHexString(msg.toArrayBuffer());
+
+  // See figure 3.2 of MQTT version 3.1.1.
+  EXPECTED_BUFFER = "0x10 08 00 06 4d 51 54 54 6f 54";
+  equal(buffer, EXPECTED_BUFFER);
+
+  // Add a 1 byte int.
+  msg.addHeaderField("Protocol Level", mqtt.MqttPacket.PROTOCOL_LEVEL);
+  buffer = ArrayBufferToHexString(msg.toArrayBuffer());
+
+  // See figure 3.3 of MQTT version 3.1.1.
+  EXPECTED_BUFFER = "0x10 09 00 06 4d 51 54 54 6f 54 03";
+  equal(buffer, EXPECTED_BUFFER);
+
+  // Add a 2 byte int.
+  msg.addHeaderField("Keep Alive", 10, 2);
+  buffer = ArrayBufferToHexString(msg.toArrayBuffer());
+
+  // See figure 3.5 of MQTT version 3.1.1.
+  EXPECTED_BUFFER = "0x10 0b 00 06 4d 51 54 54 6f 54 03 00 0a";
+  equal(buffer, EXPECTED_BUFFER);
+
+  // Add a payload. (Note that this will usually be thrift, but don't worry
+  // about that here.)
+  msg.payload = "test"
+  buffer = ArrayBufferToHexString(msg.toArrayBuffer());
+  EXPECTED_BUFFER = "0x10 0f 00 06 4d 51 54 54 6f 54 03 00 0a 74 65 73 74";
+  equal(buffer, EXPECTED_BUFFER);
+
+  run_next_test();
+}
+
+/*
+ * Test that a message can be recovered from an ArrayBuffer correctly.
+ */
+function testPacketFromArrayBuffer() {
+  // First test some cases that are not enough data.
+  let BUFFER = HexStringToArrayBuffer("0x10");
+  let msg = new mqtt.MqttPacket();
+  let count = msg.fromArrayBuffer(BUFFER);
+  equal(count, 0);
+
+  // Not enough for the variable length.
+  BUFFER = HexStringToArrayBuffer("0x10 F0");
+  msg = new mqtt.MqttPacket();
+  count = msg.fromArrayBuffer(BUFFER);
+  equal(count, 0);
+
+  // Not enough bytes.
+  BUFFER = HexStringToArrayBuffer("0x10 10");
+  msg = new mqtt.MqttPacket();
+  count = msg.fromArrayBuffer(BUFFER);
+  equal(count, 0);
+
+  // Now build the same message from testPacketToArrayBuffer.
+  // See figure 3.5 of MQTT version 3.1.1.
+  BUFFER = HexStringToArrayBuffer("0x10 0b 00 06 4d 51 54 54 6f 54 03 00 0a");
+  msg = new mqtt.MqttPacket();
+  count = msg.fromArrayBuffer(BUFFER);
+
+  equal(msg.type, mqtt.MqttPacket.TYPES.CONNECT);
+  equal(count, 13);
+  equal(msg.headers.length, 3);
+  equal(msg.headers[0].name, "Protocol Name");
+  equal(msg.headers[0].value, mqtt.MqttPacket.PROTOCOL_NAME);
+  equal(msg.headers[1].name, "Protocol Level");
+  equal(msg.headers[1].value, mqtt.MqttPacket.PROTOCOL_LEVEL);
+  equal(msg.headers[2].name, "Keep Alive");
+  equal(msg.headers[2].value, 10);
+
+  // Add a payload. (Note that this will usually be thrift, but don't worry
+  // about that here.)
+  BUFFER = HexStringToArrayBuffer("0x10 0f 00 06 4d 51 54 54 6f 54 03 00 0a 74 65 73 74");
+  msg = new mqtt.MqttPacket();
+  count = msg.fromArrayBuffer(BUFFER);
+
+  equal(msg.type, mqtt.MqttPacket.TYPES.CONNECT);
+  equal(count, 17);
+  equal(msg.headers.length, 3);
+  equal(msg.headers[0].name, "Protocol Name");
+  equal(msg.headers[0].value, mqtt.MqttPacket.PROTOCOL_NAME);
+  equal(msg.headers[1].name, "Protocol Level");
+  equal(msg.headers[1].value, mqtt.MqttPacket.PROTOCOL_LEVEL);
+  equal(msg.headers[2].name, "Keep Alive");
+  equal(msg.headers[2].value, 10);
+  equal(msg.payload, "test");
+
+  run_next_test();
+}
+
+function equal_array(arr1, arr2) {
+  equal(arr1.length, arr2.length);
+  for (let i = 0; i < arr1.length; ++i)
+    equal(arr1[i], arr2[i]);
+}
+
+/*
+ * Test that a message can be represented as a String correctly.
+ */
+function testPacketString() {
+  let msg = new mqtt.MqttPacket(mqtt.MqttPacket.TYPES.CONNECT);
+
+  // Connect packet with no flags and 0 length.
+  let EXPECTED_STRING = "Type: CONNECT (Flags: 0x00)\n";
+  equal(msg.toString(), EXPECTED_STRING);
+
+  // Add a string.
+  msg.addHeaderField("Protocol Name", mqtt.MqttPacket.PROTOCOL_NAME);
+
+  // See figure 3.2 of MQTT version 3.1.1.
+  EXPECTED_STRING += "Protocol Name: MQTToT\n";
+  equal(msg.toString(), EXPECTED_STRING);
+
+  // Add a 1 byte int.
+  msg.addHeaderField("Protocol Level", mqtt.MqttPacket.PROTOCOL_LEVEL);
+
+  // See figure 3.3 of MQTT version 3.1.1.
+  EXPECTED_STRING += "Protocol Level: 3\n";
+  equal(msg.toString(), EXPECTED_STRING);
+
+  // Add a 2 byte int.
+  msg.addHeaderField("Keep Alive", 10, 2);
+
+  // See figure 3.5 of MQTT version 3.1.1.
+  EXPECTED_STRING += "Keep Alive: 10\n";
+  equal(msg.toString(), EXPECTED_STRING);
+
+  // Add a payload. (Note that this will usually be thrift, but don't worry
+  // about that here.)
+  msg.payload = "test"
+  EXPECTED_STRING += "Payload: 0x74 65 73 74 \n";
+  equal(msg.toString(), EXPECTED_STRING);
+
+  run_next_test();
+}
diff --git a/chat/protocols/facebook/test/xpcshell.ini b/chat/protocols/facebook/test/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/test/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+
+[test_FacebookThrift.js]
+[test_MqttMessage.js]
diff --git a/chat/protocols/facebook/thrift/Buffer.jsm b/chat/protocols/facebook/thrift/Buffer.jsm
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/Buffer.jsm
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["Buffer", "byteLength"];
+
+// Buffer is essentially Uint8Array: https://nodejs.org/api/buffer.html#buffer_buffer.
+var Buffer = Uint8Array;
+
+// TODO Copied from irc.js.
+function byteLength(string, encoding='utf8') {
+  if (encoding != 'utf8')
+    throw "byteLength only works for utf8!";
+
+  // Count the number of bytes in a UTF-8 encoded string.
+  function charCodeToByteCount(c) {
+    // UTF-8 stores:
+    // - code points below U+0080 on 1 byte,
+    // - code points below U+0800 on 2 bytes,
+    // - code points U+D800 through U+DFFF are UTF-16 surrogate halves
+    // (they indicate that JS has split a 4 bytes UTF-8 character
+    // in two halves of 2 bytes each),
+    // - other code points on 3 bytes.
+    return c < 0x80 ? 1 : (c < 0x800 || (c >= 0xD800 && c <= 0xDFFF)) ? 2 : 3;
+  }
+  let bytes = 0;
+  for (let i = 0; i < string.length; ++i)
+    bytes += charCodeToByteCount(string.charCodeAt(i));
+  return bytes;
+}
+
+Buffer.byteLength = byteLength
+
+// This is essentially copyBytes from ArrayBufferUtils, but the inputs are
+// TypedArrays, not ArrayBuffers.
+Buffer.prototype.copy = function(targetBuffer, targetStart, sourceStart, sourceEnd) {
+  let target = new Uint8Array(targetBuffer.buffer, targetStart);
+  target.set(new Uint8Array(this.buffer, sourceStart, sourceEnd));
+}
+
+Buffer.prototype.readUInt32BE = function(offset, noAssert=false) {
+  let view = new DataView(this.buffer, offset, 4);
+  return view.getUint32(0, false);
+}
diff --git a/chat/protocols/facebook/thrift/Int64.js b/chat/protocols/facebook/thrift/Int64.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/Int64.js
@@ -0,0 +1,268 @@
+//     Int64.js
+//
+//     Copyright (c) 2012 Robert Kieffer
+//     MIT License - http://opensource.org/licenses/mit-license.php
+
+/**
+ * Support for handling 64-bit int numbers in Javascript (node.js)
+ *
+ * JS Numbers are IEEE-754 binary double-precision floats, which limits the
+ * range of values that can be represented with integer precision to:
+ *
+ * 2^^53 <= N <= 2^53
+ *
+ * Int64 objects wrap a node Buffer that holds the 8-bytes of int64 data.  These
+ * objects operate directly on the buffer which means that if they are created
+ * using an existing buffer then setting the value will modify the Buffer, and
+ * vice-versa.
+ *
+ * Internal Representation
+ *
+ * The internal buffer format is Big Endian.  I.e. the most-significant byte is
+ * at buffer[0], the least-significant at buffer[7].  For the purposes of
+ * converting to/from JS native numbers, the value is assumed to be a signed
+ * integer stored in 2's complement form.
+ *
+ * For details about IEEE-754 see:
+ * http://en.wikipedia.org/wiki/Double_precision_floating-point_format
+ */
+
+// Useful masks and values for bit twiddling
+var MASK31 =  0x7fffffff, VAL31 = 0x80000000;
+var MASK32 =  0xffffffff, VAL32 = 0x100000000;
+
+// Map for converting hex octets to strings
+var _HEX = [];
+for (var i = 0; i < 256; i++) {
+  _HEX[i] = (i > 0xF ? '' : '0') + i.toString(16);
+}
+
+//
+// Int64
+//
+
+/**
+ * Constructor accepts any of the following argument types:
+ *
+ * new Int64(buffer[, offset=0]) - Existing Buffer with byte offset
+ * new Int64(Uint8Array[, offset=0]) - Existing Uint8Array with a byte offset
+ * new Int64(string)             - Hex string (throws if n is outside int64 range)
+ * new Int64(number)             - Number (throws if n is outside int64 range)
+ * new Int64(hi, lo)             - Raw bits as two 32-bit values
+ */
+var Int64 = module.exports = function(a1, a2) {
+  if (a1 instanceof Buffer) {
+    this.buffer = a1;
+    this.offset = a2 || 0;
+  } else if (Object.prototype.toString.call(a1) == '[object Uint8Array]') {
+    // Under Browserify, Buffers can extend Uint8Arrays rather than an
+    // instance of Buffer. We could assume the passed in Uint8Array is actually
+    // a buffer but that won't handle the case where a raw Uint8Array is passed
+    // in. We construct a new Buffer just in case.
+    this.buffer = new Buffer(a1);
+    this.offset = a2 || 0;
+  } else {
+    this.buffer = this.buffer || new Buffer(8);
+    this.offset = 0;
+    this.setValue.apply(this, arguments);
+  }
+};
+
+
+// Max integer value that JS can accurately represent
+Int64.MAX_INT = Math.pow(2, 53);
+
+// Min integer value that JS can accurately represent
+Int64.MIN_INT = -Math.pow(2, 53);
+
+Int64.prototype = {
+
+  constructor: Int64,
+
+  /**
+   * Do in-place 2's compliment.  See
+   * http://en.wikipedia.org/wiki/Two's_complement
+   */
+  _2scomp: function() {
+    var b = this.buffer, o = this.offset, carry = 1;
+    for (var i = o + 7; i >= o; i--) {
+      var v = (b[i] ^ 0xff) + carry;
+      b[i] = v & 0xff;
+      carry = v >> 8;
+    }
+  },
+
+  /**
+   * Set the value. Takes any of the following arguments:
+   *
+   * setValue(string) - A hexidecimal string
+   * setValue(number) - Number (throws if n is outside int64 range)
+   * setValue(hi, lo) - Raw bits as two 32-bit values
+   */
+  setValue: function(hi, lo) {
+    var negate = false;
+    if (arguments.length == 1) {
+      if (typeof(hi) == 'number') {
+        // Simplify bitfield retrieval by using abs() value.  We restore sign
+        // later
+        negate = hi < 0;
+        hi = Math.abs(hi);
+        lo = hi % VAL32;
+        hi = hi / VAL32;
+        if (hi > VAL32) throw new RangeError(hi  + ' is outside Int64 range');
+        hi = hi | 0;
+      } else if (typeof(hi) == 'string') {
+        hi = (hi + '').replace(/^0x/, '');
+        lo = hi.substr(-8);
+        hi = hi.length > 8 ? hi.substr(0, hi.length - 8) : '';
+        hi = parseInt(hi, 16);
+        lo = parseInt(lo, 16);
+      } else {
+        throw new Error(hi + ' must be a Number or String');
+      }
+    }
+
+    // Technically we should throw if hi or lo is outside int32 range here, but
+    // it's not worth the effort. Anything past the 32'nd bit is ignored.
+
+    // Copy bytes to buffer
+    var b = this.buffer, o = this.offset;
+    for (var i = 7; i >= 0; i--) {
+      b[o+i] = lo & 0xff;
+      lo = i == 4 ? hi : lo >>> 8;
+    }
+
+    // Restore sign of passed argument
+    if (negate) this._2scomp();
+  },
+
+  /**
+   * Convert to a native JS number.
+   *
+   * WARNING: Do not expect this value to be accurate to integer precision for
+   * large (positive or negative) numbers!
+   *
+   * @param allowImprecise If true, no check is performed to verify the
+   * returned value is accurate to integer precision.  If false, imprecise
+   * numbers (very large positive or negative numbers) will be forced to +/-
+   * Infinity.
+   */
+  toNumber: function(allowImprecise) {
+    var b = this.buffer, o = this.offset;
+
+    // Running sum of octets, doing a 2's complement
+    var negate = b[o] & 0x80, x = 0, carry = 1;
+    for (var i = 7, m = 1; i >= 0; i--, m *= 256) {
+      var v = b[o+i];
+
+      // 2's complement for negative numbers
+      if (negate) {
+        v = (v ^ 0xff) + carry;
+        carry = v >> 8;
+        v = v & 0xff;
+      }
+
+      x += v * m;
+    }
+
+    // Return Infinity if we've lost integer precision
+    if (!allowImprecise && x >= Int64.MAX_INT) {
+      return negate ? -Infinity : Infinity;
+    }
+
+    return negate ? -x : x;
+  },
+
+  /**
+   * Convert to a JS Number. Returns +/-Infinity for values that can't be
+   * represented to integer precision.
+   */
+  valueOf: function() {
+    return this.toNumber(false);
+  },
+
+  /**
+   * Return string value
+   *
+   * @param radix Just like Number#toString()'s radix
+   */
+  toString: function(radix) {
+    return this.valueOf().toString(radix || 10);
+  },
+
+  /**
+   * Return a string showing the buffer octets, with MSB on the left.
+   *
+   * @param sep separator string. default is '' (empty string)
+   */
+  toOctetString: function(sep) {
+    var out = new Array(8);
+    var b = this.buffer, o = this.offset;
+    for (var i = 0; i < 8; i++) {
+      out[i] = _HEX[b[o+i]];
+    }
+    return out.join(sep || '');
+  },
+
+  /**
+   * Returns the int64's 8 bytes in a buffer.
+   *
+   * @param {bool} [rawBuffer=false]  If no offset and this is true, return the internal buffer.  Should only be used if
+   *                                  you're discarding the Int64 afterwards, as it breaks encapsulation.
+   */
+  toBuffer: function(rawBuffer) {
+    if (rawBuffer && this.offset === 0) return this.buffer;
+
+    var out = new Buffer(8);
+    this.buffer.copy(out, 0, this.offset, this.offset + 8);
+    return out;
+  },
+
+  /**
+   * Copy 8 bytes of int64 into target buffer at target offset.
+   *
+   * @param {Buffer} targetBuffer       Buffer to copy into.
+   * @param {number} [targetOffset=0]   Offset into target buffer.
+   */
+  copy: function(targetBuffer, targetOffset) {
+    this.buffer.copy(targetBuffer, targetOffset || 0, this.offset, this.offset + 8);
+  },
+
+  /**
+   * Returns a number indicating whether this comes before or after or is the
+   * same as the other in sort order.
+   *
+   * @param {Int64} other  Other Int64 to compare.
+   */
+  compare: function(other) {
+
+    // If sign bits differ ...
+    if ((this.buffer[this.offset] & 0x80) != (other.buffer[other.offset] & 0x80)) {
+      return other.buffer[other.offset] - this.buffer[this.offset];
+    }
+
+    // otherwise, compare bytes lexicographically
+    for (var i = 0; i < 8; i++) {
+      if (this.buffer[this.offset+i] !== other.buffer[other.offset+i]) {
+        return this.buffer[this.offset+i] - other.buffer[other.offset+i];
+      }
+    }
+    return 0;
+  },
+
+  /**
+   * Returns a boolean indicating if this integer is equal to other.
+   *
+   * @param {Int64} other  Other Int64 to compare.
+   */
+  equals: function(other) {
+    return this.compare(other) === 0;
+  },
+
+  /**
+   * Pretty output in console.log
+   */
+  inspect: function() {
+    return '[Int64 value:' + this + ' octets:' + this.toOctetString(' ') + ']';
+  }
+};
diff --git a/chat/protocols/facebook/thrift/compact_protocol.js b/chat/protocols/facebook/thrift/compact_protocol.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/compact_protocol.js
@@ -0,0 +1,904 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var log = require('./log');
+var Int64 = require('node-int64');
+var Thrift = require('./thrift');
+var Type = Thrift.Type;
+
+module.exports = TCompactProtocol;
+
+var POW_8 = Math.pow(2, 8);
+var POW_24 = Math.pow(2, 24);
+var POW_32 = Math.pow(2, 32);
+var POW_40 = Math.pow(2, 40);
+var POW_48 = Math.pow(2, 48);
+var POW_52 = Math.pow(2, 52);
+var POW_1022 = Math.pow(2, 1022);
+
+/**
+ * Constructor Function for the Compact Protocol.
+ * @constructor
+ * @param {object} [trans] - The underlying transport to read/write.
+ * @classdesc The Apache Thrift Protocol layer performs serialization
+ *     of base types, the compact protocol serializes data in binary
+ *     form with minimal space used for scalar values.
+ */
+function TCompactProtocol(trans) {
+  this.trans = trans;
+  this.lastField_ = [];
+  this.lastFieldId_ = 0;
+  this.string_limit_ = 0;
+  this.string_buf_ = null;
+  this.string_buf_size_ = 0;
+  this.container_limit_ = 0;
+  this.booleanField_ = {
+    name: null,
+    hasBoolValue: false
+  };
+  this.boolValue_ = {
+    hasBoolValue: false,
+    boolValue: false
+  };
+};
+
+
+//
+// Compact Protocol Constants
+//
+
+/**
+  * Compact Protocol ID number.
+  * @readonly
+  * @const {number} PROTOCOL_ID
+  */
+TCompactProtocol.PROTOCOL_ID = -126;  //1000 0010
+
+/**
+  * Compact Protocol version number.
+  * @readonly
+  * @const {number} VERSION_N
+  */
+TCompactProtocol.VERSION_N = 1;
+
+/**
+  * Compact Protocol version mask for combining protocol version and message type in one byte.
+  * @readonly
+  * @const {number} VERSION_MASK
+  */
+TCompactProtocol.VERSION_MASK = 0x1f; //0001 1111
+
+/**
+  * Compact Protocol message type mask for combining protocol version and message type in one byte.
+  * @readonly
+  * @const {number} TYPE_MASK
+  */
+TCompactProtocol.TYPE_MASK = -32;     //1110 0000
+
+/**
+  * Compact Protocol message type bits for ensuring message type bit size.
+  * @readonly
+  * @const {number} TYPE_BITS
+  */
+TCompactProtocol.TYPE_BITS = 7; //0000 0111
+
+/**
+  * Compact Protocol message type shift amount for combining protocol version and message type in one byte.
+  * @readonly
+  * @const {number} TYPE_SHIFT_AMOUNT
+  */
+TCompactProtocol.TYPE_SHIFT_AMOUNT = 5;
+
+/**
+ * Compact Protocol type IDs used to keep type data within one nibble.
+ * @readonly
+ * @property {number}  CT_STOP          - End of a set of fields.
+ * @property {number}  CT_BOOLEAN_TRUE  - Flag for Boolean field with true value (packed field and value).
+ * @property {number}  CT_BOOLEAN_FALSE - Flag for Boolean field with false value (packed field and value).
+ * @property {number}  CT_BYTE          - Signed 8 bit integer.
+ * @property {number}  CT_I16           - Signed 16 bit integer.
+ * @property {number}  CT_I32           - Signed 32 bit integer.
+ * @property {number}  CT_I64           - Signed 64 bit integer (2^53 max in JavaScript).
+ * @property {number}  CT_DOUBLE        - 64 bit IEEE 854 floating point.
+ * @property {number}  CT_BINARY        - Array of bytes (used for strings also).
+ * @property {number}  CT_LIST          - A collection type (unordered).
+ * @property {number}  CT_SET           - A collection type (unordered and without repeated values).
+ * @property {number}  CT_MAP           - A collection type (map/associative-array/dictionary).
+ * @property {number}  CT_STRUCT        - A multifield type.
+ */
+TCompactProtocol.Types = {
+  CT_STOP:           0x00,
+  CT_BOOLEAN_TRUE:   0x01,
+  CT_BOOLEAN_FALSE:  0x02,
+  CT_BYTE:           0x03,
+  CT_I16:            0x04,
+  CT_I32:            0x05,
+  CT_I64:            0x06,
+  CT_DOUBLE:         0x07,
+  CT_BINARY:         0x08,
+  CT_LIST:           0x09,
+  CT_SET:            0x0A,
+  CT_MAP:            0x0B,
+  CT_STRUCT:         0x0C
+};
+
+/**
+ * Array mapping Compact type IDs to standard Thrift type IDs.
+ * @readonly
+ */
+TCompactProtocol.TTypeToCType = [
+  TCompactProtocol.Types.CT_STOP,         // T_STOP
+  0,                                      // unused
+  TCompactProtocol.Types.CT_BOOLEAN_TRUE, // T_BOOL
+  TCompactProtocol.Types.CT_BYTE,         // T_BYTE
+  TCompactProtocol.Types.CT_DOUBLE,       // T_DOUBLE
+  0,                                      // unused
+  TCompactProtocol.Types.CT_I16,          // T_I16
+  0,                                      // unused
+  TCompactProtocol.Types.CT_I32,          // T_I32
+  0,                                      // unused
+  TCompactProtocol.Types.CT_I64,          // T_I64
+  TCompactProtocol.Types.CT_BINARY,       // T_STRING
+  TCompactProtocol.Types.CT_STRUCT,       // T_STRUCT
+  TCompactProtocol.Types.CT_MAP,          // T_MAP
+  TCompactProtocol.Types.CT_SET,          // T_SET
+  TCompactProtocol.Types.CT_LIST,         // T_LIST
+];
+
+
+//
+// Compact Protocol Utilities
+//
+
+/**
+ * Returns the underlying transport layer.
+ * @return {object} The underlying transport layer.
+ */TCompactProtocol.prototype.getTransport = function() {
+  return this.trans;
+};
+
+/**
+ * Lookup a Compact Protocol Type value for a given Thrift Type value.
+ * N.B. Used only internally.
+ * @param {number} ttype - Thrift type value
+ * @returns {number} Compact protocol type value
+ */
+TCompactProtocol.prototype.getCompactType = function(ttype) {
+  return TCompactProtocol.TTypeToCType[ttype];
+};
+
+/**
+ * Lookup a Thrift Type value for a given Compact Protocol Type value.
+ * N.B. Used only internally.
+ * @param {number} type - Compact Protocol type value
+ * @returns {number} Thrift Type value
+ */
+TCompactProtocol.prototype.getTType = function(type) {
+  switch (type) {
+    case Type.STOP:
+      return Type.STOP;
+    case TCompactProtocol.Types.CT_BOOLEAN_FALSE:
+    case TCompactProtocol.Types.CT_BOOLEAN_TRUE:
+      return Type.BOOL;
+    case TCompactProtocol.Types.CT_BYTE:
+      return Type.BYTE;
+    case TCompactProtocol.Types.CT_I16:
+      return Type.I16;
+    case TCompactProtocol.Types.CT_I32:
+      return Type.I32;
+    case TCompactProtocol.Types.CT_I64:
+      return Type.I64;
+    case TCompactProtocol.Types.CT_DOUBLE:
+      return Type.DOUBLE;
+    case TCompactProtocol.Types.CT_BINARY:
+      return Type.STRING;
+    case TCompactProtocol.Types.CT_LIST:
+      return Type.LIST;
+    case TCompactProtocol.Types.CT_SET:
+      return Type.SET;
+    case TCompactProtocol.Types.CT_MAP:
+      return Type.MAP;
+    case TCompactProtocol.Types.CT_STRUCT:
+      return Type.STRUCT;
+    default:
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.INVALID_DATA, "Unknown type: " + type);
+  }
+  return Type.STOP;
+};
+
+
+//
+// Compact Protocol write operations
+//
+
+/**
+ * Send any buffered bytes to the end point.
+ */
+TCompactProtocol.prototype.flush = function() {
+  return this.trans.flush();
+};
+
+/**
+ * Writes an RPC message header
+ * @param {string} name - The method name for the message.
+ * @param {number} type - The type of message (CALL, REPLY, EXCEPTION, ONEWAY).
+ * @param {number} seqid - The call sequence number (if any).
+ */
+TCompactProtocol.prototype.writeMessageBegin = function(name, type, seqid) {
+  this.writeByte(TCompactProtocol.PROTOCOL_ID);
+  this.writeByte((TCompactProtocol.VERSION_N & TCompactProtocol.VERSION_MASK) |
+                     ((type << TCompactProtocol.TYPE_SHIFT_AMOUNT) & TCompactProtocol.TYPE_MASK));
+  this.writeVarint32(seqid);
+  this.writeString(name);
+
+  // Record client seqid to find callback again
+  if (this._seqid) {
+    // TODO better logging log warning
+    log.warning('SeqId already set', { 'name': name });
+  } else {
+    this._seqid = seqid;
+    this.trans.setCurrSeqId(seqid);
+  }
+};
+
+TCompactProtocol.prototype.writeMessageEnd = function() {
+};
+
+TCompactProtocol.prototype.writeStructBegin = function(name) {
+  this.lastField_.push(this.lastFieldId_);
+  this.lastFieldId_ = 0;
+};
+
+TCompactProtocol.prototype.writeStructEnd = function() {
+  this.lastFieldId_ = this.lastField_.pop();
+};
+
+/**
+ * Writes a struct field header
+ * @param {string} name - The field name (not written with the compact protocol).
+ * @param {number} type - The field data type (a normal Thrift field Type).
+ * @param {number} id - The IDL field Id.
+ */
+TCompactProtocol.prototype.writeFieldBegin = function(name, type, id) {
+  if (type != Type.BOOL) {
+    return this.writeFieldBeginInternal(name, type, id, -1);
+  }
+
+  this.booleanField_.name = name;
+  this.booleanField_.fieldType = type;
+  this.booleanField_.fieldId = id;
+};
+
+TCompactProtocol.prototype.writeFieldEnd = function() {
+};
+
+TCompactProtocol.prototype.writeFieldStop = function() {
+  this.writeByte(TCompactProtocol.Types.CT_STOP);
+};
+
+/**
+ * Writes a map collection header
+ * @param {number} keyType - The Thrift type of the map keys.
+ * @param {number} valType - The Thrift type of the map values.
+ * @param {number} size - The number of k/v pairs in the map.
+ */
+TCompactProtocol.prototype.writeMapBegin = function(keyType, valType, size) {
+  if (size === 0) {
+    this.writeByte(0);
+  } else {
+    this.writeVarint32(size);
+    this.writeByte(this.getCompactType(keyType) << 4 | this.getCompactType(valType));
+  }
+};
+
+TCompactProtocol.prototype.writeMapEnd = function() {
+};
+
+/**
+ * Writes a list collection header
+ * @param {number} elemType - The Thrift type of the list elements.
+ * @param {number} size - The number of elements in the list.
+ */
+TCompactProtocol.prototype.writeListBegin = function(elemType, size) {
+  this.writeCollectionBegin(elemType, size);
+};
+
+TCompactProtocol.prototype.writeListEnd = function() {
+};
+
+/**
+ * Writes a set collection header
+ * @param {number} elemType - The Thrift type of the set elements.
+ * @param {number} size - The number of elements in the set.
+ */
+TCompactProtocol.prototype.writeSetBegin = function(elemType, size) {
+  this.writeCollectionBegin(elemType, size);
+};
+
+TCompactProtocol.prototype.writeSetEnd = function() {
+};
+
+TCompactProtocol.prototype.writeBool = function(value) {
+  if (this.booleanField_.name !== null) {
+    // we haven't written the field header yet
+    this.writeFieldBeginInternal(this.booleanField_.name,
+                                 this.booleanField_.fieldType,
+                                 this.booleanField_.fieldId,
+                                 (value ? TCompactProtocol.Types.CT_BOOLEAN_TRUE
+                                          : TCompactProtocol.Types.CT_BOOLEAN_FALSE));
+    this.booleanField_.name = null;
+  } else {
+    // we're not part of a field, so just write the value
+    this.writeByte((value ? TCompactProtocol.Types.CT_BOOLEAN_TRUE
+                            : TCompactProtocol.Types.CT_BOOLEAN_FALSE));
+  }
+};
+
+TCompactProtocol.prototype.writeByte = function(b) {
+  this.trans.write(new Buffer([b]));
+};
+
+TCompactProtocol.prototype.writeI16 = function(i16) {
+  this.writeVarint32(this.i32ToZigzag(i16));
+};
+
+TCompactProtocol.prototype.writeI32 = function(i32) {
+  this.writeVarint32(this.i32ToZigzag(i32));
+};
+
+TCompactProtocol.prototype.writeI64 = function(i64) {
+  this.writeVarint64(this.i64ToZigzag(i64));
+};
+
+// Little-endian, unlike TBinaryProtocol
+TCompactProtocol.prototype.writeDouble = function(v) {
+  var buff = new Buffer(8);
+  var m, e, c;
+
+  buff[7] = (v < 0 ? 0x80 : 0x00);
+
+  v = Math.abs(v);
+  if (v !== v) {
+    // NaN, use QNaN IEEE format
+    m = 2251799813685248;
+    e = 2047;
+  } else if (v === Infinity) {
+    m = 0;
+    e = 2047;
+  } else {
+    e = Math.floor(Math.log(v) / Math.LN2);
+    c = Math.pow(2, -e);
+    if (v * c < 1) {
+      e--;
+      c *= 2;
+    }
+
+    if (e + 1023 >= 2047)
+    {
+      // Overflow
+      m = 0;
+      e = 2047;
+    }
+    else if (e + 1023 >= 1)
+    {
+      // Normalized - term order matters, as Math.pow(2, 52-e) and v*Math.pow(2, 52) can overflow
+      m = (v*c-1) * POW_52;
+      e += 1023;
+    }
+    else
+    {
+      // Denormalized - also catches the '0' case, somewhat by chance
+      m = (v * POW_1022) * POW_52;
+      e = 0;
+    }
+  }
+
+  buff[6] = (e << 4) & 0xf0;
+  buff[7] |= (e >> 4) & 0x7f;
+
+  buff[0] = m & 0xff;
+  m = Math.floor(m / POW_8);
+  buff[1] = m & 0xff;
+  m = Math.floor(m / POW_8);
+  buff[2] = m & 0xff;
+  m = Math.floor(m / POW_8);
+  buff[3] = m & 0xff;
+  m >>= 8;
+  buff[4] = m & 0xff;
+  m >>= 8;
+  buff[5] = m & 0xff;
+  m >>= 8;
+  buff[6] |= m & 0x0f;
+
+  this.trans.write(buff);
+};
+
+TCompactProtocol.prototype.writeString = function(arg) {
+  this.writeBinary(arg);
+};
+
+TCompactProtocol.prototype.writeBinary = function(arg) {
+  if (typeof arg === 'string') {
+    this.writeVarint32(Buffer.byteLength(arg, 'utf8')) ;
+    this.trans.write(arg, 'utf8');
+  } else if (arg instanceof Buffer) {
+    this.writeVarint32(arg.length);
+    this.trans.write(arg);
+  } else {
+    throw new Error('writeString/writeBinary called without a string/Buffer argument: ' + arg);
+  }
+};
+
+
+//
+// Compact Protocol internal write methods
+//
+
+TCompactProtocol.prototype.writeFieldBeginInternal = function(name,
+                                                              fieldType,
+                                                              fieldId,
+                                                              typeOverride) {
+  //If there's a type override, use that.
+  var typeToWrite = (typeOverride == -1 ? this.getCompactType(fieldType) : typeOverride);
+  //Check if we can delta encode the field id
+  if (fieldId > this.lastFieldId_ && fieldId - this.lastFieldId_ <= 15) {
+    //Include the type delta with the field ID
+    this.writeByte((fieldId - this.lastFieldId_) << 4 | typeToWrite);
+  } else {
+    //Write separate type and ID values
+    this.writeByte(typeToWrite);
+    this.writeI16(fieldId);
+  }
+  this.lastFieldId_ = fieldId;
+};
+
+TCompactProtocol.prototype.writeCollectionBegin = function(elemType, size) {
+  if (size <= 14) {
+    //Combine size and type in one byte if possible
+    this.writeByte(size << 4 | this.getCompactType(elemType));
+  } else {
+    this.writeByte(0xf0 | this.getCompactType(elemType));
+    this.writeVarint32(size);
+  }
+};
+
+/**
+ * Write an i32 as a varint. Results in 1-5 bytes on the wire.
+ */
+TCompactProtocol.prototype.writeVarint32 = function(n) {
+  var buf = new Buffer(5);
+  var wsize = 0;
+  while (true) {
+    if ((n & ~0x7F) === 0) {
+      buf[wsize++] = n;
+      break;
+    } else {
+      buf[wsize++] = ((n & 0x7F) | 0x80);
+      n = n >>> 7;
+    }
+  }
+  var wbuf = new Buffer(wsize);
+  buf.copy(wbuf,0,0,wsize);
+  this.trans.write(wbuf);
+};
+
+/**
+ * Write an i64 as a varint. Results in 1-10 bytes on the wire.
+ * N.B. node-int64 is always big endian
+ */
+TCompactProtocol.prototype.writeVarint64 = function(n) {
+  if (typeof n === "number"){
+    n = new Int64(n);
+  }
+  if (! (n instanceof Int64)) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.INVALID_DATA, "Expected Int64 or Number, found: " + n);
+  }
+
+  var buf = new Buffer(10);
+  var wsize = 0;
+  var hi = n.buffer.readUInt32BE(0, true);
+  var lo = n.buffer.readUInt32BE(4, true);
+  var mask = 0;
+  while (true) {
+    if (((lo & ~0x7F) === 0) && (hi === 0)) {
+      buf[wsize++] = lo;
+      break;
+    } else {
+      buf[wsize++] = ((lo & 0x7F) | 0x80);
+      mask = hi << 25;
+      lo = lo >>> 7;
+      hi = hi >>> 7;
+      lo = lo | mask;
+    }
+  }
+  var wbuf = new Buffer(wsize);
+  buf.copy(wbuf,0,0,wsize);
+  this.trans.write(wbuf);
+};
+
+/**
+ * Convert l into a zigzag long. This allows negative numbers to be
+ * represented compactly as a varint.
+ */
+TCompactProtocol.prototype.i64ToZigzag = function(l) {
+  if (typeof l === 'string') {
+    l = new Int64(parseInt(l, 10));
+  } else if (typeof l === 'number') {
+    l = new Int64(l);
+  }
+  if (! (l instanceof Int64)) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.INVALID_DATA, "Expected Int64 or Number, found: " + l);
+  }
+  var hi = l.buffer.readUInt32BE(0, true);
+  var lo = l.buffer.readUInt32BE(4, true);
+  var sign = hi >>> 31;
+  hi = ((hi << 1) | (lo >>> 31)) ^ ((!!sign) ? 0xFFFFFFFF : 0);
+  lo = (lo << 1) ^ ((!!sign) ? 0xFFFFFFFF : 0);
+  return new Int64(hi, lo);
+};
+
+/**
+ * Convert n into a zigzag int. This allows negative numbers to be
+ * represented compactly as a varint.
+ */
+TCompactProtocol.prototype.i32ToZigzag = function(n) {
+  return (n << 1) ^ ((n & 0x80000000) ? 0xFFFFFFFF : 0);
+};
+
+
+//
+// Compact Protocol read operations
+//
+
+TCompactProtocol.prototype.readMessageBegin = function() {
+  //Read protocol ID
+  var protocolId = this.trans.readByte();
+  if (protocolId != TCompactProtocol.PROTOCOL_ID) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.BAD_VERSION, "Bad protocol identifier " + protocolId);
+  }
+
+  //Read Version and Type
+  var versionAndType = this.trans.readByte();
+  var version = (versionAndType & TCompactProtocol.VERSION_MASK);
+  if (version != TCompactProtocol.VERSION_N) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.BAD_VERSION, "Bad protocol version " + version);
+  }
+  var type = ((versionAndType >> TCompactProtocol.TYPE_SHIFT_AMOUNT) & TCompactProtocol.TYPE_BITS);
+
+  //Read SeqId
+  var seqid = this.readVarint32();
+
+  //Read name
+  var name = this.readString();
+
+  return {fname: name, mtype: type, rseqid: seqid};
+};
+
+TCompactProtocol.prototype.readMessageEnd = function() {
+};
+
+TCompactProtocol.prototype.readStructBegin = function() {
+  this.lastField_.push(this.lastFieldId_);
+  this.lastFieldId_ = 0;
+  return {fname: ''};
+};
+
+TCompactProtocol.prototype.readStructEnd = function() {
+  this.lastFieldId_ = this.lastField_.pop();
+};
+
+TCompactProtocol.prototype.readFieldBegin = function() {
+  var fieldId = 0;
+  var b = this.trans.readByte(b);
+  var type = (b & 0x0f);
+
+  if (type == TCompactProtocol.Types.CT_STOP) {
+    return {fname: null, ftype: Thrift.Type.STOP, fid: 0};
+  }
+
+  //Mask off the 4 MSB of the type header to check for field id delta.
+  var modifier = ((b & 0x000000f0) >>> 4);
+  if (modifier === 0) {
+    //If not a delta read the field id.
+    fieldId = this.readI16();
+  } else {
+    //Recover the field id from the delta
+    fieldId = (this.lastFieldId_ + modifier);
+  }
+  var fieldType = this.getTType(type);
+
+  //Boolean are encoded with the type
+  if (type == TCompactProtocol.Types.CT_BOOLEAN_TRUE ||
+      type == TCompactProtocol.Types.CT_BOOLEAN_FALSE) {
+    this.boolValue_.hasBoolValue = true;
+    this.boolValue_.boolValue =
+      (type == TCompactProtocol.Types.CT_BOOLEAN_TRUE ? true : false);
+  }
+
+  //Save the new field for the next delta computation.
+  this.lastFieldId_ = fieldId;
+  return {fname: null, ftype: fieldType, fid: fieldId};
+};
+
+TCompactProtocol.prototype.readFieldEnd = function() {
+};
+
+TCompactProtocol.prototype.readMapBegin = function() {
+  var msize = this.readVarint32();
+  if (msize < 0) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.NEGATIVE_SIZE, "Negative map size");
+  }
+
+  var kvType = 0;
+  if (msize !== 0) {
+    kvType = this.trans.readByte();
+  }
+
+  var keyType = this.getTType((kvType & 0xf0) >>> 4);
+  var valType = this.getTType(kvType & 0xf);
+  return {ktype: keyType, vtype: valType, size: msize};
+};
+
+TCompactProtocol.prototype.readMapEnd = function() {
+};
+
+TCompactProtocol.prototype.readListBegin = function() {
+  var size_and_type = this.trans.readByte();
+
+  var lsize = (size_and_type >>> 4) & 0x0000000f;
+  if (lsize == 15) {
+    lsize = this.readVarint32();
+  }
+
+  if (lsize < 0) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.NEGATIVE_SIZE, "Negative list size");
+  }
+
+  var elemType = this.getTType(size_and_type & 0x0000000f);
+
+  return {etype: elemType, size: lsize};
+};
+
+TCompactProtocol.prototype.readListEnd = function() {
+};
+
+TCompactProtocol.prototype.readSetBegin = function() {
+  return this.readListBegin();
+};
+
+TCompactProtocol.prototype.readSetEnd = function() {
+};
+
+TCompactProtocol.prototype.readBool = function() {
+  var value = false;
+  var rsize = 0;
+  if (this.boolValue_.hasBoolValue === true) {
+    value = this.boolValue_.boolValue;
+    this.boolValue_.hasBoolValue = false;
+  } else {
+    var res = this.trans.readByte();
+    rsize = res.rsize;
+    value = (res.value == TCompactProtocol.Types.CT_BOOLEAN_TRUE);
+  }
+  return value;
+};
+
+TCompactProtocol.prototype.readByte = function() {
+  return this.trans.readByte();
+};
+
+TCompactProtocol.prototype.readI16 = function() {
+  return this.readI32();
+};
+
+TCompactProtocol.prototype.readI32 = function() {
+  return this.zigzagToI32(this.readVarint32());
+};
+
+TCompactProtocol.prototype.readI64 = function() {
+  return this.zigzagToI64(this.readVarint64());
+};
+
+// Little-endian, unlike TBinaryProtocol
+TCompactProtocol.prototype.readDouble = function() {
+  var buff = this.trans.read(8);
+  var off = 0;
+
+  var signed = buff[off + 7] & 0x80;
+  var e = (buff[off+6] & 0xF0) >> 4;
+  e += (buff[off+7] & 0x7F) << 4;
+
+  var m = buff[off];
+  m += buff[off+1] << 8;
+  m += buff[off+2] << 16;
+  m += buff[off+3] * POW_24;
+  m += buff[off+4] * POW_32;
+  m += buff[off+5] * POW_40;
+  m += (buff[off+6] & 0x0F) * POW_48;
+
+  switch (e) {
+    case 0:
+      e = -1022;
+      break;
+    case 2047:
+      return m ? NaN : (signed ? -Infinity : Infinity);
+    default:
+      m += POW_52;
+      e -= 1023;
+  }
+
+  if (signed) {
+    m *= -1;
+  }
+
+  return m * Math.pow(2, e - 52);
+};
+
+TCompactProtocol.prototype.readBinary = function() {
+  var size = this.readVarint32();
+  // Catch empty string case
+  if (size === 0) {
+    return "";
+  }
+
+  // Catch error cases
+  if (size < 0) {
+    throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.NEGATIVE_SIZE, "Negative binary/string size");
+  }
+  var value = this.trans.readString(size);
+
+  return value;
+};
+
+TCompactProtocol.prototype.readString = function() {
+  return this.readBinary();
+};
+
+
+//
+// Compact Protocol internal read operations
+//
+
+/**
+ * Read an i32 from the wire as a varint. The MSB of each byte is set
+ * if there is another byte to follow. This can read up to 5 bytes.
+ */
+TCompactProtocol.prototype.readVarint32 = function() {
+  return this.readVarint64().toNumber();
+};
+
+/**
+ * Read an i64 from the wire as a proper varint. The MSB of each byte is set
+ * if there is another byte to follow. This can read up to 10 bytes.
+ */
+TCompactProtocol.prototype.readVarint64 = function() {
+  var rsize = 0;
+  var lo = 0;
+  var hi = 0;
+  var shift = 0;
+  while (true) {
+    var b = this.trans.readByte();
+    rsize ++;
+    if (shift <= 25) {
+      lo = lo | ((b & 0x7f) << shift);
+    } else if (25 < shift && shift < 32) {
+      lo = lo | ((b & 0x7f) << shift);
+      hi = hi | ((b & 0x7f) >>> (32-shift));
+    } else {
+      hi = hi | ((b & 0x7f) << (shift-32));
+    }
+    shift += 7;
+    if (!(b & 0x80)) {
+      break;
+    }
+    if (rsize >= 10) {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.INVALID_DATA, "Variable-length int over 10 bytes.");
+    }
+  }
+  return new Int64(hi, lo);
+};
+
+/**
+ * Convert from zigzag int to int.
+ */
+TCompactProtocol.prototype.zigzagToI32 = function(n) {
+  return (n >>> 1) ^ (-1 * (n & 1));
+};
+
+/**
+ * Convert from zigzag long to long.
+ */
+TCompactProtocol.prototype.zigzagToI64 = function(n) {
+  var hi = n.buffer.readUInt32BE(0, true);
+  var lo = n.buffer.readUInt32BE(4, true);
+
+  var neg = new Int64(hi & 0, lo & 1);
+  neg._2scomp();
+  var hi_neg = neg.buffer.readUInt32BE(0, true);
+  var lo_neg = neg.buffer.readUInt32BE(4, true);
+
+  var hi_lo = (hi << 31);
+  hi = (hi >>> 1) ^ (hi_neg);
+  lo = ((lo >>> 1) | hi_lo) ^ (lo_neg);
+  return new Int64(hi, lo);
+};
+
+TCompactProtocol.prototype.skip = function(type) {
+  switch (type) {
+    case Type.STOP:
+      return;
+    case Type.BOOL:
+      this.readBool();
+      break;
+    case Type.BYTE:
+      this.readByte();
+      break;
+    case Type.I16:
+      this.readI16();
+      break;
+    case Type.I32:
+      this.readI32();
+      break;
+    case Type.I64:
+      this.readI64();
+      break;
+    case Type.DOUBLE:
+      this.readDouble();
+      break;
+    case Type.STRING:
+      this.readString();
+      break;
+    case Type.STRUCT:
+      this.readStructBegin();
+      while (true) {
+        var r = this.readFieldBegin();
+        if (r.ftype === Type.STOP) {
+          break;
+        }
+        this.skip(r.ftype);
+        this.readFieldEnd();
+      }
+      this.readStructEnd();
+      break;
+    case Type.MAP:
+      var mapBegin = this.readMapBegin();
+      for (var i = 0; i < mapBegin.size; ++i) {
+        this.skip(mapBegin.ktype);
+        this.skip(mapBegin.vtype);
+      }
+      this.readMapEnd();
+      break;
+    case Type.SET:
+      var setBegin = this.readSetBegin();
+      for (var i2 = 0; i2 < setBegin.size; ++i2) {
+        this.skip(setBegin.etype);
+      }
+      this.readSetEnd();
+      break;
+    case Type.LIST:
+      var listBegin = this.readListBegin();
+      for (var i3 = 0; i3 < listBegin.size; ++i3) {
+        this.skip(listBegin.etype);
+      }
+      this.readListEnd();
+      break;
+    default:
+      throw new  Error("Invalid type: " + type);
+  }
+};
diff --git a/chat/protocols/facebook/thrift/facebook.thrift b/chat/protocols/facebook/thrift/facebook.thrift
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/facebook.thrift
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+struct User {
+    1: required i64 user_id,
+    2: string info = "",
+    3: i64 unknown_cp = 23,
+    4: i64 unknown_ecp = 26,
+    5: i32 unknown_1 = 1,
+    6: bool unknown_no_auto_fg = true,
+    7: bool visible = true,
+    8: required string device_id,
+    9: bool unknown_fg = true,
+    10: i32 unknown_nwt = 1,
+    11: i32 unknown_nwst = 0,
+    12: required i64 mqtt_id,
+    14: list<i32> unknown_2 = [], /* [0] */
+}
+
+struct Connect {
+    1: required string client_id,
+    4: required User user,
+    5: required string token,
+}
+
+struct UserPresence {
+    1: required i64 user_id,
+    2: required i32 active, /* 0 is inactive */
+    3: optional i64 last_active_timestamp,
+    4: optional i16 active_client_bits,
+    5: optional i64 voip_compatibility_bits,
+}
+
+struct Presence {
+    /* identifier string (for Facebook employees) */
+    /*string id,*/
+    1: required bool full_list,
+    2: required list<UserPresence> users,
+    
+}
diff --git a/chat/protocols/facebook/thrift/facebook_types.js b/chat/protocols/facebook/thrift/facebook_types.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/facebook_types.js
@@ -0,0 +1,582 @@
+//
+// Autogenerated by Thrift Compiler (0.9.3)
+//
+// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
+//
+var Thrift = require('thrift');
+//var thrift = require('thrift');
+//var Thrift = thrift.Thrift;
+//var Q = thrift.Q;
+
+
+var ttypes = module.exports = {};
+User = module.exports.User = function(args) {
+  this.user_id = null;
+  this.info = '';
+  this.unknown_cp = 23;
+  this.unknown_ecp = 26;
+  this.unknown_1 = 1;
+  this.unknown_no_auto_fg = true;
+  this.visible = true;
+  this.device_id = null;
+  this.unknown_fg = true;
+  this.unknown_nwt = 1;
+  this.unknown_nwst = 0;
+  this.mqtt_id = null;
+  this.unknown_2 = [];
+  if (args) {
+    if (args.user_id !== undefined && args.user_id !== null) {
+      this.user_id = args.user_id;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field user_id is unset!');
+    }
+    if (args.info !== undefined && args.info !== null) {
+      this.info = args.info;
+    }
+    if (args.unknown_cp !== undefined && args.unknown_cp !== null) {
+      this.unknown_cp = args.unknown_cp;
+    }
+    if (args.unknown_ecp !== undefined && args.unknown_ecp !== null) {
+      this.unknown_ecp = args.unknown_ecp;
+    }
+    if (args.unknown_1 !== undefined && args.unknown_1 !== null) {
+      this.unknown_1 = args.unknown_1;
+    }
+    if (args.unknown_no_auto_fg !== undefined && args.unknown_no_auto_fg !== null) {
+      this.unknown_no_auto_fg = args.unknown_no_auto_fg;
+    }
+    if (args.visible !== undefined && args.visible !== null) {
+      this.visible = args.visible;
+    }
+    if (args.device_id !== undefined && args.device_id !== null) {
+      this.device_id = args.device_id;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field device_id is unset!');
+    }
+    if (args.unknown_fg !== undefined && args.unknown_fg !== null) {
+      this.unknown_fg = args.unknown_fg;
+    }
+    if (args.unknown_nwt !== undefined && args.unknown_nwt !== null) {
+      this.unknown_nwt = args.unknown_nwt;
+    }
+    if (args.unknown_nwst !== undefined && args.unknown_nwst !== null) {
+      this.unknown_nwst = args.unknown_nwst;
+    }
+    if (args.mqtt_id !== undefined && args.mqtt_id !== null) {
+      this.mqtt_id = args.mqtt_id;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field mqtt_id is unset!');
+    }
+    if (args.unknown_2 !== undefined && args.unknown_2 !== null) {
+      this.unknown_2 = Thrift.copyList(args.unknown_2, [null]);
+    }
+  }
+};
+User.prototype = {};
+User.prototype.read = function(input) {
+  input.readStructBegin();
+  while (true)
+  {
+    var ret = input.readFieldBegin();
+    var fname = ret.fname;
+    var ftype = ret.ftype;
+    var fid = ret.fid;
+    if (ftype == Thrift.Type.STOP) {
+      break;
+    }
+    switch (fid)
+    {
+      case 1:
+      if (ftype == Thrift.Type.I64) {
+        this.user_id = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 2:
+      if (ftype == Thrift.Type.STRING) {
+        this.info = input.readString();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 3:
+      if (ftype == Thrift.Type.I64) {
+        this.unknown_cp = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 4:
+      if (ftype == Thrift.Type.I64) {
+        this.unknown_ecp = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 5:
+      if (ftype == Thrift.Type.I32) {
+        this.unknown_1 = input.readI32();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 6:
+      if (ftype == Thrift.Type.BOOL) {
+        this.unknown_no_auto_fg = input.readBool();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 7:
+      if (ftype == Thrift.Type.BOOL) {
+        this.visible = input.readBool();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 8:
+      if (ftype == Thrift.Type.STRING) {
+        this.device_id = input.readString();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 9:
+      if (ftype == Thrift.Type.BOOL) {
+        this.unknown_fg = input.readBool();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 10:
+      if (ftype == Thrift.Type.I32) {
+        this.unknown_nwt = input.readI32();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 11:
+      if (ftype == Thrift.Type.I32) {
+        this.unknown_nwst = input.readI32();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 12:
+      if (ftype == Thrift.Type.I64) {
+        this.mqtt_id = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 14:
+      if (ftype == Thrift.Type.LIST) {
+        var _size0 = 0;
+        var _rtmp34;
+        this.unknown_2 = [];
+        var _etype3 = 0;
+        _rtmp34 = input.readListBegin();
+        _etype3 = _rtmp34.etype;
+        _size0 = _rtmp34.size;
+        for (var _i5 = 0; _i5 < _size0; ++_i5)
+        {
+          var elem6 = null;
+          elem6 = input.readI32();
+          this.unknown_2.push(elem6);
+        }
+        input.readListEnd();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      default:
+        input.skip(ftype);
+    }
+    input.readFieldEnd();
+  }
+  input.readStructEnd();
+  return;
+};
+
+User.prototype.write = function(output) {
+  output.writeStructBegin('User');
+  if (this.user_id !== null && this.user_id !== undefined) {
+    output.writeFieldBegin('user_id', Thrift.Type.I64, 1);
+    output.writeI64(this.user_id);
+    output.writeFieldEnd();
+  }
+  if (this.info !== null && this.info !== undefined) {
+    output.writeFieldBegin('info', Thrift.Type.STRING, 2);
+    output.writeString(this.info);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_cp !== null && this.unknown_cp !== undefined) {
+    output.writeFieldBegin('unknown_cp', Thrift.Type.I64, 3);
+    output.writeI64(this.unknown_cp);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_ecp !== null && this.unknown_ecp !== undefined) {
+    output.writeFieldBegin('unknown_ecp', Thrift.Type.I64, 4);
+    output.writeI64(this.unknown_ecp);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_1 !== null && this.unknown_1 !== undefined) {
+    output.writeFieldBegin('unknown_1', Thrift.Type.I32, 5);
+    output.writeI32(this.unknown_1);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_no_auto_fg !== null && this.unknown_no_auto_fg !== undefined) {
+    output.writeFieldBegin('unknown_no_auto_fg', Thrift.Type.BOOL, 6);
+    output.writeBool(this.unknown_no_auto_fg);
+    output.writeFieldEnd();
+  }
+  if (this.visible !== null && this.visible !== undefined) {
+    output.writeFieldBegin('visible', Thrift.Type.BOOL, 7);
+    output.writeBool(this.visible);
+    output.writeFieldEnd();
+  }
+  if (this.device_id !== null && this.device_id !== undefined) {
+    output.writeFieldBegin('device_id', Thrift.Type.STRING, 8);
+    output.writeString(this.device_id);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_fg !== null && this.unknown_fg !== undefined) {
+    output.writeFieldBegin('unknown_fg', Thrift.Type.BOOL, 9);
+    output.writeBool(this.unknown_fg);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_nwt !== null && this.unknown_nwt !== undefined) {
+    output.writeFieldBegin('unknown_nwt', Thrift.Type.I32, 10);
+    output.writeI32(this.unknown_nwt);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_nwst !== null && this.unknown_nwst !== undefined) {
+    output.writeFieldBegin('unknown_nwst', Thrift.Type.I32, 11);
+    output.writeI32(this.unknown_nwst);
+    output.writeFieldEnd();
+  }
+  if (this.mqtt_id !== null && this.mqtt_id !== undefined) {
+    output.writeFieldBegin('mqtt_id', Thrift.Type.I64, 12);
+    output.writeI64(this.mqtt_id);
+    output.writeFieldEnd();
+  }
+  if (this.unknown_2 !== null && this.unknown_2 !== undefined) {
+    output.writeFieldBegin('unknown_2', Thrift.Type.LIST, 14);
+    output.writeListBegin(Thrift.Type.I32, this.unknown_2.length);
+    for (var iter7 in this.unknown_2)
+    {
+      if (this.unknown_2.hasOwnProperty(iter7))
+      {
+        iter7 = this.unknown_2[iter7];
+        output.writeI32(iter7);
+      }
+    }
+    output.writeListEnd();
+    output.writeFieldEnd();
+  }
+  output.writeFieldStop();
+  output.writeStructEnd();
+  return;
+};
+
+Connect = module.exports.Connect = function(args) {
+  this.client_id = null;
+  this.user = null;
+  this.token = null;
+  if (args) {
+    if (args.client_id !== undefined && args.client_id !== null) {
+      this.client_id = args.client_id;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field client_id is unset!');
+    }
+    if (args.user !== undefined && args.user !== null) {
+      this.user = new ttypes.User(args.user);
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field user is unset!');
+    }
+    if (args.token !== undefined && args.token !== null) {
+      this.token = args.token;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field token is unset!');
+    }
+  }
+};
+Connect.prototype = {};
+Connect.prototype.read = function(input) {
+  input.readStructBegin();
+  while (true)
+  {
+    var ret = input.readFieldBegin();
+    var fname = ret.fname;
+    var ftype = ret.ftype;
+    var fid = ret.fid;
+    if (ftype == Thrift.Type.STOP) {
+      break;
+    }
+    switch (fid)
+    {
+      case 1:
+      if (ftype == Thrift.Type.STRING) {
+        this.client_id = input.readString();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 4:
+      if (ftype == Thrift.Type.STRUCT) {
+        this.user = new ttypes.User();
+        this.user.read(input);
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 5:
+      if (ftype == Thrift.Type.STRING) {
+        this.token = input.readString();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      default:
+        input.skip(ftype);
+    }
+    input.readFieldEnd();
+  }
+  input.readStructEnd();
+  return;
+};
+
+Connect.prototype.write = function(output) {
+  output.writeStructBegin('Connect');
+  if (this.client_id !== null && this.client_id !== undefined) {
+    output.writeFieldBegin('client_id', Thrift.Type.STRING, 1);
+    output.writeString(this.client_id);
+    output.writeFieldEnd();
+  }
+  if (this.user !== null && this.user !== undefined) {
+    output.writeFieldBegin('user', Thrift.Type.STRUCT, 4);
+    this.user.write(output);
+    output.writeFieldEnd();
+  }
+  if (this.token !== null && this.token !== undefined) {
+    output.writeFieldBegin('token', Thrift.Type.STRING, 5);
+    output.writeString(this.token);
+    output.writeFieldEnd();
+  }
+  output.writeFieldStop();
+  output.writeStructEnd();
+  return;
+};
+
+UserPresence = module.exports.UserPresence = function(args) {
+  this.user_id = null;
+  this.active = null;
+  this.last_active_timestamp = null;
+  this.active_client_bits = null;
+  this.voip_compatibility_bits = null;
+  if (args) {
+    if (args.user_id !== undefined && args.user_id !== null) {
+      this.user_id = args.user_id;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field user_id is unset!');
+    }
+    if (args.active !== undefined && args.active !== null) {
+      this.active = args.active;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field active is unset!');
+    }
+    if (args.last_active_timestamp !== undefined && args.last_active_timestamp !== null) {
+      this.last_active_timestamp = args.last_active_timestamp;
+    }
+    if (args.active_client_bits !== undefined && args.active_client_bits !== null) {
+      this.active_client_bits = args.active_client_bits;
+    }
+    if (args.voip_compatibility_bits !== undefined && args.voip_compatibility_bits !== null) {
+      this.voip_compatibility_bits = args.voip_compatibility_bits;
+    }
+  }
+};
+UserPresence.prototype = {};
+UserPresence.prototype.read = function(input) {
+  input.readStructBegin();
+  while (true)
+  {
+    var ret = input.readFieldBegin();
+    var fname = ret.fname;
+    var ftype = ret.ftype;
+    var fid = ret.fid;
+    if (ftype == Thrift.Type.STOP) {
+      break;
+    }
+    switch (fid)
+    {
+      case 1:
+      if (ftype == Thrift.Type.I64) {
+        this.user_id = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 2:
+      if (ftype == Thrift.Type.I32) {
+        this.active = input.readI32();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 3:
+      if (ftype == Thrift.Type.I64) {
+        this.last_active_timestamp = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 4:
+      if (ftype == Thrift.Type.I16) {
+        this.active_client_bits = input.readI16();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 5:
+      if (ftype == Thrift.Type.I64) {
+        this.voip_compatibility_bits = input.readI64();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      default:
+        input.skip(ftype);
+    }
+    input.readFieldEnd();
+  }
+  input.readStructEnd();
+  return;
+};
+
+UserPresence.prototype.write = function(output) {
+  output.writeStructBegin('UserPresence');
+  if (this.user_id !== null && this.user_id !== undefined) {
+    output.writeFieldBegin('user_id', Thrift.Type.I64, 1);
+    output.writeI64(this.user_id);
+    output.writeFieldEnd();
+  }
+  if (this.active !== null && this.active !== undefined) {
+    output.writeFieldBegin('active', Thrift.Type.I32, 2);
+    output.writeI32(this.active);
+    output.writeFieldEnd();
+  }
+  if (this.last_active_timestamp !== null && this.last_active_timestamp !== undefined) {
+    output.writeFieldBegin('last_active_timestamp', Thrift.Type.I64, 3);
+    output.writeI64(this.last_active_timestamp);
+    output.writeFieldEnd();
+  }
+  if (this.active_client_bits !== null && this.active_client_bits !== undefined) {
+    output.writeFieldBegin('active_client_bits', Thrift.Type.I16, 4);
+    output.writeI16(this.active_client_bits);
+    output.writeFieldEnd();
+  }
+  if (this.voip_compatibility_bits !== null && this.voip_compatibility_bits !== undefined) {
+    output.writeFieldBegin('voip_compatibility_bits', Thrift.Type.I64, 5);
+    output.writeI64(this.voip_compatibility_bits);
+    output.writeFieldEnd();
+  }
+  output.writeFieldStop();
+  output.writeStructEnd();
+  return;
+};
+
+Presence = module.exports.Presence = function(args) {
+  this.full_list = null;
+  this.users = null;
+  if (args) {
+    if (args.full_list !== undefined && args.full_list !== null) {
+      this.full_list = args.full_list;
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field full_list is unset!');
+    }
+    if (args.users !== undefined && args.users !== null) {
+      this.users = Thrift.copyList(args.users, [ttypes.UserPresence]);
+    } else {
+      throw new Thrift.TProtocolException(Thrift.TProtocolExceptionType.UNKNOWN, 'Required field users is unset!');
+    }
+  }
+};
+Presence.prototype = {};
+Presence.prototype.read = function(input) {
+  input.readStructBegin();
+  while (true)
+  {
+    var ret = input.readFieldBegin();
+    var fname = ret.fname;
+    var ftype = ret.ftype;
+    var fid = ret.fid;
+    if (ftype == Thrift.Type.STOP) {
+      break;
+    }
+    switch (fid)
+    {
+      case 1:
+      if (ftype == Thrift.Type.BOOL) {
+        this.full_list = input.readBool();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      case 2:
+      if (ftype == Thrift.Type.LIST) {
+        var _size8 = 0;
+        var _rtmp312;
+        this.users = [];
+        var _etype11 = 0;
+        _rtmp312 = input.readListBegin();
+        _etype11 = _rtmp312.etype;
+        _size8 = _rtmp312.size;
+        for (var _i13 = 0; _i13 < _size8; ++_i13)
+        {
+          var elem14 = null;
+          elem14 = new ttypes.UserPresence();
+          elem14.read(input);
+          this.users.push(elem14);
+        }
+        input.readListEnd();
+      } else {
+        input.skip(ftype);
+      }
+      break;
+      default:
+        input.skip(ftype);
+    }
+    input.readFieldEnd();
+  }
+  input.readStructEnd();
+  return;
+};
+
+Presence.prototype.write = function(output) {
+  output.writeStructBegin('Presence');
+  if (this.full_list !== null && this.full_list !== undefined) {
+    output.writeFieldBegin('full_list', Thrift.Type.BOOL, 1);
+    output.writeBool(this.full_list);
+    output.writeFieldEnd();
+  }
+  if (this.users !== null && this.users !== undefined) {
+    output.writeFieldBegin('users', Thrift.Type.LIST, 2);
+    output.writeListBegin(Thrift.Type.STRUCT, this.users.length);
+    for (var iter15 in this.users)
+    {
+      if (this.users.hasOwnProperty(iter15))
+      {
+        iter15 = this.users[iter15];
+        iter15.write(output);
+      }
+    }
+    output.writeListEnd();
+    output.writeFieldEnd();
+  }
+  output.writeFieldStop();
+  output.writeStructEnd();
+  return;
+};
+
diff --git a/chat/protocols/facebook/thrift/log.js b/chat/protocols/facebook/thrift/log.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/log.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = {
+  'info' : function logInfo() {},
+  'warning' : function logWarning() {},
+  'error' : function logError() {},
+  'debug' : function logDebug() {},
+  'trace' : function logTrace() {}
+};
diff --git a/chat/protocols/facebook/thrift/moz.build b/chat/protocols/facebook/thrift/moz.build
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES.thrift += [
+    'Buffer.jsm',
+    'compact_protocol.js',
+    # This is an autogenerated file. It can be regenerated by installing the
+    # thrift compiler and running:
+    #   thrift --recurse --gen js:node -out chat/protocols/facebook/thrift \
+    #       chat/protocols/facebook/thrift/facebook.thrift
+    'facebook_types.js',
+    'Int64.js',
+    'log.js',
+    'thrift.js',
+    'util.js',
+]
diff --git a/chat/protocols/facebook/thrift/thrift.js b/chat/protocols/facebook/thrift/thrift.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/thrift.js
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+var util = require('util');
+
+// Monkey-patch this since it isn't defined in SpiderMonkey.
+Error.captureStackTrace = function(aObj) {
+  // TODO What to do here?
+  dump((new Error()).stack)
+}
+
+var Type = exports.Type = {
+  STOP: 0,
+  VOID: 1,
+  BOOL: 2,
+  BYTE: 3,
+  I08: 3,
+  DOUBLE: 4,
+  I16: 6,
+  I32: 8,
+  I64: 10,
+  STRING: 11,
+  UTF7: 11,
+  STRUCT: 12,
+  MAP: 13,
+  SET: 14,
+  LIST: 15,
+  UTF8: 16,
+  UTF16: 17
+};
+
+exports.MessageType = {
+  CALL: 1,
+  REPLY: 2,
+  EXCEPTION: 3,
+  ONEWAY: 4
+};
+
+exports.TException = TException;
+
+function TException(message) {
+  Error.call(this);
+  Error.captureStackTrace(this, this.constructor);
+  this.name = this.constructor.name;
+  this.message = message;
+};
+util.inherits(TException, Error);
+
+var TApplicationExceptionType = exports.TApplicationExceptionType = {
+  UNKNOWN: 0,
+  UNKNOWN_METHOD: 1,
+  INVALID_MESSAGE_TYPE: 2,
+  WRONG_METHOD_NAME: 3,
+  BAD_SEQUENCE_ID: 4,
+  MISSING_RESULT: 5,
+  INTERNAL_ERROR: 6,
+  PROTOCOL_ERROR: 7,
+  INVALID_TRANSFORM: 8,
+  INVALID_PROTOCOL: 9,
+  UNSUPPORTED_CLIENT_TYPE: 10
+};
+
+exports.TApplicationException = TApplicationException;
+
+function TApplicationException(type, message) {
+  TException.call(this);
+  Error.captureStackTrace(this, this.constructor);
+  this.type = type || TApplicationExceptionType.UNKNOWN;
+  this.name = this.constructor.name;
+  this.message = message;
+};
+util.inherits(TApplicationException, TException);
+
+TApplicationException.prototype.read = function(input) {
+  var ftype;
+  var ret = input.readStructBegin('TApplicationException');
+
+  while(1){
+      ret = input.readFieldBegin();
+      if(ret.ftype == Type.STOP)
+          break;
+
+      switch(ret.fid){
+          case 1:
+              if( ret.ftype == Type.STRING ){
+                  ret = input.readString();
+                  this.message = ret;
+              } else {
+                  ret = input.skip(ret.ftype);
+              }
+              break;
+          case 2:
+              if( ret.ftype == Type.I32 ){
+                  ret = input.readI32();
+                  this.type = ret;
+              } else {
+                  ret   = input.skip(ret.ftype);
+              }
+              break;
+          default:
+              ret = input.skip(ret.ftype);
+              break;
+      }
+      input.readFieldEnd();
+  }
+  input.readStructEnd();
+};
+
+TApplicationException.prototype.write = function(output){
+  output.writeStructBegin('TApplicationException');
+
+  if (this.message) {
+      output.writeFieldBegin('message', Type.STRING, 1);
+      output.writeString(this.message);
+      output.writeFieldEnd();
+  }
+
+  if (this.code) {
+      output.writeFieldBegin('type', Type.I32, 2);
+      output.writeI32(this.code);
+      output.writeFieldEnd();
+  }
+
+  output.writeFieldStop();
+  output.writeStructEnd();
+};
+
+var TProtocolExceptionType = exports.TProtocolExceptionType = {
+  UNKNOWN: 0,
+  INVALID_DATA: 1,
+  NEGATIVE_SIZE: 2,
+  SIZE_LIMIT: 3,
+  BAD_VERSION: 4,
+  NOT_IMPLEMENTED: 5,
+  DEPTH_LIMIT: 6
+};
+
+
+exports.TProtocolException = TProtocolException;
+
+function TProtocolException(type, message) {
+  Error.call(this);
+  Error.captureStackTrace(this, this.constructor);
+  this.name = this.constructor.name;
+  this.type = type;
+  this.message = message;
+};
+util.inherits(TProtocolException, Error);
+
+exports.objectLength = function(obj) {
+  return Object.keys(obj).length;
+};
+
+exports.inherits = function(constructor, superConstructor) {
+  util.inherits(constructor, superConstructor);
+};
+
+var copyList, copyMap;
+
+copyList = function(lst, types) {
+
+  if (!lst) {return lst; }
+
+  var type;
+
+  if (types.shift === undefined) {
+    type = types;
+  }
+  else {
+    type = types[0];
+  }
+  var Type = type;
+
+  var len = lst.length, result = [], i, val;
+  for (i = 0; i < len; i++) {
+    val = lst[i];
+    if (type === null) {
+      result.push(val);
+    }
+    else if (type === copyMap || type === copyList) {
+      result.push(type(val, types.slice(1)));
+    }
+    else {
+      result.push(new Type(val));
+    }
+  }
+  return result;
+};
+
+copyMap = function(obj, types){
+
+  if (!obj) {return obj; }
+
+  var type;
+
+  if (types.shift === undefined) {
+    type = types;
+  }
+  else {
+    type = types[0];
+  }
+  var Type = type;
+
+  var result = {}, val;
+  for(var prop in obj) {
+    if(obj.hasOwnProperty(prop)) {
+      val = obj[prop];
+      if (type === null) {
+        result[prop] = val;
+      }
+      else if (type === copyMap || type === copyList) {
+        result[prop] = type(val, types.slice(1));
+      }
+      else {
+        result[prop] = new Type(val);
+      }
+    }
+  }
+  return result;
+};
+
+module.exports.copyMap = copyMap;
+module.exports.copyList = copyList;
diff --git a/chat/protocols/facebook/thrift/util.js b/chat/protocols/facebook/thrift/util.js
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/thrift/util.js
@@ -0,0 +1,34 @@
+'use strict';
+
+/**
+ * Inherit the prototype methods from one constructor into another.
+ *
+ * The Function.prototype.inherits from lang.js rewritten as a standalone
+ * function (not on Function.prototype). NOTE: If this file is to be loaded
+ * during bootstrapping this function needs to be rewritten using some native
+ * functions as prototype setup using normal JavaScript does not work as
+ * expected during bootstrapping (see mirror.js in r114903).
+ *
+ * @param {function} ctor Constructor function which needs to inherit the
+ *     prototype.
+ * @param {function} superCtor Constructor function to inherit prototype from.
+ * @throws {TypeError} Will error if either constructor is null, or if
+ *     the super constructor lacks a prototype.
+ */
+exports.inherits = function(ctor, superCtor) {
+
+  if (ctor === undefined || ctor === null)
+    throw new TypeError('The constructor to `inherits` must not be ' +
+                        'null or undefined.');
+
+  if (superCtor === undefined || superCtor === null)
+    throw new TypeError('The super constructor to `inherits` must not ' +
+                        'be null or undefined.');
+
+  if (superCtor.prototype === undefined)
+    throw new TypeError('The super constructor to `inherits` must ' +
+                        'have a prototype.');
+
+  ctor.super_ = superCtor;
+  Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
+};
