Bug 954088 - Configurable alternate IRC nicks, r=aleth.
authorPatrick Cloke <clokep@gmail.com>
Fri, 18 Oct 2013 07:04:49 -0400
changeset 17305 82523776afa50f1f78381641a64c70a22956e7c9
parent 17304 44b666cb6f7f996fcbb7763894572bd02258557d
child 17306 6334f31da0b46712301fdd3f20650ea82ab9bf4f
push id1151
push userbugzilla@standard8.plus.com
push dateMon, 03 Feb 2014 22:50:32 +0000
treeherdercomm-aurora@267d8e9143d5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaleth
bugs954088
Bug 954088 - Configurable alternate IRC nicks, r=aleth.
chat/locales/en-US/irc.properties
chat/protocols/irc/irc.js
chat/protocols/irc/ircBase.jsm
chat/protocols/irc/test/test_tryNewNick.js
--- a/chat/locales/en-US/irc.properties
+++ b/chat/locales/en-US/irc.properties
@@ -26,16 +26,17 @@ joinChat.password=_Password
 #   account wizard windows.
 options.server=Server
 options.port=Port
 options.ssl=Use SSL
 options.encoding=Character Set
 options.quitMessage=Quit message
 options.partMessage=Part message
 options.showServerTab=Show messages from the server
+options.alternateNicks=Alternate nicks
 
 # LOCALIZATION NOTE (ctcp.ping): Semi-colon list of plural forms.
 #  See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 #   %1$S is the nickname of the user who was pinged.
 #   #2 is the delay (in seconds).
 ctcp.ping=Ping reply from %1$S in #2 second.;Ping reply from %1$S in #2 seconds.
 # LOCALIZATION NOTE (ctcp.version):
 #   %1$S is the nickname of the user whose version was requested.
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -1140,16 +1140,86 @@ ircAccount.prototype = {
       this.removeConversation(aOldNick);
       this._conversations[this.normalizeNick(aNewNick)] = conversation;
 
       conversation.updateNick(aNewNick);
       conversation.writeMessage(aOldNick, msg, {system: true});
     }
   },
 
+  /*
+   * Generate a new nick to change to if the user requested nick is already in
+   * use or is otherwise invalid.
+   *
+   * First try all the alternate nicks that were chosen by the user, and if none
+   * of them work, then generate a new nick by:
+   *  1. If there was not a digit at the end of the nick, append a 1.
+   *  2. If there was a digit, then increment the number.
+   *  3. Add leading 0s back on.
+   *  4. Ensure the nick is an appropriate length.
+   */
+  tryNewNick: function(aOldNick) {
+    // Split the string on commas, remove whitespace around the nicks and
+    // remove empty nicks.
+    let allNicks = this.getString("alternateNicks").split(",")
+                       .map(n => n.trim()).filter(n => !!n);
+    allNicks.unshift(this._accountNickname);
+
+    // If the previously tried nick is in the array and not the last
+    // element, try the next nick in the array.
+    let oldIndex = allNicks.indexOf(aOldNick);
+    if (oldIndex != -1 && oldIndex < allNicks.length - 1) {
+      let newNick = allNicks[oldIndex + 1];
+      this.LOG(aOldNick + " is already in use, trying " + newNick);
+      this.sendMessage("NICK", newNick); // Nick message.
+      return true;
+    }
+
+    // Separate the nick into the text and digits part.
+    let nickParts = /^(.+?)(\d*)$/.exec(aOldNick);
+    let newNick = nickParts[1];
+
+    // No nick found from the user's preferences, so just generating one.
+    // If there is not a digit at the end of the nick, just append 1.
+    let newDigits = "1";
+    // If there is a digit at the end of the nick, increment it.
+    if (nickParts[2]) {
+      newDigits = (parseInt(nickParts[2], 10) + 1).toString();
+      // If there are leading 0s, add them back on, after we've incremented (e.g.
+      // 009 --> 010).
+      let numLeadingZeros = nickParts[2].length - newDigits.length;
+      if (numLeadingZeros > 0)
+        newDigits = "0".repeat(numLeadingZeros) + newDigits;
+    }
+
+    // If the nick will be too long, ensure all the digits fit.
+    if (newNick.length + newDigits.length > this.maxNicknameLength) {
+      // Handle the silly case of a single letter followed by all nines.
+      if (newDigits.length == this.maxNicknameLength)
+        newDigits = newDigits.slice(1);
+      newNick = newNick.slice(0, this.maxNicknameLength - newDigits.length);
+    }
+    // Append the digits.
+    newNick += newDigits;
+
+    if (this.normalize(newNick) == this.normalize(this._nickname)) {
+      // The nick we were about to try next is our current nick. This means
+      // the user attempted to change to a version of the nick with a lower or
+      // absent number suffix, and this failed.
+      let msg = _("message.nick.fail", this._nickname);
+      for each (let conversation in this._conversations)
+        conversation.writeMessage(this._nickname, msg, {system: true});
+      return true;
+    }
+  
+    this.LOG(aOldNick + " is already in use, trying " + newNick);
+    this.sendMessage("NICK", newNick); // Nick message.
+    return true;
+  },
+
   countBytes: function(aStr) {
     // Assume that if it's not UTF-8 then each character is 1 byte.
     if (this._encoding != "UTF-8")
       return aStr.length;
 
     // Count the number of bytes in a UTF-8 encoded string.
     function charCodeToByteCount(c) {
       // UTF-8 stores:
@@ -1632,17 +1702,18 @@ ircProtocol.prototype = {
     // TODO Default to IRC over SSL.
     "port": {get label() _("options.port"), default: 6667},
     "ssl": {get label() _("options.ssl"), default: false},
     // TODO We should attempt to auto-detect encoding instead.
     "encoding": {get label() _("options.encoding"), default: "UTF-8"},
     "quitmsg": {get label() _("options.quitMessage"),
                 get default() Services.prefs.getCharPref("chat.irc.defaultQuitMessage")},
     "partmsg": {get label() _("options.partMessage"), default: ""},
-    "showServerTab": {get label() _("options.showServerTab"), default: false}
+    "showServerTab": {get label() _("options.showServerTab"), default: false},
+    "alternateNicks": {get label() _("options.alternateNicks"), default: ""}
   },
 
   get chatHasTopic() true,
   get slashCommandsNative() true,
   //  Passwords in IRC are optional, and are needed for certain functionality.
   get passwordOptional() true,
 
   getAccount: function(aImAccount) new ircAccount(this, aImAccount),
--- a/chat/protocols/irc/ircBase.jsm
+++ b/chat/protocols/irc/ircBase.jsm
@@ -118,56 +118,16 @@ function serverMessage(aAccount, aMsg, a
 function serverErrorMessage(aAccount, aMessage, aError) {
   // If we don't want to show messages from the server, just mark it as handled.
   if (!aAccount._showServerTab)
     return true;
 
   return writeMessage(aAccount, aMessage, aError, "error")
 }
 
-// Try a new nick if the previous tried nick is already in use.
-function tryNewNick(aAccount, aMessage) {
-  let nickParts = /^(.+?)(\d*)$/.exec(aMessage.params[1]);
-  let newNick = nickParts[1];
-
-  // If there was not a digit at the end of the nick, just append 1.
-  let newDigits = "1";
-  // If there was a digit at the end of the nick, increment it.
-  if (nickParts[2]) {
-    newDigits = (parseInt(nickParts[2], 10) + 1).toString();
-    // If there were leading 0s, add them back on, after we've incremented (e.g.
-    // 009 --> 010).
-    for (let len = nickParts[2].length - newDigits.length; len > 0; --len)
-      newDigits = "0" + newDigits;
-  }
-  // If the nick will be too long, ensure all the digits fit.
-  if (newNick.length + newDigits.length > aAccount.maxNicknameLength) {
-    // Handle the silly case of a single letter followed by all nines.
-    if (newDigits.length == aAccount.maxNicknameLength)
-      newDigits = newDigits.slice(1);
-    newNick = newNick.slice(0, aAccount.maxNicknameLength - newDigits.length);
-  }
-  // Append the digits.
-  newNick += newDigits;
-
-  if (aAccount.normalize(newNick) == aAccount.normalize(aAccount._nickname)) {
-    // The nick we were about to try next is our current nick. This means
-    // the user attempted to change to a version of the nick with a lower or
-    // absent number suffix, and this failed.
-    let msg = _("message.nick.fail", aAccount._nickname);
-    for each (let conversation in aAccount._conversations)
-      conversation.writeMessage(aAccount._nickname, msg, {system: true});
-    return true;
-  }
-
-  aAccount.LOG(aMessage.params[1] + " is already in use, trying " + newNick);
-  aAccount.sendMessage("NICK", newNick); // Nick message.
-  return true;
-}
-
 // See RFCs 2811 & 2812 (which obsoletes RFC 1459) for a description of these
 // commands.
 var ircBase = {
   // Parameters
   name: "RFC 2812", // Name identifier
   priority: ircHandlers.DEFAULT_PRIORITY,
   isEnabled: function() true,
 
@@ -1229,21 +1189,21 @@ var ircBase = {
         // Reset original nickname to the account nickname in case of
         // later reconnections.
         this._requestedNickname = this._accountNickname;
       }
       return true;
     },
     "433": function(aMessage) { // ERR_NICKNAMEINUSE
       // <nick> :Nickname is already in use
-      return tryNewNick(this, aMessage);
+      return this.tryNewNick(aMessage.params[1]);
     },
     "436": function(aMessage) { // ERR_NICKCOLLISION
       // <nick> :Nickname collision KILL from <user>@<host>
-      return tryNewNick(this, aMessage);
+      return this.tryNewNick(aMessage.params[1]);
     },
     "437": function(aMessage) { // ERR_UNAVAILRESOURCE
       // <nick/channel> :Nick/channel is temporarily unavailable
       // TODO
       delete this._chatRoomFieldsList[this.normalize(aMessage.params[1])];
       return false;
     },
     "441": function(aMessage) { // ERR_USERNOTINCHANNEL
--- a/chat/protocols/irc/test/test_tryNewNick.js
+++ b/chat/protocols/irc/test/test_tryNewNick.js
@@ -1,53 +1,100 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
-let ircBase = {};
-Services.scriptloader.loadSubScript("resource:///modules/ircBase.jsm", ircBase);
+let irc = {};
+Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+
+const fakeProto = {
+  id: "fake-proto",
+  options: {alternateNicks: ""},
+  _getOptionDefault: function(aOption) this.options[aOption]
+}
 
-const testData = {
-  "clokep": "clokep1",
-  "clokep1": "clokep2",
-  "clokep10": "clokep11",
-  "clokep0": "clokep1",
-  "clokep01": "clokep02",
-  "clokep09": "clokep10",
+function test_tryNewNick() {
+  const testData = {
+    "clokep": "clokep1",
+    "clokep1": "clokep2",
+    "clokep10": "clokep11",
+    "clokep0": "clokep1",
+    "clokep01": "clokep02",
+    "clokep09": "clokep10",
 
-  // Now put a number in the "first part".
-  "clo1kep": "clo1kep1",
-  "clo1kep1": "clo1kep2",
-  "clo1kep10": "clo1kep11",
-  "clo1kep0": "clo1kep1",
-  "clo1kep01": "clo1kep02",
-  "clo1kep09": "clo1kep10",
+    // Now put a number in the "first part".
+    "clo1kep": "clo1kep1",
+    "clo1kep1": "clo1kep2",
+    "clo1kep10": "clo1kep11",
+    "clo1kep0": "clo1kep1",
+    "clo1kep01": "clo1kep02",
+    "clo1kep09": "clo1kep10",
 
-  // Some to test the max length.
-  "abcdefghi": "abcdefgh1",
-  "abcdefgh0": "abcdefgh1",
-  "abcdefgh9": "abcdefg10",
-  "a99999999": "a00000000" // You'd expect 100000000, but this is not valid!
-};
+    // Some to test the max length.
+    "abcdefghi": "abcdefgh1",
+    "abcdefgh0": "abcdefgh1",
+    "abcdefgh9": "abcdefg10",
+    "a99999999": "a00000000" // You'd expect 100000000, but this is not valid!
+  };
 
-function run_test() {
-  add_test(test_tryNewNick);
+  let account = new irc.ircAccount(fakeProto,
+                                   {name: "clokep@instantbird.org"});
+  account.LOG = function(aStr) {};
+  account.maxNicknameLength = 9;
+  account.normalize = function(aStr) aStr;
+
+  for (let currentNick in testData) {
+    account.sendMessage = function(aCommand, aNewNick)
+      do_check_eq(aNewNick, testData[currentNick]);
+
+    account.tryNewNick(currentNick);
+  }
 
   run_next_test();
 }
 
-function test_tryNewNick() {
+function test_altNicks() {
+  const altNicks = ["clokep_", "clokep|"];
+  const testData = {
+    // Test account nick.
+    "clokep": [altNicks, "clokep_"],
+    // Test first element in list.
+    "clokep_": [altNicks, "clokep|"],
+    // Test last element in list.
+    "clokep|": [altNicks, "clokep|1"],
+    // Test element not in list with number at end.
+    "clokep1": [altNicks, "clokep2"],
+
+    // Test messy alternatives.
+    "clokep[": [" clokep ,\n clokep111,,,\tclokep[, clokep_", "clokep_"]
+  };
+
+  let account = new irc.ircAccount(fakeProto,
+                                   {name: "clokep@instantbird.org"});
+  account.LOG = function(aStr) {};
+  account.maxNicknameLength = 9;
+  account.normalize = function(aStr) aStr;
+
   for (let currentNick in testData) {
-    let account = {
-      LOG: function(aStr) {},
-      maxNicknameLength: 9,
-      normalize: function(aStr) aStr,
-      sendMessage: function(aCommand, aNewNick) {
-        do_check_eq(aNewNick, testData[currentNick]);
-      }
+    // Only one pref is touched in here, override the default to return
+    // what this test needs.
+    account.getString = function(aStr) {
+      let data = testData[currentNick][0];
+      if (Array.isArray(data))
+        return data.join(",");
+      return data;
     };
-    let message = {params: [null, currentNick]};
 
-    ircBase.tryNewNick(account, message);
+    account.sendMessage = function(aCommand, aNewNick)
+      do_check_eq(aNewNick, testData[currentNick][1]);
+
+    account.tryNewNick(currentNick);
   }
 
   run_next_test();
 }
+
+function run_test() {
+  add_test(test_tryNewNick);
+  add_test(test_altNicks);
+
+  run_next_test();
+}