--- a/chat/locales/en-US/irc.properties
+++ b/chat/locales/en-US/irc.properties
@@ -8,16 +8,17 @@
# configuring an IRC account.
irc.usernameHint=nick
# LOCALIZATION NOTE (connection.error.*):
# These will show in the account manager if the account is
# disconnected because of an error.
connection.error.lost=Lost connection with server
connection.error.timeOut=Connection timed out
+connection.error.invalidUsername=%S is not an allowed username
connection.error.invalidPassword=Invalid server password
connection.error.passwordRequired=Password required
# LOCALIZATION NOTE (joinChat.*):
# These show up on the join chat menu. An underscore is for the access key.
joinChat.channel=_Channel
joinChat.password=_Password
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -20,30 +20,36 @@ XPCOMUtils.defineLazyModuleGetter(this,
/*
* Parses a raw IRC message into an object (see section 2.3 of RFC 2812). This
* returns an object with the following fields:
* rawMessage The initial message string received without any processing.
* command A string that is the command or response code.
* params An array of strings for the parameters. The last parameter is
* stripped of its : prefix.
- * If the message is from a user:
- * nickname The user's nickname.
+ * origin The user's nickname or the server who sent the message. Can be
+ * a host (e.g. irc.mozilla.org) or an IPv4 address (e.g. 1.2.3.4)
+ * or an IPv6 address (e.g. 3ffe:1900:4545:3:200:f8ff:fe21:67cf).
* user The user's username, note that this can be undefined.
* host The user's hostname, note that this can be undefined.
* source A "nicely" formatted combination of user & host, which is
* <user>@<host> or <user> if host is undefined.
- * Otherwise if it's from a server:
- * servername This is the address of the server as a host (e.g.
- * irc.mozilla.org) or an IPv4 address (e.g. 1.2.3.4) or IPv6
- * address (e.g. 3ffe:1900:4545:3:200:f8ff:fe21:67cf).
+ *
+ * There are cases (e.g. localhost) where it cannot be easily determined if a
+ * message is from a server or from a user, thus the usage of a generic "origin"
+ * instead of "nickname" or "servername".
+ *
+ * Inputs:
+ * aData The raw string to parse, it should already have the \r\n
+ * stripped from the end.
+ * aOrigin The default origin to use for unprefixed messages.
*/
-function ircMessage(aData) {
+function ircMessage(aData, aOrigin) {
let message = {rawMessage: aData};
- let temp, prefix;
+ let temp;
// Splits the raw string into four parts (the second is required), the command
// is required. A raw string looks like:
// [":" <prefix> " "] <command> [" " <parameter>]* [":" <last parameter>]
// <prefix>: :(<server name> | <nickname> [["!" <user>] "@" <host>])
// <command>: /[^ ]+/
// <parameter>: /[^ ]+/
// <last parameter>: /.+/
@@ -52,42 +58,42 @@ function ircMessage(aData) {
// 2812 would allow. It allows for empty parameters (besides the last
// parameter, which can always be empty), by allowing multiple spaces.
// (This is for compatibility with Unreal's 432 response, which returns an
// empty first parameter.) It also allows a trailing space after the
// <parameter>s when no <last parameter> is present (also occurs with Unreal).
if (!(temp = aData.match(/^(?::([^ ]+) )?([^ ]+)((?: +[^: ][^ ]*)*)? *(?::([\s\S]*))?$/)))
throw "Couldn't parse message: \"" + aData + "\"";
- // Assume message is from the server if not specified
- prefix = temp[1];
message.command = temp[2];
// Space separated parameters. Since we expect a space as the first thing
// here, we want to ignore the first value (which is empty).
message.params = temp[3] ? temp[3].split(" ").slice(1) : [];
// Last parameter can contain spaces or be an empty string.
if (temp[4] != undefined)
message.params.push(temp[4]);
- // The source string can be split into multiple parts as:
- // :(server|nickname[[!user]@host])
- // If the source contains a . or a :, assume it's a server name. See RFC
- // 2812 Section 2.3 definition of servername vs. nickname.
- if (prefix &&
- (temp = prefix.match(/^([^ !@\.:]+)(?:!([^ @]+))?(?:@([^ ]+))?$/))) {
- message.nickname = temp[1];
- message.user = temp[2] || null; // Optional
- message.host = temp[3] || null; // Optional
- if (message.user)
- message.source = message.user + "@" + message.host;
- else
- message.source = message.host; // Note: this can be null!
- }
- else if (prefix)
- message.servername = prefix;
+ // Handle the prefix part of the message per RFC 2812 Section 2.3.
+
+ // If no prefix is given, assume the current server is the origin.
+ if (!temp[1])
+ temp[1] = aOrigin;
+
+ // Split the prefix into separate nickname, username and hostname fields as:
+ // :(servername|(nickname[[!user]@host]))
+ [message.origin, message.user, message.host] = temp[1].split(/[!@]/);
+
+ // It is occasionally useful to have a "source" which is a combination of
+ // user@host.
+ if (message.user)
+ message.source = message.user + "@" + message.host;
+ else if (message.host)
+ message.source = message.host;
+ else
+ message.source = "";
return message;
}
// This handles a mode change string for both channels and participants. A mode
// change string is of the form:
// aAddNewMode is true if modes are being added, false otherwise.
// aNewModes is an array of mode characters.
@@ -678,17 +684,18 @@ ircSocket.prototype = {
// Low level dequote: replace quote character \020 followed by 0, n, r or
// \020 with a \0, \n, \r or \020, respectively. Any other character is
// replaced with itself.
const lowDequote = {"0": "\0", "n": "\n", "r": "\r", "\x10": "\x10"};
let dequotedMessage = aRawMessage.replace(/\x10./g,
function(aStr) lowDequote[aStr[1]] || aStr[1]);
try {
- let message = new ircMessage(dequotedMessage);
+ let message = new ircMessage(dequotedMessage,
+ this._account._currentServerName);
this.DEBUG(JSON.stringify(message) + conversionWarning);
if (!ircHandlers.handleMessage(this._account, message)) {
// If the message was not handled, throw a warning containing
// the original quoted message.
this.WARN("Unhandled IRC message:\n" + aRawMessage);
}
} catch (e) {
// Catch the error, display it and hope the connection can continue with
@@ -778,16 +785,20 @@ function ircAccount(aProtocol, aImAccoun
this._init(aProtocol, aImAccount);
this.buddies = new NormalizedMap(this.normalizeNick.bind(this));
this.conversations = new NormalizedMap(this.normalize.bind(this));
// Split the account name into usable parts.
let splitter = this.name.lastIndexOf("@");
this._accountNickname = this.name.slice(0, splitter);
this._server = this.name.slice(splitter + 1);
+ // To avoid _currentServerName being null, initialize it to the server being
+ // connected to. This will also get overridden during the 001 response from
+ // the server.
+ this._currentServerName = this._server;
this._nickname = this._accountNickname;
this._requestedNickname = this._nickname;
// For more information, see where these are defined in the prototype below.
this.trackQueue = [];
this.pendingIsOnQueue = [];
this.whoisInformation = new NormalizedMap(this.normalizeNick.bind(this));
@@ -813,16 +824,27 @@ ircAccount.prototype = {
// The nickname stored in the account name.
_accountNickname: null,
// The nickname that was last requested by the user.
_requestedNickname: null,
// The nickname that was last requested. This can differ from
// _requestedNickname when a new nick is automatically generated (e.g. by
// adding digits).
_sentNickname: null,
+ get username() {
+ let username;
+ // Use a custom username in a hidden preference.
+ if (this.prefs.prefHasUserValue("username"))
+ username = this.getString("username");
+ // But fallback to brandShortName if no username is provided (or is empty).
+ if (!username)
+ username = Services.appinfo.name;
+
+ return username;
+ },
// The prefix minus the nick (!user@host) as returned by the server, this is
// necessary for guessing message lengths.
prefix: null,
// Parts of the specification give max lengths, keep track of them since a
// server can overwrite them. The defaults given here are from RFC 2812.
maxNicknameLength: 9, // 1.2.1 Users
maxChannelLength: 50, // 1.3 Channels
@@ -1270,29 +1292,29 @@ ircAccount.prototype = {
},
handlePingReply: function(aSource, aPongTime) {
// Received PING response, display to the user.
let sentTime = new Date(parseInt(aPongTime, 10));
// The received timestamp is invalid.
if (isNaN(sentTime)) {
- this.WARN(aMessage.servername +
+ this.WARN(aMessage.origin +
" returned an invalid timestamp from a PING: " + aPongTime);
return false;
}
// Find the delay in milliseconds.
let delay = Date.now() - sentTime;
// If the delay is negative or greater than 1 minute, something is
// feeding us a crazy value. Don't display this to the user.
if (delay < 0 || 60 * 1000 < delay) {
- this.WARN(aMessage.servername +
- " returned an invalid delay from a PING: " + delay);
+ this.WARN(aMessage.origin + " returned an invalid delay from a PING: " +
+ delay);
return false;
}
let msg = PluralForm.get(delay, _("message.ping", aSource))
.replace("#2", delay);
this.getConversation(aSource).writeMessage(aSource, msg, {system: true});
return true;
},
@@ -1651,24 +1673,17 @@ ircAccount.prototype = {
this.sendMessage("PASS", this.getString("serverPassword"),
"PASS <password not logged>");
}
// Send the nick message (section 3.1.2).
this.changeNick(this._requestedNickname);
// Send the user message (section 3.1.3).
- let username;
- // Use a custom username in a hidden preference.
- if (this.prefs.prefHasUserValue("username"))
- username = this.getString("username");
- // But fallback to brandShortName if no username is provided (or is empty).
- if (!username)
- username = Services.appinfo.name;
- this.sendMessage("USER", [username, this._mode.toString(), "*",
+ this.sendMessage("USER", [this.username, this._mode.toString(), "*",
this._realname || this._requestedNickname]);
},
_reportDisconnecting: function(aErrorReason, aErrorMessage) {
this.reportDisconnecting(aErrorReason, aErrorMessage);
// Mark all contacts on the account as having an unknown status.
this.buddies.forEach(function(aBuddy)
--- a/chat/protocols/irc/ircBase.jsm
+++ b/chat/protocols/irc/ircBase.jsm
@@ -40,19 +40,18 @@ ircRoomInfo.prototype = {
this._account.getChatRoomDefaultFieldValues(this.name)
}
function privmsg(aAccount, aMessage, aIsNotification) {
let params = {incoming: true};
if (aIsNotification)
params.notification = true;
aAccount.getConversation(aAccount.isMUCName(aMessage.params[0]) ?
- aMessage.params[0] : aMessage.nickname)
- .writeMessage(aMessage.nickname || aMessage.servername,
- aMessage.params[1], params);
+ aMessage.params[0] : aMessage.origin)
+ .writeMessage(aMessage.origin, aMessage.params[1], params);
return true;
}
// Display the message and remove them from the rooms they're in.
function leftRoom(aAccount, aNicks, aChannels, aSource, aReason, aKicked) {
let msgId = "message." + (aKicked ? "kicked" : "parted");
// If a part message was included, include it.
let reason = aReason ? _(msgId + ".reason", aReason) : "";
@@ -88,18 +87,18 @@ function leftRoom(aAccount, aNicks, aCha
}
}
return true;
}
function writeMessage(aAccount, aMessage, aString, aType) {
let type = {};
type[aType] = true;
- aAccount.getConversation(aMessage.servername)
- .writeMessage(aMessage.servername, aString, type);
+ aAccount.getConversation(aMessage.origin)
+ .writeMessage(aMessage.origin, aString, type);
return true;
}
// If aNoLastParam is true, the last parameter is not printed out.
function serverMessage(aAccount, aMsg, aNoLastParam) {
// If we don't want to show messages from the server, just mark it as handled.
if (!aAccount._showServerTab)
return true;
@@ -181,111 +180,110 @@ var ircBase = {
if (Services.prefs.getIntPref("messenger.conversations.autoAcceptChatInvitations") == 1) {
// Auto-accept the invite.
this.joinChat(this.getChatRoomDefaultFieldValues(aMessage.params[1]));
this.LOG("Received invite for " + aMessage.params[1] +
", auto-accepting.");
}
// Otherwise, just notify the user.
this.getConversation(aMessage.params[1])
- .writeMessage(aMessage.nickname,
- _("message.inviteReceived", aMessage.nickname,
+ .writeMessage(aMessage.origin,
+ _("message.inviteReceived", aMessage.origin,
aMessage.params[1]), {system: true});
return true;
},
"JOIN": function(aMessage) {
// JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] ) / "0"
// Add the buddy to each channel
for each (let channelName in aMessage.params[0].split(",")) {
let conversation = this.getConversation(channelName);
- if (this.normalize(aMessage.nickname, this.userPrefixes) ==
+ if (this.normalize(aMessage.origin, this.userPrefixes) ==
this.normalize(this._nickname)) {
// If you join, clear the participants list to avoid errors with
// repeated participants.
conversation.removeAllParticipants();
conversation.left = false;
conversation.joining = false;
// If the user parted from this room earlier, confirm the rejoin.
// If conversation.chatRoomFields is present, the rejoin was due to
// an automatic reconnection, for which we already notify the user.
if (!conversation._firstJoin && !conversation.chatRoomFields) {
- conversation.writeMessage(aMessage.nickname, _("message.rejoined"),
+ conversation.writeMessage(aMessage.origin, _("message.rejoined"),
{system: true});
}
delete conversation._firstJoin;
// Ensure chatRoomFields information is available for reconnection.
if (!conversation.chatRoomFields) {
this.WARN("Opening a MUC without storing its " +
"prplIChatRoomFieldValues first.");
conversation.chatRoomFields =
this.getChatRoomDefaultFieldValues(channelName);
}
}
else {
// Don't worry about adding ourself, RPL_NAMREPLY takes care of that
// case.
- conversation.getParticipant(aMessage.nickname, true);
- let msg = _("message.join", aMessage.nickname, aMessage.source || "");
- conversation.writeMessage(aMessage.nickname, msg, {system: true,
- noLinkification: true});
+ conversation.getParticipant(aMessage.origin, true);
+ let msg = _("message.join", aMessage.origin, aMessage.source);
+ conversation.writeMessage(aMessage.origin, msg, {system: true,
+ noLinkification: true});
}
}
// If the joiner is a buddy, mark as online.
- let buddy = this.buddies.get(aMessage.nickname);
+ let buddy = this.buddies.get(aMessage.origin);
if (buddy)
buddy.setStatus(Ci.imIStatusInfo.STATUS_AVAILABLE, "");
return true;
},
"KICK": function(aMessage) {
// KICK <channel> *( "," <channel> ) <user> *( "," <user> ) [<comment>]
let comment = aMessage.params.length == 3 ? aMessage.params[2] : null;
// Some servers (moznet) send the kicker as the comment.
- if (comment == aMessage.nickname)
+ if (comment == aMessage.origin)
comment = null;
return leftRoom(this, aMessage.params[1].split(","),
- aMessage.params[0].split(","), aMessage.nickname,
- comment, true);
+ aMessage.params[0].split(","), aMessage.origin, comment,
+ true);
},
"MODE": function(aMessage) {
// MODE <nickname> *( ( "+" / "-") *( "i" / "w" / "o" / "O" / "r" ) )
// MODE <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
if (this.isMUCName(aMessage.params[0])) {
// If the first parameter is a channel name, a channel/participant mode
// was updated.
this.getConversation(aMessage.params[0])
.setMode(aMessage.params[1], aMessage.params.slice(2),
- aMessage.nickname || aMessage.servername);
+ aMessage.origin);
return true;
}
// Otherwise the user's own mode is being returned to them.
return this.setUserMode(aMessage.params[0], aMessage.params[1],
- aMessage.nickname || aMessage.servername,
- !this._userModeReceived);
+ aMessage.origin, !this._userModeReceived);
},
"NICK": function(aMessage) {
// NICK <nickname>
- this.changeBuddyNick(aMessage.nickname, aMessage.params[0]);
+ this.changeBuddyNick(aMessage.origin, aMessage.params[0]);
return true;
},
"NOTICE": function(aMessage) {
// NOTICE <msgtarget> <text>
// If the message doesn't have a nickname, it's from the server, don't
// show it unless the user wants to see it.
- if (!aMessage.hasOwnProperty("nickname"))
+ if (!aMessage.hasOwnProperty("origin"))
return serverMessage(this, aMessage);
return privmsg(this, aMessage, true);
},
"PART": function(aMessage) {
// PART <channel> *( "," <channel> ) [ <Part Message> ]
- return leftRoom(this, [aMessage.nickname], aMessage.params[0].split(","),
- aMessage.source || "",
+ return leftRoom(this, [aMessage.origin], aMessage.params[0].split(","),
+ aMessage.source,
aMessage.params.length == 2 ? aMessage.params[1] : null);
},
"PING": function(aMessage) {
// PING <server1> [ <server2> ]
// Keep the connection alive.
this.sendMessage("PONG", aMessage.params[0]);
return true;
},
@@ -295,69 +293,71 @@ var ircBase = {
// Ping to keep the connection alive.
if (pongTime.startsWith("_")) {
this._socket.cancelDisconnectTimer();
return true;
}
// Otherwise, the ping was from a user command.
else
- return this.handlePingReply(aMessage.servername, pongTime);
+ return this.handlePingReply(aMessage.origin, pongTime);
},
"PRIVMSG": function(aMessage) {
// PRIVMSG <msgtarget> <text to be sent>
// Display message in conversation
return privmsg(this, aMessage);
},
"QUIT": function(aMessage) {
// QUIT [ < Quit Message> ]
// Some IRC servers automatically prefix a "Quit: " string. Remove the
// duplication and use a localized version.
let quitMsg = aMessage.params[0] || "";
if (quitMsg.startsWith("Quit: "))
quitMsg = quitMsg.slice(6); // "Quit: ".length
// If a quit message was included, show it.
- let msg = _("message.quit", aMessage.nickname,
+ let msg = _("message.quit", aMessage.origin,
quitMsg.length ? _("message.quit2", quitMsg) : "");
// Loop over every conversation with the user and display that they quit.
this.conversations.forEach(conversation => {
if (conversation.isChat &&
- conversation._participants.has(aMessage.nickname)) {
- conversation.writeMessage(aMessage.servername, msg, {system: true});
- conversation.removeParticipant(aMessage.nickname);
+ conversation._participants.has(aMessage.origin)) {
+ conversation.writeMessage(aMessage.origin, msg, {system: true});
+ conversation.removeParticipant(aMessage.origin);
}
});
// Remove from the whois table.
- this.removeBuddyInfo(aMessage.nickname);
+ this.removeBuddyInfo(aMessage.origin);
// If the leaver is a buddy, mark as offline.
- let buddy = this.buddies.get(aMessage.nickname);
+ let buddy = this.buddies.get(aMessage.origin);
if (buddy)
buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
return true;
},
"SQUIT": function(aMessage) {
// <server> <comment>
return true;
},
"TOPIC": function(aMessage) {
// TOPIC <channel> [ <topic> ]
// Show topic as a message.
- let source = aMessage.nickname || aMessage.servername;
let conversation = this.getConversation(aMessage.params[0]);
let topic = aMessage.params[1];
// Set the topic in the conversation and update the UI.
- conversation.setTopic(topic ? ctcpFormatToText(topic) : "", source);
+ conversation.setTopic(topic ? ctcpFormatToText(topic) : "",
+ aMessage.origin);
return true;
},
"001": function(aMessage) { // RPL_WELCOME
// Welcome to the Internet Relay Network <nick>!<user>@<host>
this._socket.resetPingTimer();
- this._currentServerName = aMessage.servername;
+ // This seems a little strange, but we don't differentiate between a
+ // nickname and the servername since it can be ambiguous.
+ this._currentServerName = aMessage.origin;
// Clear user mode.
this._modes = new Set();
this._userModeReceived = false;
// Check if our nick has changed.
if (aMessage.params[0] != this._nickname)
this.changeBuddyNick(this._nickname, aMessage.params[0]);
@@ -485,17 +485,17 @@ var ircBase = {
"219": function(aMessage) { // RPL_ENDOFSTATS
// <stats letter> :End of STATS report
return serverMessage(this, aMessage);
},
"221": function(aMessage) { // RPL_UMODEIS
// <user mode string>
return this.setUserMode(aMessage.params[0], aMessage.params[1],
- aMessage.servername, true);
+ aMessage.origin, true);
},
/*
* Services
*/
"231": function(aMessage) { // RPL_SERVICEINFO
// Non-generic
return serverMessage(this, aMessage);
@@ -814,17 +814,17 @@ var ircBase = {
/*
* Channel functions
*/
"324": function(aMessage) { // RPL_CHANNELMODEIS
// <channel> <mode> <mode params>
this.getConversation(aMessage.params[1])
.setMode(aMessage.params[2], aMessage.params.slice(3),
- aMessage.servername);
+ aMessage.origin);
return true;
},
"325": function(aMessage) { // RPL_UNIQOPIS
// <channel> <nickname>
// TODO parse this and have the UI respond accordingly.
return false;
},
@@ -851,17 +851,17 @@ var ircBase = {
/*
* Invitations
*/
"341": function(aMessage) { // RPL_INVITING
// <channel> <nick>
// Note that servers reply with parameters in the reverse order from the
// above (which is as specified by RFC 2812).
this.getConversation(aMessage.params[2])
- .writeMessage(aMessage.servername,
+ .writeMessage(aMessage.origin,
_("message.invited", aMessage.params[1],
aMessage.params[2]), {system: true});
return true;
},
"342": function(aMessage) { // RPL_SUMMONING
// <user> :Summoning user to IRC
return writeMessage(this, aMessage,
_("message.summoned", aMessage.params[0]));
@@ -986,17 +986,17 @@ var ircBase = {
let conv = this.getConversation(aMessage.params[1]);
let msg;
if (conv.banMasks.length) {
msg = [_("message.banMasks", aMessage.params[1])]
.concat(conv.banMasks).join("\n");
}
else
msg = _("message.noBanMasks", aMessage.params[1]);
- conv.writeMessage(aMessage.servername, msg, {system: true});
+ conv.writeMessage(aMessage.origin, msg, {system: true});
return true;
},
"369": function(aMessage) { // RPL_ENDOFWHOWAS
// <nick> :End of WHOWAS
// We've received everything about WHOWAS, tell the tooltip that is waiting
// for this information.
this.notifyWhois(aMessage.params[1]);
return true;
@@ -1247,17 +1247,17 @@ var ircBase = {
// <channel> :You're not on that channel
this.ERROR("A command affecting " + aMessage.params[1] +
" failed because you aren't in that channel.");
return true;
},
"443": function(aMessage) { // ERR_USERONCHANNEL
// <user> <channel> :is already on channel
this.getConversation(aMessage.params[2])
- .writeMessage(aMessage.servername,
+ .writeMessage(aMessage.origin,
_("message.alreadyInChannel", aMessage.params[1],
aMessage.params[2]), {system: true});
return true;
},
"444": function(aMessage) { // ERR_NOLOGIN
// <user> :User not logged in
// TODO
return false;
@@ -1279,17 +1279,25 @@ var ircBase = {
this.LOG("Server doesn't support CAP.");
return true;
}
// TODO
return false;
},
"461": function(aMessage) { // ERR_NEEDMOREPARAMS
// <command> :Not enough parameters
- // TODO
+
+ if (!this.connected) {
+ // The account has been set up with an illegal username.
+ this.ERROR("Erroneous username: " + this.username);
+ this.gotDisconnected(Ci.prplIAccount.ERROR_INVALID_USERNAME,
+ _("connection.error.invalidUsername", this.user));
+ return true;
+ }
+
return false;
},
"462": function(aMessage) { // ERR_ALREADYREGISTERED
// :Unauthorized command (already registered)
// TODO
return false;
},
"463": function(aMessage) { // ERR_NOPERMFORHOST
--- a/chat/protocols/irc/ircCTCP.jsm
+++ b/chat/protocols/irc/ircCTCP.jsm
@@ -94,18 +94,17 @@ function ctcpHandleMessage(aMessage) {
// Loop over each raw CTCP message.
for each (let message in ctcpMessages) {
if (!ircHandlers.handleCTCPMessage(this, message)) {
this.WARN("Unhandled CTCP message: " + message.ctcp.rawMessage +
"\nin IRC message: " + message.rawMessage);
// For unhandled CTCP message, respond with a NOTICE ERRMSG that echoes
// back the original command.
- this.sendCTCPMessage(message.nickname || message.servername, true,
- "ERRMSG",
+ this.sendCTCPMessage(message.origin, true, "ERRMSG",
[message.ctcp.rawMessage, ":Unhandled CTCP command"]);
}
}
// We have handled this message as much as we can.
return true;
}
@@ -117,26 +116,25 @@ var ctcpBase = {
isEnabled: function() true,
// These represent CTCP commands.
commands: {
"ACTION": function(aMessage) {
// ACTION <text>
// Display message in conversation
this.getConversation(this.isMUCName(aMessage.params[0]) ?
- aMessage.params[0] : aMessage.nickname)
- .writeMessage(aMessage.nickname || aMessage.servername,
- "/me " + aMessage.ctcp.param,
+ aMessage.params[0] : aMessage.origin)
+ .writeMessage(aMessage.origin, "/me " + aMessage.ctcp.param,
{incoming: true});
return true;
},
// Used when an error needs to be replied with.
"ERRMSG": function(aMessage) {
- this.WARN(aMessage.nickname + " failed to handle CTCP message: " +
+ this.WARN(aMessage.origin + " failed to handle CTCP message: " +
aMessage.ctcp.param);
return true;
},
// This is commented out since CLIENTINFO automatically returns the
// supported CTCP parameters and this is not supported.
// Returns the user's full name, and idle time.
@@ -151,41 +149,41 @@ var ctcpBase = {
for (let handler of ircHandlers._ctcpHandlers) {
for (let command in handler.commands)
info.add(command);
}
let supportedCtcp = [...info].join(" ");
this.LOG("Reporting support for the following CTCP messages: " +
supportedCtcp);
- this.sendCTCPMessage(aMessage.nickname, true, "CLIENTINFO",
+ this.sendCTCPMessage(aMessage.origin, true, "CLIENTINFO",
supportedCtcp);
}
else {
// Received a CLIENTINFO response, store the information for future
// use.
let info = aMessage.ctcp.param.split(" ");
- this.setWhois(aMessage.nickname, {clientInfo: info})
+ this.setWhois(aMessage.origin, {clientInfo: info})
}
return true;
},
// Used to measure the delay of the IRC network between clients.
"PING": function(aMessage) {
// PING timestamp
if (aMessage.command == "PRIVMSG") {
// Received PING request, send PING response.
- this.LOG("Received PING request from " + aMessage.nickname +
+ this.LOG("Received PING request from " + aMessage.origin +
". Sending PING response: \"" + aMessage.ctcp.param + "\".");
- this.sendCTCPMessage(aMessage.nickname, true, "PING",
+ this.sendCTCPMessage(aMessage.origin, true, "PING",
aMessage.ctcp.param);
return true;
}
else
- return this.handlePingReply(aMessage.nickname, aMessage.ctcp.param);
+ return this.handlePingReply(aMessage.origin, aMessage.ctcp.param);
},
// These are commented out since CLIENTINFO automatically returns the
// supported CTCP parameters and this is not supported.
// An encryption protocol between clients without any known reference.
//"SED": function(aMessage) false,
@@ -193,28 +191,28 @@ var ctcpBase = {
//"SOURCE": function(aMessage) false,
// Gets the local date and time from other clients.
"TIME": function(aMessage) {
if (aMessage.command == "PRIVMSG") {
// TIME
// Received a TIME request, send a human readable response.
let now = (new Date()).toString();
- this.LOG("Received TIME request from " + aMessage.nickname +
+ this.LOG("Received TIME request from " + aMessage.origin +
". Sending TIME response: \"" + now + "\".");
- this.sendCTCPMessage(aMessage.nickname, true, "TIME", ":" + now);
+ this.sendCTCPMessage(aMessage.origin, true, "TIME", ":" + now);
}
else {
// TIME :<human-readable-time-string>
// Received a TIME reply, display it.
// Remove the : prefix, if it exists and display the result.
let time = aMessage.ctcp.param.slice(aMessage.ctcp.param[0] == ":");
- this.getConversation(aMessage.nickname)
- .writeMessage(aMessage.nickname,
- _("ctcp.time", aMessage.nickname, time),
+ this.getConversation(aMessage.origin)
+ .writeMessage(aMessage.origin,
+ _("ctcp.time", aMessage.origin, time),
{system: true});
}
return true;
},
// This is commented out since CLIENTINFO automatically returns the
// supported CTCP parameters and this is not supported.
@@ -222,24 +220,24 @@ var ctcpBase = {
//"USERINFO": function(aMessage) false,
// The version and type of the client.
"VERSION": function(aMessage) {
if (aMessage.command == "PRIVMSG") {
// VERSION
// Received VERSION request, send VERSION response.
let version = Services.appinfo.name + " " + Services.appinfo.version;
- this.LOG("Received VERSION request from " + aMessage.nickname +
+ this.LOG("Received VERSION request from " + aMessage.origin +
". Sending VERSION response: \"" + version + "\".");
- this.sendCTCPMessage(aMessage.nickname, true, "VERSION", version);
+ this.sendCTCPMessage(aMessage.origin, true, "VERSION", version);
}
else if (aMessage.command == "NOTICE" && aMessage.ctcp.param.length) {
// VERSION #:#:#
// Received VERSION response, display to the user.
- let response = _("ctcp.version", aMessage.nickname,
+ let response = _("ctcp.version", aMessage.origin,
aMessage.ctcp.param);
- this.getConversation(aMessage.nickname)
- .writeMessage(aMessage.nickname, response, {system: true});
+ this.getConversation(aMessage.origin)
+ .writeMessage(aMessage.origin, response, {system: true});
}
return true;
}
}
};
--- a/chat/protocols/irc/ircNonStandard.jsm
+++ b/chat/protocols/irc/ircNonStandard.jsm
@@ -31,26 +31,26 @@ var ircNonStandard = {
// Try to avoid the stupid case where the user's nick is AUTH. If this
// happens, it is ambiguous if it is an AUTH message or a NOTICE to the
// user. Generally AUTH messages start with ***, but this could pretty
// easily be faked.
// Freenode simply sends * for the target. Moznet sends Auth (used to send
// AUTH); in this case, check if the user's nickname is not auth, or the
// the message starts with ***.
let target = aMessage.params[0].toLowerCase();
- let nickname = aMessage.nickname ? aMessage.nickname.toLowerCase() : "";
+ let nickname = aMessage.origin ? aMessage.origin.toLowerCase() : "";
let isAuth = target == "*" ||
(target == "auth" && (nickname != "auth" ||
aMessage.params[1].startsWith("***")));
// Some servers , e.g. irc.umich.edu, use NOTICE before connection to give
// directions to users.
if (!this.connected && !isAuth) {
- this.getConversation(aMessage.servername)
- .writeMessage(aMessage.servername, aMessage.params[1],
+ this.getConversation(aMessage.origin)
+ .writeMessage(aMessage.origin, aMessage.params[1],
{incoming: true});
return true;
}
// If we receive a ZNC error message requesting a password, the
// serverPassword preference was not set by the user. Attempt to log into
// ZNC using the account password.
if (!isAuth ||
@@ -154,31 +154,31 @@ var ircNonStandard = {
},
"464": function(aMessage) {
// :Password required
// If we receive a ZNC error message requesting a password, eat it since
// a NOTICE AUTH will follow causing us to send the password. This numeric
// is, unfortunately, also sent if you give a wrong password. The
// parameter in that case is "Invalid Password".
- return aMessage.servername == "irc.znc.in" &&
+ return aMessage.origin == "irc.znc.in" &&
aMessage.params[1] == "Password required";
},
"499": function(aMessage) { // ERR_CHANOWNPRIVNEEDED (Unreal)
// <channel> :You're not the channel owner (status +q is needed)
return conversationErrorMessage(this, aMessage, "error.notChannelOwner");
},
"671": function(aMessage) { // RPL_WHOISSECURE (Unreal & Charybdis)
// <nick> :is using a Secure connection
return this.setWhois(aMessage.params[1], {secure: true});
},
"998": function(aMessage) {
// irc.umich.edu shows an ASCII captcha that must be typed in by the user.
- this.getConversation(aMessage.servername)
- .writeMessage(aMessage.servername, aMessage.params[1],
+ this.getConversation(aMessage.origin)
+ .writeMessage(aMessage.origin, aMessage.params[1],
{incoming: true, noFormat: true});
return true;
}
}
};
--- a/chat/protocols/irc/ircServices.jsm
+++ b/chat/protocols/irc/ircServices.jsm
@@ -36,17 +36,17 @@ function ServiceMessage(aAccount, aMessa
// map "bar": "NickServ"). Note that the keys of this map should be
// normalized.
let nicknameToServiceName = {
"chanserv": "ChanServ",
"infoserv": "InfoServ",
"nickserv": "NickServ"
}
- let nickname = aAccount.normalize(aMessage.nickname);
+ let nickname = aAccount.normalize(aMessage.origin);
if (nicknameToServiceName.hasOwnProperty(nickname))
aMessage.serviceName = nicknameToServiceName[nickname];
return aMessage;
}
var ircServices = {
name: "IRC Services",
@@ -59,17 +59,17 @@ var ircServices = {
"IDENTIFY <password not logged>");
}
},
commands: {
// If we automatically reply to a NOTICE message this does not abide by RFC
// 2812. Oh well.
"NOTICE": function(aMessage) {
- if (!ircHandlers.hasServicesHandlers || !aMessage.hasOwnProperty("nickname"))
+ if (!ircHandlers.hasServicesHandlers || !aMessage.hasOwnProperty("origin"))
return false;
let message = ServiceMessage(this, aMessage);
// If no service was found, return early.
if (!message.hasOwnProperty("serviceName"))
return false;
@@ -141,34 +141,33 @@ var servicesBase = {
// Otherwise, display the message in that conversation.
let params = {incoming: true};
if (aMessage.command == "NOTICE")
params.notification = true;
// The message starts after the channel name, plus [, ] and a space.
let message = aMessage.params[1].slice(channel.length + 3);
this.getConversation(channel)
- .writeMessage(aMessage.nickname, message, params);
+ .writeMessage(aMessage.origin, message, params);
return true;
},
"InfoServ": function(aMessage) {
let text = aMessage.params[1];
// Show the message of the day in the server tab.
if (text == "*** \u0002Message(s) of the Day\u0002 ***") {
this._infoServMotd = [text];
return true;
}
else if (text == "*** \u0002End of Message(s) of the Day\u0002 ***") {
if (this._showServerTab && this._infoServMotd) {
this._infoServMotd.push(text);
- this.getConversation(aMessage.servername || this._currentServerName)
- .writeMessage(aMessage.servername || aMessage.nickname,
- this._infoServMotd.join("\n"),
+ this.getConversation(aMessage.origin)
+ .writeMessage(aMessage.origin, this._infoServMotd.join("\n"),
{incoming: true});
delete this._infoServMotd;
}
return true;
}
else if (this.hasOwnProperty("_infoServMotd")) {
this._infoServMotd.push(text);
return true;
--- a/chat/protocols/irc/ircUtils.jsm
+++ b/chat/protocols/irc/ircUtils.jsm
@@ -216,17 +216,17 @@ function mIRCColoring(aStack, aInput) {
return [stack, output, length];
}
// Print an error message into a conversation, optionally mark the conversation
// as not joined and/or not rejoinable.
function conversationErrorMessage(aAccount, aMessage, aError,
aJoinFailed = false, aRejoinable = true) {
let conv = aAccount.getConversation(aMessage.params[1]);
- conv.writeMessage(aMessage.servername, _(aError, aMessage.params[1]),
+ conv.writeMessage(aMessage.origin, _(aError, aMessage.params[1]),
{error: true, system: true});
delete conv._pendingMessage;
// Channels have a couple extra things that can be done to them.
if (aAccount.isMUCName(aMessage.params[1])) {
// If a value for joining is explictly given, mark it.
if (aJoinFailed)
conv.joining = false;
--- a/chat/protocols/irc/test/test_ircMessage.js
+++ b/chat/protocols/irc/test/test_ircMessage.js
@@ -15,18 +15,17 @@ const testData = [
"OPER foo bar",
"MODE WiZ -w",
"MODE Angel +i",
"MODE WiZ -o",
"SERVICE dict * *.fr 0 0 :French Dictionary",
"QUIT :Gone to have lunch",
":syrk!kalt@millennium.stealth.net QUIT :Gone to have lunch",
"SQUIT tolsun.oulu.fi :Bad Link ?",
- // This fails! But do we really care? It wasn't designed to handle server messages.
- //":Trillian SQUIT cm22.eng.umd.edu :Server out of control",
+ ":Trillian SQUIT cm22.eng.umd.edu :Server out of control",
"JOIN #foobar",
"JOIN &foo fubar",
"JOIN #foo,&bar fubar",
"JOIN #foo,#bar fubar,foobar",
"JOIN #foo,#bar",
"JOIN 0",
":WiZ!jto@tolsun.oulu.fi JOIN #Twilight_zone",
"PART #twilight_zone",
@@ -115,41 +114,49 @@ const testData = [
"PRIVMSG foo :", // Empty last parameter.
"PRIVMSG foo :This is :a test." // A "second" last parameter.
];
function run_test() {
add_test(testRFC2812Messages);
add_test(testBrokenUnrealMessages);
add_test(testNewLinesInMessages);
+ add_test(testLocalhost);
run_next_test();
}
+/*
+ * Test round tripping parsing and then rebuilding the messages from RFC 2812.
+ */
function testRFC2812Messages() {
for each (let expectedStringMessage in testData) {
- let message = irc.ircMessage(expectedStringMessage);
+ // Pass in an empty default origin in order to check this below.
+ let message = irc.ircMessage(expectedStringMessage, "");
let stringMessage =
irc.ircAccount.prototype.buildMessage(message.command, message.params);
// Let's do a little dance here...we don't rebuild the "source" of the
// message (the server does that), so when comparing our output message, we
// need to avoid comparing to that part.
- if (message.servername || message.source) {
+ if (message.origin) {
expectedStringMessage =
expectedStringMessage.slice(expectedStringMessage.indexOf(" ") + 1);
}
do_check_eq(stringMessage, expectedStringMessage);
}
run_next_test();
}
+/*
+ * Test if two objects have the same fields (recursively).
+ */
function isEqual(aObject1, aObject2) {
let result = true;
for (let fieldName in aObject1) {
let field1 = aObject1[fieldName];
let field2 = aObject2[fieldName];
if (typeof field1 == "object")
result &= isEqual(field1, field2);
else if (Array.isArray(field1))
@@ -159,63 +166,88 @@ function isEqual(aObject1, aObject2) {
}
return result;
}
// Unreal sends a couple of broken messages, see ircMessage in irc.js for a
// description of what's wrong.
function testBrokenUnrealMessages() {
let messages = {
+ // Two spaces after command.
":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters": {
rawMessage: ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters",
command: "432",
params: ["", "#momo", "Erroneous Nickname: Illegal characters"],
- servername: "gravel.mozilla.org"
+ origin: "gravel.mozilla.org"
},
+ // An extraneous space at the end.
":gravel.mozilla.org MODE #tckk +n ": {
rawMessage: ":gravel.mozilla.org MODE #tckk +n ",
command: "MODE",
params: ["#tckk", "+n"],
- servername: "gravel.mozilla.org"
+ origin: "gravel.mozilla.org"
},
+ // Two extraneous spaces at the end.
":services.esper.net MODE #foo-bar +o foobar ": {
rawMessage: ":services.esper.net MODE #foo-bar +o foobar ",
command: "MODE",
params: ["#foo-bar", "+o", "foobar"],
- servername: "services.esper.net"
+ origin: "services.esper.net"
}
};
for (let messageStr in messages)
- do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr)));
+ do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr, "")));
run_next_test();
}
// After unescaping we can end up with line breaks inside of IRC messages. Test
// this edge case specifically.
function testNewLinesInMessages() {
let messages = {
":test!Instantbir@host PRIVMSG #instantbird :First line\nSecond line": {
rawMessage: ":test!Instantbir@host PRIVMSG #instantbird :First line\nSecond line",
command: "PRIVMSG",
params: ["#instantbird", "First line\nSecond line"],
- nickname: "test",
+ origin: "test",
user: "Instantbir",
host: "host",
source: "Instantbir@host"
},
":test!Instantbir@host PRIVMSG #instantbird :First line\r\nSecond line": {
rawMessage: ":test!Instantbir@host PRIVMSG #instantbird :First line\r\nSecond line",
command: "PRIVMSG",
params: ["#instantbird", "First line\r\nSecond line"],
- nickname: "test",
+ origin: "test",
user: "Instantbir",
host: "host",
source: "Instantbir@host"
}
};
for (let messageStr in messages)
do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr)));
run_next_test();
}
+
+// Sometimes it is a bit hard to tell whether a prefix is a nickname or a
+// servername. Generally this happens when connecting to localhost or a local
+// hostname and is likely seen with bouncers.
+function testLocalhost() {
+ let messages = {
+ ":localhost 001 clokep :Welcome to the BitlBee gateway, clokep": {
+ rawMessage: ":localhost 001 clokep :Welcome to the BitlBee gateway, clokep",
+ command: "001",
+ params: ["clokep", "Welcome to the BitlBee gateway, clokep"],
+ origin: "localhost",
+ user: undefined,
+ host: undefined,
+ source: ""
+ }
+ };
+
+ for (let messageStr in messages)
+ do_check_true(isEqual(messages[messageStr], irc.ircMessage(messageStr)));
+
+ run_next_test();
+}
--- a/im/test/xpcshell.ini
+++ b/im/test/xpcshell.ini
@@ -1,9 +1,9 @@
; 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/.
[include:chat/modules/test/xpcshell.ini]
[include:chat/components/src/test/xpcshell.ini]
[include:chat/protocols/irc/test/xpcshell.ini]
[include:chat/protocols/yahoo/test/xpcshell.ini]
-[include:extensions/purple/purplexpcom/src/test/xpcshell.ini]
+#[include:extensions/purple/purplexpcom/src/test/xpcshell.ini]