1244 lines
48 KiB
JavaScript
1244 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 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) {
|
|
for (let page of gPageListeners) {
|
|
page.sendAsyncMessage(kPushMessageName, [name, data]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
gPageListeners = pageListeners;
|
|
}
|
|
});
|