new file mode 100644
--- /dev/null
+++ b/chat/locales/en-US/yahoo.properties
@@ -0,0 +1,31 @@
+# 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/.
+
+login.error.badCredentials=Username or password is incorrect.
+login.error.accountLockedFailed=Account locked due to too many failed login attempts.
+login.error.accountLockedGeneral=Account locked due to too many login attempts.
+login.error.accountDeactivated=Account has been deactivated.
+login.error.usernameNotExist=The username does not exist.
+login.error.unknown=Unknown Error %S
+
+network.error.http=HTTP connection error.
+
+conference.invite.message=Join my conference.
+buddy.invite.message=Would you be my chat buddy?
+
+# Some options are commented out because they aren't used. We do the same thing
+# to their description strings.
+options.pagerPort=Pager port
+#options.transferHost=File transfer server
+#options.transferPort=File transfer port
+#options.chatLocale=Chat room locale
+options.chatEncoding=Encoding
+options.ignoreInvites=Ignore conference and chatroom invitations
+#options.proxySSL=Use account proxy for HTTP and HTTPS connections
+
+# In this message, %S is replaced with the username of the user who left.
+system.message.conferenceLogoff=%S has left the conference.
+system.message.conferenceLogon=%S has joined the conference.
+
+command.help.invite=/invite <user1>[,<user2>,...] [<invite message>]: invite a user into a conference chat.
--- a/chat/locales/jar.mn
+++ b/chat/locales/jar.mn
@@ -10,8 +10,9 @@
locale/@AB_CD@/chat/commands.properties (%commands.properties)
locale/@AB_CD@/chat/conversations.properties (%conversations.properties)
locale/@AB_CD@/chat/facebook.properties (%facebook.properties)
locale/@AB_CD@/chat/irc.properties (%irc.properties)
locale/@AB_CD@/chat/logger.properties (%logger.properties)
locale/@AB_CD@/chat/status.properties (%status.properties)
locale/@AB_CD@/chat/twitter.properties (%twitter.properties)
locale/@AB_CD@/chat/xmpp.properties (%xmpp.properties)
+ locale/@AB_CD@/chat/yahoo.properties (%yahoo.properties)
--- a/chat/moz.build
+++ b/chat/moz.build
@@ -12,13 +12,14 @@ PARALLEL_DIRS += [
'locales',
'protocols/facebook',
'protocols/gtalk',
'protocols/irc',
'protocols/odnoklassniki',
'protocols/twitter',
'protocols/vkontakte',
'protocols/xmpp',
+ 'protocols/yahoo',
]
if CONFIG['MOZ_DEBUG']:
PARALLEL_DIRS += ['protocols/jsTest']
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
+
+EXTRA_COMPONENTS += [
+ 'yahoo.js',
+ 'yahoo.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'yahoo-session.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/test/test_yahooLoginHelper.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource:///modules/ArrayBufferUtils.jsm");
+Components.utils.import("resource:///modules/Services.jsm");
+Components.utils.import("resource:///modules/yahoo-session.jsm");
+let yahoo = {};
+Services.scriptloader.loadSubScript("resource:///modules/yahoo-session.jsm", yahoo);
+
+// Preset test values.
+const kUsername = "testUser";
+const kPassword = "instantbird";
+const kPagerIp = "123.456.78.9";
+const kCrumb = "MG-Z/jNG+Q==";
+const kChallengeString = "AEF08DBAC33F9EEDABCFEA==";
+const kYCookie = "OTJmMTQyOTU1ZGQ4MDA3Y2I2ODljMTU5";
+const kTCookie = "NTdlZmIzY2Q4ODI3ZTc3NTIxYTk1MDhm";
+const kToken = "MThmMzg3OWM3ODcxMW";
+
+const kPagerAddressResponse = "COLO_CAPACITY=1\r\nCS_IP_ADDRESS=" + kPagerIp;
+const kTokenResponse = "0\r\n" + kToken + "\r\npartnerid=dummyValue";
+const kCookieResponse = "0\r\ncrumb=" + kCrumb + "\r\nY=" + kYCookie +
+ "\r\nT=" + kTCookie + "\r\ncookievalidfor=86400";
+
+/* In each test, we override the function that would normally be called next in
+ * the login process. We do this so that we can intercept the login process,
+ * preventing calls to real Yahoo! servers, and do equality testing. */
+function run_test()
+{
+ add_test(test_pagerAddress);
+ add_test(test_challengeString);
+ add_test(test_loginToken);
+ add_test(test_cookies);
+ run_next_test();
+}
+
+function test_pagerAddress()
+{
+ let helper = new yahoo.YahooLoginHelper({}, {});
+
+ helper._getChallengeString = function() {
+ do_check_eq(kPagerIp, helper._session.pagerAddress);
+ run_next_test();
+ };
+
+ helper._onPagerAddressResponse(kPagerAddressResponse, null);
+}
+
+function test_challengeString()
+{
+ let helper = new yahoo.YahooLoginHelper({}, {});
+
+ helper._getLoginToken = function() {
+ do_check_eq(kChallengeString, helper._challengeString);
+ run_next_test();
+ };
+
+ let response = new yahoo.YahooPacket(yahoo.kPacketType.AuthResponse, 0, 0);
+ response.addValue(1, helper._username);
+ response.addValue(94, kChallengeString);
+ response.addValue(13, 0);
+ helper._onChallengeStringResponse(response.toArrayBuffer());
+}
+
+function test_loginToken()
+{
+ let helper = new yahoo.YahooLoginHelper({}, {});
+
+ helper._getCookies = function() {
+ do_check_eq(kToken, helper._loginToken);
+ run_next_test();
+ };
+
+ helper._onLoginTokenResponse(kTokenResponse, null);
+}
+
+function test_cookies()
+{
+ let helper = new yahoo.YahooLoginHelper({}, {});
+
+ helper._sendPagerAuthResponse = function() {
+ do_check_eq(kCrumb, helper._crumb);
+ do_check_eq(kYCookie, helper._yCookie);
+ do_check_eq(kTCookie, helper._tCookie);
+ run_next_test();
+ };
+
+ helper._onLoginCookiesResponse(kCookieResponse, null);
+}
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/test/test_yahoopacket.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource:///modules/ArrayBufferUtils.jsm");
+Components.utils.import("resource:///modules/Services.jsm");
+Components.utils.import("resource:///modules/yahoo-session.jsm");
+let yahoo = {};
+Services.scriptloader.loadSubScript("resource:///modules/yahoo-session.jsm", yahoo);
+
+const kPacketIdBytes = StringToBytes(yahoo.kPacketIdentfier);
+const kHelloKey = 1;
+const kHelloValue = "Hello";
+const kWorldKey = 20;
+const kWorldValue = "World";
+const kNumberKey = 4;
+const kNumberValue = 32;
+const kParamsKey = 60;
+const kParam1Value = "param1";
+const kParam2Value = "param2";
+const kPacketDataString = "1\xC0\x80Hello\xC0\x8020\xC0\x80World\xC0\x80" +
+ "4\xC0\x8032\xC0\x8060\xC0\x80param1\xC0\x80" +
+ "60\xC0\x80param2\xC0\x80";
+
+function run_test()
+{
+ add_test(test_headerCreation);
+ add_test(test_fullPacketCreation);
+ add_test(test_packetDecoding);
+ add_test(test_extractPackets);
+ add_test(test_malformedPacketExtraction);
+
+ run_next_test();
+}
+
+function test_headerCreation()
+{
+ let packetLength = 0;
+ // Random numbers.
+ let serviceNumber = 0x57;
+ let status = 0x04;
+ let sessionId = 0x57842390;
+
+ let packet = new yahoo.YahooPacket(serviceNumber, status, sessionId);
+ let buf = packet.toArrayBuffer();
+ let view = new DataView(buf);
+
+ // Ensure that the first 4 bytes contain the YMSG identifier.
+ for (let i = 0; i < kPacketIdBytes.length; ++i)
+ do_check_eq(kPacketIdBytes[i], view.getUint8(i));
+
+ do_check_eq(yahoo.kProtocolVersion, view.getUint16(4));
+ do_check_eq(yahoo.kVendorId, view.getUint16(6));
+ do_check_eq(packetLength, view.getUint16(8));
+ do_check_eq(serviceNumber, view.getUint16(10));
+ do_check_eq(status, view.getUint32(12));
+ do_check_eq(sessionId, view.getUint32(16));
+
+ run_next_test();
+}
+
+function test_fullPacketCreation()
+{
+ packetLength = kPacketDataString.length;
+ // Random numbers.
+ let serviceNumber = 0x55;
+ let status = 0x02;
+ let sessionId = 0x12567800;
+
+ let packet = new yahoo.YahooPacket(serviceNumber, status, sessionId);
+ packet.addValue(kHelloKey, kHelloValue);
+ packet.addValue(kWorldKey, kWorldValue);
+ packet.addValue(kNumberKey, kNumberValue);
+ packet.addValues(kParamsKey, [kParam1Value, kParam2Value]);
+ let buf = packet.toArrayBuffer();
+ let view = new DataView(buf);
+
+ // Header check.
+
+ // Ensure that the first 4 bytes contain the YMSG identifier.
+ for (let i = 0; i < kPacketIdBytes.length; ++i)
+ do_check_eq(kPacketIdBytes[i], view.getUint8(i));
+
+ do_check_eq(yahoo.kProtocolVersion, view.getUint16(4));
+ do_check_eq(yahoo.kVendorId, view.getUint16(6));
+ do_check_eq(packetLength, view.getUint16(8));
+ do_check_eq(serviceNumber, view.getUint16(10));
+ do_check_eq(status, view.getUint32(12));
+ do_check_eq(sessionId, view.getUint32(16));
+
+ // Packet data check.
+ let dataBytes = StringToBytes(kPacketDataString);
+ for (let i = 0; i < dataBytes.length; ++i)
+ do_check_eq(dataBytes[i], view.getUint8(yahoo.kPacketHeaderSize + i));
+ run_next_test()
+}
+
+function test_packetDecoding()
+{
+ let packetLength = kPacketDataString.length;
+ // Random numbers.
+ let serviceNumber = 0x20;
+ let status = 0x06;
+ let sessionId = 0x13319AB2;
+
+ let buf = new ArrayBuffer(yahoo.kPacketHeaderSize + packetLength);
+ let view = new DataView(buf);
+
+ for (let i = 0; i < kPacketIdBytes.length; ++i)
+ view.setUint8(i, kPacketIdBytes[i]);
+
+ view.setUint16(4, yahoo.kProtocolVersion);
+ view.setUint16(6, yahoo.kVendorId);
+ view.setUint16(8, packetLength);
+ view.setUint16(10, serviceNumber);
+ view.setUint32(12, status);
+ view.setUint32(16, sessionId);
+
+ let dataBuf = BytesToArrayBuffer(StringToBytes(kPacketDataString));
+ copyBytes(buf, dataBuf, 20);
+
+ // Now we decode and test.
+ let packet = new yahoo.YahooPacket();
+ packet.fromArrayBuffer(buf);
+
+ // Test header information.
+ do_check_eq(serviceNumber, packet.service);
+ do_check_eq(status, packet.status);
+ do_check_eq(sessionId, packet.sessionId);
+
+ // Test the getting of single packet data values.
+ do_check_eq(kHelloValue, packet.getValue(kHelloKey));
+ do_check_eq(kWorldValue, packet.getValue(kWorldKey));
+ do_check_eq(kNumberValue, packet.getValue(kNumberKey));
+
+ // Test the getting of multiple values with a single key.
+ let multiValue = packet.getValues(kParamsKey);
+ do_check_eq(2, multiValue.length);
+ do_check_eq(kParam1Value, multiValue[0]);
+ do_check_eq(kParam2Value, multiValue[1]);
+
+ // Test if certain keys are non-existant.
+ do_check_true(packet.hasKey(kHelloKey));
+ do_check_false(packet.hasKey(500)); // There is no key 500.
+
+ run_next_test();
+}
+
+function test_extractPackets()
+{
+ // Some constants for each packet.
+ const kP1Service = 0x47;
+ const kP1Status = 0;
+ const kP1SessionId = 0x12345678;
+ // Used for testing packet verification.
+ const kP1FuzzerKey = 42;
+ const kP1FuzzerValue = "I am using the YMSG protocol!";
+
+ const kP2Service = 0x57;
+ const kP2Status = 5;
+ const kP2SessionId = 0x87654321;
+
+ // First, create two packets and obtain their buffers.
+ let packet1 = new yahoo.YahooPacket(kP1Service, kP1Status, kP1SessionId);
+ packet1.addValue(kHelloKey, kHelloValue);
+ packet1.addValue(kP1FuzzerKey, kP1FuzzerValue);
+ let packet1Buffer = packet1.toArrayBuffer();
+
+ let packet2 = new yahoo.YahooPacket(kP2Service, kP2Status, kP2SessionId);
+ packet2.addValue(kWorldKey, kWorldValue);
+ let packet2Buffer = packet2.toArrayBuffer();
+
+ // Create one full buffer with both packets inside.
+ let fullBuffer = new ArrayBuffer(packet1Buffer.byteLength +
+ packet2Buffer.byteLength);
+ copyBytes(fullBuffer, packet1Buffer);
+ copyBytes(fullBuffer, packet2Buffer, packet1Buffer.byteLength);
+
+ // Now, run the packets through the extractPackets() method.
+ let [extractedPackets, bytesHandled] =
+ yahoo.YahooPacket.extractPackets(fullBuffer);
+ do_check_eq(2, extractedPackets.length);
+
+ // Packet 1 checks.
+ let p1 = extractedPackets[0];
+ do_check_eq(kP1Service, p1.service);
+ do_check_eq(kP1Status, p1.status);
+ do_check_eq(kP1SessionId, p1.sessionId);
+ do_check_true(p1.hasKey(kHelloKey));
+ do_check_eq(kHelloValue, p1.getValue(kHelloKey));
+
+ // Packet 2 checks.
+ let p2 = extractedPackets[1];
+ do_check_eq(kP2Service, p2.service);
+ do_check_eq(kP2Status, p2.status);
+ do_check_eq(kP2SessionId, p2.sessionId);
+ do_check_true(p2.hasKey(kWorldKey));
+ do_check_eq(kWorldValue, p2.getValue(kWorldKey));
+
+ // Check if all the bytes were handled.
+ do_check_eq(fullBuffer.byteLength, bytesHandled);
+
+ run_next_test();
+}
+
+function test_malformedPacketExtraction()
+{
+ const kInvalidPacketData = "MSYG1\xC0\x80Hello\xC0\x8020\xC0\x80World\xC0\x80";
+ let buffer = BytesToArrayBuffer(StringToBytes(kInvalidPacketData));
+ let malformed = false;
+ try {
+ yahoo.YahooPacket.extractPackets(buffer);
+ } catch(e) {
+ malformed = true;
+ }
+ do_check_true(malformed);
+ run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/test/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head =
+tail =
+run-sequentially = Avoid bustage.
+
+[test_yahoopacket.js]
+[test_yahooLoginHelper.js]
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/yahoo-session.jsm
@@ -0,0 +1,918 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["YahooSession"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/ArrayBufferUtils.jsm");
+Cu.import("resource:///modules/http.jsm");
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/socket.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+ l10nHelper("chrome://chat/locale/yahoo.properties")
+);
+
+const kProtocolVersion = 16;
+const kVendorId = 0;
+
+const kPacketDataDelimiter = "\xC0\x80";
+const kPacketIdentifier = "YMSG";
+const kPacketHeaderSize = 20;
+
+const kPacketType = {
+ // Sent by a client when logging off of the Yahoo! network.
+ Logoff: 0x02,
+ // Sent by a client when a message is sent to a buddy.
+ Message: 0x06,
+ // Used for inviting others to a conference.
+ ConfInvite: 0x18,
+ // Used as a notification when you or someone else joins a conference room.
+ ConfLogon: 0x19,
+ // Used as a notification when you or someone else leaves a conference room.
+ ConfLogoff: 0x1b,
+ // This is sent by the client when additional users are invited to the
+ // conference, but it can be sent as the first invite as well.
+ ConfAddInvite: 0x1c,
+ // Broadcast to all users in a conference room when someone posts a message.
+ ConfMessage: 0x1d,
+ // Used for typing notifications.
+ Notify: 0x4b,
+ // These two are used during initial authentication with the pager server.
+ AuthResponse: 0x54,
+ Auth: 0x57,
+ // Buddy list controls.
+ AddBuddy: 0x83,
+ RemoveBuddy: 0x84,
+ // This is sent when you reject a Yahoo! user's buddy request.
+ BuddyReqReject: 0x86,
+ // This is sent whenever a buddy changes their status.
+ StatusUpdate: 0xc6,
+ // This is sent when someone wishes to become your buddy.
+ BuddyAuth: 0xd6,
+ // Holds the initial status of all buddies when a user first logs in.
+ StatusInitial: 0xf0,
+ // Contains the buddy list sent from the server.
+ List: 0xf1,
+ // Sent back to the pager server after each received message. Sending this
+ // prevents echoed messages when chatting with the official Yahoo! client.
+ MessageAck: 0xfb
+};
+
+const kPacketStatuses = {
+ Typing: 0x16
+};
+
+// Each Yahoo! error code is mapped to a two-element array. The first element
+// contains the last part of the name of its localized string. This is appended
+// to "login.error." to obtain the string. The second element is the
+// Instantbird error that is given to the error handler.
+const kLoginStatusErrors = {
+ "1212" : ["badCredentials",
+ Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED],
+ "1213" : ["accountLockedFailed",
+ Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED],
+ "1218" : ["accountDeactivated",
+ Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED],
+ "1235" : ["usernameNotExist",
+ Ci.prplIAccount.ERROR_INVALID_USERNAME],
+ "1236" : ["accountLockedGeneral",
+ Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED]
+};
+
+// These are the status codes that buddies can send us.
+const kBuddyStatuses = {
+ // Available.
+ "0" : Ci.imIStatusInfo.STATUS_AVAILABLE,
+ // Be right back.
+ "1" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Busy.
+ "2" : Ci.imIStatusInfo.STATUS_UNAVAILABLE,
+ // Not at home.
+ "3" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Not at desk.
+ "4" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Not in office.
+ "5" : Ci.imIStatusInfo.STATUS_AWAY,
+ // On phone.
+ "6" : Ci.imIStatusInfo.STATUS_AWAY,
+ // On vacation.
+ "7" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Out to lunch.
+ "8" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Stepped out.
+ "9" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Invisible.
+ "12" : Ci.imIStatusInfo.STATUS_INVISIBLE,
+ // Custom status.
+ "99" : Ci.imIStatusInfo.STATUS_AWAY,
+ // Idle.
+ "999" : Ci.imIStatusInfo.STATUS_IDLE
+};
+
+/* The purpose of the YahooSession object is to serve as a gateway between the
+ * protocol plug-in and the Yahoo! Messenger servers. Anytime an object outside
+ * of this file wishes to communicate with the servers, it should do it through
+ * one of the methods provided by YahooSession. By centralizing such network
+ * access, we can easily catch errors, and ensure that communication is handled
+ * correctly. */
+function YahooSession(aAccount)
+{
+ this._account = aAccount;
+ this.binaryMode = true;
+}
+YahooSession.prototype = {
+ __proto__: Socket,
+ _account: null,
+ _socket: null,
+ _username: null,
+ // This is the IPv4 address to the pager server which is the gateway into the
+ // Yahoo! Messenger network.
+ pagerAddress: null,
+ // The session ID is obtained during the login process and is maintained
+ // throughout the session. This helps the pager server identify the client.
+ sessionId: null,
+
+ // Public methods.
+ login: function() {
+ this._account.reportConnecting();
+ new YahooLoginHelper(this).login(this._account);
+ },
+
+ addBuddyToServer: function(aBuddy) {
+ let packet = new YahooPacket(kPacketType.AddBuddy, 0, this.sessionId);
+ packet.addValue(14, _("buddy.invite.message"));
+ packet.addValue(65, aBuddy.tag.name);
+ packet.addValue(97, "1"); // UTF-8 encoding.
+ packet.addValue(1, this._account.cleanUsername);
+ // The purpose of these two values are unknown.
+ packet.addValue(302, "319");
+ packet.addValue(300, "319");
+ packet.addValue(7, aBuddy.userName);
+ // The purpose of these three values are also unknown.
+ packet.addValue(334, "0");
+ packet.addValue(301, "319");
+ packet.addValue(303, "319");
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ removeBuddyFromServer: function(aBuddy) {
+ let packet = new YahooPacket(kPacketType.RemoveBuddy, 0, this.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(7, aBuddy.userName);
+ packet.addValue(65, aBuddy.tag.name);
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ setStatus: function(aStatus, aMessage) {
+ let packet = new YahooPacket(kPacketType.StatusUpdate, 0, this.sessionId);
+
+ // When a custom status message is used, key 10 is set to 99, and key 97
+ // is set to 1. Otherwise, key 10 is set to our current status code.
+ if (aMessage && aMessage.length > 0) {
+ packet.addValue(10, "99");
+ packet.addValue(97, "1");
+ } else {
+ let statusCode;
+ switch(aStatus) {
+ // Available
+ case Ci.imIStatusInfo.STATUS_AVAILABLE:
+ case Ci.imIStatusInfo.STATUS_MOBILE:
+ statusCode = "0";
+ break;
+ // Away
+ case Ci.imIStatusInfo.STATUS_AWAY:
+ statusCode = "1";
+ break;
+ // Busy
+ case Ci.imIStatusInfo.STATUS_UNAVAILABLE:
+ statusCode = "2";
+ break;
+ // Invisible
+ case Ci.imIStatusInfo.STATUS_INVISIBLE:
+ statusCode = "12";
+ break;
+ // Idle
+ case Ci.imIStatusInfo.STATUS_IDLE:
+ statusCode = "999";
+ break;
+ }
+ packet.addValue(10, statusCode);
+ }
+
+ // Key 19 is always set as the status messgae, even when the message is
+ // empty. If key 10 is set to 99, the message is used.
+ packet.addValue(19, aMessage);
+
+ // Key 47 is always set to either 0, if we are available, or 1, if we are
+ // not available. The value is used by the server if key 10 is set to 99.
+ // Otherwise, the value of key 10 is used to determine our status.
+ packet.addValue(47, (aStatus == Ci.imIStatusInfo.STATUS_AVAILABLE) ?
+ "0" : "1");
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ sendChatMessage: function(aName, aMessage) {
+ let packet = new YahooPacket(kPacketType.Message, 0, this.sessionId);
+ // XXX Key 0 is the user ID, and key 1 is the active ID. We need to find
+ // the difference between these two. Alias maybe?
+ packet.addValue(0, this._account.cleanUsername);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(5, aName);
+ packet.addValue(14, aMessage);
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ sendConferenceMessage: function(aRecipients, aRoom, aMessage) {
+ let packet = new YahooPacket(kPacketType.ConfMessage, 0, this.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValues(53, aRecipients);
+ packet.addValue(57, aRoom);
+ packet.addValue(14, aMessage);
+ packet.addValue(97, "1"); // Use UTF-8 encoding.
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ sendTypingStatus: function(aBuddyName, aIsTyping) {
+ let packet = new YahooPacket(kPacketType.Notify, kPacketStatuses.Typing,
+ this.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(5, aBuddyName);
+ packet.addValue(13, aIsTyping ? "1" : "0");
+ packet.addValue(14, " "); // Key 14 contains a single space.
+ packet.addValue(49, "TYPING");
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ acceptConferenceInvite: function(aOwner, aRoom, aParticipants) {
+ let packet = new YahooPacket(kPacketType.ConfLogon, 0, this.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(3, this._account.cleanUsername);
+ packet.addValue(57, aRoom);
+ packet.addValues(3, aParticipants);
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ createConference: function(aRoom) {
+ let packet = new YahooPacket(kPacketType.ConfLogon, 0, this.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(3, this._account.cleanUsername);
+ packet.addValue(57, aRoom);
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ inviteToConference: function(aInvitees, aRoom, aParticipants, aMessage) {
+ let packet = new YahooPacket(kPacketType.ConfAddInvite, 0, this.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValues(51, aInvitees);
+ packet.addValues(53, aParticipants);
+ packet.addValue(57, aRoom);
+ packet.addValue(58, aMessage);
+ packet.addValue(13, "0");
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ sendConferenceLogoff: function(aName, aParticipants, aRoom) {
+ let packet = new YahooPacket(kPacketType.ConfLogoff, 0, this.sessionId);
+ packet.addValue(1, aName);
+ packet.addValues(3, aParticipants);
+ packet.addValue(57, aRoom);
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ // Callbacks.
+ onLoginComplete: function() {
+ this._account.reportConnected();
+ },
+
+ onSessionError: function(aError, aMessage) {
+ this._account.reportDisconnecting(aError, aMessage);
+ if (this.isConnected)
+ this.disconnect();
+ this._account.reportDisconnected();
+ },
+
+ // Private methods.
+
+ // Socket Event Callbacks.
+ LOG: function(aString) this._account.LOG(aString),
+
+ DEBUG: function(aString) this._account.DEBUG(aString),
+
+ onConnection: function() {
+ // We send an authentication request packet as soon as we connect to the
+ // pager server.
+ let packet = new YahooPacket(kPacketType.Auth, 0, 0);
+ packet.addValue(1, this._account.cleanUsername);
+ this.sendBinaryData(packet.toArrayBuffer());
+ },
+
+ onConnectionTimedOut: function() {
+ this.onSessionError(Ci.prplIAccount.NETWORK_ERROR, "");
+ },
+
+ onConnectionReset: function() {
+ this.onSessionError(Ci.prplIAccount.NETWORK_ERROR, "");
+ },
+
+ // Called when the other end has closed the connection.
+ onConnectionClosed: function() {
+ if (!this._account.connected)
+ return;
+ this._account.reportDisconnecting(Ci.prplIAccount.NO_ERROR, "");
+ this._account.reportDisconnected();
+ },
+
+ onBinaryDataReceived: function(aData) {
+ let packets;
+ let bytesHandled;
+ try {
+ [packets, bytesHandled] = YahooPacket.extractPackets(aData);
+ } catch(e) {
+ this._account.ERROR(e);
+ this.onSessionError(Ci.prplIAccount.NETWORK_ERROR, "");
+ return 0;
+ }
+
+ for each (let packet in packets) {
+ if (YahooPacketHandler.hasOwnProperty(packet.service)) {
+ try {
+ YahooPacketHandler[packet.service].call(this._account, packet);
+ } catch(e) {
+ this._account.ERROR(e);
+ }
+ } else {
+ this._account.WARN("No handler for Yahoo! packet " +
+ packet.service.toString(16) + ".");
+ }
+ }
+ return bytesHandled;
+ }
+};
+
+/* The purpose of YahooLoginHelper is to separate the complicated login logic
+ * from the YahooSession object. Logging in on Yahoo!'s network is the most
+ * complicated stage of a session due to the authentication system that is
+ * employed. The login steps are listed below.
+ *
+ * 1) Get the address of a "pager" server. This pager will be our gateway to
+ * the network.
+ *
+ * 2) Obtain the challenge string from the pager. This string is used to help
+ * create the base64 response string needed for the final step.
+ *
+ * 3) Obtain a token from the login server via HTTP.
+ *
+ * 4) Obtain the login crumb, Y-Cookie, and T-Cookie from the login server via
+ * HTTP. These will also be used in the final response packet to the pager.
+ *
+ * 5) Create the base64 response string from the MD5 hash of the crumb and
+ * challenge string, and build a packet containing the username, password,
+ * response string, version numbers, crumb, and cookies, sending it to the
+ * pager for a final authenticatcation.
+ *
+ * If all goes well after the 5th step, the user is considered logged in. */
+function YahooLoginHelper(aSession)
+{
+ this._session = aSession;
+}
+YahooLoginHelper.prototype = {
+ // YahooSession object passed in constructor.
+ _session: null,
+ // YahooAccount object passed to login().
+ _account: null,
+ // The username, stripped of any @yahoo.com or @yahoo.co.jp suffix.
+ _username: null,
+ // The authentication challenge string sent from the Yahoo!'s login server.
+ _challengeString: null,
+ // The authentication token sent from Yahoo!'s login server.
+ _loginToken: null,
+ // Crumb and cookie strings sent from Yahoo!'s login server, and used in
+ // the final authentication request to the pager server.
+ _crumb: null,
+ _yCookie: null,
+ _tCookie: null,
+
+ // Public methods.
+ login: function(aAccount) {
+ this._account = aAccount;
+ this._getPagerAddress();
+ },
+
+ // Private methods.
+ _getPagerAddress: function() {
+ doXHRequest(this._account._protocol.pagerRequestUrl, null, null,
+ this._onPagerAddressResponse, this._onHttpError, this,
+ "GET", null);
+ },
+
+ _getChallengeString: function() {
+ let port = this._account.getInt("port");
+ this._session.connect(this._session.pagerAddress, port);
+ // We want to handle a challenge string when the server responds.
+ this._session.onBinaryDataReceived =
+ this._onChallengeStringResponse.bind(this);
+ },
+
+ _getLoginToken: function() {
+ // TODO - Simplify this using map and join.
+ let url = this._account._protocol.loginTokenGetUrl;
+ url += "?src=ymsgr&";
+ url += "login=" + percentEncode(this._account.cleanUsername) + "&";
+ url += "passwd=" + percentEncode(this._account.imAccount.password) + "&";
+ url += "chal=" + percentEncode(this._challengeString);
+
+ doXHRequest(url, null, null, this._onLoginTokenResponse,
+ this._onHttpError, this, "GET", null);
+ },
+
+ _getCookies: function() {
+ // TODO - Simplify this using map and join.
+ let url = this._account._protocol.loginTokenLoginUrl;
+ url += "?src=ymsgr&";
+ url += "token=" + this._loginToken;
+
+ doXHRequest(url, null, null, this._onLoginCookiesResponse,
+ this._onHttpError, this, "GET", null);
+ },
+
+ _sendPagerAuthResponse: function() {
+ let response = this._calculatePagerResponse();
+ let packet = new YahooPacket(kPacketType.AuthResponse, 0,
+ this._session.sessionId);
+ // Build the key/value pairs.
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(0, this._account.cleanUsername);
+ packet.addValue(277, this._yCookie);
+ packet.addValue(278, this._tCookie);
+ packet.addValue(307, response);
+ packet.addValue(244, this._account.protocol.buildId);
+ packet.addValue(2, this._account.cleanUsername);
+ packet.addValue(2, "1");
+ packet.addValue(98, "us");
+ this._session.sendBinaryData(packet.toArrayBuffer());
+
+ // We want to handle a final login confirmation packet when the server
+ // responds.
+ this._session.onBinaryDataReceived = this._onFinalLoginResponse.bind(this);
+ },
+
+ _calculatePagerResponse: function() {
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.MD5);
+
+ let crypt = this._crumb + this._challengeString;
+ let cryptData = StringToBytes(crypt);
+ hasher.update(cryptData, cryptData.length);
+
+ // The protocol requires replacing + with ., / with _, and = with - within
+ // the base64 response string.
+ return btoa(hasher.finish(false)).replace(/\+/g, ".").replace(/\//g, "_")
+ .replace(/=/g, "-");
+ },
+
+ _handleLoginError: function(aErrorCode) {
+ let errorInfo = kLoginStatusErrors[aErrorCode];
+ let errorMessage;
+ let error;
+
+ // If we find information on the error code we received, we will use that
+ // information. If the error wasn't found in our error table, just throw a
+ // generic error with the code included.
+ if (errorInfo) {
+ errorMessage = _("login.error." + errorInfo[0]);
+ error = errorInfo[1];
+ } else {
+ errorMessage = _("login.error.unknown", aErrorCode);
+ error = Ci.prplIAccount.ERROR_OTHER_ERROR;
+ // We also throw a console error because we didn't expect
+ // this error code.
+ this._account.ERROR("Received unknown error from pager server. Code: " +
+ aErrorCode);
+ }
+ this._session.onSessionError(error, errorMessage);
+ },
+
+ _onHttpError: function(aError, aStatusText, aXHR) {
+ this._session.onSessionError(Ci.prplIAccount.NETWORK_ERROR,
+ _("network.error.http"));
+ },
+
+ // HTTP Response Callbacks.
+ _onPagerAddressResponse: function(aResponse, aXHR) {
+ this._session.pagerAddress =
+ aResponse.substring(aResponse.lastIndexOf("=") + 1);
+ this._getChallengeString();
+ },
+
+ _onLoginTokenResponse: function(aResponse, aXHR) {
+ let responseParams = aResponse.split("\r\n");
+ // Status code "0" means success.
+ let statusCode = responseParams[0];
+ if (statusCode != "0") {
+ this._handleLoginError(statusCode);
+ return;
+ }
+
+ this._loginToken = responseParams[1].replace("ymsgr=", "");
+ this._getCookies();
+ },
+
+ _onLoginCookiesResponse: function(aResponse, aXHR) {
+ let responseParams = aResponse.split("\r\n");
+ // Status code "0" means success.
+ let statusCode = responseParams[0];
+ if (statusCode != "0") {
+ this._handleLoginError(statusCode);
+ return;
+ }
+
+ this._crumb = responseParams[1].replace("crumb=", "");
+ this._yCookie = responseParams[2].substring(2); // Remove the "Y=" bit.
+ this._tCookie = responseParams[3].substring(2); // Remove the "T=" bit.
+ this._sendPagerAuthResponse();
+ },
+
+ // TCP Response Callbacks.
+ _onChallengeStringResponse: function(aData) {
+ let packet = new YahooPacket();
+ packet.fromArrayBuffer(aData);
+ // The value of the challenge string is associated with key 94.
+ this._challengeString = packet.getValue(94);
+ this._session.sessionId = packet.sessionId;
+ this._getLoginToken();
+ },
+
+ _onFinalLoginResponse: function(aData) {
+ this._session.onLoginComplete();
+ // We need to restore data handling to the YahooSession object since our
+ // login steps are complete.
+ this._session.onBinaryDataReceived =
+ YahooSession.prototype.onBinaryDataReceived.bind(this._session);
+ }
+};
+
+/* The YahooPacket class represents a single Yahoo! Messenger data packet.
+ * Using this class allows you to easily create packets, stuff them with
+ * required data, and convert them to/from ArrayBuffer objects. */
+function YahooPacket(aService, aStatus, aSessionId)
+{
+ this.service = aService;
+ this.status = aStatus;
+ this.sessionId = aSessionId;
+ this.keyValuePairs = [];
+}
+YahooPacket.prototype = {
+ service: null,
+ status: null,
+ sessionId: null,
+ keyValuePairs: null,
+
+ // Public methods.
+
+ // Add a single key/value pair.
+ addValue: function(aKey, aValue) {
+ let pair = {
+ key: aKey.toString(), // The server handles keys as ASCII number values.
+ value: aValue
+ };
+
+ this.keyValuePairs.push(pair);
+ },
+
+ // Add multiple key/value pairs with the same key but different values
+ // stored in an array.
+ addValues: function(aKey, aValues) {
+ for each (let value in aValues)
+ this.addValue(aKey, value);
+ },
+
+ // This method returns the first value found with the given key.
+ getValue: function(aKey) {
+ for (let i = 0; i < this.keyValuePairs.length; ++i) {
+ let pair = this.keyValuePairs[i];
+ // The server handles keys as ASCII number values.
+ if (pair.key == aKey.toString())
+ return pair.value;
+ }
+
+ // Throw an error if the key wasn't found.
+ throw "Required key " + aKey + " wasn't found. Packet Service: " +
+ this.service.toString(16);
+ },
+
+ // This method returns all of the values found with the given key. In some
+ // packets, one key is associated with multiple values. If that is the case,
+ // use this method to retrieve all of them instead of just the first one.
+ getValues: function(aKey) {
+ let values = [];
+ for (let i = 0; i < this.keyValuePairs.length; ++i) {
+ let pair = this.keyValuePairs[i];
+ // The server handles keys as ASCII number values.
+ if (pair.key == aKey.toString())
+ values.push(pair.value);
+ }
+
+ // Throw an error if no keys were found.
+ if (values.length == 0) {
+ throw "Required key " + aKey + " wasn't found. Packet Service: " +
+ this.service.toString(16);
+ }
+ return values;
+ },
+
+ hasKey: function(aKey) {
+ for (let i = 0; i < this.keyValuePairs.length; ++i) {
+ // The server handles keys as ASCII number values.
+ if (this.keyValuePairs[i].key == aKey.toString())
+ return true;
+ }
+ return false;
+ },
+
+ toArrayBuffer: function() {
+ let dataString = "";
+ for (let i = 0; i < this.keyValuePairs.length; ++i) {
+ let pair = this.keyValuePairs[i];
+ dataString += pair.key + kPacketDataDelimiter;
+ dataString += pair.value + kPacketDataDelimiter;
+ }
+
+ let packetLength = dataString.length;
+ let buffer = new ArrayBuffer(kPacketHeaderSize + packetLength);
+
+ // Build header.
+ let view = new DataView(buffer);
+ let idBytes = StringToBytes(kPacketIdentifier);
+ view.setUint8(0, idBytes[0]);
+ view.setUint8(1, idBytes[1]);
+ view.setUint8(2, idBytes[2]);
+ view.setUint8(3, idBytes[3]);
+ view.setUint16(4, kProtocolVersion);
+ view.setUint16(6, 0); // Vendor ID
+ view.setUint16(8, packetLength);
+ view.setUint16(10, this.service);
+ view.setUint32(12, this.status);
+ view.setUint32(16, this.sessionId);
+
+ // Copy in data.
+ copyBytes(buffer, BytesToArrayBuffer(StringToBytes(dataString)), kPacketHeaderSize);
+
+ return buffer;
+ },
+
+ fromArrayBuffer: function(aBuffer) {
+ let view = new DataView(aBuffer);
+ this.length = view.getUint16(8) + kPacketHeaderSize;
+ this.service = view.getUint16(10);
+ this.status = view.getUint32(12);
+ this.sessionId = view.getUint32(16);
+
+ let dataString = ArrayBufferToString(aBuffer).substring(kPacketHeaderSize);
+ let delimitedData = dataString.split(kPacketDataDelimiter);
+ // Since the data should also end with a trailing delmiter, split() will
+ // add an empty element at the end. We need to pop this element off.
+ delimitedData.pop();
+
+ // If we don't have an even number of delimitedData elements, that means
+ // we are either missing a key or a value.
+ if (delimitedData.length % 2 != 0) {
+ throw "Odd number of data elements. Either a key or value is missing. "
+ "Num of elements: " + delimitedData.length;
+ }
+
+ for (let i = 0; i < delimitedData.length; i += 2) {
+ let key = delimitedData[i];
+ let value = delimitedData[i + 1];
+ if (key && value) {
+ let pair = {
+ key: key,
+ value: value
+ };
+ this.keyValuePairs.push(pair);
+ }
+ }
+ }
+};
+YahooPacket.extractPackets = function(aData, aOnNetworkError) {
+ let packets = [];
+ let bytesHandled = 0;
+
+ while (aData.byteLength >= kPacketHeaderSize) {
+ if (ArrayBufferToString(aData.slice(0, kPacketIdentifier.length)) !=
+ kPacketIdentifier) {
+ throw "Malformed packet received. Packet content: " +
+ ArrayBufferToHexString(aData);
+ }
+
+ let packetView = new DataView(aData);
+ let packetLength = packetView.getUint16(8) + kPacketHeaderSize;
+ // Don't process half packets.
+ if (packetLength > aData.byteLength)
+ break;
+ let packet = new YahooPacket();
+ packet.fromArrayBuffer(aData.slice(0, packetLength));
+ packets.push(packet);
+ bytesHandled += packetLength;
+ aData = aData.slice(packetLength);
+ }
+ return [packets, bytesHandled];
+}
+
+/* In YahooPacketHandler, each handler function is assosiated with a packet
+ * service number. You can use the kPacketType enumeration to understand
+ * what kind of packet each number is linked to.
+ *
+ * Keep in mind too that "this" in each function will be bound to a
+ * YahooAccount object, since they are all invoked using call(). */
+const YahooPacketHandler = {
+ // Buddy logoff.
+ 0x02: function(aPacket) {
+ let name = aPacket.getValue(7);
+ this.setBuddyStatus(name, Ci.imIStatusInfo.STATUS_OFFLINE, "");
+ },
+
+ // Incoming chat message.
+ 0x06: function(aPacket) {
+ let from = aPacket.getValue(4);
+ let to = aPacket.getValue(5);
+ let message = aPacket.getValue(14);
+ this.receiveMessage(from, message);
+
+ // The official Yahoo! Messenger desktop client requires message ACKs to be
+ // sent back to the server. The web client doesn't require this. A good
+ // indication of when an ACK is required is when key 429 is sent, which
+ // contains the ID of the message. When a message is sent from the official
+ // desktop client, and no ACK is sent back, the message is resent seconds
+ // later.
+ if (aPacket.hasKey(429)) {
+ let messageId = aPacket.getValue(429);
+ let packet = new YahooPacket(kPacketType.MessageAck, 0, aPacket.sessionId);
+ // Some keys have an unknown purpose, so we set a constant value.
+ packet.addValue(1, to);
+ packet.addValue(5, from);
+ packet.addValue(302, "430");
+ packet.addValue(430, messageId);
+ packet.addValue(303, 430);
+ packet.addValue(450, 0);
+ this._session._socket.sendBinaryData(packet.toArrayBuffer());
+ }
+ },
+
+ // New mail notification.
+ // TODO: Implement this handler when mail notifications are handled in the
+ // base code.
+ 0x0b: function(aPacket) {},
+
+ // Server ping.
+ // TODO: Add support for ping replies.
+ 0x12: function(aPacket) {},
+
+ // Conference invitation.
+ 0x18: function(aPacket) {
+ let owner = aPacket.getValue(50);
+ let roomName = aPacket.getValue(57);
+ let participants = aPacket.getValues(52);
+ // The owner is also a participant.
+ participants.push(owner);
+ let message = aPacket.getValue(58);
+ this.receiveConferenceInvite(owner, roomName, participants, message);
+ },
+
+ // Conference logon.
+ 0x19: function(aPacket) {
+ let userName = aPacket.getValue(53);
+ let room = aPacket.getValue(57);
+ this.receiveConferenceLogon(room, userName);
+ },
+
+ // Conference logoff
+ 0x1b: function(aPacket) {
+ let userName = aPacket.getValue(56);
+ let roomName = aPacket.getValue(57);
+ this.receiveConferenceLogoff(roomName, userName);
+ },
+
+ // Conference additional invitation. NOTE: Since this packet has the same
+ // structure as the normal conference invite (packet 0x18), we simply
+ // reuse that handler.
+ 0x1c: function(aPacket) YahooPacketHandler[0x18].call(this, aPacket),
+
+ // Conference message.
+ 0x1d: function(aPacket) {
+ let from = aPacket.getValue(3);
+ let room = aPacket.getValue(57);
+ let message = aPacket.getValue(14);
+ this.receiveConferenceMessage(from, room, message);
+ },
+
+ // Typing notification.
+ 0x4b: function(aPacket) {
+ let name = aPacket.getValue(4);
+ let isTyping = (aPacket.getValue(13) == "1");
+ this.receiveTypingNotification(name, isTyping);
+ },
+
+ // Legacy Yahoo! buddy list. Packet 0xf1 has replaced this.
+ 0x55: function(aPacket) {},
+
+ // Authentication acknowledgement. We can ignore this since we are known
+ // to be authenticated if we are receiving other packets anyway.
+ 0x57: function(aPacket) {},
+
+ // Buddy status update.
+ 0xc6: function (aPacket) {
+ let name = aPacket.getValue(7);
+ // Try to grab a status from the well-used buddy statuses. If we can't
+ // find it there, try the extra ones.
+ let status = kBuddyStatuses[aPacket.getValue(10)];
+ let message = "";
+ if (aPacket.hasKey(19))
+ message = aPacket.getValue(19);
+
+ this.setBuddyStatus(name, status, message);
+ },
+
+ // Buddy authorization request.
+ 0xd6: function(aPacket) {
+ let authRequest = {
+ _account: this,
+ _session: this._session,
+ get account() this.imAccount,
+ userName: aPacket.getValue(4),
+ grant: function() {
+ let packet = new YahooPacket(kPacketType.BuddyAuth, 0,
+ this._session.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(5, this.userName);
+ // Misc. Unknown flags.
+ packet.addValue(13, 1);
+ packet.addValue(334, 0);
+ this._session._socket.sendBinaryData(packet.toArrayBuffer());
+
+ // TODO - Possibly allow different tags to be used.
+ this._account.addBuddy(Services.tags.createTag("Friends"),
+ this.userName);
+ },
+ deny: function() {
+ let packet = new YahooPacket(kPacketType.BuddyReqReject, 0,
+ this._session.sessionId);
+ packet.addValue(1, this._account.cleanUsername);
+ packet.addValue(7, this.userName);
+ packet.addValue(14, "");
+ this._session._socket.sendBinaryData(packet.toArrayBuffer());
+ },
+ cancel: function() {
+ Services.obs.notifyObservers(this,
+ "buddy-authorization-request-canceled",
+ null);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.prplIBuddyRequest])
+ };
+ Services.obs.notifyObservers(authRequest, "buddy-authorization-request",
+ null);
+ },
+
+ // XXX: What does this packet do?
+ 0xef: function(aPacket) {},
+
+ // Initial user status.
+ 0xf0: function (aPacket) {
+ // The protocol correctly orders groups of key/values per buddy. So we can
+ // place all occurances of a certain key in each array, and match them up.
+
+ // Return early if we find no buddy names.
+ if (!aPacket.hasKey(7))
+ return;
+ let buddyNames = aPacket.getValues(7);
+ let buddyStatuses = aPacket.getValues(10);
+ let statusMessages;
+ if (aPacket.hasKey(19))
+ statusMessages = aPacket.getValues(19);
+
+ for (let i in buddyNames) {
+ let name = buddyNames[i];
+ let status = kBuddyStatuses[buddyStatuses[i]];
+ let message = statusMessages ? statusMessages[i] : "";
+
+ this.setBuddyStatus(name, status, message);
+ }
+ },
+
+ // Friends and groups list.
+ 0xf1: function(aPacket) {
+ let tagName = "";
+ for each (let pair in aPacket.keyValuePairs) {
+ if (pair.key == "65")
+ tagName = pair.value;
+ else if (pair.key == "7") {
+ let buddyName = pair.value;
+ this.addBuddyFromServer(Services.tags.createTag(tagName), buddyName);
+ }
+ }
+ }
+};
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/yahoo.js
@@ -0,0 +1,480 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+Cu.import("resource:///modules/yahoo-session.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", function()
+ l10nHelper("chrome://chat/locale/yahoo.properties")
+);
+
+function YahooConversation(aAccount, aName)
+{
+ this._buddyUserName = aName;
+ this._account = aAccount;
+ this.buddy = aAccount.getBuddy(aName);
+ this._init(aAccount);
+}
+YahooConversation.prototype = {
+ _account: null,
+ _buddyUserName: null,
+ _typingTimer: null,
+
+ close: function() {
+ this._account.deleteConversation(this._buddyUserName);
+ GenericConvChatPrototype.close.call(this);
+ },
+
+ sendMsg: function (aMsg) {
+ // Deliver the message, then write it to the window.
+ this._account._session.sendChatMessage(this._buddyUserName,
+ this._account.encodeMessage(aMsg));
+ this.finishedComposing();
+ this.writeMessage(this._account.imAccount.alias, aMsg, {outgoing: true});
+ },
+
+ sendTyping: function(aString) {
+ if (aString.length) {
+ if (!this._typingTimer)
+ this._account._session.sendTypingStatus(this._buddyUserName, true);
+ this._refreshTypingTimer();
+ }
+ return Ci.prplIConversation.NO_TYPING_LIMIT;
+ },
+
+ finishedComposing: function() {
+ this._account._session.sendTypingStatus(this._buddyUserName, false);
+ this._cancelTypingTimer();
+ },
+
+ _refreshTypingTimer: function() {
+ this._cancelTypingTimer();
+ this._typingTimer = setTimeout(this.finishedComposing.bind(this), 10000);
+ },
+
+ _cancelTypingTimer: function() {
+ if (!this._typingTimer)
+ return;
+ clearTimeout(this._typingTimer);
+ delete this._typingTimer
+ this._typingTimer = null;
+ },
+
+ get name() this._buddyUserName
+};
+YahooConversation.prototype.__proto__ = GenericConvIMPrototype;
+
+function YahooConference(aAccount, aRoom, aOwner)
+{
+ this._account = aAccount;
+ this._roomName = aRoom;
+ this._owner = aOwner;
+ this._init(aAccount, aRoom, aAccount.cleanUsername);
+}
+YahooConference.prototype = {
+ _account: null,
+ _roomName: null,
+ _owner: null,
+
+ close: function() {
+ this._account._session.sendConferenceLogoff(this._account.cleanUsername,
+ this.getParticipantNames(),
+ this._roomName);
+ this._account.deleteConference(this._roomName);
+ GenericConvChatPrototype.close.call(this);
+ },
+
+ sendMsg: function(aMsg) {
+ this._account._session.sendConferenceMessage(this.getParticipantNames(),
+ this._roomName,
+ this._account.encodeMessage(aMsg));
+ },
+
+ addParticipant: function(aName) {
+ let buddy = new YahooConferenceBuddy(aName, this);
+ this._participants[aName] = buddy;
+ this.notifyObservers(new nsSimpleEnumerator([buddy]), "chat-buddy-add");
+ this.writeMessage(this._roomName,
+ _("system.message.conferenceLogon", aName),
+ {system: true});
+ },
+
+ removeParticipant: function(aName) {
+ // In case we receive two logoff packets, make sure that the user is
+ // actually here before continuing.
+ if (!this._participants[aName])
+ return;
+
+ let stringNickname = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ stringNickname.data = aName;
+ this.notifyObservers(new nsSimpleEnumerator([stringNickname]),
+ "chat-buddy-remove");
+ delete this._participants[aName];
+ this.writeMessage(this._roomName,
+ _("system.message.conferenceLogoff", aName),
+ {system: true});
+ },
+
+ getParticipantNames: function()
+ [this._participants[i].name for (i in this._participants)],
+};
+YahooConference.prototype.__proto__ = GenericConvChatPrototype;
+
+function YahooConferenceBuddy(aName, aConference)
+{
+ this._name = aName;
+ this._conference = aConference;
+}
+YahooConferenceBuddy.prototype = {
+ _conference: null,
+
+ get founder() this._conference._owner == this._name
+}
+YahooConferenceBuddy.prototype.__proto__ = GenericConvChatBuddyPrototype;
+
+function YahooAccountBuddy(aAccount, aBuddy, aTag, aUserName)
+{
+ this._init(aAccount, aBuddy, aTag, aUserName);
+}
+YahooAccountBuddy.prototype = {
+ // This removes the buddy locally, and from the Yahoo! servers.
+ remove: function() this._account.removeBuddy(this, true),
+ // This removes the buddy locally, but keeps him on the servers.
+ removeLocal: function() this._account.removeBuddy(this, false),
+ createConversation: function() this._account.createConversation(this.userName)
+}
+YahooAccountBuddy.prototype.__proto__ = GenericAccountBuddyPrototype;
+
+function YahooAccount(aProtoInstance, aImAccount)
+{
+ this._init(aProtoInstance, aImAccount);
+ this._buddies = new Map();
+ this._conversations = new Map();
+ this._conferences = new Map();
+ this._protocol = aProtoInstance;
+ this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ this._converter.charset = this.getString("local_charset") || "UTF-8";
+}
+YahooAccount.prototype = {
+ // YahooSession object passed in constructor.
+ _session: null,
+ // A Map holding the list of buddies associated with their usernames.
+ _buddies: null,
+ // A Map holding the list of open buddy conversations associated with the
+ // username of the buddy.
+ _conversations: null,
+ // A Map holding the list of open conference rooms associated with the room
+ // name.
+ _conferences: null,
+ // YahooProtocol object passed in the constructor.
+ _protocol: null,
+ // An nsIScriptableUnicodeConverter used to convert incoming/outgoing chat
+ // messages to the correct charset.
+ _converter: null,
+ // This is simply incremented by one everytime a new conference room is
+ // created. It is appened to the end of the room name when a new room is
+ // created, ensuring name uniqueness.
+ _roomsCreated: 0,
+
+ connect: function() {
+ this._session = new YahooSession(this);
+ this._session.login(this.imAccount.name, this.imAccount.password);
+ },
+
+ disconnect: function(aSilent) {
+ if (this.connected) {
+ this.reportDisconnecting(Ci.prplIAccount.NO_ERROR, "");
+ if (this._session.isConnected)
+ this._session.disconnect();
+ this.reportDisconnected();
+ }
+ // buddy[1] is the actual object.
+ for (let buddy of this._buddies)
+ buddy[1].setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, "");
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "status-changed")
+ return;
+
+ this._session.setStatus(aSubject.statusType, aData);
+ },
+
+ remove: function() {
+ for each(let conv in this._conversations)
+ conv.close();
+ delete this._conversations;
+ for (let buddy of this._buddies)
+ buddy[1].removeLocal(); // buddy[1] is the actual object.
+ },
+
+ unInit: function() {
+ this.disconnect(true);
+ delete this.imAccount;
+ },
+
+ createConversation: function(aName) {
+ let conv = new YahooConversation(this, aName);
+ this._conversations.set(aName, conv);
+ return conv;
+ },
+
+ deleteConversation: function(aName) {
+ if (this._conversations.has(aName))
+ this._conversations.delete(aName);
+ },
+
+ receiveConferenceInvite: function(aOwner, aRoom, aParticipants, aMessage) {
+ // Do nothing if we wish to ignore invites.
+ if (!Services.prefs.getIntPref("messenger.conversations.autoAcceptChatInvitations") ||
+ this.getBool("ignore_invites"))
+ return;
+
+ let conf = new YahooConference(this, aRoom, aOwner);
+ this._conferences.set(aRoom, conf);
+
+ for each (let participant in aParticipants)
+ conf.addParticipant(participant);
+
+ this._session.acceptConferenceInvite(aOwner, aRoom,
+ conf.getParticipantNames());
+ },
+
+ receiveConferenceLogon: function(aRoom, aUsername) {
+ if (!this._conferences.has(aRoom))
+ return;
+ let conf = this._conferences.get(aRoom);
+ conf.addParticipant(aUsername);
+ },
+
+ receiveConferenceLogoff: function(aRoom, aUsername) {
+ if (!this._conferences.has(aRoom))
+ return;
+ let conf = this._conferences.get(aRoom);
+ conf.removeParticipant(aUsername);
+ },
+
+ deleteConference: function(aName) {
+ if (this._conferences.has(aName))
+ this._conferences.delete(aName);
+ },
+
+ // Called when the user adds a new contact within Instantbird.
+ addBuddy: function(aTag, aName) {
+ let buddy = new YahooAccountBuddy(this, null, aTag, aName);
+ this._buddies.set(buddy.userName, buddy);
+ this._session.addBuddyToServer(buddy);
+ Services.contacts.accountBuddyAdded(buddy);
+ },
+
+ // Called for each buddy that is sent in a list packet from Yahoo! on login.
+ addBuddyFromServer: function(aTag, aName) {
+ let buddy;
+ if (this._buddies.has(aName))
+ buddy = this._buddies.get(aName);
+ else {
+ buddy = new YahooAccountBuddy(this, null, aTag, aName);
+ Services.contacts.accountBuddyAdded(buddy);
+ this._buddies.set(aName, buddy);
+ }
+
+ // Set all new buddies as offline because a following status packet will
+ // tell their status if they are online.
+ buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
+ },
+
+ // Called when a user removes a contact from within Instantbird.
+ removeBuddy: function(aBuddy, aRemoveFromServer) {
+ if (aRemoveFromServer)
+ this._session.removeBuddyFromServer(aBuddy);
+ this._buddies.delete(aBuddy.userName);
+ Services.contacts.accountBuddyRemoved(aBuddy);
+ },
+
+ loadBuddy: function(aBuddy, aTag) {
+ let buddy = new YahooAccountBuddy(this, aBuddy, aTag);
+ this._buddies.set(buddy.userName, buddy);
+
+ return buddy;
+ },
+
+ setBuddyStatus: function(aName, aStatus, aMessage) {
+ if (!this._buddies.has(aName))
+ return;
+ this._buddies.get(aName).setStatus(aStatus, aMessage);
+ },
+
+ getBuddy: function(aName) {
+ if (this._buddies.has(aName))
+ return this._buddies.get(aName);
+ return null;
+ },
+
+ receiveMessage: function(aName, aMessage) {
+ let conv;
+ // Check if we have an existing converstaion open with this user. If not,
+ // create one and add it to the list.
+ if (!this._conversations.has(aName))
+ conv = this.createConversation(aName);
+ else
+ conv = this._conversations.get(aName);
+
+ conv.writeMessage(aName, this.decodeMessage(aMessage), {incoming: true});
+ },
+
+ receiveConferenceMessage: function(aName, aRoom, aMessage) {
+ if (!this._conferences.has(aRoom))
+ return;
+
+ this._conferences.get(aRoom).writeMessage(aName,
+ this.decodeMessage(aMessage),
+ {incoming: true});
+ },
+
+ receiveTypingNotification: function(aName, aIsTyping) {
+ if (!this._conversations.has(aName))
+ return;
+
+ if (aIsTyping)
+ this._conversations.get(aName).updateTyping(Ci.prplIConvIM.TYPING);
+ else
+ this._conversations.get(aName).updateTyping(Ci.prplIConvIM.NOT_TYPING);
+ },
+
+ encodeMessage: function(aMessage) {
+ // Try to perform a convertion from JavaScript UTF-16 into the charset
+ // specified in the options. If the conversion fails, just leave
+ // the message as it is.
+ let encodedMsg;
+ try {
+ encodedMsg = this._account._converter.ConvertFromUnicode(aMessage);
+ } catch (e) {
+ encodedMsg = aMessage;
+ this.WARN("Could not encode UTF-16 message into " +
+ this._converter.charset + ". Message: " + aMessage);
+ }
+ return encodedMsg;
+ },
+
+ decodeMessage: function(aMessage) {
+ // Try to perform a convertion from the charset specified in the options
+ // to JavaScript UTF-16. If the conversion fails, just leave the message
+ // as it is.
+ let decodedMsg;
+ try {
+ decodedMsg = this._account._converter.ConvertToUnicode(aMessage);
+ } catch (e) {
+ decodedMsg = aMessage;
+ this.WARN("Could not deccode " + this._converter.charset +
+ " message into UTF-16. Message: " + aMessage);
+ }
+ return decodedMsg;
+ },
+
+ get canJoinChat() true,
+ // Strip @yahoo.com or @yahoo.co.jp from username. Other email domains
+ // can be left alone.
+ get cleanUsername() this.name.replace(this._protocol.emailSuffix, ""),
+ chatRoomFields: {},
+ joinChat: function(aComponents) {
+ // Use _roomsCreated to append a unique number to the room name. We add 1
+ // so that we can start the room numbers from 1 instead of 0.
+ let roomName = this.cleanUsername + "-" + (++this._roomsCreated);
+ let conf = new YahooConference(this, roomName, this.cleanUsername);
+ this._conferences.set(roomName, conf);
+ this._session.createConference(roomName);
+ this._roomsCreated++;
+ }
+};
+YahooAccount.prototype.__proto__ = GenericAccountPrototype;
+
+function YahooProtocol() {
+ this.registerCommands();
+}
+YahooProtocol.prototype = {
+ __proto__: GenericProtocolPrototype,
+ // Protocol specific connection parameters.
+ pagerRequestUrl: "http://vcs1.msg.yahoo.com/capacity",
+ loginTokenGetUrl: "https://login.yahoo.com/config/pwtoken_get",
+ loginTokenLoginUrl: "https://login.yahoo.com/config/pwtoken_login",
+ buildId: "4194239",
+ emailSuffix: "@yahoo.com",
+
+ get id() "prpl-yahoo",
+ get name() "Yahoo",
+ options: {
+ port: {get label() _("options.pagerPort"), default: 5050},
+ //xfer_host: {get label() _("options.transferHost"), default: "filetransfer.msg.yahoo.com"},
+ //xfer_port: {get label() _("options.transferPort"), default: 80},
+ //room_list_locale: {get label() _("options.chatLocale"), default: "us"},
+ local_charset: {get label() _("options.chatEncoding"), default: "UTF-8"},
+ ignore_invites: {get label() _("options.ignoreInvites"), default: false}
+ //proxy_ssl: {get label() _("options.proxySSL"), default: false}
+ },
+ commands: [
+ {
+ name: "invite",
+ get helpString() _("command.help.invite"),
+ run: function(aMsg, aConv) {
+ if (aMsg.trim().length == 0)
+ return false;
+
+ let splitPosition = aMsg.indexOf(" "); // Split at first space.
+ let invitees;
+ let message;
+
+ // If we have an invite message.
+ if (splitPosition > 0) {
+ invitees = aMsg.substring(0, splitPosition).split(",");
+ message = aMsg.substring(splitPosition);
+ } else {
+ invitees = aMsg.split(",");
+ message = _("conference.invite.message"); // Use default message.
+ }
+
+ let conf = aConv.wrappedJSObject;
+ conf._account._session.inviteToConference(invitees, conf._roomName,
+ conf.getParticipantNames(),
+ message);
+ return true;
+ }
+ }
+ ],
+ getAccount: function(aImAccount) new YahooAccount(this, aImAccount),
+ classID: Components.ID("{50ea817e-5d79-4657-91ae-aa0a52bdb98c}")
+};
+
+function YahooJapanProtocol() {
+ this.registerCommands();
+}
+YahooJapanProtocol.prototype = {
+ __proto__: YahooProtocol.prototype,
+ // Protocol specific connection parameters.
+ pagerRequestUrl: "http://cs1.yahoo.co.jp/capacity",
+ loginTokenGetUrl: "https://login.yahoo.co.jp/config/pwtoken_get",
+ loginTokenLoginUrl: "https://login.yahoo.co.jp/config/pwtoken_login",
+ buildId: "4186047",
+ emailSuffix: "@yahoo.co.jp",
+
+ get id() "prpl-yahoojp",
+ get name() "Yahoo JAPAN",
+ options: {
+ port: {get label() _("options.pagerPort"), default: 5050},
+ //xfer_host: {get label() _("options.transferHost"), default: "filetransfer.msg.yahoo.com"},
+ //xfer_port: {get label() _("options.transferPort"), default: 80},
+ //room_list_locale: {get label() _("options.chatLocale"), default: "jp"},
+ local_charset: {get label() _("options.chatEncoding"), default: "UTF-8"},
+ ignore_invites: {get label() _("options.ignoreInvites"), default: false}
+ //proxy_ssl: {get label() _("options.proxySSL"), default: false}
+ },
+ classID: Components.ID("{5f6dc733-ec0d-4de8-8adc-e4967064ed38}")
+};
+
+const NSGetFactory = XPCOMUtils.generateNSGetFactory([YahooProtocol, YahooJapanProtocol]);
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/yahoo.manifest
@@ -0,0 +1,7 @@
+component {50ea817e-5d79-4657-91ae-aa0a52bdb98c} yahoo.js
+contract @mozilla.org/chat/yahoo;1 {50ea817e-5d79-4657-91ae-aa0a52bdb98c}
+category im-protocol-plugin prpl-yahoo @mozilla.org/chat/yahoo;1
+
+component {5f6dc733-ec0d-4de8-8adc-e4967064ed38} yahoo.js
+contract @mozilla.org/chat/yahoojp;1 {5f6dc733-ec0d-4de8-8adc-e4967064ed38}
+category im-protocol-plugin prpl-yahoojp @mozilla.org/chat/yahoojp;1