Files
tubestation/browser/components/loop/modules/MozLoopAPI.jsm

1261 lines
48 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://services-common/utils.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
Cu.import("resource:///modules/loop/LoopRooms.jsm");
Cu.importGlobalProperties(["Blob"]);
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
"resource://gre/modules/PageMetadata.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
"resource://gre/modules/UpdateUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
"resource:///modules/UITour.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Social",
"resource:///modules/Social.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
return Cc["@mozilla.org/xre/app-info;1"]
.getService(Ci.nsIXULAppInfo)
.QueryInterface(Ci.nsIXULRuntime);
});
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
XPCOMUtils.defineLazyServiceGetter(this, "extProtocolSvc",
"@mozilla.org/uriloader/external-protocol-service;1",
"nsIExternalProtocolService");
this.EXPORTED_SYMBOLS = ["LoopAPI"];
const cloneableError = function(source) {
// Simple Object that can be cloned over.
let error = {};
if (typeof source == "string") {
source = new Error(source);
}
let props = Object.getOwnPropertyNames(source);
// nsIException properties are not enumerable, so we'll try to copy the most
// common and useful ones.
if (!props.length) {
props.push("message", "filename", "lineNumber", "columnNumber", "stack");
}
for (let prop of props) {
let value = source[prop];
let type = typeof value;
// Functions can't be cloned. Period.
// For nsIException objects, the property may not be defined.
if (type == "function" || type == "undefined") {
continue;
}
// Don't do anything to members that are already cloneable.
if (/boolean|number|string/.test(type)) {
error[prop] = value;
} else {
// Convert non-compatible types to a String.
error[prop] = "" + value;
}
}
// Mark the object as an Error, otherwise it won't be discernable from other,
// regular objects.
error.isError = true;
return error;
};
const getObjectAPIFunctionName = function(action) {
let funcName = action.split(":").pop();
return funcName.charAt(0).toLowerCase() + funcName.substr(1);
};
/**
* Retrieves a list of Social Providers from the Social API that are explicitly
* capable of sharing URLs.
* It also adds a listener that is fired whenever a new Provider is added or
* removed.
*
* @return {Array} Sorted list of share-capable Social Providers.
*/
const updateSocialProvidersCache = function() {
let providers = [];
for (let provider of Social.providers) {
if (!provider.shareURL) {
continue;
}
// Only pass the relevant data on to content.
providers.push({
iconURL: provider.iconURL,
name: provider.name,
origin: provider.origin
});
}
let providersWasSet = !!gSocialProviders;
// Replace old with new.
gSocialProviders = providers.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
// Start listening for changes in the social provider list, if we're not
// doing that yet.
if (!providersWasSet) {
Services.obs.addObserver(updateSocialProvidersCache, "social:providers-changed", false);
} else {
// Dispatch an event to content to let stores freshen-up.
LoopAPIInternal.broadcastPushMessage("SocialProvidersChanged");
}
return gSocialProviders;
};
var gAppVersionInfo = null;
var gBrowserSharingListenerCount = 0;
var gBrowserSharingWindows = new Set();
var gPageListeners = null;
var gOriginalPageListeners = null;
var gSocialProviders = null;
var gStringBundle = null;
const kBatchMessage = "Batch";
const kMaxLoopCount = 10;
const kMessageName = "Loop:Message";
const kPushMessageName = "Loop:Message:Push";
const kPushSubscription = "pushSubscription";
const kRoomsPushPrefix = "Rooms:";
const kMessageHandlers = {
/**
* Start browser sharing, which basically means to start listening for tab
* switches and passing the new window ID to the sender whenever that happens.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
AddBrowserSharingListener: function(message, reply) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
let browser = win && win.gBrowser.selectedBrowser;
if (!win || !browser) {
// This may happen when an undocked conversation window is the only
// window left.
let err = new Error("No tabs available to share.");
MozLoopService.log.error(err);
reply(cloneableError(err));
return;
}
if (browser.getAttribute("remote") == "true") {
// Tab sharing is not supported yet for e10s-enabled browsers. This will
// be fixed in bug 1137634.
let err = new Error("Tab sharing is not supported for e10s-enabled browsers");
MozLoopService.log.error(err);
reply(cloneableError(err));
return;
}
win.LoopUI.startBrowserSharing();
gBrowserSharingWindows.add(Cu.getWeakReference(win));
++gBrowserSharingListenerCount;
},
/**
* Associates a session-id and a call-id with a window for debugging.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} windowId The window id.
* {String} sessionId OT session id.
* {String} callId The callId on the server.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
AddConversationContext: function(message, reply) {
let [windowId, sessionId, callid] = message.data;
MozLoopService.addConversationContext(windowId, {
sessionId: sessionId,
callId: callid
});
reply();
},
/**
* Activates the Social Share panel with the Social Provider panel opened
* when the popup open.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
AddSocialShareProvider: function(message, reply) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win || !win.SocialShare) {
reply();
return;
}
win.SocialShare.showDirectory(win.LoopUI.toolbarButton.anchor);
reply();
},
/**
* Composes an email via the external protocol service.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} subject Subject of the email to send
* {String} body Body message of the email to send
* {String} recipient Recipient email address (optional)
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
ComposeEmail: function(message) {
let [subject, body, recipient] = message.data;
recipient = recipient || "";
let mailtoURL = "mailto:" + encodeURIComponent(recipient) +
"?subject=" + encodeURIComponent(subject) +
"&body=" + encodeURIComponent(body);
extProtocolSvc.loadURI(CommonUtils.makeURI(mailtoURL));
},
/**
* Show a confirmation dialog with the standard - localized - 'Yes'/ 'No'
* buttons or custom labels.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {Object} options Options for the confirm dialog:
* - {String} message Message body for the dialog
* - {String} [okButton] Label for the OK button
* - {String} [cancelButton] Label for the Cancel button
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
Confirm: function(message, reply) {
let options = message.data[0];
let buttonFlags;
if (options.okButton && options.cancelButton) {
buttonFlags =
(Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) +
(Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING);
} else if (!options.okButton && !options.cancelButton) {
buttonFlags = Services.prompt.STD_YES_NO_BUTTONS;
} else {
reply(cloneableError("confirm: missing button options"));
return;
}
try {
let chosenButton = Services.prompt.confirmEx(null, "",
options.message, buttonFlags, options.okButton, options.cancelButton,
null, null, {});
reply(chosenButton == 0);
} catch (ex) {
reply(ex);
}
},
/**
* Copies passed string onto the system clipboard.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} str The string to copy
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
CopyString: function(message, reply) {
let str = message.data[0];
clipboardHelper.copyString(str);
reply();
},
/**
* Returns a new GUID (UUID) in curly braces format.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GenerateUUID: function(message, reply) {
reply(MozLoopService.generateUUID());
},
/**
* Fetch the JSON blob of localized strings from the loop.properties bundle.
* @see MozLoopService#getStrings
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetAllStrings: function(message, reply) {
if (gStringBundle) {
reply(gStringBundle);
return;
}
// Get the map of strings.
let strings = MozLoopService.getStrings();
// Convert it to an object.
gStringBundle = {};
for (let [key, value] of strings.entries()) {
gStringBundle[key] = value;
}
reply(gStringBundle);
},
/**
* Fetch all constants that are used both on the client and the chrome-side.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetAllConstants: function(message, reply) {
reply({
LOOP_SESSION_TYPE: LOOP_SESSION_TYPE,
ROOM_CONTEXT_ADD: ROOM_CONTEXT_ADD,
ROOM_CREATE: ROOM_CREATE,
ROOM_DELETE: ROOM_DELETE,
SHARING_ROOM_URL: SHARING_ROOM_URL,
SHARING_STATE_CHANGE: SHARING_STATE_CHANGE,
TWO_WAY_MEDIA_CONN_LENGTH: TWO_WAY_MEDIA_CONN_LENGTH
});
},
/**
* Returns the app version information for use during feedback.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return {Object} An object containing:
* - channel: The update channel the application is on
* - version: The application version
* - OS: The operating system the application is running on
*/
GetAppVersionInfo: function(message, reply) {
if (!gAppVersionInfo) {
// If the lazy getter explodes, we're probably loaded in xpcshell,
// which doesn't have what we need, so log an error.
try {
gAppVersionInfo = {
channel: UpdateUtils.UpdateChannel,
version: appInfo.version,
OS: appInfo.OS
};
} catch (ex) {}
}
reply(gAppVersionInfo);
},
/**
* Fetch the contents of a specific audio file and return it as a Blob object.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} name Name of the sound to fetch
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetAudioBlob: function(message, reply) {
let name = message.data[0];
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
let url = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = () => {
if (request.status < 200 || request.status >= 300) {
reply(cloneableError(request.status + " " + request.statusText));
return;
}
let blob = new Blob([request.response], {type: "audio/ogg"});
reply(blob);
};
request.send();
},
/**
* Returns the window data for a specific conversation window id.
*
* This data will be relevant to the type of window, e.g. rooms or calls.
* See LoopRooms for more information.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} conversationWindowId
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @returns {Object} The window data or null if error.
*/
GetConversationWindowData: function(message, reply) {
reply(MozLoopService.getConversationWindowData(message.data[0]));
},
/**
* Gets the "do not disturb" mode activation flag.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetDoNotDisturb: function(message, reply) {
reply(MozLoopService.doNotDisturb);
},
/**
* Retrieve the list of errors that are currently pending on the MozLoopService
* class.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetErrors: function(message, reply) {
let errors = {};
for (let [type, error] of MozLoopService.errors) {
// if error.error is an nsIException, just delete it since it's hard
// to clone across the boundary.
if (error.error instanceof Ci.nsIException) {
MozLoopService.log.debug("Warning: Some errors were omitted from MozLoopAPI.errors " +
"due to issues copying nsIException across boundaries.",
error.error);
delete error.error;
}
errors[type] = cloneableError(error);
}
return reply(errors);
},
/**
* Returns TRUE if Firefox Accounts are enabled and can be used.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetFxAEnabled: function(message, reply) {
reply(MozLoopService.fxAEnabled);
},
/**
* Returns true if this profile has an encryption key.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return {Boolean} True if the profile has an encryption key.
*/
GetHasEncryptionKey: function(message, reply) {
reply(MozLoopService.hasEncryptionKey);
},
/**
* Returns the current locale of the browser.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @returns {String} The locale string
*/
GetLocale: function(message, reply) {
reply(MozLoopService.locale);
},
/**
* Return any preference under "loop.".
* Any errors thrown by the Mozilla pref API are logged to the console
* and cause null to be returned. This includes the case of the preference
* not being found.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} prefName The name of the pref without
* the preceding "loop."
* {Enum} prefType Type of preference, defined
* at Ci.nsIPrefBranch. Optional.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return {*} on success, null on error
*/
GetLoopPref: function(message, reply) {
let [prefName, prefType] = message.data;
reply(MozLoopService.getLoopPref(prefName, prefType));
},
/**
* Retrieve the plural rule number of the active locale.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetPluralRule: function(message, reply) {
reply(PluralForm.ruleNum);
},
/**
* Gets the metadata related to the currently selected tab in
* the most recent window.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetSelectedTabMetadata: function(message, reply) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.messageManager.addMessageListener("PageMetadata:PageDataResult", function onPageDataResult(msg) {
win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult);
let pageData = msg.json;
win.LoopUI.getFavicon(function(err, favicon) {
if (err) {
MozLoopService.log.error("Error occurred whilst fetching favicon", err);
// We don't return here intentionally to make sure the callback is
// invoked at all times. We just report the error here.
}
pageData.favicon = favicon || null;
reply(pageData);
});
});
win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
},
/**
* Returns a sorted list of Social Providers that can share URLs. See
* `updateSocialProvidersCache()` for more information.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return {Array} Sorted list of share-capable Social Providers.
*/
GetSocialShareProviders: function(message, reply) {
if (!gSocialProviders) {
updateSocialProvidersCache();
}
reply(gSocialProviders);
},
/**
* Compose a URL pointing to the location of an avatar by email address.
* At the moment we use the Gravatar service to match email addresses with
* avatars. If no email address is found we return null.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} emailAddress Users' email address
* {Number} size Size of the avatar image
* to return in pixels. Optional.
* Default value: 40.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return the URL pointing to an avatar matching the provided email address
* or null if this is not available.
*/
GetUserAvatar: function(message, reply) {
let [emailAddress, size] = message.data;
if (!emailAddress || !MozLoopService.getLoopPref("contacts.gravatars.show")) {
reply(null);
return;
}
// Do the MD5 dance.
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.MD5);
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stringStream.data = emailAddress.trim().toLowerCase();
hasher.updateFromStream(stringStream, -1);
let hash = hasher.finish(false);
// Convert the binary hash data to a hex string.
let md5Email = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
// Compose the Gravatar URL.
reply("https://www.gravatar.com/avatar/" + md5Email +
".jpg?default=blank&s=" + (size || 40));
},
/**
* Gets an object with data that represents the currently
* authenticated user's identity.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return null if user not logged in; profile object otherwise
*/
GetUserProfile: function(message, reply) {
if (!MozLoopService.userProfile) {
reply(null);
return;
}
reply({
email: MozLoopService.userProfile.email,
uid: MozLoopService.userProfile.uid
});
},
/**
* Hangup and close all chat windows that are open.
*/
HangupAllChatWindows: function() {
MozLoopService.hangupAllChatWindows();
},
/**
* Start the FxA login flow using the OAuth client and params from the Loop
* server.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {Boolean} forceReAuth Set to true to force FxA
* into a re-auth even if the
* user is already logged in.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return {Promise} Returns a promise that is resolved on successful
* completion, or rejected otherwise.
*/
LoginToFxA: function(message, reply) {
let forceReAuth = message.data[0];
MozLoopService.logInToFxA(forceReAuth);
reply();
},
/**
* Logout completely from FxA.
* @see MozLoopService#logOutFromFxA
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
LogoutFromFxA: function(message, reply) {
MozLoopService.logOutFromFxA();
reply();
},
/**
* Notifies the UITour module that an event occurred that it might be
* interested in.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} subject Subject of the notification
* {mixed} [params] Optional parameters, providing
* more details to the notification
* subject
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
NotifyUITour: function(message, reply) {
let [subject, params] = message.data;
UITour.notify(subject, params);
reply();
},
/**
* Opens the Getting Started tour in the browser.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} src Origin that starts or resumes the tour
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
OpenGettingStartedTour: function(message, reply) {
var src = message.data[0];
MozLoopService.openGettingStartedTour(src);
reply();
},
/**
* Open the FxA profile/ settings page.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
OpenFxASettings: function(message, reply) {
MozLoopService.openFxASettings();
reply();
},
/**
* Opens a URL in a new tab in the browser.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} url The new url to open
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
OpenURL: function(message, reply) {
let url = message.data[0];
MozLoopService.openURL(url);
reply();
},
/**
* Removes a listener that was previously added.
*/
RemoveBrowserSharingListener: function() {
if (!gBrowserSharingListenerCount) {
return;
}
if (--gBrowserSharingListenerCount > 0) {
// There are still clients listening in, so keep on listening...
return;
}
for (let win of gBrowserSharingWindows) {
win = win.get();
if (!win) {
continue;
}
win.LoopUI.stopBrowserSharing();
}
gBrowserSharingWindows.clear();
},
"Rooms:*": function(action, message, reply) {
LoopAPIInternal.handleObjectAPIMessage(LoopRooms, kRoomsPushPrefix,
action, message, reply);
},
/**
* Sets the "do not disturb" mode activation flag.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
SetDoNotDisturb: function(message, reply) {
MozLoopService.doNotDisturb = message.data[0];
reply();
},
/**
* Set any preference under "loop."
* Any errors thrown by the Mozilla pref API are logged to the console
* and cause false to be returned.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} prefName The name of the pref without
* the preceding "loop."
* {*} value The value to set.
* {Enum} prefType Type of preference, defined at
* Ci.nsIPrefBranch. Optional.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
SetLoopPref: function(message) {
let [prefName, value, prefType] = message.data;
MozLoopService.setLoopPref(prefName, value, prefType);
},
/**
* Used to record the screen sharing state for a window so that it can
* be reflected on the toolbar button.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} windowId The id of the conversation window
* the state is being changed for.
* {Boolean} active Whether or not screen sharing
* is now active.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
SetScreenShareState: function(message) {
let [windowId, active] = message.data;
MozLoopService.setScreenShareState(windowId, active);
},
/**
* Share a room URL with the Social API.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} providerOrigin URL fragment that identifies
* a social provider
* {String} roomURL URL of a room
* {String} title Title of the sharing message
* {String} body Body of the sharing message
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
SocialShareRoom: function(message, reply) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win || !win.SocialShare) {
reply();
return;
}
let [providerOrigin, roomURL, title, body] = message.data;
let graphData = {
url: roomURL,
title: title
};
if (body) {
graphData.body = body;
}
win.SocialShare.sharePage(providerOrigin, graphData, null,
win.LoopUI.toolbarButton.anchor);
reply();
},
/**
* Starts alerting the user about an incoming call
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
StartAlerting: function(message, reply) {
let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
chromeWindow.getAttention();
ringer = new chromeWindow.Audio();
ringer.src = Services.prefs.getCharPref("loop.ringtone");
ringer.loop = true;
ringer.load();
ringer.play();
targetWindow.document.addEventListener("visibilitychange",
ringerStopper = function(event) {
if (event.currentTarget.hidden) {
kMessageHandlers.StopAlerting();
}
});
reply();
},
/**
* Stops alerting the user about an incoming call
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
StopAlerting: function(message, reply) {
if (!ringer) {
reply();
return;
}
if (ringerStopper) {
ringer.ownerDocument.removeEventListener("visibilitychange",
ringerStopper);
ringerStopper = null;
}
ringer.pause();
ringer = null;
reply();
},
/**
* Adds a value to a telemetry histogram.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} histogramId Name of the telemetry histogram
* to update.
* {String} value Label of bucket to increment
* in the histogram.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
TelemetryAddValue: function(message, reply) {
let [histogramId, value] = message.data;
Services.telemetry.getHistogramById(histogramId).add(value);
reply();
}
};
const LoopAPIInternal = {
/**
* Initialize the Loop API, which means:
* 1) setup RemotePageManager to hook into loop documents as channels and
* start listening for messages therein.
* 2) start listening for other events that may be interesting.
*/
initialize: function() {
if (gPageListeners) {
return;
}
Cu.import("resource://gre/modules/RemotePageManager.jsm");
gPageListeners = [new RemotePages("about:looppanel"), new RemotePages("about:loopconversation")];
for (let page of gPageListeners) {
page.addMessageListener(kMessageName, this.handleMessage.bind(this));
}
// Subscribe to global events:
Services.obs.addObserver(this.handleStatusChanged, "loop-status-changed", false);
},
/**
* Handles incoming messages from RemotePageManager that are sent from Loop
* content pages.
*
* @param {Object} message Object containing the following fields:
* - {MessageManager} target Where the message came from
* - {String} name Name of the message
* - {Array} data Payload of the message
* @param {Function} [reply]
*/
handleMessage: function(message, reply) {
let seq = message.data.shift();
let action = message.data.shift();
let actionParts = action.split(":");
// The name that is supposed to match with a handler function is tucked inside
// the second part of the message name. If all is well.
let handlerName = actionParts.shift();
if (!reply) {
reply = result => {
message.target.sendAsyncMessage(message.name, [seq, result]);
}
}
// First, check if this is a batch call.
if (handlerName == kBatchMessage) {
this.handleBatchMessage(seq, message, reply);
return;
}
// Second, check if the message is meant for one of our Object APIs.
// If so, a wildcard entry should exist for the message name in the
// `kMessageHandlers` dictionary.
let wildcardName = handlerName + ":*";
if (kMessageHandlers[wildcardName]) {
// Alright, pass the message forward.
kMessageHandlers[wildcardName](action, message, reply);
// Aaaaand we're done.
return;
}
if (!kMessageHandlers[handlerName]) {
let msg = "Ouch, no message handler available for '" + handlerName + "'";
MozLoopService.log.error(msg);
reply(cloneableError(msg));
return;
}
kMessageHandlers[handlerName](message, reply);
},
/**
* If `sendMessage` above detects that the incoming message consists of a whole
* set of messages, this function is tasked with handling them.
* It iterates over all the messages, sends each to their appropriate handler
* and collects their results. The results will be sent back in one go as response
* to the batch message.
*
* @param {Number} seq Sequence ID of this message
* @param {Object} message Message containing the following parameters in
* its `data` property:
* [
* {Array} requests Sequence of messages
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
handleBatchMessage: function(seq, message, reply) {
let requests = message.data[0];
if (!requests.length) {
MozLoopService.log.error("Ough, a batch call with no requests is not much " +
"of a batch, now is it?");
return;
}
// Since `handleBatchMessage` can be called recursively, but the replies are
// collected and sent only once, we'll make sure only one exists for the
// entire tail.
// We count the amount of recursive calls, because we don't want any consumer
// to cause an infinite loop, now do we?
if (!("loopCount" in reply)) {
reply.loopCount = 0;
} else if (++reply.loopCount > kMaxLoopCount) {
reply(cloneableError("Too many nested calls"));
return;
}
let resultSet = {};
Promise.all(requests.map(requestSet => {
let requestSeq = requestSet[0];
return new Promise(resolve => this.handleMessage({ data: requestSet }, result => {
resultSet[requestSeq] = result;
resolve();
}));
})).then(() => reply(resultSet));
},
/**
* Separate handler that is specialized in dealing with messages meant for sub-APIs,
* like LoopRooms.
*
* @param {Object} api Pointer to the sub-API.
* @param {String} pushMessagePrefix
* @param {String} action Action name that translates to a function
* name present on the sub-API.
* @param {Object} message Message containing parameters required to
* perform the action on the sub-API in its
* `data` property.
* @param {Function} reply Callback function, invoked with the result
* of this message handler. The result will
* be sent back to the senders' channel.
*/
handleObjectAPIMessage: function(api, pushMessagePrefix, action, message, reply) {
let funcName = getObjectAPIFunctionName(action);
if (funcName == kPushSubscription) {
// Incoming event listener request!
let events = message.data[0];
if (!events || !events.length) {
let msg = "Oops, don't forget to pass in event names when you try to " +
"subscribe to them!";
MozLoopService.log.error(msg);
reply(cloneableError(msg));
return;
}
let handlerFunc = (e, ...data) => {
let prettyEventName = e.charAt(0).toUpperCase() + e.substr(1);
try {
message.target.sendAsyncMessage(kPushMessageName, [pushMessagePrefix +
prettyEventName, data]);
} catch(ex) {
MozLoopService.log.debug("Unable to send event through to target: " +
ex.message);
// Unregister event handlers when the message port is unreachable.
for (let eventName of events) {
api.off(eventName, handlerFunc);
}
}
};
for (let eventName of events) {
api.on(eventName, handlerFunc);
}
reply();
return;
}
if (typeof api[funcName] != "function") {
reply(cloneableError("Sorry, function '" + funcName + "' does not exist!"));
return;
}
api[funcName](...message.data, (err, result) => {
reply(err ? cloneableError(err) : result);
});
},
/**
* Observer function for the 'loop-status-changed' event.
*/
handleStatusChanged: function() {
LoopAPIInternal.broadcastPushMessage("LoopStatusChanged");
},
/**
* Send an event to the content window to indicate that the state on the chrome
* side was updated.
*
* @param {name} name Name of the event
*/
broadcastPushMessage: function(name, data) {
if (!gPageListeners) {
return;
}
for (let page of gPageListeners) {
try {
page.sendAsyncMessage(kPushMessageName, [name, data]);
} catch (ex if ex.result == Components.results.NS_ERROR_NOT_INITIALIZED) {
// Don't make noise when the Remote Page Manager needs more time to
// initialize.
}
}
},
/**
* De the reverse of `initialize` above; unhook page and event listeners.
*/
destroy: function() {
if (!gPageListeners) {
return;
}
[for (listener of gPageListeners) listener.destroy()];
gPageListeners = null;
// Unsubscribe from global events.
Services.obs.removeObserver(this.handleStatusChanged, "loop-status-changed");
// Stop listening for changes in the social provider list, if necessary.
if (gSocialProviders) {
Services.obs.removeObserver(updateSocialProvidersCache, "social:providers-changed");
}
}
};
this.LoopAPI = Object.freeze({
/* @see LoopAPIInternal#initialize */
initialize: function() {
LoopAPIInternal.initialize();
},
/* @see LoopAPIInternal#broadcastPushMessage */
broadcastPushMessage: function(name, data) {
LoopAPIInternal.broadcastPushMessage(name, data);
},
/* @see LoopAPIInternal#destroy */
destroy: function() {
LoopAPIInternal.destroy();
},
// The following functions are only used in unit tests.
inspect: function() {
return [Object.create(LoopAPIInternal), Object.create(kMessageHandlers),
gPageListeners ? [...gPageListeners] : null];
},
stub: function(pageListeners) {
if (!gOriginalPageListeners) {
gOriginalPageListeners = gPageListeners;
}
gPageListeners = pageListeners;
},
restore: function() {
if (gOriginalPageListeners) {
gPageListeners = gOriginalPageListeners;
}
}
});