From e9b4146acbcd04ea329c1e4e403eb13822074476 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 27 Feb 2012 15:19:30 +0100 Subject: [PATCH] Bug 953944 - Implement IRC in JavaScript, r=florian. --- chat/Makefile.in | 1 + chat/chat-prefs.js | 3 + chat/components/src/logger.js | 14 +- chat/locales/en-US/irc.properties | 148 ++ chat/locales/jar.mn | 1 + chat/makefiles.sh | 1 + chat/modules/imXPCOMUtils.jsm | 3 +- chat/modules/jsProtoHelper.jsm | 2 + chat/modules/socket.jsm | 26 +- chat/protocols/irc/Makefile.in | 64 + chat/protocols/irc/icons/prpl-irc-32.png | Bin 0 -> 695 bytes chat/protocols/irc/icons/prpl-irc-48.png | Bin 0 -> 1003 bytes chat/protocols/irc/icons/prpl-irc.png | Bin 0 -> 454 bytes chat/protocols/irc/irc.js | 917 +++++++++++++ chat/protocols/irc/irc.manifest | 3 + chat/protocols/irc/ircBase.jsm | 1203 +++++++++++++++++ chat/protocols/irc/ircCTCP.jsm | 278 ++++ chat/protocols/irc/ircCommands.jsm | 315 +++++ chat/protocols/irc/ircDCC.jsm | 100 ++ chat/protocols/irc/ircHandlers.jsm | 153 +++ chat/protocols/irc/ircISUPPORT.jsm | 255 ++++ chat/protocols/irc/ircUtils.jsm | 207 +++ chat/protocols/irc/jar.mn | 5 + chat/protocols/irc/test/test_ctcpColoring.js | 70 + .../protocols/irc/test/test_ctcpFormatting.js | 57 + chat/protocols/irc/test/xpcshell.ini | 6 + im/app/profile/all-instantbird.js | 2 + im/content/conversation.xml | 19 +- im/installer/package-manifest.in | 4 +- im/test/xpcshell.ini | 1 + 30 files changed, 3835 insertions(+), 23 deletions(-) create mode 100644 chat/locales/en-US/irc.properties create mode 100644 chat/protocols/irc/Makefile.in create mode 100644 chat/protocols/irc/icons/prpl-irc-32.png create mode 100644 chat/protocols/irc/icons/prpl-irc-48.png create mode 100644 chat/protocols/irc/icons/prpl-irc.png create mode 100644 chat/protocols/irc/irc.js create mode 100644 chat/protocols/irc/irc.manifest create mode 100644 chat/protocols/irc/ircBase.jsm create mode 100644 chat/protocols/irc/ircCTCP.jsm create mode 100644 chat/protocols/irc/ircCommands.jsm create mode 100644 chat/protocols/irc/ircDCC.jsm create mode 100644 chat/protocols/irc/ircHandlers.jsm create mode 100644 chat/protocols/irc/ircISUPPORT.jsm create mode 100644 chat/protocols/irc/ircUtils.jsm create mode 100644 chat/protocols/irc/jar.mn create mode 100644 chat/protocols/irc/test/test_ctcpColoring.js create mode 100644 chat/protocols/irc/test/test_ctcpFormatting.js create mode 100644 chat/protocols/irc/test/xpcshell.ini diff --git a/chat/Makefile.in b/chat/Makefile.in index 307a072f89..3c6ce7dbe2 100644 --- a/chat/Makefile.in +++ b/chat/Makefile.in @@ -44,6 +44,7 @@ include $(DEPTH)/config/autoconf.mk PROTOCOLS = \ facebook \ gtalk \ + irc \ twitter \ xmpp \ $(NULL) diff --git a/chat/chat-prefs.js b/chat/chat-prefs.js index 12b5ee59fe..86199702fe 100644 --- a/chat/chat-prefs.js +++ b/chat/chat-prefs.js @@ -62,6 +62,9 @@ pref("messenger.status.defaultIdleAwayMessage", "chrome://chat/locale/status.pro pref("messenger.status.userIconFileName", ""); pref("messenger.status.userDisplayName", ""); +// Default message used when quitting IRC. This is overridable per account. +pref("chat.irc.defaultQuitMessage", ""); + // loglevel is the minimum severity level that a libpurple message // must have to be reported in the Error Console. // diff --git a/chat/components/src/logger.js b/chat/components/src/logger.js index ba16559560..5b54a11d02 100644 --- a/chat/components/src/logger.js +++ b/chat/components/src/logger.js @@ -102,7 +102,10 @@ ConversationLog.prototype = { format: "txt", _init: function cl_init() { let file = getLogFolderForAccount(this._conv.account, true); - file.append(this._conv.normalizedName); + let name = this._conv.normalizedName; + if (this._conv.isChat && this._conv.account.protocol.id != "prpl-twitter") + name += ".chat"; + file.append(name); if (!file.exists()) file.create(Ci.nsIFile.DIRECTORY_TYPE, 0777); if (Services.prefs.getCharPref("purple.logging.format") == "json") @@ -443,8 +446,13 @@ Logger.prototype = { }, getLogsForAccountBuddy: function logger_getLogsForAccountBuddy(aAccountBuddy) this._enumerateLogs(aAccountBuddy.account, aAccountBuddy.normalizedName), - getLogsForConversation: function logger_getLogsForConversation(aConversation) - this._enumerateLogs(aConversation.account, aConversation.normalizedName), + getLogsForConversation: function logger_getLogsForConversation(aConversation) { + let name = aConversation.normalizedName; + if (aConversation.isChat && + aConversation.account.protocol.id != "prpl-twitter") + name += ".chat"; + return this._enumerateLogs(aConversation.account, name); + }, getSystemLogsForAccount: function logger_getSystemLogsForAccount(aAccount) this._enumerateLogs(aAccount, ".system"), diff --git a/chat/locales/en-US/irc.properties b/chat/locales/en-US/irc.properties new file mode 100644 index 0000000000..54eebe0601 --- /dev/null +++ b/chat/locales/en-US/irc.properties @@ -0,0 +1,148 @@ +# LOCALIZATION NOTE (connection.*) +# These will be displayed in the account manager in order to show the progress +# of the connection. +# (These will be displayed in account.connection.progress from +# accounts.properties, which adds … at the end, so do not include +# periods at the end of these messages.) +connection.quitting=Sending the QUIT message +# LOCALIZATION NOTE (connection.error.*) +# These will show in the account manager if an error occurs during the +# connection attempt. +connection.error.lost=Lost connection with server +connection.error.timeOut=Connection timed out +connection.error.certError=Certification error when connecting to server + +# LOCALIZATION NOTE +# These show up on the join chat menu. An underscore is for the access key. +joinChat.channel=_Channel +joinChat.password=_Password + +# LOCALIZATION NOTE +# These are the protocol specific options shown in the account manager and +# 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 + +# LOCALIZATION NOTE: +# %1$S is the nickname of the user who was pinged. +# %2$S is the delay (in seconds). +ctcp.ping=Ping reply from %1$S in %2$S seconds. +# %1$S is the nickname of the user whose version was requested. +# %2$S is the version response from the client. +ctcp.version=%1$S is using "%2$S" +# %1$S is the nickname of the user whose time was requested. +# %2$S is the time response. +ctcp.time=The time for %1$S is %2$S. + +# LOCALZIATION NOTE (command.*): +# These are the help messages for each command, the %S is the command name +# Each command first gives the parameter it accepts and then a description of +# the command. +command.action=%S <action to perform>: Perform an action. +command.ctcp=%S <nick> <msg>: Sends a CTCP message to the nick. +command.chanserv=%S <command>: Send a command to ChanServ. +command.deop=%S <nick1>[,<nick2>]*: Remove channel operator status from someone. You must be a channel operator to do this. +command.devoice=%S <nick1>[,<nick2>]*: Remove channel voice status from someone, preventing them from speaking if the channel is moderated (+m). You must be a channel operator to do this. +command.invite=%S <nick> [<room>]: Invite someone to join you in the specified channel, or the current channel. +command.join=%S <room1>[,<room2>]* [<key1>[,<key2>]*]: Enter one or more channels, optionally providing a channel key for each if needed. +command.kick=%S <nick> [<message>]: Remove someone from a channel. You must be a channel operator to do this. +command.list=%S: Display a list of chat rooms on the network. Warning, some servers may disconnect you upon doing this. +command.memoserv=%S <command>: Send a command to MemoServ. +command.mode=%S (<nick>|<channel>) (+|-)<new mode>: Set or unset a channel or user mode. +command.msg=%S <nick> <message>: Send a private message to a user (as opposed to a channel). +command.nick=%S <new nickname>: Change your nickname. +command.nickserv=%S <command>: Send a command to NickServ. +command.notice=%S <target> <message>: Send a notice to a user or channel. +command.op=%S <nick1>[,<nick2>]*: Grant channel operator status to someone. You must be a channel operator to do this. +command.operserv=%S <command>: Send a command to OperServ. +command.part=%S [message]: Leave the current channel with an optional message. +command.ping=%S [<nick>]: Asks how much lag a user (or the server if no user specified) has. +command.quit=%S <message>: Disconnect from the server, with an optional message. +command.quote=%S <command>: Send a raw command to the server. +command.time=%S: Displays the current local time at the IRC server. +command.topic=%S [<new topic>]: View or change the channel topic. +command.umode=%S (+|-)<new mode>: Set or unset a user mode. +command.version=%S <nick>: Request the version of a user's client. +command.voice=%S <nick1>[,<nick2>]*: Grant channel voice status to someone. You must be a channel operator to do this. +command.wallops=%S <message>: If you don't know what this is, you probably can't use it (sends a command to all connected with the +w flag and all operators on the server. +command.whowas=%S <nick>: Get information on a user that has logged off. + +# LOCALIZATION NOTE (message.*): +# These are shown as system messages in the conversation. +# %1$S is the nick and %2$S is the nick and host of the user who joined. +message.join=%1$S [%2$S] entered the room. +# %1$S is the nick of who kicked you. +# %2$S is message.kicked.reason, if a kick message was given. +message.kicked.you=You have been kicked by %1$S%2$S. +# %1$S is the nick that is kicked, %2$S the nick of the person who kicked +# %1$S. %3$S is message.kicked.reason, if a kick message was given. +message.kicked=%1$S has been kicked by %2$S%3$S. +# %S is the kick message +message.kicked.reason=: %S +# %1$S is the nickname of the user whose mode was changed, %2$S is the new +# mode and %3$S is who set the mode. +message.mode=mode (%1$S %2$S) by %3$S. +# %1$S is the old nick and %2$S is the new nick. +message.nick=%1$S is now known as %2$S. +# %S is your new nick. +message.nick.you=You are now known as %S. +# The paramter is the message.parted.reason, if a part message is given. +message.parted.you=You have left the room (Part%1$S). +# %1$S is the user's nick, %2$S is message.parted.reason, if a part message is given. +message.parted=%1$S has left the room (Part%2$S). +# %S is the part message supplied by the user. +message.parted.reason=: %S +# %1$S is the user's nick, %2$S is message.quit2 if a quit message is given. +message.quit=%1$S has left the room (Quit%2$S). +# The paramter is the quit message given by the user. +message.quit2=: %S +# %1$S is the user who changed the topic, %2$S is the new topic. +message.topicChanged=%1$S has changed the topic to: %2$S. +# %1$S is the user who cleared the topic. +message.topicCleared=%1$S has cleared the topic. +# %1$S is the conversation name, %2$S is the topic. +message.topic=The topic for %1$S is: %2$S. +# %S is the conversation name. +message.topicRemoved=The topic for %S was removed. +# %1$S is the nickname of the invited user, %2$S is the conversation name +# they were invited to. +message.invited=%1$S was successfully invited to %2$S. +# %S is the nickname of the user who was summoned. +message.summoned=%S was summoned. + +# LOCALIZATION NOTE (error.*): +# These are shown as error messages in the conversation. +# %S is the channel name. +error.noChannel=There is no channel: %S. +error.tooManyChannels=Cannot join %S; you've joined too many channels. +# %1$S is your new nick, %2$S is the kill message from the server. +error.nickCollision=Nick already in use, changing nick to %1$S [%2$S]. +error.banned=You are banned from this server. +error.bannedSoon=You will soon be banned from this server. +error.mode.wrongUser=You cannot change modes for other users. + +# LOCALIZATION NOTE (tooltip.*): +# These are the descriptions given in a tooltip with information received +# from a whois response. +# The username is set by the user's IRC client, usually to the client's name +# but the user can change it. +tooltip.user=Username +# The host name that the user connects from (usually based on the +# reverse DNS of the user's IP, but often mangled by the server to +# protect users). +tooltip.host=Host name +# The real name is a description of the user (including spaces). +tooltip.realname=Real name +# The away message of the user +tooltip.away=Away +tooltip.ircOp=IRC Operator +tooltip.channels=Currently on +tooltip.server=Server +# %1$S is the server name, %2$S is the server location. +tooltip.serverValue=%1$S (%2$S) +tooltip.idleTime=Idle for diff --git a/chat/locales/jar.mn b/chat/locales/jar.mn index 9c36ba4452..3eaad77be8 100644 --- a/chat/locales/jar.mn +++ b/chat/locales/jar.mn @@ -6,6 +6,7 @@ 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/status.properties (%status.properties) locale/@AB_CD@/chat/twitter.properties (%twitter.properties) locale/@AB_CD@/chat/xmpp.properties (%xmpp.properties) diff --git a/chat/makefiles.sh b/chat/makefiles.sh index e00d75270a..da72063529 100644 --- a/chat/makefiles.sh +++ b/chat/makefiles.sh @@ -43,6 +43,7 @@ chat/locales/Makefile chat/modules/Makefile chat/protocols/facebook/Makefile chat/protocols/gtalk/Makefile +chat/protocols/irc/Makefile chat/protocols/jsTest/Makefile chat/protocols/twitter/Makefile chat/protocols/xmpp/Makefile diff --git a/chat/modules/imXPCOMUtils.jsm b/chat/modules/imXPCOMUtils.jsm index 7cb83c2268..e64b5aaf36 100644 --- a/chat/modules/imXPCOMUtils.jsm +++ b/chat/modules/imXPCOMUtils.jsm @@ -114,7 +114,8 @@ function setTimeout(aFunction, aDelay) } function clearTimeout(aTimer) { - aTimer.cancel(); + if (aTimer) + aTimer.cancel(); } function executeSoon(aFunction) diff --git a/chat/modules/jsProtoHelper.jsm b/chat/modules/jsProtoHelper.jsm index 22a0e59e02..1662a21faa 100644 --- a/chat/modules/jsProtoHelper.jsm +++ b/chat/modules/jsProtoHelper.jsm @@ -101,6 +101,7 @@ const ForwardAccountPrototype = { const GenericAccountPrototype = { __proto__: ClassInfo("prplIAccount", "generic account object"), + get wrappedJSObject() this, _init: function _init(aProtocol, aImAccount) { this.protocol = aProtocol; this.imAccount = aImAccount; @@ -411,6 +412,7 @@ Message.prototype = GenericMessagePrototype; const GenericConversationPrototype = { __proto__: ClassInfo("prplIConversation", "generic conversation object"), flags: Ci.nsIClassInfo.DOM_OBJECT, + get wrappedJSObject() this, _init: function(aAccount, aName) { this._account = aAccount; diff --git a/chat/modules/socket.jsm b/chat/modules/socket.jsm index 7b390d5866..9e73486e49 100644 --- a/chat/modules/socket.jsm +++ b/chat/modules/socket.jsm @@ -132,10 +132,6 @@ const Socket = { // Set this for the segment size of outgoing binary streams. outputSegmentSize: 0, - // Use this to specify a URI scheme to the hostname when resolving the proxy, - // this may be unnecessary for some protocols. - uriScheme: "http://", - // Flags used by nsIProxyService when resolving a proxy. proxyFlags: Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY, @@ -170,9 +166,10 @@ const Socket = { // Add a URI scheme since, by default, some protocols (i.e. IRC) don't // have a URI scheme before the host. - let uri = Services.io.newURI(this.uriScheme + this.host, null, null); + let uri = Services.io.newURI("http://" + this.host, null, null); this._proxyCancel = proxyService.asyncResolve(uri, this.proxyFlags, this); } catch(e) { + Cu.reportError(e); // We had some error getting the proxy service, just don't use one. this._createTransport(null); } @@ -229,9 +226,10 @@ const Socket = { this.serverSocket.close(); }, - // Send data on the output stream. - sendData: function(/* string */ aData) { - this.log("Sending:\n" + aData + "\n"); + // Send data on the output stream. Provide aLoggedData to log something + // different than what is actually sent. + sendData: function(/* string */ aData, aLoggedData) { + this.log("Sending:\n" + (aLoggedData || aData)); try { this._outputStream.write(aData + this.delimiter, @@ -241,13 +239,15 @@ const Socket = { } }, - sendString: function(aString, aEncoding) { - this.log("Sending:\n" + aString + "\n"); + // Send a string to the output stream after converting the encoding. Provide + // aLoggedData to log something different than what is actually sent. + sendString: function(aString, aEncoding, aLoggedData) { + this.log("Sending:\n" + (aLoggedData || aString)); let converter = new ScriptableUnicodeConverter(); converter.charset = aEncoding || "UTF-8"; try { - let stream = converter.convertToInputStream(aString); + let stream = converter.convertToInputStream(aString + this.delimiter); this._outputStream.writeFrom(stream, stream.available()); } catch(e) { Cu.reportError(e); @@ -290,6 +290,10 @@ const Socket = { * nsIProtocolProxyCallback methods */ onProxyAvailable: function(aRequest, aURI, aProxyInfo, aStatus) { + if (aProxyInfo) { + this.log("using " + aProxyInfo.type + " proxy: " + + aProxyInfo.host + ":" + aProxyInfo.port); + } this._createTransport(aProxyInfo); delete this._proxyCancel; }, diff --git a/chat/protocols/irc/Makefile.in b/chat/protocols/irc/Makefile.in new file mode 100644 index 0000000000..99b9b1cccd --- /dev/null +++ b/chat/protocols/irc/Makefile.in @@ -0,0 +1,64 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Instantbird. +# +# The Initial Developer of the Original Code is +# Patrick Cloke . +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +EXTRA_COMPONENTS = \ + irc.js \ + irc.manifest \ + $(NULL) + +EXTRA_JS_MODULES = \ + ircBase.jsm \ + ircCTCP.jsm \ + ircDCC.jsm \ + ircCommands.jsm \ + ircHandlers.jsm \ + ircISUPPORT.jsm \ + ircUtils.jsm \ + $(NULL) + +ifdef ENABLE_TESTS +relativesrcdir = chat/protocols/irc +XPCSHELL_TESTS = test +endif + +include $(topsrcdir)/config/rules.mk diff --git a/chat/protocols/irc/icons/prpl-irc-32.png b/chat/protocols/irc/icons/prpl-irc-32.png new file mode 100644 index 0000000000000000000000000000000000000000..003103914c44cf260b957fc4f8928264212ca418 GIT binary patch literal 695 zcmV;o0!aOdP)_pC29NuqD`(60vZWiilCqbLM~yAup0MGimaDaP7n7mo5F!D-rF}b zznS^N;}J6>NE3l(-vj&)00-MT7>02_7K@z$IP#J2t7TcIrBdm$Zvaix-srmiOo;89 zC#8gGngYOc-vC5(D1^XdGT9nHRaFqtp?5EE4FnFbGy=mgjx|j)iRj)2pNF+t4N}Ub zO+`_VOePVJ$NdAuVzDz_*H8QktX{7plgao80B|dTO{deX{Q}p(9$*g;IKb9-!6p+z zAR3KctsTd~Y&JtA5&;oGN(pBEzB7Q)X!O%G&5v$!kxHeG)9G})*=$<9Uhl$joXhce ze7Z9LBI@RHxhIQtKA(R&91h>ww(VrI*~i!U>{J8AV)0$4(|I%)4BoFhvvU!a%jJ;} z;>EiEt0VX(rG%=gkW#|7Z77O@P$-0(-VY0b5z!s*HZ#u)g@Rn?w*+8W)+^IA52Tbw ziA3T-CX>+`jmD35yL};qU}h%wD6M|XeA(@G3jpqVLe7au0$>0RfH^bIy*bHS84(eH zFo1BSQu(AP3N;#ym(^dzn3%d@E8^Dh(Ix6wE3v*_6t`2Mxmpc;hEncuU#n=e& dFD&rO=r;q`=+rK*U1b0O002ovPDHLkV1f@IEHwZC literal 0 HcmV?d00001 diff --git a/chat/protocols/irc/icons/prpl-irc-48.png b/chat/protocols/irc/icons/prpl-irc-48.png new file mode 100644 index 0000000000000000000000000000000000000000..606425fabb7aa1d5e9e490ad5d74bc55c4c94262 GIT binary patch literal 1003 zcmVM5 zo9u$QTeMi(k2LaXy!)N^-Fa_Dv!u1gFvF4#;B83_Bu#1{X;K48W5g-8w6ye@VHn>k zrN-jaWsJQRLVQ^+mw&kVU#tcU!+1VFKR;&McAT>CeSfT4tv(0v?*GP&b0DRZW7{^G z&1Rgk$mjD=N;zTw4Sfxy29hQ%)EOh6&tr0O z5~HJ|ZxCuYp$4+q?02)Xvrh|!LTm#8!1?((Ha9mhJv|+pW~c!vGn2I}3q*vqwKdUhw-o?z&Rar=ufntvNg*la z6Dj2r&+}~0^K7NmJB>!;lhxJL=IQCFmQup^{ogs~1 z#l^)>2f6=I%|I`eO6A8v?t8Qb1|>9w7-PN=!nQ2yx-SR<1VI4PH2c=*`yhk>5&1+E zmAXo6eXmngsE|^sw6d~N8Xq4I0Kha&V`^$DXBY;w*4WzG;>~7L5)lBP_qTi9g^0iy z(*SUCa$<7MzXp(th({8U2GHA-)>=jxpr|hhb8~Z!VHocTA=t>s$ctjJIC^<`iS_mM zzw7mS(eu1twAKUwjIr*9L}7j1>$9`7SG&8re?$cXppC1|O#@IpO#+Z%MnY$Rh%x}` z0R9A!SzcZa4h|0F&dyHr`1ttqTCMimEkWEx)Z0fV_{@#KMW-J`gqtR!a*=u(lTwQ9 z?d@Op_V&Km-`{@?;LVEQZm6RzI!)thp;0~Y^buvCF9&_!|E5-}ecxy_x^Lv6K36CJ z0s7NN-H4};$QtORQMa+47Fkd+Vc;G$5NY80hASfs|4m_6{2}PRq_tY{BBgkX!%&002ovPDHLkV1o1+&{Y5c literal 0 HcmV?d00001 diff --git a/chat/protocols/irc/icons/prpl-irc.png b/chat/protocols/irc/icons/prpl-irc.png new file mode 100644 index 0000000000000000000000000000000000000000..19d578deda123c41b441aef78f147fdf74bd426d GIT binary patch literal 454 zcmV;%0XhDOP)MLv|g`;Qfl4_kf!O& zVzGEC%kl=mZ5+qnoo+#E9Ti1!2Ve=n039s=K$0Y%0Pgp-1#DAx90$w{B7$Wda`%BX z$x=#vOsCU_!C=5q6v;f#O_pVSBI3sE!>;M1& literal 0 HcmV?d00001 diff --git a/chat/protocols/irc/irc.js b/chat/protocols/irc/irc.js new file mode 100644 index 0000000000..3d5c7eb4d1 --- /dev/null +++ b/chat/protocols/irc/irc.js @@ -0,0 +1,917 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Instantbird. + * + * The Initial Developer of the Original Code is + * Patrick Cloke . + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/ircUtils.jsm"); +Cu.import("resource:///modules/ircHandlers.jsm"); +Cu.import("resource:///modules/jsProtoHelper.jsm"); +Cu.import("resource:///modules/socket.jsm"); + +// Parses a raw IRC message into an object (see section 2.3 of RFC 2812). +function ircMessage(aData) { + LOG(aData); + let message = {rawMessage: aData}; + let temp; + + // Splits the raw string into four parts (the second is required), the command + // is required. A raw string looks like: + // [":" " "] [" " ]* [":" ] + // : :( | [["!" ] "@" ]) + // : /[^ ]+/ + // : /[^ ]+/ + // : /.+/ + // See http://joshualuckers.nl/2010/01/10/regular-expression-to-match-raw-irc-messages/ + if ((temp = aData.match(/^(?::([^ ]+) )?([^ ]+)(?: ((?:[^: ][^ ]* ?)*))?(?: ?:(.*))?$/))) { + // Assume message is from the server if not specified + message.source = temp[1] || this._server; + message.command = temp[2]; + // Space separated parameters + message.params = temp[3] ? temp[3].trim().split(/ +/) : []; + if (temp[4]) // Last parameter can contain spaces + 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 (message.source && + (temp = message.source.match(/^([^ !@\.:]+)(?:!([^ @]+))?(?:@([^ ]+))?$/))) { + message.nickname = temp[1]; + message.user = temp[2] || null; // Optional + message.host = temp[3] || null; // Optional + } + } + + return message; +} + +function ircChannel(aAccount, aName, aNick) { + this._init(aAccount, aName, aNick); +} +ircChannel.prototype = { + __proto__: GenericConvChatPrototype, + sendMsg: function(aMessage) { + this._account.sendMessage("PRIVMSG", [this.name, aMessage]); + + // Since we don't receive a message back from the server, just assume it + // was received and write it. An IRC bouncer will send us our message back + // though, try to handle that. + if (this.hasParticipant(this._account._nickname)) + this.writeMessage(this.nick, aMessage, {outgoing: true}); + }, + // Overwrite the writeMessage function to apply CTCP formatting before + // display. + writeMessage: function(aWho, aText, aProperties) { + GenericConvChatPrototype.writeMessage.call(this, aWho, + ctcpFormatToHTML(aText), + aProperties); + }, + + // Section 3.2.2 of RFC 2812. + part: function(aMessage) { + let params = [this.name]; + + // If a valid message was given, use it as the part message. + // Otherwise, fall back to the default part message, if it exists. + let msg = aMessage || this._account.getString("partmsg"); + if (msg) + params.push(msg); + + this._account.sendMessage("PART", params); + }, + + unInit: function() { + // Tell the server about the part if connected. + if (this._account.connected) + this.part(); + + // Always remove the conversation. + this._account.removeConversation(this.name); + + GenericConvChatPrototype.unInit.call(this); + }, + + getNormalizedChatBuddyName: function(aNick) + this._account.normalize(aNick, this._account.userPrefixes), + + hasParticipant: function(aNick) + hasOwnProperty(this._participants, this.getNormalizedChatBuddyName(aNick)), + + getParticipant: function(aNick, aNotifyObservers) { + let normalizedNick = this.getNormalizedChatBuddyName(aNick); + if (this.hasParticipant(aNick)) + return this._participants[normalizedNick]; + + let participant = new ircParticipant(aNick, this._account); + this._participants[normalizedNick] = participant; + if (aNotifyObservers) { + this.notifyObservers(new nsSimpleEnumerator([participant]), + "chat-buddy-add"); + } + return participant; + }, + updateNick: function(aOldNick, aNewNick) { + if (!this.hasParticipant(aOldNick)) { + ERROR("Trying to rename nick that doesn't exist! " + aOldNick + " to " + + aNewNick); + return; + } + + // Get the original ircParticipant and then remove it. + let participant = this.getParticipant(aOldNick); + this.removeParticipant(aOldNick); + + // Update the nickname and add it under the new nick. + participant._name = aNewNick; + this._participants[this.getNormalizedChatBuddyName(aNewNick)] = participant; + + this.notifyObservers(participant, "chat-buddy-update", aOldNick); + }, + removeParticipant: function(aNick, aNotifyObservers) { + if (!this.hasParticipant(aNick)) + return; + + if (aNotifyObservers) { + let stringNickname = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + stringNickname.data = aNick; + this.notifyObservers(new nsSimpleEnumerator([stringNickname]), + "chat-buddy-remove"); + } + delete this._participants[this.getNormalizedChatBuddyName(aNick)]; + }, + // Use this before joining to avoid errors of trying to re-add an existing + // participant + removeAllParticipants: function() { + let stringNicknames = []; + for (let nickname in this._participants) { + let stringNickname = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + stringNickname.data = this._participants[nickname].name; + stringNicknames.push(stringNickname); + } + this.notifyObservers(new nsSimpleEnumerator(stringNicknames), + "chat-buddy-remove"); + this._participants = {}; + }, + + _left: false, + get left() this._left, + set left(aLeft) { + this._left = aLeft; + + // If we've left, notify observers. + if (this._left) + this.notifyObservers(null, "update-conv-chatleft"); + }, + get topic() this._topic, // can't add a setter without redefining the getter + set topic(aTopic) { + this._account.sendMessage("TOPIC", [this.name, aTopic]); + }, + get topicSettable() true, + + get normalizedName() this._account.normalize(this.name), +}; + +function ircParticipant(aName, aAccount) { + this._name = aName; + this._account = aAccount; + this._modes = []; + + if (this._name[0] in this._account.userPrefixToModeMap) { + this._modes.push(this._account.userPrefixToModeMap[this._name[0]]); + this._name = this._name.slice(1); + } +} +ircParticipant.prototype = { + __proto__: GenericConvChatBuddyPrototype, + + // This takes a mode change string and reflects it into the + // prplIConvChatBuddy, a mode change string is of the form: + // ("+" | "-")[]* + // e.g. +iaw or -i + setMode: function(aNewMode) { + // Are we going to add or remove the modes? + if (aNewMode[0] != "+" && aNewMode[0] != "-") { + WARN("Invalid mode string: " + aNewMode); + return; + } + let addNewMode = aNewMode[0] == "+"; + + // Check each mode being added and update the user + for (let i = 1; i < aNewMode.length; i++) { + let index = this._modes.indexOf(aNewMode[i]); + // If the mode is in the list of modes and we want to remove it. + if (index != -1 && !addNewMode) + this._modes.splice(index, 1); + // If the mode is not in the list of modes and we want to add it. + else if (index == -1 && addNewMode) + this._modes.push(aNewMode[i]); + } + }, + + get voiced() this._modes.indexOf("v") != -1, + get halfOp() this._modes.indexOf("h") != -1, + get op() this._modes.indexOf("o") != -1, + get founder() this._modes.indexOf("n") != -1, + get typing() false +}; + +function ircConversation(aAccount, aName) { + this._init(aAccount, aName); +} +ircConversation.prototype = { + __proto__: GenericConvIMPrototype, + sendMsg: function(aMessage) { + this._account.sendMessage("PRIVMSG", [this.name, aMessage]); + + // Since the server doesn't send us a message back, just assume the message + // was received and immediately show it. + this.writeMessage(this._account._nickname, aMessage, {outgoing: true}); + }, + + // Overwrite the writeMessage function to apply CTCP formatting before + // display. + writeMessage: function(aWho, aText, aProperties) { + GenericConvIMPrototype.writeMessage.call(this, aWho, + ctcpFormatToHTML(aText), + aProperties); + }, + + get normalizedName() this._account.normalize(this.name), + + unInit: function() { + this._account.removeConversation(this.name); + GenericConvIMPrototype.unInit.call(this); + }, + + updateNick: function(aNewNick) { + this._name = aNewNick; + this.notifyObservers(null, "update-conv-title"); + } +}; + +function ircSocket(aAccount) { + this._account = aAccount; + this._initCharsetConverter(); +} +ircSocket.prototype = { + __proto__: Socket, + delimiter: "\r\n", + connectTimeout: 60, // Failure to connect after 1 minute + readWriteTimeout: 300, // Failure when no data for 5 minutes + _converter: null, + + _initCharsetConverter: function() { + this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + try { + this._converter.charset = this._account._encoding; + } catch (e) { + delete this._converter; + ERROR("Failed to set character set to: " + this._account._encoding + " for " + + this._account.name + "."); + } + }, + + // Implement Section 5 of RFC 2812. + onDataReceived: function(aRawMessage) { + DEBUG(aRawMessage); + if (this._converter) { + try { + aRawMessage = this._converter.ConvertToUnicode(aRawMessage); + } catch (e) { + WARN("This message doesn't seem to be " + this._account._encoding + + " encoded: " + aRawMessage); + // Unfortunately, if the unicode converter failed once, + // it will keep failing so we need to reinitialize it. + this._initCharsetConverter(); + } + } + + // If nothing handled the message, throw an error. + if (!ircHandlers.handleMessage(this._account, new ircMessage(aRawMessage))) + WARN("Unhandled IRC message: " + aRawMessage); + }, + onConnection: function() { + this._account._connectionRegistration.call(this._account); + }, + + // Throw errors if the socket has issues. + onConnectionClosed: function () { + if (!this._account.imAccount || this._account.disconnecting || + this._account.disconnected) + return; + + ERROR("Connection closed by server."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + }, + onConnectionReset: function () { + ERROR("Connection reset."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + }, + onConnectionTimedOut: function() { + ERROR("Connection timed out."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.timeOut")); + }, + onCertificationError: function(aSocketInfo, aStatus, aTargetSite) { + ERROR("Certification error."); + this._account.gotDisconnected(Ci.prplIAccount.ERROR_CERT_OTHER_ERROR, + _("connection.error.certError")); + }, + log: LOG +}; + +function ircAccountBuddy(aAccount, aBuddy, aTag, aUserName) { + this._init(aAccount, aBuddy, aTag, aUserName); +} +ircAccountBuddy.prototype = { + __proto__: GenericAccountBuddyPrototype, + + // Returns a list of imITooltipInfo objects to be displayed when the user + // hovers over the buddy. + getTooltipInfo: function() this._account.getBuddyInfo(this.normalizedName), + + get normalizedName() this._account.normalize(this.userName), + + // Can not send messages to buddies who appear offline. + get canSendMessage() this.account.connected, + + // Called when the user wants to chat with the buddy. + createConversation: function() { +ERROR(this.userName); + return this._account.createConversation(this.userName); + } +}; + +function ircAccount(aProtocol, aImAccount) { + this._buddies = {}; + this._init(aProtocol, aImAccount); + this._conversations = {}; + + // Split the account name into usable parts. + let splitter = aImAccount.name.lastIndexOf("@"); + this._nickname = aImAccount.name.slice(0, splitter); + this._server = aImAccount.name.slice(splitter + 1); + + // For more information, see where these are defined in the prototype below. + this._isOnQueue = []; + this.pendingIsOnQueue = []; + this.whoisInformation = {}; +} +ircAccount.prototype = { + __proto__: GenericAccountPrototype, + _socket: null, + _MODE_WALLOPS: 1 << 2, // mode 'w' + _MODE_INVISIBLE: 1 << 3, // mode 'i' + get _mode() 0, + + get noNewlines() true, + + get normalizedName() this.normalize(this.name), + + // 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 + maxMessageLength: 512, // 2.3 Messages + maxHostnameLength: 63, // 2.3.1 Message format in Augmented BNF + + // The default prefixes. + userPrefixes: ["@", "!", "%", "+"], + // The default prefixes to modes. + userPrefixToModeMap: {"@": "o", "!": "n", "%": "h", "+": "v"}, + channelPrefixes: ["&", "#", "+", "!"], // 1.3 Channels + + // Handle Scandanavian lower case (optionally remove status indicators). + // See Section 2.2 of RFC 2812: the characters {}|^ are considered to be the + // lower case equivalents of the characters []\~, respectively. + normalize: function(aStr, aPrefixes) { + let str = aStr; + if (aPrefixes && aPrefixes.indexOf(aStr[0]) != -1) + str = str.slice(1); + + return str.replace(/[\x41-\x5E]/g, + function(c) String.fromCharCode(c.charCodeAt(0) + 0x20)); + }, + + isMUCName: function(aStr) { + return (this.channelPrefixes.indexOf(aStr[0]) != -1); + }, + + // When Instantbird changes status, tell the server. IRC is only away or not + // away; consider away, idle and unavailable to be away. This will also + // connect or disconnect if set to offline/available. + isAway: false, + observe: function(aSubject, aTopic, aData) { + if (aTopic != "status-changed") + return; + + this.updateStatus(); + }, + // Update the accounts status when Instantbird requests a status change and + // when we receive information from the server that the status has changed. + updateStatus: function() { + let {statusType: type, statusText: text} = this.imAccount.statusInfo; + LOG("New status received: " + type + "\r\n" + text); + + // Tell the server to mark us as away. + if (type < Ci.imIStatusInfo.STATUS_AVAILABLE && !this.isAway) { + // We have to have a string in order to set IRC as AWAY. + if (!text) { + // If no status is given, use the the default idle/away message. + const IDLE_PREF_BRANCH = "messenger.status."; + const IDLE_PREF = "defaultIdleAwayMessage"; + text = Services.prefs.getComplexValue(IDLE_PREF_BRANCH + IDLE_PREF, + Ci.nsIPrefLocalizedString).data; + + if (!text) { + // Get the default value of the localized preference. + text = Services.prefs.getDefaultBranch(IDLE_PREF_BRANCH) + .getComplexValue(IDLE_PREF, + Ci.nsIPrefLocalizedString).data; + } + // The last resort, fallback to a non-localized string. + if (!text) + text = "Away"; + } + this.sendMessage("AWAY", text); // Mark as away. + } + else if (type == Ci.imIStatusInfo.STATUS_AVAILABLE && this.isAway) + this.sendMessage("AWAY"); // Mark as back. + }, + + // The whois information: nicks are used as keys and refer to a map of field + // to value. + whoisInformation: {}, + // Request WHOIS information on a buddy when the user requests more + // information. + requestBuddyInfo: function(aBuddyName) { + this.sendMessage("WHOIS", aBuddyName); + }, + // Return an nsISimpleEnumerator of imITooltipInfo for a given nick. + getBuddyInfo: function(aNick) { + let nick = this.normalize(aNick); + if (!hasOwnProperty(this.whoisInformation, nick)) + return EmptyEnumerator; + + let whoisInformation = this.whoisInformation[nick]; + let tooltipInfo = []; + for (let field in whoisInformation) { + let value = whoisInformation[field]; + tooltipInfo.push(new TooltipInfo(_("tooltip." + field), value)); + } + + return new nsSimpleEnumerator(tooltipInfo); + }, + + addBuddy: function(aTag, aName) { + let buddy = new ircAccountBuddy(this, null, aTag, aName); + this._buddies[buddy.normalizedName] = buddy; + + // Put the username as the first to be checked on the next ISON call. + this._isOnQueue.unshift(buddy.userName); + + Services.contacts.accountBuddyAdded(buddy); + }, + // Loads a buddy from the local storage. Called for each buddy locally stored + // before connecting to the server. + loadBuddy: function(aBuddy, aTag) { + let buddy = new ircAccountBuddy(this, aBuddy, aTag); + this._buddies[buddy.normalizedName] = buddy; + // Put each buddy name into the ISON queue. + this._isOnQueue.push(buddy.userName); + return buddy; + }, + hasBuddy: function(aName) + hasOwnProperty(this._buddies, this.normalize(aName, this.userPrefixes)), + // Return an array of buddy names. + getBuddyNames: function() { + let buddies = []; + for each (let buddyName in Object.keys(this._buddies)) + buddies.push(this._buddies[buddyName].userName); + return buddies; + }, + getBuddy: function(aName) { + if (this.hasBuddy(aName)) + return this._buddies[this.normalize(aName, this.userPrefixes)]; + return null; + }, + changeBuddyNick: function(aOldNick, aNewNick) { + let msg; + // Your nickname changed! + if (this.normalize(aOldNick) == this.normalize(this._nickname)) { + this._nickname = aNewNick; + msg = _("message.nick.you", aNewNick); + } + else + msg = _("message.nick", aOldNick, aNewNick); + + for each (let conversation in this._conversations) { + if (conversation.isChat && conversation.hasParticipant(aOldNick)) { + // Update the nick in every chat conversation the user is in. + conversation.updateNick(aOldNick, aNewNick); + conversation.writeMessage(aOldNick, msg, {system: true}); + } + } + + // If a private conversation is open with that user, change its title. + if (this.hasConversation(aOldNick)) { + // Get the current conversation and rename it. + let conversation = this.getConversation(aOldNick); + + // Remove the old reference to the conversation and create a new one. + this.removeConversation(aOldNick); + this._conversations[this.normalize(aNewNick)] = conversation; + + conversation.updateNick(aNewNick); + conversation.writeMessage(aOldNick, msg, {system: 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) { + // Unicode characters with a code point > 127 are 2 bytes long. + // Unicode characters with a code point > 2047 are 3 bytes long. + return c < 128 ? 1 : c < 2048 ? 2 : 3; + } + let bytes = 0; + for (let i = 0; i < aStr.length; i++) + bytes += charCodeToByteCount(aStr.charCodeAt(i)); + return bytes; + }, + + // To check if users are online, we need to queue multiple messages. + // An internal queue of all nicks that we wish to know the status of. + _isOnQueue: [], + // The nicks that were last sent to the server that we're waiting for a + // response about. + pendingIsOnQueue: [], + // The time between sending isOn messages (milliseconds). + _isOnDelay: 60 * 1000, + _isOnTimer: null, + // The number of characters that are available to be filled with nicks for + // each ISON message. + _isOnLength: null, + // Generate and send an ISON message to poll for each nick's status. + sendIsOn: function() { + // If no buddies, just look again after the timeout. + if (this._isOnQueue.length) { + // Calculate the possible length of names we can send. + if (!this._isOnLength) { + let length = this.countBytes(this.buildMessage("ISON", " ")) + 2; + this._isOnLength = this.maxMessageLength - length + 1; + } + + // Add any previously pending queue to the end of the ISON queue. + if (this.pendingIsOnQueue) + this._isOnQueue = this._isOnQueue.concat(this.pendingIsOnQueue); + + // Always add the next nickname to the pending queue, this handles a silly + // case where the next nick is greater than or equal to the maximum + // message length. + this.pendingIsOnQueue = [this._isOnQueue.shift()]; + + // Attempt to maximize the characters used in each message, this may mean + // that a specific user gets sent very often since they have a short name! + let buddiesLength = this.countBytes(this.pendingIsOnQueue[0]); + for (let i = 0; i < this._isOnQueue.length; i++) { + // If we can fit the nick, add it to the current buffer. + if ((buddiesLength + this.countBytes(this._isOnQueue[i])) < this._isOnLength) { + // Remove the name from the list and add it to the pending queue. + let nick = this._isOnQueue.splice(i--, 1)[0]; + this.pendingIsOnQueue.push(nick); + + // Keep track of the length of the string, the + 1 is for the spaces. + buddiesLength += this.countBytes(nick) + 1; + + // If we've filled up the message, stop looking for more nicks. + if (buddiesLength >= this._isOnLength) + break; + } + } + + // Send the message. + this.sendMessage("ISON", this.pendingIsOnQueue.join(" ")); + } + + // Call this function again in _isOnDelay seconds. + // This makes the assumption that this._isOnDelay >> the response to ISON + // from the server. + this._isOnTimer = setTimeout(this.sendIsOn.bind(this), this._isOnDelay); + }, + + connect: function() { + this.reportConnecting(); + + // Load preferences. + this._port = this.getInt("port"); + this._ssl = this.getBool("ssl"); + // Use the display name as the user's real name. + this._realname = this.imAccount.statusInfo.displayName; + this._encoding = this.getString("encoding") || "UTF-8"; + this._showServerTab = this.getBool("showServerTab"); + + // Open the socket connection. + this._socket = new ircSocket(this); + this._socket.connect(this._server, this._port, this._ssl ? ["ssl"] : []); + }, + + // Used to wait for a response from the server. + _quitTimer: null, + // RFC 2812 Section 3.1.7. + quit: function(aMessage) { + this.sendMessage("QUIT", + aMessage || this.getString("quitmsg") || undefined); + }, + // When the user clicks "Disconnect" in account manager + disconnect: function() { + if (this.disconnected || this.disconnecting) + return; + + this.reportDisconnecting(Ci.prplIAccount.NO_ERROR, + _("connection.quitting")); + + // Let the server know we're going to disconnect. + this.quit(); + + // Give the server 2 seconds to respond, otherwise just forcefully + // disconnect the socket. This will be cancelled if a response is heard from + // the server. + this._quitTimer = setTimeout(this.gotDisconnected.bind(this), 2 * 1000); + }, + + createConversation: function(aName) this.getConversation(aName), + + // aComponents implements prplIChatRoomFieldValues. + joinChat: function(aComponents) { + let params = [aComponents.getValue("channel")]; + let password = aComponents.getValue("password"); + if (password) + params.push(password); + this.sendMessage("JOIN", params); + }, + + chatRoomFields: { + "channel": {"label": _("joinChat.channel"), "required": true}, + "password": {"label": _("joinChat.password"), "isPassword": true} + }, + + parseDefaultChatName: function(aDefaultName) ({"channel": aDefaultName}), + + // Attributes + get canJoinChat() true, + + hasConversation: function(aConversationName) + hasOwnProperty(this._conversations, this.normalize(aConversationName)), + + // Returns a conversation (creates it if it doesn't exist) + getConversation: function(aName) { + let name = this.normalize(aName); + if (!this.hasConversation(aName)) { + let constructor = this.isMUCName(aName) ? ircChannel : ircConversation; + this._conversations[name] = new constructor(this, aName, this._nickname); + } + return this._conversations[name]; + }, + + removeConversation: function(aConversationName) { + if (this.hasConversation(aConversationName)) + delete this._conversations[this.normalize(aConversationName)]; + }, + + // This builds the message string that will be sent to the server. + buildMessage: function(aCommand, aParams) { + if (!aCommand) { + ERROR("IRC messages must have a command."); + return null; + } + + // Ensure a command is only characters or numbers. + if (!/^[A-Z0-9]+$/i.test(aCommand)) { + ERROR("IRC command invalid: " + aCommand); + return null; + } + + let message = aCommand; + // If aParams is empty, then use an empty array. If aParams is not an array, + // consider it to be a single parameter and put it into an array. + let params = !aParams ? [] : Array.isArray(aParams) ? aParams : [aParams]; + if (params.length) { + if (params.slice(0, -1).some(function(p) p.indexOf(" ") != -1)) { + ERROR("IRC parameters cannot have spaces: " + params.slice(0, -1)); + return null; + } + // Join the parameters with spaces, except the last parameter which gets + // joined with a " :" before it (and can contain spaces). + message += " " + params.concat(":" + params.pop()).join(" "); + } + + return message; + }, + + // Shortcut method to build & send a message at once. Use aLoggedData to log + // something different than what is actually sent. + sendMessage: function(aCommand, aParams, aLoggedData) { + this.sendRawMessage(this.buildMessage(aCommand, aParams), aLoggedData); + }, + + // This sends a message over the socket and catches any errors. Use + // aLoggedData to log something different than what is actually sent. + sendRawMessage: function(aMessage, aLoggedData) { + // XXX This should escape any characters that can't be used in IRC (e.g. + // \001, \r\n). + + let length = this.countBytes(aMessage) + 2; + if (length > this.maxMessageLength) { + // Log if the message is too long, but try to send it anyway. + WARN("Message length too long (" + length + " > " + + this.maxMessageLength + "\n" + aMessage); + } + + try { + this._socket.sendString(aMessage, this._encoding, aLoggedData); + } catch (e) { + try { + WARN("Failed to convert " + aMessage + " from Unicode to " + + this._encoding + "."); + this._socket.sendData(aMessage, aLoggedData); + } catch(e) { + ERROR("Socket error: " + e); + this.gotDisconnected(Ci.prplIAccount.ERROR_NETWORK_ERROR, + _("connection.error.lost")); + } + } + }, + + // CTCP messages are \001 []*\001. + sendCTCPMessage: function(aCommand, aParams, aTarget, aIsNotice) { + // Combine the CTCP command and parameters into the single IRC param. + let ircParam = "\x01" + aCommand; + // If aParams is empty, then use an empty array. If aParams is not an array, + // consider it to be a single parameter and put it into an array. + let params = !aParams ? [] : Array.isArray(aParams) ? aParams : [aParams]; + if (params.length) + ircParam += " " + params.join(" "); + ircParam += "\x01"; + + // Send the IRC message as a NOTICE or PRIVMSG. + this.sendMessage(aIsNotice ? "NOTICE" : "PRIVMSG", [aTarget, ircParam]); + }, + + // Implement section 3.1 of RFC 2812 + _connectionRegistration: function() { + // Send the password message, if provided (section 3.1.1). + if (this.imAccount.password) { + this.sendMessage("PASS", this.imAccount.password, + "PASS "); + } + // Send the nick message (section 3.1.2). + this.sendMessage("NICK", this._nickname); + + // Send the user message (section 3.1.3). + // Use brandShortName as the username. + let username = + l10nHelper("chrome://branding/locale/brand.properties")("brandShortName"); + this.sendMessage("USER", [username, this._mode.toString(), "*", + this._realname || this._nickname]); + }, + + gotDisconnected: function(aError, aErrorMessage) { + if (!this.imAccount || this.disconnected) + return; + + if (aError === undefined) + aError = Ci.prplIAccount.NO_ERROR; + // If we are already disconnecting, this call to gotDisconnected + // is when the server acknowledges our disconnection. + // Otherwise it's because we lost the connection. + if (!this.disconnecting) + this.reportDisconnecting(aError, aErrorMessage); + this._socket.disconnect(); + delete this._socket; + + clearTimeout(this._isOnTimer); + delete this._isOnTimer; + + // Clean up each conversation: mark as left and remove participant. + for (let conversation in this._conversations) { + if (this.isMUCName(conversation)) { + // Remove the user's nick and mark the conversation as left as that's + // the final known state of the room. + this._conversations[conversation].removeParticipant(this._nickname, true); + this._conversations[conversation].left = true; + } + } + + this.reportDisconnected(); + }, + + unInit: function() { + delete this.imAccount; + // Disconnect if we're online while this gets called. + if (this._socket) + this._socket.disconnect(); + clearTimeout(this._isOnTimer); + clearTimeout(this._quitTimer); + } +}; + +function ircProtocol() { + // ircCommands.jsm exports one variable: commands. Import this directly into + // the protocol object. + Cu.import("resource:///modules/ircCommands.jsm", this); + this.registerCommands(); + + // Register the standard handlers + let tempScope = {}; + Cu.import("resource:///modules/ircBase.jsm", tempScope); + Cu.import("resource:///modules/ircISUPPORT.jsm", tempScope); + Cu.import("resource:///modules/ircCTCP.jsm", tempScope); + Cu.import("resource:///modules/ircDCC.jsm", tempScope); + + // Register default IRC handlers (IRC base, CTCP). + ircHandlers.registerHandler(tempScope.ircBase); + ircHandlers.registerHandler(tempScope.ircISUPPORT); + ircHandlers.registerHandler(tempScope.ircCTCP); + // Register default CTCP handlers (CTCP base, DCC). + ircHandlers.registerCTCPHandler(tempScope.ctcpBase); + ircHandlers.registerCTCPHandler(tempScope.ctcpDCC); +} +ircProtocol.prototype = { + __proto__: GenericProtocolPrototype, + get name() "IRC", + get iconBaseURI() "chrome://prpl-irc/skin/", + get baseId() "prpl-irc", + + usernameSplits: [ + {label: _("options.server"), separator: "@", + defaultValue: "chat.freenode.net", reverse: true} + ], + + options: { + // Default to IRC over SSL. + "port": {label: _("options.port"), default: 6667}, + "ssl": {label: _("options.ssl"), default: false}, + // XXX We should attempt to auto-detect encoding instead. + "encoding": {label: _("options.encoding"), default: "UTF-8"}, + "quitmsg": {label: _("options.quitMessage"), + get default() Services.prefs.getCharPref("chat.irc.defaultQuitMessage")}, + "partmsg": {label: _("options.partMessage"), default: ""}, + "showServerTab": {label: _("options.showServerTab"), default: false} + }, + + 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), + classID: Components.ID("{607b2c0b-9504-483f-ad62-41de09238aec}") +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([ircProtocol]); diff --git a/chat/protocols/irc/irc.manifest b/chat/protocols/irc/irc.manifest new file mode 100644 index 0000000000..69ca2fb1ba --- /dev/null +++ b/chat/protocols/irc/irc.manifest @@ -0,0 +1,3 @@ +component {607b2c0b-9504-483f-ad62-41de09238aec} irc.js +contract @mozilla.org/chat/irc;1 {607b2c0b-9504-483f-ad62-41de09238aec} +category im-protocol-plugin prpl-irc @mozilla.org/chat/irc;1 diff --git a/chat/protocols/irc/ircBase.jsm b/chat/protocols/irc/ircBase.jsm new file mode 100644 index 0000000000..8535a31411 --- /dev/null +++ b/chat/protocols/irc/ircBase.jsm @@ -0,0 +1,1203 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Instantbird. + * + * The Initial Developer of the Original Code is + * Patrick Cloke . + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * 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 + */ +const EXPORTED_SYMBOLS = ["ircBase"]; + +const {interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource:///modules/imXPCOMUtils.jsm"); +Cu.import("resource:///modules/imServices.jsm"); +Cu.import("resource:///modules/ircHandlers.jsm"); +Cu.import("resource:///modules/ircUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "DownloadUtils", function() { + Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); + return DownloadUtils; +}); + +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.source, + 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 = aKicked ? "kicked" : "parted"; + // If a part message was included, include it. + let reason = aReason ? _("message." + msgId + ".reason", aReason) : ""; + function __(aYou, aNick) { + // If the user is kicked, we need to say who kicked them. + if (aKicked) + return _("message." + msgId + aYou, aNick, aSource, reason); + return _("message." + msgId + aYou, aNick, reason); + } + + for each (let channelName in aChannels) { + for each (let nick in aNicks) { + if (!aAccount.hasConversation(channelName)) + continue; // Handle when we closed the window + let conversation = aAccount.getConversation(channelName); + + let msg; + if (aAccount.normalize(nick) == aAccount.normalize(aAccount._nickname)) { + msg = __(".you", reason); + // If the user left, mark the conversation as no longer being active. + conversation.left = true; + conversation.notifyObservers(conversation, "update-conv-chatleft"); + } + else + msg = __("", nick); + + conversation.writeMessage(aSource, msg, {system: true}); + conversation.removeParticipant(nick, true); + } + } + return true; +} + +function writeMessage(aAccount, aMessage, aString, aType) { + let type = {}; + type[aType] = true; + aAccount.getConversation(aMessage.source) + .writeMessage(aMessage.source, 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 errorMessage(aAccount, aMessage, aError) + writeMessage(aAccount, aMessage, aError, "error") + +function setWhoIs(aAccount, aMessage, aFields) { + let buddyName = aAccount.normalize(aMessage.params[1], aAccount.userPrefixes); + // If the buddy isn't in the list yet, add it. + if (!hasOwnProperty(aAccount.whoisInformation, buddyName)) + aAccount.whoisInformation[buddyName] = {}; + + // Set the WHOIS fields. + for (let field in aFields) + aAccount.whoisInformation[buddyName][field] = aFields[field]; + + return true; +} + +// Try a new nick if the previous tried nick is already in use. +function tryNewNick(aAccount, aMessage) { + // Take the returned nick and increment the last character. + aAccount._nickname = aMessage.params[1].slice(0, -1) + + String.fromCharCode( + aMessage.params[1].charCodeAt(aMessage.params[1].length - 1) + 1 + ); + // Inform the user. + LOG(aMessage.params[1] + " is already in use, trying " + aAccount._nickname); + + aAccount.sendMessage("NICK", aAccount._nickname); // 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, + + // The IRC commands that can be handled. + commands: { + "ERROR": function(aMessage) { + // ERROR + // Client connection has been terminated. + clearTimeout(this._quitTimer); + this.gotDisconnected(Ci.prplIAccount.NO_ERROR, + aMessage.params[0]); // Notify account manager. + return true; + }, + "INVITE": function(aMessage) { + // INVITE + // XXX prompt user to join channel + return false; + }, + "JOIN": function(aMessage) { + // JOIN ( *( "," ) [ *( "," ) ] ) / "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) == + this.normalize(this._nickname)) { + // If you join, clear the participants list to avoid errors w/ + // repeated participants. + conversation.removeAllParticipants(); + conversation.left = false; + conversation.notifyObservers(conversation, "update-conv-chatleft"); + } + else { + // Don't worry about adding ourself, RPL_NAMES 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}); + } + } + return true; + }, + "KICK": function(aMessage) { + // KICK *( "," ) *( "," ) [] + return leftRoom(this, aMessage.params[1].split(","), + aMessage.params[0].split(","), aMessage.nickname, + aMessage.params.length == 3 ? aMessage.params[2] : null, + true); + }, + "MODE": function(aMessage) { + // MODE *( ( "+" / "-") *( "i" / "w" / "o" / "O" / "r" ) ) + // If less than 3 parameter is given, the mode is your usermode. + if (aMessage.params.length >= 3) { + // Update the mode of the ConvChatBuddy. + let conversation = this.getConversation(aMessage.params[0]); + let convChatBuddy = conversation.getParticipant(aMessage.params[2]); + convChatBuddy.setMode(aMessage.params[1]); + + // Notify the UI of changes. + let msg = _("message.mode", aMessage.params[1], aMessage.params[2], + aMessage.nickname); + conversation.writeMessage(aMessage.nickname, msg, {system: true}); + conversation.notifyObservers(convChatBuddy, "chat-buddy-update"); + } + return true; + }, + "NICK": function(aMessage) { + // NICK + this.changeBuddyNick(aMessage.nickname, aMessage.params[0]); + return true; + }, + "NOTICE": function(aMessage) { + // NOTICE + // 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")) + return serverMessage(this, aMessage); + return privmsg(this, aMessage, true); + }, + "PART": function(aMessage) { + // PART *( "," ) [ ] + return leftRoom(this, [aMessage.nickname], aMessage.params[0].split(","), + aMessage.source, + aMessage.params.length == 2 ? aMessage.params[1] : null); + }, + "PING": function(aMessage) { + // PING ] + // Keep the connection alive + this.sendMessage("PONG", aMessage.params[0]); + return true; + }, + "PRIVMSG": function(aMessage) { + // PRIVMSG + // Display message in conversation + return privmsg(this, aMessage); + }, + "QUIT": function(aMessage) { + // QUIT [ < Quit Message> ] + // If a quit message was included, show it. + let msg = _("message.quit", aMessage.nickname, + aMessage.params.length ? + _("message.quit2", aMessage.params[0]) : ""); + // Loop over every conversation with the user and display that they quit. + for each (let conversation in this._conversations) { + if (conversation.isChat && + conversation.hasParticipant(aMessage.nickname)) { + conversation.writeMessage(aMessage.source, msg, {system: true}); + conversation.removeParticipant(aMessage.nickname, true); + } + } + return true; + }, + "SQUIT": function(aMessage) { + // XXX do we need this? + return false; + }, + "TOPIC": function(aMessage) { + // TOPIC [ ] + // Show topic as a message. + let source = aMessage.nickname || aMessage.source; + let conversation = this.getConversation(aMessage.params[0]); + let topic = aMessage.params[1]; + let message; + if (topic) + message = _("message.topicChanged", source, topic); + else + message = _("message.topicCleared", source); + conversation.writeMessage(source, message, {system: true}); + // Set the topic in the conversation and update the UI. + conversation.setTopic(topic ? ctcpFormatToText(topic) : "", source); + return true; + }, + "001": function(aMessage) { // RPL_WELCOME + // Welcome to the Internet Relay Network !@ + this.reportConnected(); + // Check if any of our buddies are online! + this.sendIsOn(); + return serverMessage(this, aMessage); + }, + "002": function(aMessage) { // RPL_YOURHOST + // Your host is , running version + // XXX Use the host instead of the user for all the "server" messages? + return serverMessage(this, aMessage); + }, + "003": function(aMessage) { // RPL_CREATED + //This server was created + // XXX parse this date and keep it for some reason? Do we care? + return serverMessage(this, aMessage); + }, + "004": function(aMessage) { // RPL_MYINFO + // + // XXX 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": function(aMessage) { // RPL_TRACELINK + // Link + // V + // + return serverMessage(this, aMessage); + }, + "201": function(aMessage) { // RPL_TRACECONNECTING + // Try. + return serverMessage(this, aMessage); + }, + "202": function(aMessage) { // RPL_TRACEHANDSHAKE + // H.S. + return serverMessage(this, aMessage); + }, + "203": function(aMessage) { // RPL_TRACEUNKNOWN + // ???? [] + return serverMessage(this, aMessage); + }, + "204": function(aMessage) { // RPL_TRACEOPERATOR + // Oper + return serverMessage(this, aMessage); + }, + "205": function(aMessage) { // RPL_TRACEUSER + // User + return serverMessage(this, aMessage); + }, + "206": function(aMessage) { // RPL_TRACESERVER + // Serv S C @ + // V + return serverMessage(this, aMessage); + }, + "207": function(aMessage) { // RPL_TRACESERVICE + // Service + return serverMessage(this, aMessage); + }, + "208": function(aMessage) { // RPL_TRACENEWTYPE + // 0 + return serverMessage(this, aMessage); + }, + "209": function(aMessage) { // RPL_TRACECLASS + // Class + return serverMessage(this, aMessage); + }, + "210": function(aMessage) { // RPL_TRACERECONNECTION + // Unused. + return serverMessage(this, aMessage); + }, + + /* + * Handle stats messages. + **/ + "211": function(aMessage) { // RPL_STATSLINKINFO + // + //