Files
tubestation/browser/components/loop/MozLoopService.jsm

344 lines
11 KiB
JavaScript

/* 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/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["MozLoopService"];
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
"resource:///modules/loop/MozLoopAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
/**
* We don't have push notifications on desktop currently, so this is a
* workaround to get them going for us.
*/
let PushHandlerHack = {
// This is the uri of the push server.
pushServerUri: Services.prefs.getCharPref("services.push.serverURL"),
// This is the channel id we're using for notifications
channelID: "8b1081ce-9b35-42b5-b8f5-3ff8cb813a50",
// Stores the push url if we're registered and we have one.
pushUrl: undefined,
/**
* Call to start the connection to the push socket server. On
* connection, it will automatically say hello and register the channel
* id with the server.
*
* @param {Function} registerCallback Callback to be called once we are
* registered.
* @param {Function} notificationCallback Callback to be called when a
* push notification is received.
*/
initialize: function(registerCallback, notificationCallback) {
this.registerCallback = registerCallback;
this.notificationCallback = notificationCallback;
this.websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
.createInstance(Ci.nsIWebSocketChannel);
this.websocket.protocol = "push-notification";
var pushURI = Services.io.newURI(this.pushServerUri, null, null);
this.websocket.asyncOpen(pushURI, this.pushServerUri, this, null);
},
/**
* Listener method, handles the start of the websocket stream.
* Sends a hello message to the server.
*
* @param {nsISupports} aContext Not used
*/
onStart: function() {
var helloMsg = { messageType: "hello", uaid: "", channelIDs: [] };
this.websocket.sendMsg(JSON.stringify(helloMsg));
},
/**
* Listener method, called when the websocket is closed.
*
* @param {nsISupports} aContext Not used
* @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
*/
onStop: function(aContext, aStatusCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed! Code: " + aStatusCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket is closed by the server.
* If there are errors, onStop may be called without ever calling this
* method.
*
* @param {nsISupports} aContext Not used
* @param {integer} aCode the websocket closing handshake close code
* @param {String} aReason the websocket closing handshake close reason
*/
onServerClose: function(aContext, aCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed (server)! Code: " + aCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket receives a message.
*
* @param {nsISupports} aContext Not used
* @param {String} aMsg The message data
*/
onMessageAvailable: function(aContext, aMsg) {
var msg = JSON.parse(aMsg);
switch(msg.messageType) {
case "hello":
this._registerChannel();
break;
case "register":
this.pushUrl = msg.pushEndpoint;
this.registerCallback(this.pushUrl);
break;
case "notification":
msg.updates.forEach(function(update) {
if (update.channelID === this.channelID) {
this.notificationCallback(update.version);
}
}.bind(this));
break;
}
},
/**
* Handles registering a service
*/
_registerChannel: function() {
this.websocket.sendMsg(JSON.stringify({
messageType: "register",
channelID: this.channelID
}));
}
};
/**
* Internal helper methods and state
*/
let MozLoopServiceInternal = {
// The uri of the Loop server.
loopServerUri: Services.prefs.getCharPref("loop.server"),
/**
* The initial delay for push registration.
*
* XXX We keep this short at the moment, as we don't handle delayed
* registrations from the user perspective. Bug 994151 will extend this.
*/
pushRegistrationDelay: 100,
/**
* Starts the initialization of the service, which goes and registers
* with the push server and the loop server.
*/
initialize: function() {
if (this.initialized)
return;
this.initialized = true;
// Kick off the push notification service into registering after a timeout
// this ensures we're not doing too much straight after the browser's finished
// starting up.
this.initializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.initializeTimer.initWithCallback(this.registerPushHandler.bind(this),
this.pushRegistrationDelay, Ci.nsITimer.TYPE_ONE_SHOT);
},
/**
* Starts registration of Loop with the push server.
*/
registerPushHandler: function() {
PushHandlerHack.initialize(this.onPushRegistered.bind(this),
this.onHandleNotification.bind(this));
},
/**
* Callback from PushHandlerHack - The push server has been registered
* and has given us a push url.
*
* @param {String} pushUrl The push url given by the push server.
*/
onPushRegistered: function(pushUrl) {
this.registerXhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
this.registerXhr.open('POST', MozLoopServiceInternal.loopServerUri + "/registration",
true);
this.registerXhr.setRequestHeader('Content-Type', 'application/json');
this.registerXhr.channel.loadFlags = Ci.nsIChannel.INHIBIT_CACHING
| Ci.nsIChannel.LOAD_BYPASS_CACHE
| Ci.nsIChannel.LOAD_EXPLICIT_CREDENTIALS;
this.registerXhr.onreadystatechange = this.onRegistrationResult.bind(this);
this.registerXhr.sendAsBinary(JSON.stringify({
simple_push_url: pushUrl
}));
},
/**
* Callback from PushHandlerHack - A push notification has been received from
* the server.
*
* @param {String} version The version information from the server.
*/
onHandleNotification: function(version) {
this.openChatWindow(null, "LooP", "about:loopconversation#start/" + version);
},
/**
* Callback from the registation xhr. Checks the registration result.
*/
onRegistrationResult: function() {
if (this.registerXhr.readyState != Ci.nsIXMLHttpRequest.DONE)
return;
if (this.registerXhr.status != 200) {
// XXX Bubble this up to the UI somehow, bug 994151 will handle some of this
Cu.reportError("Failed to register with the loop server. Code: " +
this.registerXhr.status + " Text: " + this.registerXhr.statusText);
return;
}
// Otherwise we registered just fine.
// XXX For now, we'll just save this fact, bug 994151 (again) will make use of
// this more.
this.registeredLoopServer = true;
},
/**
* A getter to obtain and store the strings for loop. This is structured
* for use by l10n.js.
*
* @returns {Object} a map of element ids with attributes to set.
*/
get localizedStrings() {
if (this._localizedStrings)
return this._localizedStrings;
var stringBundle =
Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
var map = {};
var enumerator = stringBundle.getSimpleEnumeration();
while (enumerator.hasMoreElements()) {
var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
// 'textContent' is the default attribute to set if none are specified.
var key = string.key, property = 'textContent';
var i = key.lastIndexOf('.');
if (i >= 0) {
property = key.substring(i + 1);
key = key.substring(0, i);
}
if (!(key in map))
map[key] = {};
map[key][property] = string.value;
}
return this._localizedStrings = map;
},
/**
* Opens the chat window
*
* @param {Object} contentWindow The window to open the chat window in, may
* be null.
* @param {String} title The title of the chat window.
* @param {String} url The page to load in the chat window.
* @param {String} mode May be "minimized" or undefined.
*/
openChatWindow: function(contentWindow, title, url, mode) {
// So I guess the origin is the loop server!?
let origin = this.loopServerUri;
url = url.spec || url;
let callback = chatbox => {
// We need to use DOMContentLoaded as otherwise the injection will happen
// in about:blank and then get lost.
// Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
// involves event loop spins, which means it might be too late.
// Have we already done it?
if (chatbox.contentWindow.navigator.mozLoop) {
return;
}
chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
if (event.target != chatbox.contentDocument) {
return;
}
chatbox.removeEventListener("DOMContentLoaded", loaded, true);
injectLoopAPI(chatbox.contentWindow);
}, true);
};
Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
}
};
/**
* Public API
*/
this.MozLoopService = {
/**
* Initialized the loop service, and starts registration with the
* push and loop servers.
*/
initialize: function() {
MozLoopServiceInternal.initialize();
},
/**
* Returns the strings for the specified element. Designed for use
* with l10n.js.
*
* @param {key} The element id to get strings for.
* @return {String} A JSON string containing the localized
* attribute/value pairs for the element.
*/
getStrings: function(key) {
var stringData = MozLoopServiceInternal.localizedStrings;
if (!(key in stringData)) {
Cu.reportError('No string for key: ' + key + 'found');
return "";
}
return JSON.stringify(stringData[key]);
},
/**
* Returns the current locale
*
* @return {String} The code of the current locale.
*/
get locale() {
try {
return Services.prefs.getComplexValue("general.useragent.locale",
Ci.nsISupportsString).data;
} catch (ex) {
return "en-US";
}
}
};