/* 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/. */ /* * This contains the implementation for the basic Internet Relay Chat (IRC) * protocol covered by RFCs 2810, 2811, 2812 and 2813 (which obsoletes RFC * 1459). RFC 2812 covers the client commands and protocol. * RFC 2810: Internet Relay Chat: Architecture * http://tools.ietf.org/html/rfc2810 * RFC 2811: Internet Relay Chat: Channel Management * http://tools.ietf.org/html/rfc2811 * RFC 2812: Internet Relay Chat: Client Protocol * http://tools.ietf.org/html/rfc2812 * RFC 2813: Internet Relay Chat: Server Protocol * http://tools.ietf.org/html/rfc2813 * RFC 1459: Internet Relay Chat Protocol * http://tools.ietf.org/html/rfc1459 */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { l10nHelper, nsSimpleEnumerator, } from "resource:///modules/imXPCOMUtils.sys.mjs"; import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ircHandlerPriorities } from "resource:///modules/ircHandlerPriorities.sys.mjs"; import { ctcpFormatToText, conversationErrorMessage, displayMessage, kListRefreshInterval, } from "resource:///modules/ircUtils.sys.mjs"; const lazy = {}; XPCOMUtils.defineLazyGetter(lazy, "_", () => l10nHelper("chrome://chat/locale/irc.properties") ); // 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 ? lazy._(msgId + ".reason", aReason) : ""; function __(aNick, aYou) { // If the user is kicked, we need to say who kicked them. let msgId2 = msgId + (aYou ? ".you" : ""); if (aKicked) { if (aYou) { return lazy._(msgId2, aSource, reason); } return lazy._(msgId2, aNick, aSource, reason); } if (aYou) { return lazy._(msgId2, reason); } return lazy._(msgId2, aNick, reason); } for (let channelName of aChannels) { if (!aAccount.conversations.has(channelName)) { // Handle when we closed the window. continue; } let conversation = aAccount.getConversation(channelName); for (let nick of aNicks) { let msg; if (aAccount.normalize(nick) == aAccount.normalize(aAccount._nickname)) { msg = __(nick, true); // If the user left, mark the conversation as no longer being active. conversation.left = true; } else { msg = __(nick); } conversation.writeMessage(aSource, msg, { system: true }); conversation.removeParticipant(nick); } } return true; } function writeMessage(aAccount, aMessage, aString, aType) { let type = {}; type[aType] = true; type.tags = aMessage.tags; 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; } return writeMessage( aAccount, aMsg, aMsg.params.slice(1, aNoLastParam ? -1 : undefined).join(" "), "system" ); } 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"); } function addMotd(aAccount, aMessage) { // If there is no current MOTD to append to, start a new one. if (!aAccount._motd) { aAccount._motd = []; } // Traditionally, MOTD messages start with "- ", but this is not always // true, try to handle that sanely. let message = aMessage.params[1]; if (message.startsWith("-")) { message = message.slice(1).trim(); } // And traditionally, the initial message ends in " -", remove that. if (message.endsWith("-")) { message = message.slice(0, -1).trim(); } // Actually add the message (if it still exists). if (message) { aAccount._motd.push(message); } // Oh, also some servers don't send a RPL_ENDOFMOTD (e.g. irc.ppy.sh), so if // we don't receive another MOTD message after 1 second, consider it to be // RPL_ENDOFMOTD. clearTimeout(aAccount._motdTimer); aAccount._motdTimer = setTimeout( ircBase.commands["376"].bind(aAccount), 1000, aMessage ); return true; } // See RFCs 2811 & 2812 (which obsoletes RFC 1459) for a description of these // commands. export var ircBase = { // Parameters name: "RFC 2812", // Name identifier priority: ircHandlerPriorities.DEFAULT_PRIORITY, isEnabled: () => true, // The IRC commands that can be handled. commands: { ERROR(aMessage) { // ERROR // Client connection has been terminated. if (!this.disconnecting) { // We received an ERROR message when we weren't expecting it, this is // probably the server giving us a ping timeout. this.WARN("Received unexpected ERROR response:\n" + aMessage.params[0]); this.gotDisconnected( Ci.prplIAccount.ERROR_NETWORK_ERROR, lazy._("connection.error.lost") ); } else { // We received an ERROR message when expecting it (i.e. we've sent a // QUIT command). Notify account manager. this.gotDisconnected(); } return true; }, INVITE(aMessage) { // INVITE let channel = aMessage.params[1]; this.addChatRequest( channel, () => { this.joinChat(this.getChatRoomDefaultFieldValues(channel)); }, request => { // Inform the user when an invitation was automatically ignored. if (!request) { // Otherwise just notify the user. this.getConversation(channel).writeMessage( aMessage.origin, lazy._("message.inviteReceived", aMessage.origin, channel), { system: true } ); } } ); return true; }, JOIN(aMessage) { // JOIN ( *( "," ) [ *( "," ) ] ) / "0" // Iterate over each channel. for (let channelName of aMessage.params[0].split(",")) { let conversation = this.getConversation(channelName); // Check whether we joined the channel or if someone else did. if ( this.normalize(aMessage.origin, this.userPrefixes) == this.normalize(this._nickname) ) { // If we join, clear the participants list to avoid errors with // repeated participants. conversation.removeAllParticipants(); conversation.left = false; conversation.joining = false; // Update the channel name if it has improper capitalization. if (channelName != conversation.name) { conversation._name = channelName; conversation.notifyObservers(null, "update-conv-title"); } // If the user parted from this room earlier, confirm the rejoin. if (conversation._rejoined) { conversation.writeMessage( aMessage.origin, lazy._("message.rejoined"), { system: true, } ); delete conversation._rejoined; } // 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.origin, true); let msg = lazy._("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.origin); if (buddy) { buddy.setStatus(Ci.imIStatusInfo.STATUS_AVAILABLE, ""); } return true; }, KICK(aMessage) { // KICK *( "," ) *( "," ) [] let comment = aMessage.params.length == 3 ? aMessage.params[2] : null; // Some servers (moznet) send the kicker as the comment. if (comment == aMessage.origin) { comment = null; } return leftRoom( this, aMessage.params[1].split(","), aMessage.params[0].split(","), aMessage.origin, comment, true ); }, MODE(aMessage) { // MODE *( ( "+" / "-") *( "i" / "w" / "o" / "O" / "r" ) ) // MODE *( ( "-" / "+" ) * * ) 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.origin ); return true; } // Otherwise the user's own mode is being returned to them. return this.setUserMode( aMessage.params[0], aMessage.params[1], aMessage.origin, !this._userModeReceived ); }, NICK(aMessage) { // NICK this.changeBuddyNick(aMessage.origin, aMessage.params[0]); return true; }, NOTICE(aMessage) { // NOTICE // If the message is from the server, don't show it unless the user wants // to see it. if (!this.connected || aMessage.origin == this._currentServerName) { return serverMessage(this, aMessage); } return displayMessage(this, aMessage, { notification: true }); }, PART(aMessage) { // PART *( "," ) [ ] return leftRoom( this, [aMessage.origin], aMessage.params[0].split(","), aMessage.source, aMessage.params.length == 2 ? aMessage.params[1] : null ); }, PING(aMessage) { // PING [ ] // Keep the connection alive. this.sendMessage("PONG", aMessage.params[0]); return true; }, PONG(aMessage) { // PONG [ ] let pongTime = aMessage.params[1]; // Ping to keep the connection alive. if (pongTime.startsWith("_")) { this._socket.cancelDisconnectTimer(); return true; } // Otherwise, the ping was from a user command. return this.handlePingReply(aMessage.origin, pongTime); }, PRIVMSG(aMessage) { // PRIVMSG // Display message in conversation return displayMessage(this, aMessage); }, QUIT(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 nick = aMessage.origin; let msg = lazy._( "message.quit", nick, quitMsg.length ? lazy._("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(nick)) { conversation.writeMessage(nick, msg, { system: true }); conversation.removeParticipant(nick); } }); // Remove from the whois table. this.removeBuddyInfo(nick); // If the leaver is a buddy, mark as offline. let buddy = this.buddies.get(nick); if (buddy) { buddy.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, ""); } // If we wanted this nickname, grab it. if (nick == this._requestedNickname && nick != this._nickname) { this.changeNick(this._requestedNickname); clearTimeout(this._nickInUseTimeout); delete this._nickInUseTimeout; } return true; }, SQUIT(aMessage) { // return true; }, TOPIC(aMessage) { // TOPIC [ ] // Show topic as a message. 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) : "", aMessage.origin ); return true; }, "001": function (aMessage) { // RPL_WELCOME // Welcome to the Internet Relay Network !@ this._socket.resetPingTimer(); // 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 autoUserMode is set in the account preferences. If it is set, // then notify the server that the user wants a specific mode. if (this.prefs.prefHasUserValue("autoUserMode")) { this.sendMessage("MODE", [ this._nickname, this.getString("autoUserMode"), ]); } // Check if our nick has changed. if (aMessage.params[0] != this._nickname) { this.changeBuddyNick(this._nickname, aMessage.params[0]); } // Request our own whois entry so we can set the prefix. this.requestCurrentWhois(this._nickname); // If our status is Unavailable, tell the server. if ( this.imAccount.statusInfo.statusType < Ci.imIStatusInfo.STATUS_AVAILABLE ) { this.observe(null, "status-changed"); } // Check if any of our buddies are online! const kInitialIsOnDelay = 1000; this._isOnTimer = setTimeout(this.sendIsOn.bind(this), kInitialIsOnDelay); // If we didn't handle all the CAPs we added, something is wrong. if (this._requestedCAPs.size) { this.ERROR( "Connected without removing CAPs: " + [...this._requestedCAPs] ); } // Done! this.reportConnected(); return serverMessage(this, aMessage); }, "002": function (aMessage) { // RPL_YOURHOST // Your host is , running version return serverMessage(this, aMessage); }, "003": function (aMessage) { // RPL_CREATED // This server was created // TODO parse this date and keep it for some reason? Do we care? return serverMessage(this, aMessage); }, "004": function (aMessage) { // RPL_MYINFO // // TODO parse the available modes, let the UI respond and inform the user return serverMessage(this, aMessage); }, "005": function (aMessage) { // RPL_BOUNCE // Try server , port return serverMessage(this, aMessage); }, /* * Handle response to TRACE message */ 200(aMessage) { // RPL_TRACELINK // Link // V // return serverMessage(this, aMessage); }, 201(aMessage) { // RPL_TRACECONNECTING // Try. return serverMessage(this, aMessage); }, 202(aMessage) { // RPL_TRACEHANDSHAKE // H.S. return serverMessage(this, aMessage); }, 203(aMessage) { // RPL_TRACEUNKNOWN // ???? [] return serverMessage(this, aMessage); }, 204(aMessage) { // RPL_TRACEOPERATOR // Oper return serverMessage(this, aMessage); }, 205(aMessage) { // RPL_TRACEUSER // User return serverMessage(this, aMessage); }, 206(aMessage) { // RPL_TRACESERVER // Serv S C @ // V return serverMessage(this, aMessage); }, 207(aMessage) { // RPL_TRACESERVICE // Service return serverMessage(this, aMessage); }, 208(aMessage) { // RPL_TRACENEWTYPE // 0 return serverMessage(this, aMessage); }, 209(aMessage) { // RPL_TRACECLASS // Class return serverMessage(this, aMessage); }, 210(aMessage) { // RPL_TRACERECONNECTION // Unused. return serverMessage(this, aMessage); }, /* * Handle stats messages. **/ 211(aMessage) { // RPL_STATSLINKINFO // //