eric6/Network/IRC/IrcWidget.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7096
6ce0678583e5
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Network/IRC/IrcWidget.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,1122 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the IRC window.
+"""
+
+from __future__ import unicode_literals
+try:
+    str = unicode
+except NameError:
+    pass
+
+import re
+import logging
+
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QByteArray, QTimer, \
+    QDateTime
+from PyQt5.QtWidgets import QWidget, QToolButton, QLabel, QTabWidget
+from PyQt5.QtNetwork import QTcpSocket, QAbstractSocket
+try:
+    from PyQt5.QtNetwork import QSslSocket, QSslConfiguration
+    from E5Network.E5SslErrorHandler import E5SslErrorHandler
+    SSL_AVAILABLE = True
+except ImportError:
+    SSL_AVAILABLE = False
+
+from E5Gui import E5MessageBox
+
+from .Ui_IrcWidget import Ui_IrcWidget
+
+import Preferences
+import UI.PixmapCache
+
+from Globals import isMacPlatform
+
+from UI.Info import Version, Copyright
+
+
+class IrcWidget(QWidget, Ui_IrcWidget):
+    """
+    Class implementing the IRC window.
+    
+    @signal autoConnected() emitted after an automatic connection was initiated
+    """
+    autoConnected = pyqtSignal()
+    
+    ServerDisconnected = 1
+    ServerConnected = 2
+    ServerConnecting = 3
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        super(IrcWidget, self).__init__(parent)
+        self.setupUi(self)
+        
+        from .IrcNetworkManager import IrcNetworkManager
+        self.__ircNetworkManager = IrcNetworkManager(self)
+        
+        self.__leaveButton = QToolButton(self)
+        self.__leaveButton.setIcon(
+            UI.PixmapCache.getIcon("ircCloseChannel.png"))
+        self.__leaveButton.setToolTip(
+            self.tr("Press to leave the current channel"))
+        self.__leaveButton.clicked.connect(self.__leaveChannel)
+        self.__leaveButton.setEnabled(False)
+        self.channelsWidget.setCornerWidget(
+            self.__leaveButton, Qt.BottomRightCorner)
+        self.channelsWidget.setTabsClosable(False)
+        if not isMacPlatform():
+            self.channelsWidget.setTabPosition(QTabWidget.South)
+        
+        height = self.height()
+        self.splitter.setSizes([height * 0.6, height * 0.4])
+        
+        self.__channelList = []
+        self.__channelTypePrefixes = ""
+        self.__userName = ""
+        self.__identityName = ""
+        self.__quitMessage = ""
+        self.__nickIndex = -1
+        self.__nickName = ""
+        self.__server = None
+        self.__registering = False
+        
+        self.__connectionState = IrcWidget.ServerDisconnected
+        self.__sslErrorLock = False
+        
+        self.__buffer = ""
+        self.__userPrefix = {}
+        
+        self.__socket = None
+        if SSL_AVAILABLE:
+            self.__sslErrorHandler = E5SslErrorHandler(self)
+        else:
+            self.__sslErrorHandler = None
+        
+        self.__patterns = [
+            # :foo_!n=foo@foohost.bar.net PRIVMSG bar_ :some long message
+            (re.compile(r":([^!]+)!([^ ]+)\sPRIVMSG\s([^ ]+)\s:(.*)"),
+             self.__query),
+            # :foo.bar.net COMMAND some message
+            (re.compile(r""":([^ ]+)\s+([A-Z]+)\s+(.+)"""),
+             self.__handleNamedMessage),
+            # :foo.bar.net 123 * :info
+            (re.compile(r""":([^ ]+)\s+(\d{3})\s+(.+)"""),
+             self.__handleNumericMessage),
+            # PING :ping message
+            (re.compile(r"""PING\s+:(.*)"""), self.__ping),
+        ]
+        self.__prefixRe = re.compile(r""".*\sPREFIX=\((.*)\)([^ ]+).*""")
+        self.__chanTypesRe = re.compile(r""".*\sCHANTYPES=([^ ]+).*""")
+        
+        ircPic = UI.PixmapCache.getPixmap("irc128.png")
+        self.__emptyLabel = QLabel()
+        self.__emptyLabel.setPixmap(ircPic)
+        self.__emptyLabel.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
+        self.channelsWidget.addTab(self.__emptyLabel, "")
+        
+        # all initialized, do connections now
+        self.__ircNetworkManager.dataChanged.connect(self.__networkDataChanged)
+        self.networkWidget.initialize(self.__ircNetworkManager)
+        self.networkWidget.connectNetwork.connect(self.__connectNetwork)
+        self.networkWidget.editNetwork.connect(self.__editNetwork)
+        self.networkWidget.joinChannel.connect(self.joinChannel)
+        self.networkWidget.nickChanged.connect(self.__changeNick)
+        self.networkWidget.sendData.connect(self.__send)
+        self.networkWidget.away.connect(self.__away)
+        self.networkWidget.autoConnected.connect(self.autoConnected)
+    
+    def shutdown(self):
+        """
+        Public method to shut down the widget.
+        
+        @return flag indicating successful shutdown (boolean)
+        """
+        if self.__server:
+            if Preferences.getIrc("AskOnShutdown"):
+                ok = E5MessageBox.yesNo(
+                    self,
+                    self.tr("Disconnect from Server"),
+                    self.tr(
+                        """<p>Do you really want to disconnect from"""
+                        """ <b>{0}</b>?</p><p>All channels will be closed."""
+                        """</p>""").format(self.__server.getName()))
+            else:
+                ok = True
+            if ok:
+                self.__connectNetwork("", False, True)
+        else:
+            ok = True
+        
+        if ok:
+            self.__ircNetworkManager.close()
+        
+        return ok
+    
+    def autoConnect(self):
+        """
+        Public method to initiate the IRC auto connection.
+        """
+        self.networkWidget.autoConnect()
+
+    def __connectNetwork(self, name, connect, silent):
+        """
+        Private slot to connect to or disconnect from the given network.
+        
+        @param name name of the network to connect to (string)
+        @param connect flag indicating to connect (boolean)
+        @param silent flag indicating a silent connect/disconnect (boolean)
+        """
+        if connect:
+            network = self.__ircNetworkManager.getNetwork(name)
+            if network:
+                self.__server = network.getServer()
+                self.__identityName = network.getIdentityName()
+                identity = self.__ircNetworkManager.getIdentity(
+                    self.__identityName)
+                if identity:
+                    self.__userName = identity.getIdent()
+                    self.__quitMessage = identity.getQuitMessage()
+                    if self.__server:
+                        useSSL = self.__server.useSSL()
+                        if useSSL and not SSL_AVAILABLE:
+                            E5MessageBox.critical(
+                                self,
+                                self.tr("SSL Connection"),
+                                self.tr(
+                                    """An encrypted connection to the IRC"""
+                                    """ network was requested but SSL is not"""
+                                    """ available. Please change the server"""
+                                    """ configuration."""))
+                            return
+                        
+                        if useSSL:
+                            # create SSL socket
+                            self.__socket = QSslSocket(self)
+                            self.__socket.encrypted.connect(
+                                self.__hostConnected)
+                            self.__socket.sslErrors.connect(
+                                self.__sslErrors)
+                        else:
+                            # create TCP socket
+                            self.__socket = QTcpSocket(self)
+                            self.__socket.connected.connect(
+                                self.__hostConnected)
+                        self.__socket.hostFound.connect(
+                            self.__hostFound)
+                        self.__socket.disconnected.connect(
+                            self.__hostDisconnected)
+                        self.__socket.readyRead.connect(
+                            self.__readyRead)
+                        self.__socket.error.connect(
+                            self.__tcpError)
+                        
+                        self.__connectionState = IrcWidget.ServerConnecting
+                        if useSSL:
+                            self.networkWidget.addServerMessage(
+                                self.tr("Info"),
+                                self.tr("Looking for server {0} (port {1})"
+                                        " using an SSL encrypted connection"
+                                        "...").format(self.__server.getName(),
+                                                      self.__server.getPort()))
+                            self.__socket.connectToHostEncrypted(
+                                self.__server.getName(),
+                                self.__server.getPort()
+                            )
+                        else:
+                            self.networkWidget.addServerMessage(
+                                self.tr("Info"),
+                                self.tr(
+                                    "Looking for server {0} (port {1})...")
+                                .format(
+                                    self.__server.getName(),
+                                    self.__server.getPort()))
+                            self.__socket.connectToHost(
+                                self.__server.getName(),
+                                self.__server.getPort())
+        else:
+            if silent:
+                ok = True
+            else:
+                ok = E5MessageBox.yesNo(
+                    self,
+                    self.tr("Disconnect from Server"),
+                    self.tr("""<p>Do you really want to disconnect from"""
+                            """ <b>{0}</b>?</p><p>All channels will be"""
+                            """ closed.</p>""")
+                    .format(self.__server.getName()))
+            if ok:
+                if self.__server is not None:
+                    self.networkWidget.addServerMessage(
+                        self.tr("Info"),
+                        self.tr("Disconnecting from server {0}...").format(
+                            self.__server.getName()))
+                elif name:
+                    self.networkWidget.addServerMessage(
+                        self.tr("Info"),
+                        self.tr("Disconnecting from network {0}...").format(
+                            name))
+                else:
+                    self.networkWidget.addServerMessage(
+                        self.tr("Info"),
+                        self.tr("Disconnecting from server."))
+                self.__closeAllChannels()
+                self.__send("QUIT :" + self.__quitMessage)
+                if self.__socket:
+                    self.__socket.flush()
+                    self.__socket.close()
+                    if self.__socket:
+                        # socket is still existing
+                        self.__socket.deleteLater()
+                        self.__socket = None
+                self.__userName = ""
+                self.__identityName = ""
+                self.__quitMessage = ""
+    
+    def __editNetwork(self, name):
+        """
+        Private slot to edit the network configuration.
+        
+        @param name name of the network to edit (string)
+        """
+        from .IrcNetworkListDialog import IrcNetworkListDialog
+        dlg = IrcNetworkListDialog(self.__ircNetworkManager, self)
+        dlg.exec_()
+    
+    def __networkDataChanged(self):
+        """
+        Private slot handling changes of the network and identity definitions.
+        """
+        identity = self.__ircNetworkManager.getIdentity(self.__identityName)
+        if identity:
+            partMsg = identity.getPartMessage()
+            for channel in self.__channelList:
+                channel.setPartMessage(partMsg)
+    
+    def joinChannel(self, name, key=""):
+        """
+        Public slot to join a channel.
+        
+        @param name name of the channel (string)
+        @param key key of the channel (string)
+        """
+        # step 1: check, if this channel is already joined
+        for channel in self.__channelList:
+            if channel.name() == name:
+                return
+        
+        from .IrcChannelWidget import IrcChannelWidget
+        channel = IrcChannelWidget(self)
+        channel.setName(name)
+        channel.setUserName(self.__nickName)
+        identity = self.__ircNetworkManager.getIdentity(self.__identityName)
+        if identity:
+            channel.setPartMessage(identity.getPartMessage())
+        channel.setUserPrivilegePrefix(self.__userPrefix)
+        channel.initAutoWho()
+        
+        channel.sendData.connect(self.__send)
+        channel.sendCtcpRequest.connect(self.__sendCtcpRequest)
+        channel.sendCtcpReply.connect(self.__sendCtcpReply)
+        channel.channelClosed.connect(self.__closeChannel)
+        channel.openPrivateChat.connect(self.__openPrivate)
+        channel.awayCommand.connect(self.networkWidget.handleAwayCommand)
+        channel.leaveChannels.connect(self.__leaveChannels)
+        channel.leaveAllChannels.connect(self.__leaveAllChannels)
+        
+        self.channelsWidget.addTab(channel, name)
+        self.__channelList.append(channel)
+        self.channelsWidget.setCurrentWidget(channel)
+        
+        joinCommand = ["JOIN", name]
+        if key:
+            joinCommand.append(key)
+        self.__send(" ".join(joinCommand))
+        self.__send("MODE " + name)
+        
+        emptyIndex = self.channelsWidget.indexOf(self.__emptyLabel)
+        if emptyIndex > -1:
+            self.channelsWidget.removeTab(emptyIndex)
+            self.__leaveButton.setEnabled(True)
+        self.channelsWidget.setTabsClosable(True)
+    
+    def __query(self, match):
+        """
+        Private method to handle a new private connection.
+        
+        @param match reference to the match object
+        @return flag indicating, if the message was handled (boolean)
+        """
+        # group(1)   sender user name
+        # group(2)   sender user@host
+        # group(3)   target nick
+        # group(4)   message
+        if match.group(4).startswith("\x01"):
+            return self.__handleCtcp(match)
+        
+        self.__openPrivate(match.group(1))
+        # the above call sets the new channel as the current widget
+        channel = self.channelsWidget.currentWidget()
+        channel.addMessage(match.group(1), match.group(4))
+        channel.setPrivateInfo(
+            "{0} - {1}".format(match.group(1), match.group(2)))
+        
+        return True
+    
+    @pyqtSlot(str)
+    def __openPrivate(self, name):
+        """
+        Private slot to open a private chat with the given user.
+        
+        @param name name of the user (string)
+        """
+        from .IrcChannelWidget import IrcChannelWidget
+        channel = IrcChannelWidget(self)
+        channel.setName(self.__nickName)
+        channel.setUserName(self.__nickName)
+        identity = self.__ircNetworkManager.getIdentity(self.__identityName)
+        if identity:
+            channel.setPartMessage(identity.getPartMessage())
+        channel.setUserPrivilegePrefix(self.__userPrefix)
+        channel.setPrivate(True, name)
+        channel.addUsers([name, self.__nickName])
+        
+        channel.sendData.connect(self.__send)
+        channel.sendCtcpRequest.connect(self.__sendCtcpRequest)
+        channel.sendCtcpReply.connect(self.__sendCtcpReply)
+        channel.channelClosed.connect(self.__closeChannel)
+        channel.awayCommand.connect(self.networkWidget.handleAwayCommand)
+        channel.leaveChannels.connect(self.__leaveChannels)
+        channel.leaveAllChannels.connect(self.__leaveAllChannels)
+        
+        self.channelsWidget.addTab(channel, name)
+        self.__channelList.append(channel)
+        self.channelsWidget.setCurrentWidget(channel)
+    
+    @pyqtSlot()
+    def __leaveChannel(self):
+        """
+        Private slot to leave a channel and close the associated tab.
+        """
+        channel = self.channelsWidget.currentWidget()
+        channel.requestLeave()
+    
+    @pyqtSlot(list)
+    def __leaveChannels(self, channelNames):
+        """
+        Private slot to leave a list of channels and close their associated
+        tabs.
+        
+        @param channelNames list of channels to leave
+        @type list of str
+        """
+        for channelName in channelNames:
+            for channel in self.__channelList:
+                if channel.name() == channelName:
+                    channel.leaveChannel()
+    
+    @pyqtSlot()
+    def __leaveAllChannels(self):
+        """
+        Private slot to leave all channels and close their tabs.
+        """
+        while self.__channelList:
+            channel = self.__channelList[0]
+            channel.leaveChannel()
+    
+    def __closeAllChannels(self):
+        """
+        Private method to close all channels.
+        """
+        while self.__channelList:
+            channel = self.__channelList.pop()
+            self.channelsWidget.removeTab(self.channelsWidget.indexOf(channel))
+            channel.deleteLater()
+            channel = None
+        
+        self.channelsWidget.addTab(self.__emptyLabel, "")
+        self.__emptyLabel.show()
+        self.__leaveButton.setEnabled(False)
+        self.channelsWidget.setTabsClosable(False)
+    
+    def __closeChannel(self, name):
+        """
+        Private slot handling the closing of a channel.
+        
+        @param name name of the closed channel (string)
+        """
+        for channel in self.__channelList:
+            if channel.name() == name:
+                self.channelsWidget.removeTab(
+                    self.channelsWidget.indexOf(channel))
+                self.__channelList.remove(channel)
+                channel.deleteLater()
+        
+        if self.channelsWidget.count() == 0:
+            self.channelsWidget.addTab(self.__emptyLabel, "")
+            self.__emptyLabel.show()
+            self.__leaveButton.setEnabled(False)
+            self.channelsWidget.setTabsClosable(False)
+    
+    @pyqtSlot(int)
+    def on_channelsWidget_tabCloseRequested(self, index):
+        """
+        Private slot to close a channel by pressing the close button of
+        the channels widget.
+        
+        @param index index of the tab to be closed (integer)
+        """
+        channel = self.channelsWidget.widget(index)
+        channel.requestLeave()
+    
+    def __send(self, data):
+        """
+        Private slot to send data to the IRC server.
+        
+        @param data data to be sent (string)
+        """
+        if self.__socket:
+            self.__socket.write(
+                QByteArray("{0}\r\n".format(data).encode("utf-8")))
+    
+    def __sendCtcpRequest(self, receiver, request, arguments):
+        """
+        Private slot to send a CTCP request.
+        
+        @param receiver nick name of the receiver
+        @type str
+        @param request CTCP request to be sent
+        @type str
+        @param arguments arguments to be sent
+        @type str
+        """
+        request = request.upper()
+        if request == "PING":
+            arguments = "Eric IRC {0}".format(
+                QDateTime.currentMSecsSinceEpoch())
+            
+        self.__send("PRIVMSG {0} :\x01{1} {2}\x01".format(
+            receiver, request, arguments))
+    
+    def __sendCtcpReply(self, receiver, text):
+        """
+        Private slot to send a CTCP reply.
+        
+        @param receiver nick name of the receiver
+        @type str
+        @param text text to be sent
+        @type str
+        """
+        self.__send("NOTICE {0} :\x01{1}\x01".format(receiver, text))
+    
+    def __hostFound(self):
+        """
+        Private slot to indicate the host was found.
+        """
+        self.networkWidget.addServerMessage(
+            self.tr("Info"),
+            self.tr("Server found,connecting..."))
+    
+    def __hostConnected(self):
+        """
+        Private slot to log in to the server after the connection was
+        established.
+        """
+        self.networkWidget.addServerMessage(
+            self.tr("Info"),
+            self.tr("Connected,logging in..."))
+        self.networkWidget.setConnected(True)
+        
+        self.__registering = True
+        serverPassword = self.__server.getPassword()
+        if serverPassword:
+            self.__send("PASS " + serverPassword)
+        
+        identity = self.__ircNetworkManager.getIdentity(
+            self.__identityName)
+        nick = self.networkWidget.getNickname()
+        if not nick and identity:
+            self.__nickIndex = 0
+            try:
+                nick = identity.getNickNames()[self.__nickIndex]
+            except IndexError:
+                nick = ""
+        if not nick:
+            nick = self.__userName
+        self.__nickName = nick
+        self.networkWidget.setNickName(nick)
+        if identity:
+            realName = identity.getRealName()
+            if not realName:
+                realName = "eric IDE chat"
+            self.__send("NICK " + nick)
+            self.__send("USER " + self.__userName + " 0 * :" + realName)
+    
+    def __hostDisconnected(self):
+        """
+        Private slot to indicate the host was disconnected.
+        """
+        if self.networkWidget.isConnected():
+            self.__closeAllChannels()
+            self.networkWidget.addServerMessage(
+                self.tr("Info"),
+                self.tr("Server disconnected."))
+            self.networkWidget.setRegistered(False)
+            self.networkWidget.setConnected(False)
+            self.__server = None
+            self.__nickName = ""
+            self.__nickIndex = -1
+            self.__channelTypePrefixes = ""
+            
+            self.__socket.deleteLater()
+            self.__socket = None
+            
+            self.__connectionState = IrcWidget.ServerDisconnected
+            self.__sslErrorLock = False
+    
+    def __readyRead(self):
+        """
+        Private slot to read data from the socket.
+        """
+        if self.__socket:
+            self.__buffer += str(
+                self.__socket.readAll(),
+                Preferences.getSystem("IOEncoding"),
+                'replace')
+            if self.__buffer.endswith("\r\n"):
+                for line in self.__buffer.splitlines():
+                    line = line.strip()
+                    if line:
+                        logging.debug("<IRC> %s", line)
+                        handled = False
+                        # step 1: give channels a chance to handle the message
+                        for channel in self.__channelList:
+                            handled = channel.handleMessage(line)
+                            if handled:
+                                break
+                        else:
+                            # step 2: try to process the message ourselves
+                            for patternRe, patternFunc in self.__patterns:
+                                match = patternRe.match(line)
+                                if match is not None:
+                                    if patternFunc(match):
+                                        break
+                            else:
+                                # Oops, the message wasn't handled
+                                self.networkWidget.addErrorMessage(
+                                    self.tr("Message Error"),
+                                    self.tr(
+                                        "Unknown message received from server:"
+                                        "<br/>{0}").format(line))
+                
+                self.__updateUsersCount()
+                self.__buffer = ""
+    
+    def __handleCtcpReply(self, match):
+        """
+        Private method to handle a server message containing a CTCP reply.
+        
+        @param match reference to the match object
+        """
+        if "!" in match.group(1):
+            sender = match.group(1).split("!", 1)[0]
+            
+            try:
+                ctcpCommand = match.group(3).split(":", 1)[1]
+            except IndexError:
+                ctcpCommand = match.group(3)
+            ctcpCommand = ctcpCommand[1:].split("\x01", 1)[0]
+            if " " in ctcpCommand:
+                ctcpReply, ctcpArg = ctcpCommand.split(" ", 1)
+            else:
+                ctcpReply, ctcpArg = ctcpCommand, ""
+            ctcpReply = ctcpReply.upper()
+            
+            if ctcpReply == "PING" and ctcpArg.startswith("Eric IRC "):
+                # it is a response to a ping request
+                pingDateTime = int(ctcpArg.split()[-1])
+                latency = QDateTime.currentMSecsSinceEpoch() - pingDateTime
+                self.networkWidget.addServerMessage(
+                    self.tr("CTCP"),
+                    self.tr(
+                        "Received CTCP-PING response from {0} with latency"
+                        " of {1} ms.").format(sender, latency))
+            else:
+                self.networkWidget.addServerMessage(
+                    self.tr("CTCP"),
+                    self.tr(
+                        "Received unknown CTCP-{0} response from {1}.")
+                    .format(ctcpReply, sender))
+    
+    def __handleNamedMessage(self, match):
+        """
+        Private method to handle a server message containing a message name.
+        
+        @param match reference to the match object
+        @return flag indicating, if the message was handled (boolean)
+        """
+        name = match.group(2)
+        if name == "NOTICE":
+            try:
+                msg = match.group(3).split(":", 1)[1]
+            except IndexError:
+                msg = match.group(3)
+            
+            if msg.startswith("\x01"):
+                self.__handleCtcpReply(match)
+                return True
+            
+            if "!" in match.group(1):
+                name = match.group(1).split("!", 1)[0]
+                msg = "-{0}- {1}".format(name, msg)
+            self.networkWidget.addServerMessage(self.tr("Notice"), msg)
+            return True
+        elif name == "MODE":
+            self.__registering = False
+            if ":" in match.group(3):
+                # :foo MODE foo :+i
+                name, modes = match.group(3).split(" :")
+                sourceNick = match.group(1)
+                if not self.isChannelName(name):
+                    if name == self.__nickName:
+                        if sourceNick == self.__nickName:
+                            msg = self.tr(
+                                "You have set your personal modes to"
+                                " <b>[{0}]</b>.").format(modes)
+                        else:
+                            msg = self.tr(
+                                "{0} has changed your personal modes to"
+                                " <b>[{1}]</b>.").format(sourceNick, modes)
+                        self.networkWidget.addServerMessage(
+                            self.tr("Mode"), msg, filterMsg=False)
+                        return True
+        elif name == "PART":
+            nick = match.group(1).split("!", 1)[0]
+            if nick == self.__nickName:
+                channel = match.group(3).split(None, 1)[0]
+                self.networkWidget.addMessage(
+                    self.tr("You have left channel {0}.").format(channel))
+                return True
+        elif name == "QUIT":
+            # don't do anything with it here
+            return True
+        elif name == "NICK":
+            # :foo_!n=foo@foohost.bar.net NICK :newnick
+            oldNick = match.group(1).split("!", 1)[0]
+            newNick = match.group(3).split(":", 1)[1]
+            if oldNick == self.__nickName:
+                self.networkWidget.addMessage(
+                    self.tr("You are now known as {0}.").format(newNick))
+                self.__nickName = newNick
+                self.networkWidget.setNickName(newNick)
+            else:
+                self.networkWidget.addMessage(
+                    self.tr("User {0} is now known as {1}.").format(
+                        oldNick, newNick))
+            return True
+        elif name == "PONG":
+            nick = match.group(3).split(":", 1)[1]
+            self.networkWidget.addMessage(
+                self.tr("Received PONG from {0}").format(nick))
+            return True
+        elif name == "ERROR":
+            self.networkWidget.addErrorMessage(
+                self.tr("Server Error"), match.group(3).split(":", 1)[1])
+            return True
+        
+        return False
+    
+    def __handleNumericMessage(self, match):
+        """
+        Private method to handle a server message containing a numeric code.
+        
+        @param match reference to the match object
+        @return flag indicating, if the message was handled (boolean)
+        """
+        code = int(match.group(2))
+        if code < 400:
+            return self.__handleServerReply(
+                code, match.group(1), match.group(3))
+        else:
+            return self.__handleServerError(
+                code, match.group(1), match.group(3))
+    
+    def __handleServerError(self, code, server, message):
+        """
+        Private slot to handle a server error reply.
+        
+        @param code numerical code sent by the server (integer)
+        @param server name of the server (string)
+        @param message message sent by the server (string)
+        @return flag indicating, if the message was handled (boolean)
+        """
+        if code == 433:
+            if self.__registering:
+                self.__handleNickInUseLogin()
+            else:
+                self.__handleNickInUse()
+        else:
+            self.networkWidget.addServerMessage(self.tr("Error"), message)
+        
+        return True
+    
+    def __handleServerReply(self, code, server, message):
+        """
+        Private slot to handle a server reply.
+        
+        @param code numerical code sent by the server (integer)
+        @param server name of the server (string)
+        @param message message sent by the server (string)
+        @return flag indicating, if the message was handled (boolean)
+        """
+        # determine message type
+        if code in [1, 2, 3, 4]:
+            msgType = self.tr("Welcome")
+        elif code == 5:
+            msgType = self.tr("Support")
+        elif code in [250, 251, 252, 253, 254, 255, 265, 266]:
+            msgType = self.tr("User")
+        elif code in [372, 375, 376]:
+            msgType = self.tr("MOTD")
+        elif code in [305, 306]:
+            msgType = self.tr("Away")
+        else:
+            msgType = self.tr("Info ({0})").format(code)
+        
+        # special treatment for some messages
+        if code == 375:
+            message = self.tr("Message of the day")
+        elif code == 376:
+            message = self.tr("End of message of the day")
+        elif code == 4:
+            parts = message.strip().split()
+            message = self.tr(
+                "Server {0} (Version {1}), User-Modes: {2},"
+                " Channel-Modes: {3}")\
+                .format(parts[1], parts[2], parts[3], parts[4])
+        elif code == 265:
+            parts = message.strip().split()
+            message = self.tr(
+                "Current users on {0}: {1}, max. {2}").format(
+                server, parts[1], parts[2])
+        elif code == 266:
+            parts = message.strip().split()
+            message = self.tr(
+                "Current users on the network: {0}, max. {1}").format(
+                parts[1], parts[2])
+        elif code == 305:
+            message = self.tr("You are no longer marked as being away.")
+        elif code == 306:
+            message = self.tr("You have been marked as being away.")
+        else:
+            first, message = message.split(None, 1)
+            if message.startswith(":"):
+                message = message[1:]
+            else:
+                message = message.replace(":", "", 1)
+        
+        self.networkWidget.addServerMessage(msgType, message)
+        
+        if code == 1:
+            # register with services after the welcome message
+            self.__connectionState = IrcWidget.ServerConnected
+            self.__registerWithServices()
+            self.networkWidget.setRegistered(True)
+            QTimer.singleShot(1000, self.__autoJoinChannels)
+        elif code == 5:
+            # extract the user privilege prefixes
+            # ... PREFIX=(ov)@+ ...
+            m = self.__prefixRe.match(message)
+            if m:
+                self.__setUserPrivilegePrefix(m.group(1), m.group(2))
+            # extract the channel type prefixes
+            # ... CHANTYPES=# ...
+            m = self.__chanTypesRe.match(message)
+            if m:
+                self.__setChannelTypePrefixes(m.group(1))
+        
+        return True
+    
+    def __registerWithServices(self):
+        """
+        Private method to register to services.
+        """
+        identity = self.__ircNetworkManager.getIdentity(self.__identityName)
+        if identity:
+            service = identity.getServiceName()
+            password = identity.getPassword()
+            if service and password:
+                self.__send("PRIVMSG " + service + " :identify " + password)
+    
+    def __autoJoinChannels(self):
+        """
+        Private slot to join channels automatically once a server got
+        connected.
+        """
+        for channel in self.networkWidget.getNetworkChannels():
+            if channel.autoJoin():
+                name = channel.getName()
+                key = channel.getKey()
+                self.joinChannel(name, key)
+    
+    def __tcpError(self, error):
+        """
+        Private slot to handle errors reported by the TCP socket.
+        
+        @param error error code reported by the socket
+            (QAbstractSocket.SocketError)
+        """
+        if error == QAbstractSocket.RemoteHostClosedError:
+            # ignore this one, it's a disconnect
+            if self.__sslErrorLock:
+                self.networkWidget.addErrorMessage(
+                    self.tr("SSL Error"),
+                    self.tr(
+                        """Connection to server {0} (port {1}) lost while"""
+                        """ waiting for user response to an SSL error.""")
+                    .format(self.__server.getName(), self.__server.getPort()))
+                self.__connectionState = IrcWidget.ServerDisconnected
+        elif error == QAbstractSocket.HostNotFoundError:
+            self.networkWidget.addErrorMessage(
+                self.tr("Socket Error"),
+                self.tr(
+                    "The host was not found. Please check the host name"
+                    " and port settings."))
+        elif error == QAbstractSocket.ConnectionRefusedError:
+            self.networkWidget.addErrorMessage(
+                self.tr("Socket Error"),
+                self.tr(
+                    "The connection was refused by the peer. Please check the"
+                    " host name and port settings."))
+        elif error == QAbstractSocket.SslHandshakeFailedError:
+            self.networkWidget.addErrorMessage(
+                self.tr("Socket Error"),
+                self.tr("The SSL handshake failed."))
+        else:
+            if self.__socket:
+                self.networkWidget.addErrorMessage(
+                    self.tr("Socket Error"),
+                    self.tr(
+                        "The following network error occurred:<br/>{0}")
+                    .format(self.__socket.errorString()))
+            else:
+                self.networkWidget.addErrorMessage(
+                    self.tr("Socket Error"),
+                    self.tr("A network error occurred."))
+    
+    def __sslErrors(self, errors):
+        """
+        Private slot to handle SSL errors.
+        
+        @param errors list of SSL errors (list of QSslError)
+        """
+        ignored, defaultChanged = self.__sslErrorHandler.sslErrors(
+            errors, self.__server.getName(), self.__server.getPort())
+        if ignored == E5SslErrorHandler.NotIgnored:
+            self.networkWidget.addErrorMessage(
+                self.tr("SSL Error"),
+                self.tr(
+                    """Could not connect to {0} (port {1}) using an SSL"""
+                    """ encrypted connection. Either the server does not"""
+                    """ support SSL (did you use the correct port?) or"""
+                    """ you rejected the certificate.""")
+                .format(self.__server.getName(), self.__server.getPort()))
+            self.__socket.close()
+        else:
+            if defaultChanged:
+                self.__socket.setSslConfiguration(
+                    QSslConfiguration.defaultConfiguration())
+            if ignored == E5SslErrorHandler.UserIgnored:
+                self.networkWidget.addErrorMessage(
+                    self.tr("SSL Error"),
+                    self.tr(
+                        """The SSL certificate for the server {0} (port {1})"""
+                        """ failed the authenticity check. SSL errors"""
+                        """ were accepted by you.""")
+                    .format(self.__server.getName(), self.__server.getPort()))
+            if self.__connectionState == IrcWidget.ServerConnecting:
+                self.__socket.ignoreSslErrors()
+    
+    def __setUserPrivilegePrefix(self, prefix1, prefix2):
+        """
+        Private method to set the user privilege prefix.
+        
+        @param prefix1 first part of the prefix (string)
+        @param prefix2 indictors the first part gets mapped to (string)
+        """
+        # PREFIX=(ov)@+
+        # o = @ -> @ircbot , channel operator
+        # v = + -> +userName , voice operator
+        for i in range(len(prefix1)):
+            self.__userPrefix["+" + prefix1[i]] = prefix2[i]
+            self.__userPrefix["-" + prefix1[i]] = ""
+    
+    def __ping(self, match):
+        """
+        Private method to handle a PING message.
+        
+        @param match reference to the match object
+        @return flag indicating, if the message was handled (boolean)
+        """
+        self.__send("PONG " + match.group(1))
+        return True
+    
+    def __handleCtcp(self, match):
+        """
+        Private method to handle a CTCP command.
+        
+        @param match reference to the match object
+        @return flag indicating, if the message was handled (boolean)
+        """
+        # group(1)   sender user name
+        # group(2)   sender user@host
+        # group(3)   target nick
+        # group(4)   message
+        if match.group(4).startswith("\x01"):
+            ctcpCommand = match.group(4)[1:].split("\x01", 1)[0]
+            if " " in ctcpCommand:
+                ctcpRequest, ctcpArg = ctcpCommand.split(" ", 1)
+            else:
+                ctcpRequest, ctcpArg = ctcpCommand, ""
+            ctcpRequest = ctcpRequest.lower()
+            if ctcpRequest == "version":
+                if Version.startswith("@@"):
+                    vers = ""
+                else:
+                    vers = " " + Version
+                msg = "Eric IRC client{0}, {1}".format(vers, Copyright)
+                self.networkWidget.addServerMessage(
+                    self.tr("CTCP"),
+                    self.tr("Received Version request from {0}.").format(
+                        match.group(1)))
+                self.__sendCtcpReply(match.group(1), "VERSION " + msg)
+            elif ctcpRequest == "ping":
+                self.networkWidget.addServerMessage(
+                    self.tr("CTCP"),
+                    self.tr(
+                        "Received CTCP-PING request from {0},"
+                        " sending answer.").format(match.group(1)))
+                self.__sendCtcpReply(
+                    match.group(1), "PING {0}".format(ctcpArg))
+            elif ctcpRequest == "clientinfo":
+                self.networkWidget.addServerMessage(
+                    self.tr("CTCP"),
+                    self.tr(
+                        "Received CTCP-CLIENTINFO request from {0},"
+                        " sending answer.").format(match.group(1)))
+                self.__sendCtcpReply(
+                    match.group(1),
+                    "CLIENTINFO CLIENTINFO PING VERSION")
+            else:
+                self.networkWidget.addServerMessage(
+                    self.tr("CTCP"),
+                    self.tr(
+                        "Received unknown CTCP-{0} request from {1}.")
+                    .format(ctcpRequest, match.group(1)))
+            return True
+        
+        return False
+    
+    def __updateUsersCount(self):
+        """
+        Private method to update the users count on the channel tabs.
+        """
+        for channel in self.__channelList:
+            index = self.channelsWidget.indexOf(channel)
+            self.channelsWidget.setTabText(
+                index,
+                self.tr("{0} ({1})", "channel name, users count").format(
+                    channel.name(), channel.getUsersCount()))
+    
+    def __handleNickInUseLogin(self):
+        """
+        Private method to handle a 443 server error at login.
+        """
+        self.__nickIndex += 1
+        try:
+            identity = self.__ircNetworkManager.getIdentity(
+                self.__identityName)
+            if identity:
+                nick = identity.getNickNames()[self.__nickIndex]
+                self.__nickName = nick
+            else:
+                self.__connectNetwork("", False, True)
+                self.__nickName = ""
+                self.__nickIndex = -1
+                return
+        except IndexError:
+            self.networkWidget.addServerMessage(
+                self.tr("Critical"),
+                self.tr(
+                    "No nickname acceptable to the server configured"
+                    " for <b>{0}</b>. Disconnecting...")
+                .format(self.__userName),
+                filterMsg=False)
+            self.__connectNetwork("", False, True)
+            self.__nickName = ""
+            self.__nickIndex = -1
+            return
+        
+        self.networkWidget.setNickName(nick)
+        self.__send("NICK " + nick)
+    
+    def __handleNickInUse(self):
+        """
+        Private method to handle a 443 server error.
+        """
+        self.networkWidget.addServerMessage(
+            self.tr("Critical"),
+            self.tr("The given nickname is already in use."))
+    
+    def __changeNick(self, nick):
+        """
+        Private slot to use a new nick name.
+        
+        @param nick nick name to use (str)
+        """
+        if nick and nick != self.__nickName:
+            self.__send("NICK " + nick)
+    
+    def __setChannelTypePrefixes(self, prefixes):
+        """
+        Private method to set the channel type prefixes.
+        
+        @param prefixes channel prefix characters (string)
+        """
+        self.__channelTypePrefixes = prefixes
+    
+    def isChannelName(self, name):
+        """
+        Public method to check, if the given name is a channel name.
+        
+        @param name name to check (string)
+        @return flag indicating a channel name (boolean)
+        """
+        if not name:
+            return False
+        
+        if self.__channelTypePrefixes:
+            return name[0] in self.__channelTypePrefixes
+        else:
+            return name[0] in "#&"
+    
+    def __away(self, isAway):
+        """
+        Private slot handling the change of the away state.
+        
+        @param isAway flag indicating the current away state (boolean)
+        """
+        if isAway and self.__identityName:
+            identity = self.__ircNetworkManager.getIdentity(
+                self.__identityName)
+            if identity and identity.rememberAwayPosition():
+                for channel in self.__channelList:
+                    channel.setMarkerLine()

eric ide

mercurial