summaryrefslogtreecommitdiffstats
path: root/comm/chat/modules/jsProtoHelper.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/modules/jsProtoHelper.sys.mjs')
-rw-r--r--comm/chat/modules/jsProtoHelper.sys.mjs1796
1 files changed, 1796 insertions, 0 deletions
diff --git a/comm/chat/modules/jsProtoHelper.sys.mjs b/comm/chat/modules/jsProtoHelper.sys.mjs
new file mode 100644
index 0000000000..b792a02ffe
--- /dev/null
+++ b/comm/chat/modules/jsProtoHelper.sys.mjs
@@ -0,0 +1,1796 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import {
+ initLogModule,
+ nsSimpleEnumerator,
+ l10nHelper,
+ ClassInfo,
+} from "resource:///modules/imXPCOMUtils.sys.mjs";
+import { IMServices } from "resource:///modules/IMServices.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "_", () =>
+ l10nHelper("chrome://chat/locale/conversations.properties")
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "TXTToHTML", function () {
+ let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(Ci.mozITXTToHTMLConv);
+ return aTXT => cs.scanTXT(aTXT, cs.kEntities);
+});
+
+function OutgoingMessage(aMsg, aConversation) {
+ this.message = aMsg;
+ this.conversation = aConversation;
+}
+OutgoingMessage.prototype = {
+ __proto__: ClassInfo("imIOutgoingMessage", "Outgoing Message"),
+ cancelled: false,
+ action: false,
+ notification: false,
+};
+
+export var GenericAccountPrototype = {
+ __proto__: ClassInfo("prplIAccount", "generic account object"),
+ get wrappedJSObject() {
+ return this;
+ },
+ _init(aProtocol, aImAccount) {
+ this.protocol = aProtocol;
+ this.imAccount = aImAccount;
+ initLogModule(aProtocol.id, this);
+ },
+ observe(aSubject, aTopic, aData) {},
+ remove() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ unInit() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ connect() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ disconnect() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ createConversation(aName) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ joinChat(aComponents) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ setBool(aName, aVal) {},
+ setInt(aName, aVal) {},
+ setString(aName, aVal) {},
+
+ get name() {
+ return this.imAccount.name;
+ },
+ get connected() {
+ return this.imAccount.connected;
+ },
+ get connecting() {
+ return this.imAccount.connecting;
+ },
+ get disconnected() {
+ return this.imAccount.disconnected;
+ },
+ get disconnecting() {
+ return this.imAccount.disconnecting;
+ },
+ _connectionErrorReason: Ci.prplIAccount.NO_ERROR,
+ get connectionErrorReason() {
+ return this._connectionErrorReason;
+ },
+
+ /**
+ * Convert a socket's nsITransportSecurityInfo into a prplIAccount connection error. Store
+ * the nsITransportSecurityInfo and the connection location on the account so the
+ * certificate exception dialog can access the information.
+ *
+ * @param {Socket} aSocket - Socket where the connection error occurred.
+ * @returns {number} The prplIAccount error constant describing the problem.
+ */
+ handleConnectionSecurityError(aSocket) {
+ // Stash away the connectionTarget and securityInfo.
+ this._connectionTarget = aSocket.host + ":" + aSocket.port;
+ let securityInfo = (this._securityInfo = aSocket.securityInfo);
+
+ if (!securityInfo) {
+ return Ci.prplIAccount.ERROR_CERT_NOT_PROVIDED;
+ }
+
+ if (securityInfo.isUntrusted) {
+ if (securityInfo.serverCert && securityInfo.serverCert.isSelfSigned) {
+ return Ci.prplIAccount.ERROR_CERT_SELF_SIGNED;
+ }
+ return Ci.prplIAccount.ERROR_CERT_UNTRUSTED;
+ }
+
+ if (securityInfo.isNotValidAtThisTime) {
+ if (
+ securityInfo.serverCert &&
+ securityInfo.serverCert.validity.notBefore < Date.now() * 1000
+ ) {
+ return Ci.prplIAccount.ERROR_CERT_NOT_ACTIVATED;
+ }
+ return Ci.prplIAccount.ERROR_CERT_EXPIRED;
+ }
+
+ if (securityInfo.isDomainMismatch) {
+ return Ci.prplIAccount.ERROR_CERT_HOSTNAME_MISMATCH;
+ }
+
+ // XXX ERROR_CERT_FINGERPRINT_MISMATCH
+
+ return Ci.prplIAccount.ERROR_CERT_OTHER_ERROR;
+ },
+ _connectionTarget: "",
+ get connectionTarget() {
+ return this._connectionTarget;
+ },
+ _securityInfo: null,
+ get securityInfo() {
+ return this._securityInfo;
+ },
+
+ reportConnected() {
+ this.imAccount.observe(this, "account-connected", null);
+ },
+ reportConnecting(aConnectionStateMsg) {
+ // Delete any leftover errors from the previous connection.
+ delete this._connectionTarget;
+ delete this._securityInfo;
+
+ if (!this.connecting) {
+ this.imAccount.observe(this, "account-connecting", null);
+ }
+ if (aConnectionStateMsg) {
+ this.imAccount.observe(
+ this,
+ "account-connect-progress",
+ aConnectionStateMsg
+ );
+ }
+ },
+ reportDisconnected() {
+ this.imAccount.observe(this, "account-disconnected", null);
+ },
+ reportDisconnecting(aConnectionErrorReason, aConnectionErrorMessage) {
+ this._connectionErrorReason = aConnectionErrorReason;
+ this.imAccount.observe(
+ this,
+ "account-disconnecting",
+ aConnectionErrorMessage
+ );
+ this.cancelPendingBuddyRequests();
+ this.cancelPendingChatRequests();
+ this.cancelPendingVerificationRequests();
+ },
+
+ // Called when the user adds a new buddy from the UI.
+ addBuddy(aTag, aName) {
+ IMServices.contacts.accountBuddyAdded(
+ new AccountBuddy(this, null, aTag, aName)
+ );
+ },
+ // Called during startup for each of the buddies in the local buddy list.
+ loadBuddy(aBuddy, aTag) {
+ try {
+ return new AccountBuddy(this, aBuddy, aTag);
+ } catch (x) {
+ dump(x + "\n");
+ return null;
+ }
+ },
+
+ _pendingBuddyRequests: null,
+ addBuddyRequest(aUserName, aGrantCallback, aDenyCallback) {
+ if (!this._pendingBuddyRequests) {
+ this._pendingBuddyRequests = [];
+ }
+ let buddyRequest = {
+ get account() {
+ return this._account.imAccount;
+ },
+ get userName() {
+ return aUserName;
+ },
+ _account: this,
+ // Grant and deny callbacks both receive the auth request object as an
+ // argument for further use.
+ grant() {
+ aGrantCallback(this);
+ this._remove();
+ },
+ deny() {
+ aDenyCallback(this);
+ this._remove();
+ },
+ cancel() {
+ Services.obs.notifyObservers(
+ this,
+ "buddy-authorization-request-canceled"
+ );
+ this._remove();
+ },
+ _remove() {
+ this._account.removeBuddyRequest(this);
+ },
+ QueryInterface: ChromeUtils.generateQI(["prplIBuddyRequest"]),
+ };
+ this._pendingBuddyRequests.push(buddyRequest);
+ Services.obs.notifyObservers(buddyRequest, "buddy-authorization-request");
+ },
+ removeBuddyRequest(aRequest) {
+ if (!this._pendingBuddyRequests) {
+ return;
+ }
+
+ this._pendingBuddyRequests = this._pendingBuddyRequests.filter(
+ r => r !== aRequest
+ );
+ },
+ /**
+ * Cancel a pending buddy request.
+ *
+ * @param {string} aUserName - The username the request is for.
+ */
+ cancelBuddyRequest(aUserName) {
+ if (!this._pendingBuddyRequests) {
+ return;
+ }
+
+ for (let request of this._pendingBuddyRequests) {
+ if (request.userName == aUserName) {
+ request.cancel();
+ break;
+ }
+ }
+ },
+ cancelPendingBuddyRequests() {
+ if (!this._pendingBuddyRequests) {
+ return;
+ }
+
+ for (let request of this._pendingBuddyRequests) {
+ request.cancel();
+ }
+ delete this._pendingBuddyRequests;
+ },
+
+ _pendingChatRequests: null,
+ /**
+ * Inform the user about a new conversation invitation.
+ *
+ * @param {string} conversationName - Name of the conversation the user is
+ * invited to.
+ * @param {(prplIChatRequest) => void} grantCallback - Function to be called
+ * when the invite is accepted.
+ * @param {(prplIChatRequest?, boolean) => void} [denyCallback] - Function to
+ * be called when the invite is rejected. If omitted, |canDeny| will be
+ * |false|. Callback is passed a boolean indicating whether the rejection should be
+ * sent to the other party. It being false is equivalent to ignoring the invite, in
+ * which case the callback should try to apply the ignore on the protocol level.
+ */
+ addChatRequest(conversationName, grantCallback, denyCallback) {
+ if (!this._pendingChatRequests) {
+ this._pendingChatRequests = new Set();
+ }
+ let inviteHandling = Services.prefs.getIntPref(
+ "messenger.conversations.autoAcceptChatInvitations"
+ );
+ // Only auto-reject invites that can be denied.
+ if (inviteHandling <= 0 && denyCallback) {
+ const shouldReject = inviteHandling == -1;
+ denyCallback(null, shouldReject);
+ return;
+ }
+ let resolvePromise;
+ let rejectPromise;
+ let completePromise = new Promise((resolve, reject) => {
+ resolvePromise = resolve;
+ rejectPromise = reject;
+ });
+ /** @implements {prplIChatRequest} */
+ let chatRequest = {
+ get account() {
+ return this._account.imAccount;
+ },
+ get conversationName() {
+ return conversationName;
+ },
+ get canDeny() {
+ return Boolean(denyCallback);
+ },
+ _account: this,
+ // Grant and deny callbacks both receive the auth request object as an
+ // argument for further use.
+ grant() {
+ resolvePromise(true);
+ grantCallback(this);
+ this._remove();
+ },
+ deny() {
+ if (!denyCallback) {
+ throw new Error("Can not deny this invitation.");
+ }
+ resolvePromise(false);
+ denyCallback(this, true);
+ this._remove();
+ },
+ cancel() {
+ rejectPromise(new Error("Cancelled"));
+ this._remove();
+ },
+ completePromise,
+ _remove() {
+ this._account.removeChatRequest(this);
+ },
+ QueryInterface: ChromeUtils.generateQI(["prplIChatRequest"]),
+ };
+ this._pendingChatRequests.add(chatRequest);
+ Services.obs.notifyObservers(chatRequest, "conv-authorization-request");
+ },
+ removeChatRequest(aRequest) {
+ if (!this._pendingChatRequests) {
+ return;
+ }
+
+ this._pendingChatRequests.delete(aRequest);
+ },
+ /**
+ * Cancel a pending chat request.
+ *
+ * @param {string} conversationName - The conversation the request is for.
+ */
+ cancelChatRequest(conversationName) {
+ if (!this._pendingChatRequests) {
+ return;
+ }
+
+ for (let request of this._pendingChatRequests) {
+ if (request.conversationName == conversationName) {
+ request.cancel();
+ break;
+ }
+ }
+ },
+ cancelPendingChatRequests() {
+ if (!this._pendingChatRequests) {
+ return;
+ }
+
+ for (let request of this._pendingChatRequests) {
+ request.cancel();
+ }
+ this._pendingChatRequests = null;
+ },
+
+ requestBuddyInfo(aBuddyName) {},
+
+ get canJoinChat() {
+ return false;
+ },
+ getChatRoomFields() {
+ if (!this.chatRoomFields) {
+ return [];
+ }
+ let fieldNames = Object.keys(this.chatRoomFields);
+ return fieldNames.map(
+ fieldName => new ChatRoomField(fieldName, this.chatRoomFields[fieldName])
+ );
+ },
+ getChatRoomDefaultFieldValues(aDefaultChatName) {
+ if (!this.chatRoomFields) {
+ return new ChatRoomFieldValues({});
+ }
+
+ let defaultFieldValues = {};
+ for (let fieldName in this.chatRoomFields) {
+ defaultFieldValues[fieldName] = this.chatRoomFields[fieldName].default;
+ }
+
+ if (aDefaultChatName && "parseDefaultChatName" in this) {
+ let parsedDefaultChatName = this.parseDefaultChatName(aDefaultChatName);
+ for (let field in parsedDefaultChatName) {
+ defaultFieldValues[field] = parsedDefaultChatName[field];
+ }
+ }
+
+ return new ChatRoomFieldValues(defaultFieldValues);
+ },
+ requestRoomInfo(aCallback) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ getRoomInfo(aName) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ get isRoomInfoStale() {
+ return false;
+ },
+
+ getPref(aName, aType) {
+ return this.prefs.prefHasUserValue(aName)
+ ? this.prefs["get" + aType + "Pref"](aName)
+ : this.protocol._getOptionDefault(aName);
+ },
+ getInt(aName) {
+ return this.getPref(aName, "Int");
+ },
+ getBool(aName) {
+ return this.getPref(aName, "Bool");
+ },
+ getString(aName) {
+ return this.prefs.prefHasUserValue(aName)
+ ? this.prefs.getStringPref(aName)
+ : this.protocol._getOptionDefault(aName);
+ },
+
+ get prefs() {
+ return (
+ this._prefs ||
+ (this._prefs = Services.prefs.getBranch(
+ "messenger.account." + this.imAccount.id + ".options."
+ ))
+ );
+ },
+
+ get normalizedName() {
+ return this.normalize(this.name);
+ },
+ normalize(aName) {
+ return aName.toLowerCase();
+ },
+
+ getSessions() {
+ return [];
+ },
+ reportSessionsChanged() {
+ Services.obs.notifyObservers(this.imAccount, "account-sessions-changed");
+ },
+
+ _pendingVerificationRequests: null,
+ /**
+ *
+ * @param {string} aDisplayName - Display name the request is from.
+ * @param {() => Promise<{challenge: string, challengeDescription: string?}>} aGetChallenge - Accept request and generate
+ * the challenge.
+ * @param {AbortSignal} [aAbortSignal] - Abort signal to indicate the request
+ * was cancelled.
+ * @returns {Promise<boolean>} Completion promise for the verification.
+ * Boolean indicates the result of the verification, rejection is a cancel.
+ */
+ addVerificationRequest(aDisplayName, aGetChallenge, aAbortSignal) {
+ if (!this._pendingVerificationRequests) {
+ this._pendingVerificationRequests = [];
+ }
+ let verificationRequest = {
+ _account: this,
+ get account() {
+ return this._account.imAccount;
+ },
+ get subject() {
+ return aDisplayName;
+ },
+ get challengeType() {
+ return Ci.imISessionVerification.CHALLENGE_TEXT;
+ },
+ get challenge() {
+ return this._challenge;
+ },
+ get challengeDescription() {
+ return this._challengeDescription;
+ },
+ _challenge: "",
+ _challengeDescription: "",
+ _canceled: false,
+ completePromise: null,
+ async verify() {
+ const { challenge, challengeDescription = "" } = await aGetChallenge();
+ this._challenge = challenge;
+ this._challengeDescription = challengeDescription;
+ },
+ submitResponse(challengeMatches) {
+ this._accept(challengeMatches);
+ this._remove();
+ },
+ cancel() {
+ if (this._canceled) {
+ return;
+ }
+ this._canceled = true;
+ Services.obs.notifyObservers(
+ this,
+ "buddy-verification-request-canceled"
+ );
+ this._deny();
+ this._remove();
+ },
+ _remove() {
+ this._account.removeVerificationRequest(this);
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "imIIncomingSessionVerification",
+ ]),
+ };
+ verificationRequest.completePromise = new Promise((resolve, reject) => {
+ verificationRequest._accept = resolve;
+ verificationRequest._deny = reject;
+ });
+ this._pendingVerificationRequests.push(verificationRequest);
+ Services.obs.notifyObservers(
+ verificationRequest,
+ "buddy-verification-request"
+ );
+ if (aAbortSignal) {
+ aAbortSignal.addEventListener(
+ "abort",
+ () => {
+ verificationRequest.cancel();
+ },
+ { once: true }
+ );
+ if (aAbortSignal.aborted) {
+ verificationRequest.cancel();
+ }
+ }
+ return verificationRequest.completePromise;
+ },
+ /**
+ * Remove a verification request for this account.
+ *
+ * @param {imIIncomingSessionVerification} aRequest
+ */
+ removeVerificationRequest(aRequest) {
+ if (!this._pendingVerificationRequests) {
+ return;
+ }
+ this._pendingVerificationRequests =
+ this._pendingVerificationRequests.filter(r => r !== aRequest);
+ },
+ cancelPendingVerificationRequests() {
+ if (!this._pendingVerificationRequests) {
+ return;
+ }
+ for (let request of this._pendingVerificationRequests) {
+ request.cancel();
+ }
+ this._pendingVerificationRequests = null;
+ },
+
+ _encryptionStatus: [],
+ get encryptionStatus() {
+ return this._encryptionStatus;
+ },
+ set encryptionStatus(newStatus) {
+ this._encryptionStatus = newStatus;
+ Services.obs.notifyObservers(
+ this.imAccount,
+ "account-encryption-status-changed",
+ newStatus
+ );
+ },
+};
+
+export var GenericAccountBuddyPrototype = {
+ __proto__: ClassInfo("prplIAccountBuddy", "generic account buddy object"),
+ get DEBUG() {
+ return this._account.DEBUG;
+ },
+ get LOG() {
+ return this._account.LOG;
+ },
+ get WARN() {
+ return this._account.WARN;
+ },
+ get ERROR() {
+ return this._account.ERROR;
+ },
+
+ _init(aAccount, aBuddy, aTag, aUserName) {
+ if (!aBuddy && !aUserName) {
+ throw new Error("aUserName is required when aBuddy is null");
+ }
+
+ this._tag = aTag;
+ this._account = aAccount;
+ this._buddy = aBuddy;
+ if (aBuddy) {
+ let displayName = aBuddy.displayName;
+ if (displayName != aUserName) {
+ this._serverAlias = displayName;
+ }
+ }
+ this._userName = aUserName;
+ },
+ unInit() {
+ delete this._tag;
+ delete this._account;
+ delete this._buddy;
+ },
+
+ get account() {
+ return this._account.imAccount;
+ },
+ set buddy(aBuddy) {
+ if (this._buddy) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+ this._buddy = aBuddy;
+ },
+ get buddy() {
+ return this._buddy;
+ },
+ get tag() {
+ return this._tag;
+ },
+ set tag(aNewTag) {
+ let oldTag = this._tag;
+ this._tag = aNewTag;
+ IMServices.contacts.accountBuddyMoved(this, oldTag, aNewTag);
+ },
+
+ _notifyObservers(aTopic, aData) {
+ try {
+ this._buddy.observe(this, "account-buddy-" + aTopic, aData);
+ } catch (e) {
+ this.ERROR(e);
+ }
+ },
+
+ _userName: "",
+ get userName() {
+ return this._userName || this._buddy.userName;
+ },
+ get normalizedName() {
+ return this._account.normalize(this.userName);
+ },
+ _serverAlias: "",
+ get serverAlias() {
+ return this._serverAlias;
+ },
+ set serverAlias(aNewAlias) {
+ let old = this.displayName;
+ this._serverAlias = aNewAlias;
+ if (old != this.displayName) {
+ this._notifyObservers("display-name-changed", old);
+ }
+ },
+
+ /**
+ * Method called to start verification of the buddy. Same signature as
+ * _startVerification of GenericSessionPrototype. If the property is not a
+ * function, |canVerifyIdentity| is false.
+ *
+ * @type {() => {challenge: string, challengeDescription: string?, handleResult: (boolean) => void, cancel: () => void, cancelPromise: Promise}?}
+ */
+ _startVerification: null,
+ get canVerifyIdentity() {
+ return typeof this._startVerification === "function";
+ },
+ _identityVerified: false,
+ get identityVerified() {
+ return this.canVerifyIdentity && this._identityVerified;
+ },
+ verifyIdentity() {
+ if (!this.canVerifyIdentity) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ if (this.identityVerified) {
+ return Promise.resolve();
+ }
+ return this._startVerification().then(
+ ({
+ challenge,
+ challengeDescription,
+ handleResult,
+ cancel,
+ cancelPromise,
+ }) => {
+ const verifier = new SessionVerification(
+ challenge,
+ this.userName,
+ challengeDescription
+ );
+ verifier.completePromise.then(
+ result => handleResult(result),
+ () => cancel()
+ );
+ cancelPromise.then(() => verifier.cancel());
+ return verifier;
+ }
+ );
+ },
+
+ remove() {
+ IMServices.contacts.accountBuddyRemoved(this);
+ },
+
+ // imIStatusInfo implementation
+ get displayName() {
+ return this.serverAlias || this.userName;
+ },
+ _buddyIconFilename: "",
+ get buddyIconFilename() {
+ return this._buddyIconFilename;
+ },
+ set buddyIconFilename(aNewFileName) {
+ this._buddyIconFilename = aNewFileName;
+ this._notifyObservers("icon-changed");
+ },
+ _statusType: 0,
+ get statusType() {
+ return this._statusType;
+ },
+ get online() {
+ return this._statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
+ },
+ get available() {
+ return this._statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
+ },
+ get idle() {
+ return this._statusType == Ci.imIStatusInfo.STATUS_IDLE;
+ },
+ get mobile() {
+ return this._statusType == Ci.imIStatusInfo.STATUS_MOBILE;
+ },
+ _statusText: "",
+ get statusText() {
+ return this._statusText;
+ },
+
+ // This is for use by the protocol plugin, it's not exposed in the
+ // imIStatusInfo interface.
+ // All parameters are optional and will be ignored if they are null
+ // or undefined.
+ setStatus(aStatusType, aStatusText, aAvailabilityDetails) {
+ // Ignore omitted parameters.
+ if (aStatusType === undefined || aStatusType === null) {
+ aStatusType = this._statusType;
+ }
+ if (aStatusText === undefined || aStatusText === null) {
+ aStatusText = this._statusText;
+ }
+ if (aAvailabilityDetails === undefined || aAvailabilityDetails === null) {
+ aAvailabilityDetails = this._availabilityDetails;
+ }
+
+ // Decide which notifications should be fired.
+ let notifications = [];
+ if (
+ this._statusType != aStatusType ||
+ this._availabilityDetails != aAvailabilityDetails
+ ) {
+ notifications.push("availability-changed");
+ }
+ if (this._statusType != aStatusType || this._statusText != aStatusText) {
+ notifications.push("status-changed");
+ if (this.online && aStatusType <= Ci.imIStatusInfo.STATUS_OFFLINE) {
+ notifications.push("signed-off");
+ }
+ if (!this.online && aStatusType > Ci.imIStatusInfo.STATUS_OFFLINE) {
+ notifications.push("signed-on");
+ }
+ }
+
+ // Actually change the stored status.
+ [this._statusType, this._statusText, this._availabilityDetails] = [
+ aStatusType,
+ aStatusText,
+ aAvailabilityDetails,
+ ];
+
+ // Fire the notifications.
+ notifications.forEach(function (aTopic) {
+ this._notifyObservers(aTopic);
+ }, this);
+ },
+
+ _availabilityDetails: 0,
+ get availabilityDetails() {
+ return this._availabilityDetails;
+ },
+
+ get canSendMessage() {
+ return this.online;
+ },
+
+ getTooltipInfo: () => [],
+ createConversation() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+// aUserName is required only if aBuddy is null, i.e., we are adding a buddy.
+function AccountBuddy(aAccount, aBuddy, aTag, aUserName) {
+ this._init(aAccount, aBuddy, aTag, aUserName);
+}
+AccountBuddy.prototype = GenericAccountBuddyPrototype;
+
+export var GenericMessagePrototype = {
+ __proto__: ClassInfo("prplIMessage", "generic message object"),
+
+ _lastId: 0,
+ _init(aWho, aMessage, aObject, aConversation) {
+ this.id = ++GenericMessagePrototype._lastId;
+ this.time = Math.floor(new Date() / 1000);
+ this.who = aWho;
+ this.message = aMessage;
+ this.originalMessage = aMessage;
+ this.conversation = aConversation;
+
+ if (aObject) {
+ for (let i in aObject) {
+ this[i] = aObject[i];
+ }
+ }
+ },
+ _alias: "",
+ get alias() {
+ return this._alias || this.who;
+ },
+ _iconURL: "",
+ get iconURL() {
+ // If the protocol plugin has explicitly set an icon for the message, use it.
+ if (this._iconURL) {
+ return this._iconURL;
+ }
+
+ // Otherwise, attempt to find a buddy for incoming messages, and forward the call.
+ if (this.incoming && this.conversation && !this.conversation.isChat) {
+ let buddy = this.conversation.buddy;
+ if (buddy) {
+ return buddy.buddyIconFilename;
+ }
+ }
+ return "";
+ },
+ conversation: null,
+ remoteId: "",
+
+ outgoing: false,
+ incoming: false,
+ system: false,
+ autoResponse: false,
+ containsNick: false,
+ noLog: false,
+ error: false,
+ delayed: false,
+ noFormat: false,
+ containsImages: false,
+ notification: false,
+ noLinkification: false,
+ noCollapse: false,
+ isEncrypted: false,
+ action: false,
+ deleted: false,
+
+ getActions() {
+ return [];
+ },
+
+ whenDisplayed() {},
+ whenRead() {},
+};
+
+export function Message(aWho, aMessage, aObject, aConversation) {
+ this._init(aWho, aMessage, aObject, aConversation);
+}
+
+Message.prototype = GenericMessagePrototype;
+
+export var GenericConversationPrototype = {
+ __proto__: ClassInfo("prplIConversation", "generic conversation object"),
+ get wrappedJSObject() {
+ return this;
+ },
+
+ get DEBUG() {
+ return this._account.DEBUG;
+ },
+ get LOG() {
+ return this._account.LOG;
+ },
+ get WARN() {
+ return this._account.WARN;
+ },
+ get ERROR() {
+ return this._account.ERROR;
+ },
+
+ _init(aAccount, aName) {
+ this._account = aAccount;
+ this._name = aName;
+ this._observers = [];
+ this._date = new Date() * 1000;
+ IMServices.conversations.addConversation(this);
+ },
+
+ _id: 0,
+ get id() {
+ return this._id;
+ },
+ set id(aId) {
+ if (this._id) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+ this._id = aId;
+ },
+
+ addObserver(aObserver) {
+ if (!this._observers.includes(aObserver)) {
+ this._observers.push(aObserver);
+ }
+ },
+ removeObserver(aObserver) {
+ this._observers = this._observers.filter(o => o !== aObserver);
+ },
+ notifyObservers(aSubject, aTopic, aData) {
+ for (let observer of this._observers) {
+ try {
+ observer.observe(aSubject, aTopic, aData);
+ } catch (e) {
+ this.ERROR(e);
+ }
+ }
+ },
+
+ prepareForSending: aOutgoingMessage => [aOutgoingMessage.message],
+ prepareForDisplaying(aImMessage) {
+ if (aImMessage.displayMessage !== aImMessage.message) {
+ this.DEBUG(
+ "Preparing:\n" +
+ aImMessage.message +
+ "\nDisplaying:\n" +
+ aImMessage.displayMessage
+ );
+ }
+ },
+ sendMsg(aMsg, aAction = false, aNotification = false) {
+ // Add-ons (eg. pastebin) have an opportunity to cancel the message at this
+ // point, or change the text content of the message.
+ // If an add-on wants to split a message, it should truncate the first
+ // message, and insert new messages using the conversation's sendMsg method.
+ let om = new OutgoingMessage(aMsg, this);
+ om.action = aAction;
+ om.notification = aNotification;
+ this.notifyObservers(om, "preparing-message");
+ if (om.cancelled) {
+ return;
+ }
+
+ // Protocols have an opportunity here to preprocess messages before they are
+ // sent (eg. split long messages). If a message is split here, the split
+ // will be visible in the UI.
+ let messages = this.prepareForSending(om);
+ let isAction = om.action;
+ let isNotification = om.notification;
+
+ for (let msg of messages) {
+ // Add-ons (eg. OTR) have an opportunity to tweak or cancel the message
+ // at this point.
+ om = new OutgoingMessage(msg, this);
+ om.action = isAction;
+ om.notification = isNotification;
+ this.notifyObservers(om, "sending-message");
+ if (om.cancelled) {
+ continue;
+ }
+ this.dispatchMessage(om.message, om.action, om.notification);
+ }
+ },
+ dispatchMessage(message, action, notification) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ sendTyping: aString => Ci.prplIConversation.NO_TYPING_LIMIT,
+
+ close() {
+ Services.obs.notifyObservers(this, "closing-conversation");
+ IMServices.conversations.removeConversation(this);
+ },
+ unInit() {
+ delete this._account;
+ delete this._observers;
+ },
+
+ /**
+ * Create a prplIMessage instance from params.
+ *
+ * @param {string} who - Nick of the participant who sent the message.
+ * @param {string} text - Raw message contents.
+ * @param {object} properties - Additional properties of the message.
+ * @returns {prplIMessage}
+ */
+ createMessage(who, text, properties) {
+ return new Message(who, text, properties, this);
+ },
+
+ writeMessage(aWho, aText, aProperties) {
+ const message = this.createMessage(aWho, aText, aProperties);
+ this.notifyObservers(message, "new-text");
+ },
+
+ /**
+ * Update the contents of a message.
+ *
+ * @param {string} who - Nick of the participant who sent the message.
+ * @param {string} text - Raw contents of the message.
+ * @param {object} properties - Additional properties of the message. Should
+ * specify a |remoteId| to find the previous version of this message.
+ */
+ updateMessage(who, text, properties) {
+ const message = this.createMessage(who, text, properties);
+ this.notifyObservers(message, "update-text");
+ },
+
+ /**
+ * Remove a message from the conversation. Does not affect logs, use
+ * updateMessage with a deleted property to remove from logs.
+ *
+ * @param {string} remoteId - Remote ID of the event to remove.
+ */
+ removeMessage(remoteId) {
+ this.notifyObservers(null, "remove-text", remoteId);
+ },
+
+ get account() {
+ return this._account.imAccount;
+ },
+ get name() {
+ return this._name;
+ },
+ get normalizedName() {
+ return this._account.normalize(this.name);
+ },
+ get title() {
+ return this.name;
+ },
+ get startDate() {
+ return this._date;
+ },
+ _convIconFilename: "",
+ get convIconFilename() {
+ return this._convIconFilename;
+ },
+ set convIconFilename(aNewFilename) {
+ this._convIconFilename = aNewFilename;
+ this.notifyObservers(this, "update-conv-icon");
+ },
+
+ get encryptionState() {
+ return Ci.prplIConversation.ENCRYPTION_NOT_SUPPORTED;
+ },
+ initializeEncryption() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+export var GenericConvIMPrototype = {
+ __proto__: GenericConversationPrototype,
+ _interfaces: [Ci.prplIConversation, Ci.prplIConvIM],
+ classDescription: "generic ConvIM object",
+
+ updateTyping(aState, aName) {
+ if (aState == this.typingState) {
+ return;
+ }
+
+ if (aState == Ci.prplIConvIM.NOT_TYPING) {
+ delete this.typingState;
+ } else {
+ this.typingState = aState;
+ }
+ this.notifyObservers(null, "update-typing", aName);
+ },
+
+ get isChat() {
+ return false;
+ },
+ buddy: null,
+ typingState: Ci.prplIConvIM.NOT_TYPING,
+ get convIconFilename() {
+ // By default, pass through information from the buddy for IM conversations
+ // that don't have their own icon.
+ const convIconFilename = this._convIconFilename;
+ if (convIconFilename) {
+ return convIconFilename;
+ }
+ return this.buddy?.buddyIconFilename;
+ },
+};
+
+export var GenericConvChatPrototype = {
+ __proto__: GenericConversationPrototype,
+ _interfaces: [Ci.prplIConversation, Ci.prplIConvChat],
+ classDescription: "generic ConvChat object",
+
+ _init(aAccount, aName, aNick) {
+ // _participants holds prplIConvChatBuddy objects.
+ this._participants = new Map();
+ this.nick = aNick;
+ GenericConversationPrototype._init.call(this, aAccount, aName);
+ },
+
+ get isChat() {
+ return true;
+ },
+
+ // Stores the prplIChatRoomFieldValues required to join this channel
+ // to enable later reconnections. If null, the MUC will not be reconnected
+ // automatically after disconnections.
+ chatRoomFields: null,
+
+ _topic: "",
+ _topicSetter: null,
+ get topic() {
+ return this._topic;
+ },
+ get topicSettable() {
+ return false;
+ },
+ get topicSetter() {
+ return this._topicSetter;
+ },
+ /**
+ * Set the topic of a conversation.
+ *
+ * @param {string} aTopic - The new topic. If an update message is sent to
+ * the conversation, this will be HTML escaped before being sent.
+ * @param {string} aTopicSetter - The user who last modified the topic.
+ * @param {string} aQuiet - If false, a message notifying about the topic
+ * change will be sent to the conversation.
+ */
+ setTopic(aTopic, aTopicSetter, aQuiet) {
+ // Only change the topic if the topic and/or topic setter has changed.
+ if (
+ this._topic == aTopic &&
+ (!this._topicSetter || this._topicSetter == aTopicSetter)
+ ) {
+ return;
+ }
+
+ this._topic = aTopic;
+ this._topicSetter = aTopicSetter;
+
+ this.notifyObservers(null, "chat-update-topic");
+
+ if (aQuiet) {
+ return;
+ }
+
+ // Send the topic as a message.
+ let message;
+ if (aTopicSetter) {
+ if (aTopic) {
+ message = lazy._("topicChanged", aTopicSetter, lazy.TXTToHTML(aTopic));
+ } else {
+ message = lazy._("topicCleared", aTopicSetter);
+ }
+ } else {
+ aTopicSetter = null;
+ if (aTopic) {
+ message = lazy._("topicSet", this.name, lazy.TXTToHTML(aTopic));
+ } else {
+ message = lazy._("topicNotSet", this.name);
+ }
+ }
+ this.writeMessage(aTopicSetter, message, { system: true });
+ },
+
+ get nick() {
+ return this._nick;
+ },
+ set nick(aNick) {
+ this._nick = aNick;
+ let escapedNick = this._nick.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&");
+ this._pingRegexp = new RegExp("(?:^|\\W)" + escapedNick + "(?:\\W|$)", "i");
+ },
+
+ _left: false,
+ get left() {
+ return this._left;
+ },
+ set left(aLeft) {
+ if (aLeft == this._left) {
+ return;
+ }
+ this._left = aLeft;
+ this.notifyObservers(null, "update-conv-chatleft");
+ },
+
+ _joining: false,
+ get joining() {
+ return this._joining;
+ },
+ set joining(aJoining) {
+ if (aJoining == this._joining) {
+ return;
+ }
+ this._joining = aJoining;
+ this.notifyObservers(null, "update-conv-chatjoining");
+ },
+
+ getParticipant(aName) {
+ return this._participants.has(aName) ? this._participants.get(aName) : null;
+ },
+ getParticipants() {
+ // Convert the values of the Map into an array.
+ return Array.from(this._participants.values());
+ },
+ getNormalizedChatBuddyName: aChatBuddyName => aChatBuddyName,
+
+ // Updates the nick of a participant in conversation to a new one.
+ updateNick(aOldNick, aNewNick, isOwnNick) {
+ let message;
+ let isParticipant = this._participants.has(aOldNick);
+ if (isOwnNick) {
+ // If this is the user's nick, change it.
+ this.nick = aNewNick;
+ message = lazy._("nickSet.you", aNewNick);
+
+ // If the account was disconnected, it's OK the user is not a participant.
+ if (!isParticipant) {
+ return;
+ }
+ } else if (!isParticipant) {
+ this.ERROR(
+ "Trying to rename nick that doesn't exist! " +
+ aOldNick +
+ " to " +
+ aNewNick
+ );
+ return;
+ } else {
+ message = lazy._("nickSet", aOldNick, aNewNick);
+ }
+
+ // Get the original participant and then remove it.
+ let participant = this._participants.get(aOldNick);
+ this._participants.delete(aOldNick);
+
+ // Update the nickname and add it under the new nick.
+ participant.name = aNewNick;
+ this._participants.set(aNewNick, participant);
+
+ this.notifyObservers(participant, "chat-buddy-update", aOldNick);
+ this.writeMessage(aOldNick, message, { system: true });
+ },
+
+ // Removes a participant from conversation.
+ removeParticipant(aNick) {
+ if (!this._participants.has(aNick)) {
+ return;
+ }
+
+ let stringNickname = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ stringNickname.data = aNick;
+ this.notifyObservers(
+ new nsSimpleEnumerator([stringNickname]),
+ "chat-buddy-remove"
+ );
+ this._participants.delete(aNick);
+ },
+
+ // Removes all participant in conversation.
+ removeAllParticipants() {
+ let stringNicknames = [];
+ this._participants.forEach(function (aParticipant) {
+ let stringNickname = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ stringNickname.data = aParticipant.name;
+ stringNicknames.push(stringNickname);
+ });
+ this.notifyObservers(
+ new nsSimpleEnumerator(stringNicknames),
+ "chat-buddy-remove"
+ );
+ this._participants.clear();
+ },
+
+ createMessage(who, text, properties) {
+ properties.containsNick =
+ "incoming" in properties && this._pingRegexp.test(text);
+ return GenericConversationPrototype.createMessage.apply(this, arguments);
+ },
+};
+
+export var GenericConvChatBuddyPrototype = {
+ __proto__: ClassInfo("prplIConvChatBuddy", "generic ConvChatBuddy object"),
+
+ _name: "",
+ get name() {
+ return this._name;
+ },
+ set name(aName) {
+ this._name = aName;
+ },
+ alias: "",
+ buddy: false,
+ buddyIconFilename: "",
+
+ voiced: false,
+ moderator: false,
+ admin: false,
+ founder: false,
+ typing: false,
+
+ /**
+ * Method called to start verification of the buddy. Same signature as
+ * _startVerification of GenericSessionPrototype. If the property is not a
+ * function, |canVerifyIdentity| is false.
+ *
+ * @type {() => {challenge: string, challengeDescription: string?, handleResult: (boolean) => void, cancel: () => void, cancelPromise: Promise}?}
+ */
+ _startVerification: null,
+ get canVerifyIdentity() {
+ return typeof this._startVerification === "function";
+ },
+ _identityVerified: false,
+ get identityVerified() {
+ return this.canVerifyIdentity && this._identityVerified;
+ },
+ verifyIdentity() {
+ if (!this.canVerifyIdentity) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ if (this.identityVerified) {
+ return Promise.resolve();
+ }
+ return this._startVerification().then(
+ ({
+ challenge,
+ challengeDescription,
+ handleResult,
+ cancel,
+ cancelPromise,
+ }) => {
+ const verifier = new SessionVerification(
+ challenge,
+ this.name,
+ challengeDescription
+ );
+ verifier.completePromise.then(
+ result => handleResult(result),
+ () => cancel()
+ );
+ cancelPromise.then(() => verifier.cancel());
+ return verifier;
+ }
+ );
+ },
+};
+
+export function TooltipInfo(aLabel, aValue, aType = Ci.prplITooltipInfo.pair) {
+ this.type = aType;
+ if (aType == Ci.prplITooltipInfo.status) {
+ this.label = aLabel.toString();
+ this.value = aValue || "";
+ } else if (aType == Ci.prplITooltipInfo.icon) {
+ this.value = aValue;
+ } else if (
+ aLabel === undefined ||
+ aType == Ci.prplITooltipInfo.sectionBreak
+ ) {
+ this.type = Ci.prplITooltipInfo.sectionBreak;
+ } else {
+ this.label = aLabel;
+ if (aValue === undefined) {
+ this.type = Ci.prplITooltipInfo.sectionHeader;
+ } else {
+ this.value = aValue;
+ }
+ }
+}
+
+TooltipInfo.prototype = ClassInfo("prplITooltipInfo", "generic tooltip info");
+
+/* aOption is an object containing:
+ * - label: localized text to display (recommended: use a getter with _)
+ * - default: the default value for this option. The type of the
+ * option will be determined based on the type of the default value.
+ * If the default value is a string, the option will be of type
+ * list if listValues has been provided. In that case the default
+ * value should be one of the listed values.
+ * - [optional] listValues: only if this option can only take a list of
+ * predefined values. This is an object of the form:
+ * {value1: localizedLabel, value2: ...}.
+ * - [optional] masked: boolean, if true the UI shouldn't display the value.
+ * This could typically be used for password field.
+ * Warning: The UI currently doesn't support this.
+ */
+function purplePref(aName, aOption) {
+ this.name = aName; // Preference name
+ this.label = aOption.label; // Text to display
+
+ if (aOption.default === undefined || aOption.default === null) {
+ throw new Error(
+ "A default value for the option is required to determine its type."
+ );
+ }
+ this._defaultValue = aOption.default;
+
+ const kTypes = { boolean: "Bool", string: "String", number: "Int" };
+ let type = kTypes[typeof aOption.default];
+ if (!type) {
+ throw new Error("Invalid option type");
+ }
+
+ if (type == "String" && "listValues" in aOption) {
+ type = "List";
+ this._listValues = aOption.listValues;
+ }
+ this.type = Ci.prplIPref["type" + type];
+
+ if ("masked" in aOption && aOption.masked) {
+ this.masked = true;
+ }
+}
+purplePref.prototype = {
+ __proto__: ClassInfo("prplIPref", "generic account option preference"),
+
+ masked: false,
+
+ // Default value
+ getBool() {
+ return this._defaultValue;
+ },
+ getInt() {
+ return this._defaultValue;
+ },
+ getString() {
+ return this._defaultValue;
+ },
+ getList() {
+ // Convert a JavaScript object map {"value 1": "label 1", ...}
+ let keys = Object.keys(this._listValues);
+ return keys.map(key => new purpleKeyValuePair(this._listValues[key], key));
+ },
+ getListDefault() {
+ return this._defaultValue;
+ },
+};
+
+function purpleKeyValuePair(aName, aValue) {
+ this.name = aName;
+ this.value = aValue;
+}
+purpleKeyValuePair.prototype = ClassInfo(
+ "prplIKeyValuePair",
+ "generic Key Value Pair"
+);
+
+function UsernameSplit(aValues) {
+ this._values = aValues;
+}
+UsernameSplit.prototype = {
+ __proto__: ClassInfo("prplIUsernameSplit", "username split object"),
+
+ get label() {
+ return this._values.label;
+ },
+ get separator() {
+ return this._values.separator;
+ },
+ get defaultValue() {
+ return this._values.defaultValue;
+ },
+};
+
+function ChatRoomField(aIdentifier, aField) {
+ this.identifier = aIdentifier;
+ this.label = aField.label;
+ this.required = !!aField.required;
+
+ let type = "TEXT";
+ if (typeof aField.default == "number") {
+ type = "INT";
+ this.min = aField.min;
+ this.max = aField.max;
+ } else if (aField.isPassword) {
+ type = "PASSWORD";
+ }
+ this.type = Ci.prplIChatRoomField["TYPE_" + type];
+}
+ChatRoomField.prototype = ClassInfo(
+ "prplIChatRoomField",
+ "ChatRoomField object"
+);
+
+function ChatRoomFieldValues(aMap) {
+ this.values = aMap;
+}
+ChatRoomFieldValues.prototype = {
+ __proto__: ClassInfo("prplIChatRoomFieldValues", "ChatRoomFieldValues"),
+
+ getValue(aIdentifier) {
+ return this.values.hasOwnProperty(aIdentifier)
+ ? this.values[aIdentifier]
+ : null;
+ },
+ setValue(aIdentifier, aValue) {
+ this.values[aIdentifier] = aValue;
+ },
+};
+
+// the name getter and the getAccount method need to be implemented by
+// protocol plugins.
+export var GenericProtocolPrototype = {
+ __proto__: ClassInfo("prplIProtocol", "Generic protocol object"),
+
+ init(aId) {
+ if (aId != this.id) {
+ throw new Error(
+ "Creating an instance of " +
+ aId +
+ " but this object implements " +
+ this.id
+ );
+ }
+ },
+ get id() {
+ return "prpl-" + this.normalizedName;
+ },
+ get iconBaseURI() {
+ return "chrome://chat/skin/prpl-generic/";
+ },
+
+ getAccount(aImAccount) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ _getOptionDefault(aName) {
+ if (this.options && this.options.hasOwnProperty(aName)) {
+ return this.options[aName].default;
+ }
+ throw new Error(aName + " has no default value in " + this.id + ".");
+ },
+ getOptions() {
+ if (!this.options) {
+ return [];
+ }
+
+ let purplePrefs = [];
+ for (let [name, option] of Object.entries(this.options)) {
+ purplePrefs.push(new purplePref(name, option));
+ }
+ return purplePrefs;
+ },
+ usernamePrefix: "",
+ getUsernameSplit() {
+ if (!this.usernameSplits || !this.usernameSplits.length) {
+ return [];
+ }
+ return this.usernameSplits.map(split => new UsernameSplit(split));
+ },
+
+ /**
+ * Protocol agnostic implementation that splits the username by the pattern
+ * defined with |usernamePrefix| and |usernameSplits| on the protocol.
+ * Prefers the first occurrence of a separator.
+ *
+ * @param {string} aName - Username to split.
+ * @returns {string[]} Parts of the username or empty array if the username
+ * doesn't match the splitting format.
+ */
+ splitUsername(aName) {
+ let remainingName = aName;
+ if (this.usernamePrefix) {
+ if (!remainingName.startsWith(this.usernamePrefix)) {
+ return [];
+ }
+ remainingName = remainingName.slice(this.usernamePrefix.length);
+ }
+ if (!this.usernameSplits || !this.usernameSplits.length) {
+ return [remainingName];
+ }
+ const parts = [];
+ for (const split of this.usernameSplits) {
+ if (!remainingName.includes(split.separator)) {
+ return [];
+ }
+ const separatorIndex = remainingName.indexOf(split.separator);
+ parts.push(remainingName.slice(0, separatorIndex));
+ remainingName = remainingName.slice(
+ separatorIndex + split.separator.length
+ );
+ }
+ parts.push(remainingName);
+ return parts;
+ },
+
+ registerCommands() {
+ if (!this.commands) {
+ return;
+ }
+
+ this.commands.forEach(function (command) {
+ if (!command.hasOwnProperty("name") || !command.hasOwnProperty("run")) {
+ throw new Error("Every command must have a name and a run function.");
+ }
+ if (!("QueryInterface" in command)) {
+ command.QueryInterface = ChromeUtils.generateQI(["imICommand"]);
+ }
+ if (!command.hasOwnProperty("usageContext")) {
+ command.usageContext = Ci.imICommand.CMD_CONTEXT_ALL;
+ }
+ if (!command.hasOwnProperty("priority")) {
+ command.priority = Ci.imICommand.CMD_PRIORITY_PRPL;
+ }
+ IMServices.cmd.registerCommand(command, this.id);
+ }, this);
+ },
+
+ // NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED errors are too noisy
+ get usernameEmptyText() {
+ return "";
+ },
+ accountExists: () => false, // FIXME
+
+ get chatHasTopic() {
+ return false;
+ },
+ get noPassword() {
+ return false;
+ },
+ get passwordOptional() {
+ return false;
+ },
+ get slashCommandsNative() {
+ return false;
+ },
+ get canEncrypt() {
+ return false;
+ },
+
+ get classDescription() {
+ return this.name + " Protocol";
+ },
+ get contractID() {
+ return "@mozilla.org/chat/" + this.normalizedName + ";1";
+ },
+};
+
+/**
+ * Text challenge session verification flow. Starts the UI flow.
+ *
+ * @param {string} challenge - String the challenge should display.
+ * @param {string} subject - Human readable identifier of the other side of the
+ * challenge.
+ * @param {string} [challengeDescription] - Description of the challenge
+ * contents.
+ */
+function SessionVerification(challenge, subject, challengeDescription) {
+ this._challenge = challenge;
+ this._subject = subject;
+ if (challengeDescription) {
+ this._description = challengeDescription;
+ }
+ this._responsePromise = new Promise((resolve, reject) => {
+ this._submit = resolve;
+ this._cancel = reject;
+ });
+}
+SessionVerification.prototype = {
+ __proto__: ClassInfo(
+ "imISessionVerification",
+ "generic session verification object"
+ ),
+ _challengeType: Ci.imISessionVerification.CHALLENGE_TEXT,
+ _challenge: "",
+ _description: "",
+ _responsePromise: null,
+ _submit: null,
+ _cancel: null,
+ _cancelled: false,
+ get challengeType() {
+ return this._challengeType;
+ },
+ get challenge() {
+ return this._challenge;
+ },
+ get challengeDescription() {
+ return this._description;
+ },
+ get subject() {
+ return this._subject;
+ },
+ get completePromise() {
+ return this._responsePromise;
+ },
+ submitResponse(challengeMatches) {
+ this._submit(challengeMatches);
+ },
+ cancel() {
+ if (this._cancelled) {
+ return;
+ }
+ this._cancelled = true;
+ this._cancel();
+ },
+};
+
+export var GenericSessionPrototype = {
+ __proto__: ClassInfo("prplISession", "generic session object"),
+ /**
+ * Initialize the session.
+ *
+ * @param {prplIAccount} account - Account the session is related to.
+ * @param {string} id - ID of the session.
+ * @param {boolean} [trusted=false] - If the session is trusted.
+ * @param {boolean} [currentSession=false] - If the session represents the.
+ * session we're connected as.
+ */
+ _init(account, id, trusted = false, currentSession = false) {
+ this._account = account;
+ this._id = id;
+ this._trusted = trusted;
+ this._currentSession = currentSession;
+ },
+ _account: null,
+ _id: "",
+ _trusted: false,
+ _currentSession: false,
+ get id() {
+ return this._id;
+ },
+ get trusted() {
+ return this._trusted;
+ },
+ set trusted(newTrust) {
+ this._trusted = newTrust;
+ this._account.reportSessionsChanged();
+ },
+ get currentSession() {
+ return this._currentSession;
+ },
+ /**
+ * Handle the start of the session verification process. The protocol is
+ * expected to update the trusted property on the session if it becomes
+ * trusted after verification.
+ *
+ * @returns {Promise<{challenge: string, challengeDescription: string?, handleResult: (boolean) => void, cancel: () => void, cancelPromise: Promise<void>}>}
+ * Promise resolves to an object holding the challenge string, as well as a
+ * callback that handles the result of the verification flow. The cancel
+ * callback is called when the verification is cancelled and the cancelPromise
+ * is used for the protocol to report when the other side cancels.
+ * The cancel callback will be called when the cancel promise resolves.
+ */
+ _startVerification() {
+ return Promise.reject(
+ Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED)
+ );
+ },
+ verify() {
+ if (this.trusted) {
+ return Promise.resolve();
+ }
+ return this._startVerification().then(
+ ({
+ challenge,
+ challengeDescription,
+ handleResult,
+ cancel,
+ cancelPromise,
+ }) => {
+ const verifier = new SessionVerification(
+ challenge,
+ this.id,
+ challengeDescription
+ );
+ verifier.completePromise.then(
+ result => handleResult(result),
+ () => cancel()
+ );
+ cancelPromise.then(() => verifier.cancel());
+ return verifier;
+ }
+ );
+ },
+};