--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ExtensionIrc/IrcChannelWidget.py Fri Apr 18 18:36:48 2025 +0200 @@ -0,0 +1,2257 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the IRC channel widget. +""" + +import pathlib +import re +from itertools import zip_longest + +from PyQt6.QtCore import ( + QCoreApplication, + QDateTime, + QPoint, + QTimer, + QUrl, + pyqtSignal, + pyqtSlot, +) +from PyQt6.QtGui import QDesktopServices, QIcon, QPainter, QTextCursor +from PyQt6.QtWidgets import ( + QApplication, + QInputDialog, + QLineEdit, + QListWidgetItem, + QMenu, + QWidget, +) + +from eric7 import EricUtilities +from eric7.__version__ import Version +from eric7.EricGui import EricPixmapCache +from eric7.EricWidgets import EricFileDialog, EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import OSUtilities +from eric7.UI.Info import Copyright +from PluginExtensionIrc import ircExtensionPluginObject + +from .IrcUtilities import getChannelModesDict, ircFilter, ircTimestamp +from .Ui_IrcChannelWidget import Ui_IrcChannelWidget + + +class IrcUserItem(QListWidgetItem): + """ + Class implementing a list widget item containing an IRC channel user. + """ + + Normal = 0x00 # no privileges + Operator = 0x01 # channel operator + Voice = 0x02 # voice operator + Admin = 0x04 # administrator + Halfop = 0x08 # half operator + Owner = 0x10 # channel owner + Away = 0x80 # user away + + PrivilegeMapping = { + "a": Away, + "o": Operator, + "O": Owner, + "v": Voice, + } + + def __init__(self, name, parent=None): + """ + Constructor + + @param name string with user name and privilege prefix + @type str + @param parent reference to the parent widget + @type QListWidget or QListWidgetItem + """ + super().__init__(name, parent) + + self.__privilege = IrcUserItem.Normal + self.__name = name + self.__ignored = False + + self.__setText() + self.__setIcon() + + def name(self): + """ + Public method to get the user name. + + @return user name + @rtype str + """ + return self.__name + + def setName(self, name): + """ + Public method to set a new nick name. + + @param name new nick name for the user + @type str + """ + self.__name = name + self.__setText() + + def changePrivilege(self, privilege): + """ + Public method to set or unset a user privilege. + + @param privilege privilege to set or unset + @type str + """ + oper = privilege[0] + priv = privilege[1] + if priv in IrcUserItem.PrivilegeMapping: + if oper == "+": + self.__privilege |= IrcUserItem.PrivilegeMapping[priv] + elif oper == "-": + self.__privilege &= ~IrcUserItem.PrivilegeMapping[priv] + self.__setIcon() + + def clearPrivileges(self): + """ + Public method to clear the user privileges. + """ + self.__privilege = IrcUserItem.Normal + self.__setIcon() + + def __setText(self): + """ + Private method to set the user item text. + """ + if self.__ignored: + self.setText( + QCoreApplication.translate("IrcUserItem", "{0} (ignored)").format( + self.__name + ) + ) + else: + self.setText(self.__name) + + def __setIcon(self): + """ + Private method to set the icon dependent on user privileges. + """ + # step 1: determine the icon + if self.__privilege & IrcUserItem.Voice: + icon = ircExtensionPluginObject.getIcon("ircVoice") + elif self.__privilege & IrcUserItem.Owner: + icon = ircExtensionPluginObject.getIcon("ircOwner") + elif self.__privilege & IrcUserItem.Operator: + icon = ircExtensionPluginObject.getIcon("ircOp") + elif self.__privilege & IrcUserItem.Halfop: + icon = ircExtensionPluginObject.getIcon("ircHalfop") + elif self.__privilege & IrcUserItem.Admin: + icon = ircExtensionPluginObject.getIcon("ircAdmin") + else: + icon = ircExtensionPluginObject.getIcon("ircNormal") + if self.__privilege & IrcUserItem.Away: + icon = self.__awayIcon(icon) + + # step 2: set the icon + self.setIcon(icon) + + def __awayIcon(self, icon): + """ + Private method to convert an icon to an away icon. + + @param icon icon to be converted + @type QIcon + @return away icon + @rtype QIcon + """ + pix1 = icon.pixmap(16, 16) + pix2 = ircExtensionPluginObject.getPixmap("ircAway") + painter = QPainter(pix1) + painter.drawPixmap(0, 0, pix2) + painter.end() + return QIcon(pix1) + + def parseWhoFlags(self, flags): + """ + Public method to parse the user flags reported by a WHO command. + + @param flags user flags as reported by WHO + @type str + """ + # H The user is not away. + # G The user is set away. + # * The user is an IRC operator. + # @ The user is a channel op in the channel listed in the first field. + # + The user is voiced in the channel listed. + if flags.endswith("@"): + privilege = IrcUserItem.Operator + elif flags.endswith("+"): + privilege = IrcUserItem.Voice + else: + privilege = IrcUserItem.Normal + if "*" in flags: + privilege = IrcUserItem.Admin + if flags.startswith("G"): + privilege |= IrcUserItem.Away + self.__privilege = privilege + self.__setIcon() + + def canChangeTopic(self): + """ + Public method to check, if the user is allowed to change the topic. + + @return flag indicating that the topic can be changed + @rtype bool + """ + return ( + bool(self.__privilege & IrcUserItem.Operator) + or bool(self.__privilege & IrcUserItem.Admin) + or bool(self.__privilege & IrcUserItem.Owner) + ) + + def setIgnored(self, ignored): + """ + Public method to set the user status to ignored. + + @param ignored flag indicating the new ignored status + @type bool + """ + self.__ignored = ignored + self.__setText() + + def isIgnored(self): + """ + Public method to check, if this user is ignored. + + @return flag indicating the ignored status + @rtype bool + """ + return self.__ignored + + +class IrcChannelWidget(QWidget, Ui_IrcChannelWidget): + """ + Class implementing the IRC channel widget. + + @signal sendData(str) emitted to send a message to the channel + @signal sendCtcpRequest(str, str, str) emitted to send a CTCP request + @signal sendCtcpReply(str, str) emitted to send a CTCP reply + @signal channelClosed(str) emitted after the user has left the channel + @signal openPrivateChat(str) emitted to open a "channel" for private + messages + @signal awayCommand(str) emitted to set the away status via the /away + command + @signal leaveChannels(list) emitted to leave a list of channels + @signal leaveAllChannels() emitted to leave all channels + """ + + sendData = pyqtSignal(str) + sendCtcpRequest = pyqtSignal(str, str, str) + sendCtcpReply = pyqtSignal(str, str) + channelClosed = pyqtSignal(str) + openPrivateChat = pyqtSignal(str) + awayCommand = pyqtSignal(str) + leaveChannels = pyqtSignal(list) + leaveAllChannels = pyqtSignal() + + UrlRe = re.compile( + r"""((?:http|ftp|https):\/\/[\w\-_]+(?:\.[\w\-_]+)+""" + r"""(?:[\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)""" + ) + + JoinIndicator = "-->" + LeaveIndicator = "<--" + MessageIndicator = "***" + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + self.setupUi(self) + + self.__ui = ericApp().getObject("UserInterface") + self.__ircWidget = parent + + self.editTopicButton.setIcon(ircExtensionPluginObject.getIcon("ircEditTopic")) + self.editTopicButton.hide() + + height = self.usersList.height() + self.messages.height() + self.splitter.setSizes([int(height * 0.3), int(height * 0.7)]) + + self.__initMessagesMenu() + self.__initUsersMenu() + + self.__name = "" + self.__userName = "" + self.__partMessage = "" + self.__prefixToPrivilege = {} + self.__private = False + self.__privatePartner = "" + self.__whoIsNick = "" + + self.__markerLine = "" + self.__hidden = True + + self.__serviceNamesLower = ["nickserv", "chanserv", "memoserv"] + + self.__patterns = [ + # :foo_!n=foo@foohost.bar.net PRIVMSG #eric-ide :some long message + # :foo_!n=foo@foohost.bar.net PRIVMSG bar_ :some long message + (re.compile(r":([^!]+)!([^ ]+)\sPRIVMSG\s([^ ]+)\s:(.*)"), self.__message), + # :foo_!n=foo@foohost.bar.net JOIN :#eric-ide + (re.compile(r":([^!]+)!([^ ]+)\sJOIN\s:?([^ ]+)"), self.__userJoin), + # :foo_!n=foo@foohost.bar.net PART #eric-ide :part message + (re.compile(r":([^!]+).*\sPART\s([^ ]+)\s:(.*)"), self.__userPart), + # :foo_!n=foo@foohost.bar.net PART #eric-ide + (re.compile(r":([^!]+).*\sPART\s([^ ]+)\s*"), self.__userPart), + # :foo_!n=foo@foohost.bar.net QUIT :quit message + (re.compile(r":([^!]+).*\sQUIT\s:(.*)"), self.__userQuit), + # :foo_!n=foo@foohost.bar.net QUIT + (re.compile(r":([^!]+).*\sQUIT\s*"), self.__userQuit), + # :foo_!n=foo@foohost.bar.net NICK :newnick + (re.compile(r":([^!]+).*\sNICK\s:(.*)"), self.__userNickChange), + # :foo_!n=foo@foohost.bar.net MODE #eric-ide +o foo_ + ( + re.compile(r":([^!]+).*\sMODE\s([^ ]+)\s([+-][ovO]+)\s([^ ]+).*"), + self.__setUserPrivilege, + ), + # :cameron.libera.chat MODE #eric-ide +ns + (re.compile(r":([^ ]+)\sMODE\s([^ ]+)\s(.+)"), self.__updateChannelModes), + # :foo_!n=foo@foohost.bar.net TOPIC #eric-ide :eric - Python IDE + (re.compile(r":.*\sTOPIC\s([^ ]+)\s:(.*)"), self.__setTopic), + # :sturgeon.libera.chat 301 foo_ bar :Gone away for now + (re.compile(r":.*\s301\s([^ ]+)\s([^ ]+)\s:(.+)"), self.__userAway), + # :sturgeon.libera.chat 315 foo_ #eric-ide :End of /WHO list. + (re.compile(r":.*\s315\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoEnd), + # :zelazny.libera.chat 324 foo_ #eric-ide +cnt + (re.compile(r":.*\s324\s.*\s([^ ]+)\s(.+)"), self.__channelModes), + # :zelazny.libera.chat 328 foo_ #eric-ide :http://www.bugger.com/ + (re.compile(r":.*\s328\s.*\s([^ ]+)\s:(.+)"), self.__channelUrl), + # :zelazny.libera.chat 329 foo_ #eric-ide 1353001005 + (re.compile(r":.*\s329\s.*\s([^ ]+)\s(.+)"), self.__channelCreated), + # :zelazny.libera.chat 332 foo_ #eric-ide :eric support channel + (re.compile(r":.*\s332\s.*\s([^ ]+)\s:(.*)"), self.__setTopic), + # :zelazny.libera.chat foo_ 333 #eric-ide foo 1353089020 + (re.compile(r":.*\s333\s.*\s([^ ]+)\s([^ ]+)\s(\d+)"), self.__topicCreated), + # :cameron.libera.chat 352 detlev_ #eric-ide ~foo foohost.bar.net + # cameron.libera.chat foo_ H :0 Foo Bar + ( + re.compile( + r":.*\s352\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s[^ ]+\s([^ ]+)" + r"\s([^ ]+)\s:\d+\s(.*)" + ), + self.__whoEntry, + ), + # :zelazny.libera.chat 353 foo_ @ #eric-ide :@user1 +user2 user3 + (re.compile(r":.*\s353\s.*\s.\s([^ ]+)\s:(.*)"), self.__userList), + # :sturgeon.libera.chat 354 foo_ 42 ChanServ H@ + (re.compile(r":.*\s354\s[^ ]+\s42\s([^ ]+)\s(.*)"), self.__autoWhoEntry), + # :zelazny.libera.chat 366 foo_ #eric-ide :End of /NAMES list. + (re.compile(r":.*\s366\s.*\s([^ ]+)\s:(.*)"), self.__ignore), + # :sturgeon.libera.chat 704 foo_ index :Help topics available: + (re.compile(r":.*\s70[456]\s[^ ]+\s([^ ]+)\s:(.*)"), self.__help), + # WHOIS replies + # :sturgeon.libera.chat 311 foo_ bar ~bar barhost.foo.net * :Bar + ( + re.compile(r":.*\s311\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s\*\s:(.*)"), + self.__whoIsUser, + ), + # :sturgeon.libera.chat 319 foo_ bar :@#eric-ide + (re.compile(r":.*\s319\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsChannels), + # :sturgeon.libera.chat 312 foo_ bar sturgeon.libera.chat :London + ( + re.compile(r":.*\s312\s[^ ]+\s([^ ]+)\s([^ ]+)\s:(.*)"), + self.__whoIsServer, + ), + # :sturgeon.libera.chat 671 foo_ bar :is using a secure connection + (re.compile(r":.*\s671\s[^ ]+\s([^ ]+)\s:.*"), self.__whoIsSecure), + # :sturgeon.libera.chat 317 foo_ bar 3758 1355046912 :seconds + # idle, signon time + ( + re.compile(r":.*\s317\s[^ ]+\s([^ ]+)\s(\d+)\s(\d+)\s:.*"), + self.__whoIsIdle, + ), + # :sturgeon.libera.chat 330 foo_ bar bar :is logged in as + ( + re.compile(r":.*\s330\s[^ ]+\s([^ ]+)\s([^ ]+)\s:.*"), + self.__whoIsAccount, + ), + # :sturgeon.libera.chat 318 foo_ bar :End of /WHOIS list. + (re.compile(r":.*\s318\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsEnd), + # :sturgeon.libera.chat 307 foo_ bar :is an identified user + (re.compile(r":.*\s307\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsIdentify), + # :sturgeon.libera.chat 320 foo_ bar :is an identified user + (re.compile(r":.*\s320\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsIdentify), + # :sturgeon.libera.chat 310 foo_ bar :is available for help + (re.compile(r":.*\s310\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsHelper), + # :sturgeon.libera.chat 338 foo_ bar real.ident@real.host + # 12.34.56.78 :Actual user@host, Actual IP + ( + re.compile(r":.*\s338\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s:.*"), + self.__whoIsActually, + ), + # :sturgeon.libera.chat 313 foo_ bar :is an IRC Operator + (re.compile(r":.*\s313\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsOperator), + # :sturgeon.libera.chat 378 foo_ bar :is connecting from + # *@mnch-4d044d5a.pool.mediaWays.net 77.4.77.90 + ( + re.compile(r":.*\s378\s[^ ]+\s([^ ]+)\s:.*\s([^ ]+)\s([^ ]+)"), + self.__whoIsConnection, + ), + ] + + self.__autoWhoTemplate = "WHO {0} %tnf,42" + self.__autoWhoTimer = QTimer() + self.__autoWhoTimer.setSingleShot(True) + self.__autoWhoTimer.timeout.connect(self.__sendAutoWhoCommand) + self.__autoWhoRequested = False + + @pyqtSlot() + def on_messageEdit_returnPressed(self): + """ + Private slot to send a message to the channel. + """ + msg = self.messageEdit.text() + if msg: + self.__processUserMessage(msg) + + def __processUserMessage(self, msg): + """ + Private method to process a message entered by the user or via the + user list context menu. + + @param msg message to be processed + @type str + """ + self.messages.append( + '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' + "<b>></b> {4}</font>".format( + ircExtensionPluginObject.getPreferences("ChannelMessageColour"), + ircExtensionPluginObject.getPreferences("OwnNickColour"), + ircTimestamp(), + self.__userName, + EricUtilities.html_encode(msg), + ) + ) + + if msg.startswith("/"): + if self.__private: + EricMessageBox.information( + self, + self.tr("Send Message"), + self.tr( + """Messages starting with a '/' are not allowed""" + """ in private chats.""" + ), + ) + else: + sendData = True + # flag set to False, if command was handled + + msgList = msg.split() + cmd = msgList[0][1:].upper() + if cmd in ["MSG", "QUERY"]: + cmd = "PRIVMSG" + if len(msgList) > 1: + if msgList[1].strip().lower() in self.__serviceNamesLower: + msg = ( + "PRIVMSG " + + msgList[1].strip().lower() + + " :" + + " ".join(msgList[2:]) + ) + else: + msg = "PRIVMSG {0} :{1}".format( + msgList[1], " ".join(msgList[2:]) + ) + else: + msgList[0] = cmd + msg = " ".join(msgList) + elif cmd == "NOTICE": + if len(msgList) > 2: + msg = "NOTICE {0} :{1}".format( + msgList[1], " ".join(msgList[2:]) + ) + else: + msg = "NOTICE {0}".format(" ".join(msgList[1:])) + elif cmd == "PING": + receiver = msgList[1] + msg = "PING {0} " + self.sendCtcpRequest.emit(receiver, "PING", "") + sendData = False + elif cmd == "IGNORE": + sendData = False + if len(msgList) > 1: + if msgList[1] == "-r": + ignored = False + userNamesList = msgList[2:] + else: + ignored = True + userNamesList = msgList[1:] + else: + userNamesList = [] + userNames = ",".join(u.rstrip(",") for u in userNamesList).split( + "," + ) + for userName in userNames: + itm = self.__findUser(userName) + if itm: + itm.setIgnored(ignored) + elif cmd == "UNIGNORE": + sendData = False + if len(msgList) > 1: + userNamesList = msgList[1:] + else: + userNamesList = [] + userNames = ",".join(u.rstrip(",") for u in userNamesList).split( + "," + ) + for userName in userNames: + itm = self.__findUser(userName) + if itm: + itm.setIgnored(False) + elif cmd == "AWAY": + sendData = False + if len(msgList) > 1: + msg = " ".join(msgList[1:]) + else: + msg = "" + self.awayCommand.emit(msg) + elif cmd == "JOIN": + sendData = False + if len(msgList) > 1: + channels = msgList[1].split(",") + if len(msgList) > 2: + keys = msgList[2].split(",") + else: + keys = [] + for channel, key in zip_longest(channels, keys, fillvalue=""): + self.__ircWidget.joinChannel(channel, key) + elif cmd == "PART": + sendData = False + if len(msgList) == 1: + self.leaveChannel() + else: + self.leaveChannels.emit(msgList[1:]) + elif cmd == "PARTALL": + sendData = False + self.leaveAllChannels.emit() + else: + msg = msg[1:] + if sendData: + self.sendData.emit(msg) + else: + if self.__private: + self.sendData.emit("PRIVMSG " + self.__privatePartner + " :" + msg) + else: + self.sendData.emit("PRIVMSG " + self.__name + " :" + msg) + + self.messageEdit.clear() + self.unsetMarkerLine() + + def requestLeave(self): + """ + Public method to leave the channel. + """ + ok = EricMessageBox.yesNo( + self, + self.tr("Leave IRC channel"), + self.tr( + """Do you really want to leave the IRC channel <b>{0}</b>?""" + ).format(self.__name), + ) + if ok: + self.leaveChannel() + + def leaveChannel(self): + """ + Public slot to leave the channel. + """ + if not self.__private: + self.sendData.emit("PART " + self.__name + " :" + self.__partMessage) + self.channelClosed.emit(self.__name) + + def name(self): + """ + Public method to get the name of the channel. + + @return name of the channel + @rtype str + """ + return self.__name + + def setName(self, name): + """ + Public method to set the name of the channel. + + @param name of the channel + @type str + """ + self.__name = name + + def getUsersCount(self): + """ + Public method to get the users count of the channel. + + @return users count of the channel + @rtype int + """ + return self.usersList.count() + + def userName(self): + """ + Public method to get the nick name of the user. + + @return nick name of the user + @rtype str + """ + return self.__userName + + def setUserName(self, name): + """ + Public method to set the user name for the channel. + + @param name user name for the channel + @type str + """ + self.__userName = name + + def partMessage(self): + """ + Public method to get the part message. + + @return part message + @rtype str + """ + return self.__partMessage + + def setPartMessage(self, message): + """ + Public method to set the part message. + + @param message message to be used for PART messages + @type str + """ + self.__partMessage = message + + def setPrivate(self, private, partner=""): + """ + Public method to set the private chat mode. + + @param private flag indicating private chat mode + @type bool + @param partner name of the partner user + @type str + """ + self.__private = private + self.__privatePartner = partner + self.editTopicButton.setEnabled(private) + + def setPrivateInfo(self, infoText): + """ + Public method to set some info text for private chat mode. + + @param infoText info text to be shown + @type str + """ + if self.__private: + self.topicLabel.setText(infoText) + + def handleMessage(self, line): + """ + Public method to handle the message sent by the server. + + @param line server message + @type str + @return flag indicating, if the message was handled + @rtype bool + """ + for patternRe, patternFunc in self.__patterns: + match = patternRe.match(line) + if match is not None and patternFunc(match): + return True + + return False + + def __message(self, match): + """ + Private method to handle messages to the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) sender user name + # group(2) sender user@host + # group(3) target nick + # group(4) message + if match.group(3).lower() == self.__name.lower(): + senderName = match.group(1) + itm = self.__findUser(senderName) + if itm and itm.isIgnored(): + # user should be ignored + return True + + if match.group(4).startswith("\x01"): + return self.__handleCtcp(match) + + self.addMessage(senderName, match.group(4)) + if self.__private and not self.topicLabel.text(): + self.setPrivateInfo("{0} - {1}".format(match.group(1), match.group(2))) + return True + + return False + + def addMessage(self, sender, msg): + """ + Public method to add a message from external. + + @param sender nick name of the sender + @type str + @param msg message received from sender + @type str + """ + self.__appendMessage( + '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' + "<b>></b> {4}</font>".format( + ircExtensionPluginObject.getPreferences("ChannelMessageColour"), + ircExtensionPluginObject.getPreferences("NickColour"), + ircTimestamp(), + sender, + ircFilter(msg), + ) + ) + if ircExtensionPluginObject.getPreferences("ShowNotifications"): + if ircExtensionPluginObject.getPreferences("NotifyMessage"): + self.__ui.showNotification( + ircExtensionPluginObject.getPixmap("irc48"), + self.tr("Channel Message"), + msg, + ) + elif ( + ircExtensionPluginObject.getPreferences("NotifyNick") + and self.__userName.lower() in msg.lower() + ): + self.__ui.showNotification( + ircExtensionPluginObject.getPixmap("irc48"), + self.tr("Nick mentioned"), + msg, + ) + + def addUsers(self, users): + """ + Public method to add users to the channel. + + @param users list of user names to add + @type list of str + """ + for user in users: + itm = self.__findUser(user) + if itm is None: + IrcUserItem(name=user, parent=self.usersList) + + def __userJoin(self, match): + """ + Private method to handle a user joining the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(3).lower() == self.__name.lower(): + if self.__userName != match.group(1): + IrcUserItem(name=match.group(1), parent=self.usersList) + msg = self.tr("{0} has joined the channel {1} ({2}).").format( + match.group(1), self.__name, match.group(2) + ) + self.__addManagementMessage(IrcChannelWidget.JoinIndicator, msg) + else: + msg = self.tr("You have joined the channel {0} ({1}).").format( + self.__name, match.group(2) + ) + self.__addManagementMessage(IrcChannelWidget.JoinIndicator, msg) + if ircExtensionPluginObject.getPreferences( + "ShowNotifications" + ) and ircExtensionPluginObject.getPreferences("NotifyJoinPart"): + self.__ui.showNotification( + ircExtensionPluginObject.getPixmap("irc48"), + self.tr("Join Channel"), + msg, + ) + return True + + return False + + def __userPart(self, match): + """ + Private method to handle a user leaving the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(2).lower() == self.__name.lower(): + itm = self.__findUser(match.group(1)) + self.usersList.takeItem(self.usersList.row(itm)) + del itm + if match.lastindex == 2: + msg = self.tr("{0} has left {1}.").format(match.group(1), self.__name) + nmsg = msg + self.__addManagementMessage(IrcChannelWidget.LeaveIndicator, msg) + else: + msg = self.tr("{0} has left {1}: {2}.").format( + match.group(1), self.__name, ircFilter(match.group(3)) + ) + nmsg = self.tr("{0} has left {1}: {2}.").format( + match.group(1), self.__name, match.group(3) + ) + self.__addManagementMessage(IrcChannelWidget.LeaveIndicator, msg) + if ircExtensionPluginObject.getPreferences( + "ShowNotifications" + ) and ircExtensionPluginObject.getPreferences("NotifyJoinPart"): + self.__ui.showNotification( + ircExtensionPluginObject.getPixmap("irc48"), + self.tr("Leave Channel"), + nmsg, + ) + return True + + return False + + def __userQuit(self, match): + """ + Private method to handle a user logging off the server. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + itm = self.__findUser(match.group(1)) + if itm: + self.usersList.takeItem(self.usersList.row(itm)) + del itm + if match.lastindex == 1: + msg = self.tr("{0} has quit {1}.").format(match.group(1), self.__name) + self.__addManagementMessage(IrcChannelWidget.MessageIndicator, msg) + else: + msg = self.tr("{0} has quit {1}: {2}.").format( + match.group(1), self.__name, ircFilter(match.group(2)) + ) + self.__addManagementMessage(IrcChannelWidget.MessageIndicator, msg) + if ircExtensionPluginObject.getPreferences( + "ShowNotifications" + ) and ircExtensionPluginObject.getPreferences("NotifyJoinPart"): + self.__ui.showNotification( + ircExtensionPluginObject.getPixmap("irc48"), self.tr("Quit"), msg + ) + + # always return False for other channels and server to process + return False + + def __userNickChange(self, match): + """ + Private method to handle a nickname change of a user. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + itm = self.__findUser(match.group(1)) + if itm: + itm.setName(match.group(2)) + if match.group(1) == self.__userName: + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + self.tr("You are now known as {0}.").format(match.group(2)), + ) + self.__userName = match.group(2) + else: + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + self.tr("User {0} is now known as {1}.").format( + match.group(1), match.group(2) + ), + ) + + # always return False for other channels and server to process + return False + + def __userList(self, match): + """ + Private method to handle the receipt of a list of users of the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + users = match.group(2).split() + for user in users: + userPrivileges, userName = self.__extractPrivilege(user) + itm = self.__findUser(userName) + if itm is None: + itm = IrcUserItem(name=userName, parent=self.usersList) + for privilege in userPrivileges: + itm.changePrivilege(privilege) + + self.__setEditTopicButton() + return True + + return False + + def __userAway(self, match): + """ + Private method to handle a topic change of the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + self.__addManagementMessage( + self.tr("Away"), + self.tr("{0} is away: {1}").format(match.group(2), match.group(3)), + ) + return True + + return False + + def __setTopic(self, match): + """ + Private method to handle a topic change of the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + self.topicLabel.setText(match.group(2)) + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + ircFilter( + self.tr('The channel topic is: "{0}".').format(match.group(2)) + ), + ) + return True + + return False + + def __topicCreated(self, match): + """ + Private method to handle a topic created message. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + self.tr("The topic was set by {0} on {1}.").format( + match.group(2), + QDateTime.fromSecsSinceEpoch(int(match.group(3))).toString( + "yyyy-MM-dd hh:mm" + ), + ), + ) + return True + + return False + + def __channelUrl(self, match): + """ + Private method to handle a channel URL message. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + ircFilter(self.tr("Channel URL: {0}").format(match.group(2))), + ) + return True + + return False + + def __channelModes(self, match): + """ + Private method to handle a message reporting the channel modes. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + modesDict = getChannelModesDict() + modesParameters = match.group(2).split() + modeString = modesParameters.pop(0) + modes = [] + for modeChar in modeString: + if modeChar == "+": + continue + elif modeChar == "k": + parameter = modesParameters.pop(0) + modes.append(self.tr("password protected ({0})").format(parameter)) + elif modeChar == "l": + parameter = modesParameters.pop(0) + modes.append(self.tr("limited to %n user(s)", "", int(parameter))) + elif modeChar in modesDict: + modes.append(modesDict[modeChar]) + else: + modes.append(modeChar) + + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + self.tr("Channel modes: {0}.").format(", ".join(modes)), + ) + + return True + + return False + + def __channelCreated(self, match): + """ + Private method to handle a channel created message. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + self.tr("This channel was created on {0}.").format( + QDateTime.fromSecsSinceEpoch(int(match.group(2))).toString( + "yyyy-MM-dd hh:mm" + ) + ), + ) + return True + + return False + + def __updateChannelModes(self, match): + """ + Private method to handle a message reporting the channel modes. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) user or server + # group(2) channel + # group(3) modes and parameter list + if match.group(2).lower() == self.__name.lower(): + nick = match.group(1) + modesParameters = match.group(3).split() + modeString = modesParameters.pop(0) + isPlus = True + message = "" + for mode in modeString: + if mode == "+": + isPlus = True + continue + elif mode == "-": + isPlus = False + continue + elif mode == "a": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'anonymous'." + ).format(nick) + else: + message = self.tr( + "{0} removes the 'anonymous' mode from the channel." + ).format(nick) + elif mode == "b": + if isPlus: + message = self.tr("{0} sets a ban on {1}.").format( + nick, modesParameters.pop(0) + ) + else: + message = self.tr("{0} removes the ban on {1}.").format( + nick, modesParameters.pop(0) + ) + elif mode == "c": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'no colors allowed'." + ).format(nick) + else: + message = self.tr( + "{0} sets the channel mode to 'allow color codes'." + ).format(nick) + elif mode == "e": + if isPlus: + message = self.tr("{0} sets a ban exception on {1}.").format( + nick, modesParameters.pop(0) + ) + else: + message = self.tr( + "{0} removes the ban exception on {1}." + ).format(nick, modesParameters.pop(0)) + elif mode == "i": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'invite only'." + ).format(nick) + else: + message = self.tr( + "{0} removes the 'invite only' mode from the channel." + ).format(nick) + elif mode == "k": + if isPlus: + message = self.tr("{0} sets the channel key to '{1}'.").format( + nick, modesParameters.pop(0) + ) + else: + message = self.tr("{0} removes the channel key.").format(nick) + elif mode == "l": + if isPlus: + message = self.tr( + "{0} sets the channel limit to %n nick(s).", + "", + int(modesParameters.pop(0)), + ).format(nick) + else: + message = self.tr("{0} removes the channel limit.").format(nick) + elif mode == "m": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'moderated'." + ).format(nick) + else: + message = self.tr( + "{0} sets the channel mode to 'unmoderated'." + ).format(nick) + elif mode == "n": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'no messages from" + " outside'." + ).format(nick) + else: + message = self.tr( + "{0} sets the channel mode to 'allow messages" + " from outside'." + ).format(nick) + elif mode == "p": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'private'." + ).format(nick) + else: + message = self.tr( + "{0} sets the channel mode to 'public'." + ).format(nick) + elif mode == "q": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'quiet'." + ).format(nick) + else: + message = self.tr( + "{0} removes the 'quiet' mode from the channel." + ).format(nick) + elif mode == "r": + continue + elif mode == "s": + if isPlus: + message = self.tr( + "{0} sets the channel mode to 'secret'." + ).format(nick) + else: + message = self.tr( + "{0} sets the channel mode to 'visible'." + ).format(nick) + elif mode == "t": + if isPlus: + message = self.tr("{0} switches on 'topic protection'.").format( + nick + ) + else: + message = self.tr( + "{0} switches off 'topic protection'." + ).format(nick) + elif mode == "I": + if isPlus: + message = self.tr("{0} sets invitation mask {1}.").format( + nick, modesParameters.pop(0) + ) + else: + message = self.tr( + "{0} removes the invitation mask {1}." + ).format(nick, modesParameters.pop(0)) + + self.__addManagementMessage(self.tr("Mode"), message) + + return True + + return False + + def __setUserPrivilege(self, match): + """ + Private method to handle a change of user privileges for the channel. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(2).lower() == self.__name.lower(): + itm = self.__findUser(match.group(4)) + if itm: + itm.changePrivilege(match.group(3)) + self.__setEditTopicButton() + self.__addManagementMessage( + IrcChannelWidget.MessageIndicator, + self.tr("{0} sets mode for {1}: {2}.").format( + match.group(1), match.group(4), match.group(3) + ), + ) + return True + + return False + + def __ignore(self, match): + """ + Private method to handle a channel message we are not interested in. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + return True + + return False + + def __help(self, match): + """ + Private method to handle a help message. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + self.__addManagementMessage( + self.tr("Help"), "{0} {1}".format(match.group(1), ircFilter(match.group(2))) + ) + return True + + def __handleCtcp(self, match): + """ + Private method to handle a CTCP channel command. + + @param match reference to the match object + @type re.Match + @return flag indicating, if the message was handled + @rtype bool + """ + # 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": + msg = "Eric IRC client {0}, {1}".format(Version, Copyright) + self.__addManagementMessage( + self.tr("CTCP"), + self.tr("Received Version request from {0}.").format( + match.group(1) + ), + ) + self.sendCtcpReply.emit(match.group(1), "VERSION " + msg) + elif ctcpRequest == "ping": + self.__addManagementMessage( + self.tr("CTCP"), + self.tr( + "Received CTCP-PING request from {0}, sending answer." + ).format(match.group(1)), + ) + self.sendCtcpReply.emit(match.group(1), "PING {0}".format(ctcpArg)) + elif ctcpRequest == "clientinfo": + self.__addManagementMessage( + self.tr("CTCP"), + self.tr( + "Received CTCP-CLIENTINFO request from {0}, sending answer." + ).format(match.group(1)), + ) + self.sendCtcpReply.emit( + match.group(1), "CLIENTINFO CLIENTINFO PING VERSION" + ) + else: + self.__addManagementMessage( + self.tr("CTCP"), + self.tr("Received unknown CTCP-{0} request from {1}.").format( + ctcpRequest, match.group(1) + ), + ) + return True + + return False + + def setUserPrivilegePrefix(self, prefixes): + """ + Public method to set the user privilege to prefix mapping. + + @param prefixes dictionary with privilege as key and prefix as value + @type dict + """ + self.__prefixToPrivilege = {} + for privilege, prefix in prefixes.items(): + if prefix: + self.__prefixToPrivilege[prefix] = privilege + + def __findUser(self, name): + """ + Private method to find the user in the list of users. + + @param name user name to search for + @type str + @return reference to the list entry + @rtype QListWidgetItem + """ + for row in range(self.usersList.count()): + itm = self.usersList.item(row) + if itm.name() == name: + return itm + + return None + + def __extractPrivilege(self, name): + """ + Private method to extract the user privileges out of the name. + + @param name user name and prefixes + @type str + @return tuple containing a list of privileges and user name + @rtype tuple of (list of str, str) + """ + privileges = [] + while name[0] in self.__prefixToPrivilege: + prefix = name[0] + privileges.append(self.__prefixToPrivilege[prefix]) + name = name[1:] + if name[0] == ",": + name = name[1:] + + return privileges, name + + def __addManagementMessage(self, indicator, message): + """ + Private method to add a channel management message to the list. + + @param indicator indicator to be shown + @type str + @param message message to be shown + @type str + """ + if indicator == self.JoinIndicator: + color = ircExtensionPluginObject.getPreferences("JoinChannelColour") + elif indicator == self.LeaveIndicator: + color = ircExtensionPluginObject.getPreferences("LeaveChannelColour") + else: + color = ircExtensionPluginObject.getPreferences("ChannelInfoColour") + self.__appendMessage( + '<font color="{0}">{1} <b>[</b>{2}<b>]</b> {3}</font>'.format( + color, ircTimestamp(), indicator, message + ) + ) + + def __appendMessage(self, message): + """ + Private slot to append a message. + + @param message message to be appended + @type str + """ + if ( + self.__hidden + and self.__markerLine == "" + and ircExtensionPluginObject.getPreferences("MarkPositionWhenHidden") + ): + self.setMarkerLine() + self.messages.append(message) + + def setMarkerLine(self): + """ + Public method to draw a line to mark the current position. + """ + self.unsetMarkerLine() + self.__markerLine = ( + '<span style=" color:{0}; background-color:{1};">{2}</span>'.format( + ircExtensionPluginObject.getPreferences("MarkerLineForegroundColour"), + ircExtensionPluginObject.getPreferences("MarkerLineBackgroundColour"), + self.tr("--- New From Here ---"), + ) + ) + self.messages.append(self.__markerLine) + + def unsetMarkerLine(self): + """ + Public method to remove the marker line. + """ + if self.__markerLine: + txt = self.messages.toHtml() + if txt.endswith(self.__markerLine + "</p></body></html>"): + # remove empty last paragraph + pos = txt.rfind("<p") + txt = txt[:pos] + "</body></html>" + else: + txt = txt.replace(self.__markerLine, "") + self.messages.setHtml(txt) + self.__markerLine = "" + self.messages.moveCursor(QTextCursor.MoveOperation.End) + + def __clearMessages(self): + """ + Private slot to clear the contents of the messages display. + """ + self.messages.clear() + + def __copyMessages(self): + """ + Private slot to copy the selection of the messages display to the + clipboard. + """ + self.messages.copy() + + def __copyAllMessages(self): + """ + Private slot to copy the contents of the messages display to the + clipboard. + """ + txt = self.messages.toPlainText() + if txt: + cb = QApplication.clipboard() + cb.setText(txt) + + def __cutAllMessages(self): + """ + Private slot to cut the contents of the messages display to the + clipboard. + """ + txt = self.messages.toPlainText() + if txt: + cb = QApplication.clipboard() + cb.setText(txt) + self.messages.clear() + + def __saveMessages(self): + """ + Private slot to save the contents of the messages display. + """ + hasText = not self.messages.document().isEmpty() + if hasText: + if OSUtilities.isWindowsPlatform(): + htmlExtension = "htm" + else: + htmlExtension = "html" + fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + self.tr("Save Messages"), + "", + self.tr("HTML Files (*.{0});;Text Files (*.txt);;All Files (*)").format( + htmlExtension + ), + None, + EricFileDialog.DontConfirmOverwrite, + ) + if fname: + fpath = pathlib.Path(fname) + if not fpath.suffix: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fpath = fpath.with_suffix(ex) + if fpath.exists(): + res = EricMessageBox.yesNo( + self, + self.tr("Save Messages"), + self.tr( + "<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>" + ).format(fpath), + icon=EricMessageBox.Warning, + ) + if not res: + return + + try: + txt = ( + self.messages.toHtml() + if fpath.suffix.lower() in [".htm", ".html"] + else self.messages.toPlainText() + ) + with fpath.open("w", encoding="utf-8") as f: + f.write(txt) + except OSError as err: + EricMessageBox.critical( + self, + self.tr("Error saving Messages"), + self.tr( + """<p>The messages contents could not be written""" + """ to <b>{0}</b></p><p>Reason: {1}</p>""" + ).format(fpath, str(err)), + ) + + def __initMessagesMenu(self): + """ + Private slot to initialize the context menu of the messages pane. + """ + self.__messagesMenu = QMenu(self) + self.__copyMessagesAct = self.__messagesMenu.addAction( + EricPixmapCache.getIcon("editCopy"), self.tr("Copy"), self.__copyMessages + ) + self.__messagesMenu.addSeparator() + self.__cutAllMessagesAct = self.__messagesMenu.addAction( + EricPixmapCache.getIcon("editCut"), + self.tr("Cut all"), + self.__cutAllMessages, + ) + self.__copyAllMessagesAct = self.__messagesMenu.addAction( + EricPixmapCache.getIcon("editCopy"), + self.tr("Copy all"), + self.__copyAllMessages, + ) + self.__messagesMenu.addSeparator() + self.__clearMessagesAct = self.__messagesMenu.addAction( + EricPixmapCache.getIcon("editDelete"), + self.tr("Clear"), + self.__clearMessages, + ) + self.__messagesMenu.addSeparator() + self.__saveMessagesAct = self.__messagesMenu.addAction( + EricPixmapCache.getIcon("fileSave"), self.tr("Save"), self.__saveMessages + ) + self.__messagesMenu.addSeparator() + self.__setMarkerMessagesAct = self.__messagesMenu.addAction( + self.tr("Mark Current Position"), self.setMarkerLine + ) + self.__unsetMarkerMessagesAct = self.__messagesMenu.addAction( + self.tr("Remove Position Marker"), self.unsetMarkerLine + ) + + self.on_messages_copyAvailable(False) + + @pyqtSlot(bool) + def on_messages_copyAvailable(self, yes): + """ + Private slot to react to text selection/deselection of the messages + edit. + + @param yes flag signaling the availability of selected text + @type bool + """ + self.__copyMessagesAct.setEnabled(yes) + + @pyqtSlot(QPoint) + def on_messages_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu of the messages pane. + + @param pos the position of the mouse pointer + @type QPoint + """ + enable = not self.messages.document().isEmpty() + self.__cutAllMessagesAct.setEnabled(enable) + self.__copyAllMessagesAct.setEnabled(enable) + self.__saveMessagesAct.setEnabled(enable) + self.__setMarkerMessagesAct.setEnabled(self.__markerLine == "") + self.__unsetMarkerMessagesAct.setEnabled(self.__markerLine != "") + self.__messagesMenu.popup(self.messages.mapToGlobal(pos)) + + def __whoIs(self): + """ + Private slot to get information about the selected user. + """ + self.__whoIsNick = self.usersList.selectedItems()[0].text() + self.sendData.emit("WHOIS " + self.__whoIsNick) + + def __openPrivateChat(self): + """ + Private slot to open a chat with the selected user. + """ + user = self.usersList.selectedItems()[0].text() + self.openPrivateChat.emit(user) + + def __sendUserMessage(self): + """ + Private slot to send a private message to a specific user. + """ + from eric7.EricWidgets import EricTextInputDialog + + user = self.usersList.selectedItems()[0].text() + ok, message = EricTextInputDialog.getText( + self, + self.tr("Send Message"), + self.tr("Enter the message to be sent:"), + minimumWidth=400, + ) + if ok and message: + self.__processUserMessage("/MSG {0} {1}".format(user, message)) + + def __sendUserQuery(self): + """ + Private slot to send a query message to a specific user. + """ + from eric7.EricWidgets import EricTextInputDialog + + user = self.usersList.selectedItems()[0].text() + ok, message = EricTextInputDialog.getText( + self, + self.tr("Send Query"), + self.tr("Enter the message to be sent:"), + minimumWidth=400, + ) + if ok and message: + self.__processUserMessage("/QUERY {0} {1}".format(user, message)) + + def __sendUserNotice(self): + """ + Private slot to send a notice message to a specific user. + """ + from eric7.EricWidgets import EricTextInputDialog + + user = self.usersList.selectedItems()[0].text() + ok, message = EricTextInputDialog.getText( + self, + self.tr("Send Notice"), + self.tr("Enter the message to be sent:"), + minimumWidth=400, + ) + if ok and message: + self.__processUserMessage("/NOTICE {0} {1}".format(user, message)) + + def __pingUser(self): + """ + Private slot to send a ping to a specific user. + """ + user = self.usersList.selectedItems()[0].text() + self.__processUserMessage("/PING {0}".format(user)) + + def __ignoreUser(self): + """ + Private slot to ignore a specific user. + """ + user = self.usersList.selectedItems()[0].text() + self.__processUserMessage("/IGNORE {0}".format(user)) + + def __initUsersMenu(self): + """ + Private slot to initialize the users list context menu. + """ + self.__usersMenu = QMenu(self) + self.__whoIsAct = self.__usersMenu.addAction(self.tr("Who Is"), self.__whoIs) + self.__usersMenu.addSeparator() + self.__privateChatAct = self.__usersMenu.addAction( + self.tr("Private Chat"), self.__openPrivateChat + ) + self.__usersMenu.addSeparator() + self.__sendUserMessageAct = self.__usersMenu.addAction( + self.tr("Send Message"), self.__sendUserMessage + ) + self.__sendUserQueryAct = self.__usersMenu.addAction( + self.tr("Send Query"), self.__sendUserQuery + ) + self.__sendUserNoticeAct = self.__usersMenu.addAction( + self.tr("Send Notice"), self.__sendUserNotice + ) + self.__usersMenu.addSeparator() + self.__pingUserAct = self.__usersMenu.addAction( + self.tr("Send Ping"), self.__pingUser + ) + self.__ignoreUserAct = self.__usersMenu.addAction( + self.tr("Ignore User"), self.__ignoreUser + ) + self.__usersMenu.addSeparator() + self.__usersListRefreshAct = self.__usersMenu.addAction( + self.tr("Refresh"), self.__sendAutoWhoCommand + ) + + @pyqtSlot(QPoint) + def on_usersList_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu of the users list. + + @param pos the position of the mouse pointer + @type QPoint + """ + enable = len(self.usersList.selectedItems()) > 0 + enablePrivate = enable and not self.__private + itm = self.usersList.itemAt(pos) + if itm and enablePrivate: + enablePrivate = itm.text().lower() not in [ + "chanserv", + self.__userName.lower(), + ] + self.__whoIsAct.setEnabled(enable) + self.__privateChatAct.setEnabled(enablePrivate) + self.__usersListRefreshAct.setEnabled( + self.usersList.count() + <= ircExtensionPluginObject.getPreferences("AutoUserInfoMax") + ) + self.__usersMenu.popup(self.usersList.mapToGlobal(pos)) + + def hideEvent(self, _evt): + """ + Protected method handling hide events. + + @param _evt reference to the hide event (unused) + @type QHideEvent + """ + self.__hidden = True + + def showEvent(self, _evt): + """ + Protected method handling show events. + + @param _evt reference to the show event (unused) + @type QShowEvent + """ + self.__hidden = False + + def initAutoWho(self): + """ + Public method to initialize the Auto Who system. + """ + if ircExtensionPluginObject.getPreferences("AutoUserInfoLookup"): + self.__autoWhoTimer.setInterval( + ircExtensionPluginObject.getPreferences("AutoUserInfoInterval") * 1000 + ) + self.__autoWhoTimer.start() + + @pyqtSlot() + def __sendAutoWhoCommand(self): + """ + Private slot to send the WHO command to update the users list. + """ + if self.usersList.count() <= ircExtensionPluginObject.getPreferences( + "AutoUserInfoMax" + ): + self.__autoWhoRequested = True + self.sendData.emit(self.__autoWhoTemplate.format(self.__name)) + + def __autoWhoEntry(self, match): + """ + Private method to handle a WHO entry returned by the server as + requested automatically. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) user flags + if self.__autoWhoRequested: + itm = self.__findUser(match.group(1)) + if itm: + itm.parseWhoFlags(match.group(2)) + return True + + return False + + def __whoEnd(self, match): + """ + Private method to handle the end of the WHO list. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + if match.group(1).lower() == self.__name.lower(): + if self.__autoWhoRequested: + self.__autoWhoRequested = False + self.initAutoWho() + else: + self.__addManagementMessage( + self.tr("Who"), + self.tr("End of WHO list for {0}.").format(match.group(1)), + ) + return True + + return False + + def __whoEntry(self, match): + """ + Private method to handle a WHO entry returned by the server as + requested manually. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) channel + # group(2) user + # group(3) host + # group(4) nick + # group(5) user flags + # group(6) real name + if match.group(1).lower() == self.__name.lower(): + away = self.tr(" (Away)") if match.group(5).startswith("G") else "" + self.__addManagementMessage( + self.tr("Who"), + self.tr("{0} is {1}@{2} ({3}){4}").format( + match.group(4), match.group(2), match.group(3), match.group(6), away + ), + ) + return True + + return False + + def __whoIsUser(self, match): + """ + Private method to handle the WHOIS user reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) user + # group(3) host + # group(4) real name + if match.group(1) == self.__whoIsNick: + realName = match.group(4).replace("<", "<").replace(">", ">") + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is {1}@{2} ({3}).").format( + match.group(1), match.group(2), match.group(3), realName + ), + ) + return True + + return False + + def __whoIsChannels(self, match): + """ + Private method to handle the WHOIS channels reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) channels + if match.group(1) == self.__whoIsNick: + userChannels = [] + voiceChannels = [] + opChannels = [] + halfopChannels = [] + ownerChannels = [] + adminChannels = [] + + # generate the list of channels the user is in + channelList = match.group(2).split() + for channel in channelList: + if channel.startswith(("*", "&")): + adminChannels.append(channel[1:]) + elif channel.startswith(("!", "~")) and self.__ircWidget.isChannelName( + channel[1:] + ): + ownerChannels.append(channel[1:]) + elif channel.startswith("@+"): + opChannels.append(channel[2:]) + elif channel.startswith("@"): + opChannels.append(channel[1:]) + elif channel.startswith("%"): + halfopChannels.append(channel[1:]) + elif channel.startswith("+"): + voiceChannels.append(channel[1:]) + else: + userChannels.append(channel) + + # show messages + if userChannels: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is a user on channels: {1}").format( + match.group(1), " ".join(userChannels) + ), + ) + if voiceChannels: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} has voice on channels: {1}").format( + match.group(1), " ".join(voiceChannels) + ), + ) + if halfopChannels: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is a halfop on channels: {1}").format( + match.group(1), " ".join(halfopChannels) + ), + ) + if opChannels: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is an operator on channels: {1}").format( + match.group(1), " ".join(opChannels) + ), + ) + if ownerChannels: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is owner of channels: {1}").format( + match.group(1), " ".join(ownerChannels) + ), + ) + if adminChannels: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is admin on channels: {1}").format( + match.group(1), " ".join(adminChannels) + ), + ) + return True + + return False + + def __whoIsServer(self, match): + """ + Private method to handle the WHOIS server reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) server + # group(3) server info + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is online via {1} ({2}).").format( + match.group(1), match.group(2), match.group(3) + ), + ) + return True + + return False + + def __whoIsOperator(self, match): + """ + Private method to handle the WHOIS operator reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) message + if match.group(1) == self.__whoIsNick: + if match.group(2).lower().startswith("is an irc operator"): + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is an IRC Operator.").format(match.group(1)), + ) + else: + self.__addManagementMessage( + self.tr("Whois"), "{0} {1}".format(match.group(1), match.group(2)) + ) + return True + + return False + + def __whoIsIdle(self, match): + """ + Private method to handle the WHOIS idle reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) idle seconds + # group(3) signon time + if match.group(1) == self.__whoIsNick: + seconds = int(match.group(2)) + minutes = seconds // 60 + hours = minutes // 60 + days = hours // 24 + + signonTimestamp = int(match.group(3)) + signonTime = QDateTime() + signonTime.setTime_t(signonTimestamp) + + if days: + daysString = self.tr("%n day(s)", "", days) + hoursString = self.tr("%n hour(s)", "", hours) + minutesString = self.tr("%n minute(s)", "", minutes) + secondsString = self.tr("%n second(s)", "", seconds) + self.__addManagementMessage( + self.tr("Whois"), + self.tr( + "{0} has been idle for {1}, {2}, {3}, and {4}.", + "{0} = name of person, {1} = (x days)," + " {2} = (x hours), {3} = (x minutes)," + " {4} = (x seconds)", + ).format( + match.group(1), + daysString, + hoursString, + minutesString, + secondsString, + ), + ) + elif hours: + hoursString = self.tr("%n hour(s)", "", hours) + minutesString = self.tr("%n minute(s)", "", minutes) + secondsString = self.tr("%n second(s)", "", seconds) + self.__addManagementMessage( + self.tr("Whois"), + self.tr( + "{0} has been idle for {1}, {2}, and {3}.", + "{0} = name of person, {1} = (x hours), " + "{2} = (x minutes), {3} = (x seconds)", + ).format(match.group(1), hoursString, minutesString, secondsString), + ) + elif minutes: + minutesString = self.tr("%n minute(s)", "", minutes) + secondsString = self.tr("%n second(s)", "", seconds) + self.__addManagementMessage( + self.tr("Whois"), + self.tr( + "{0} has been idle for {1} and {2}.", + "{0} = name of person, {1} = (x minutes), {3} = (x seconds)", + ).format(match.group(1), minutesString, secondsString), + ) + else: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} has been idle for %n second(s).", "", seconds).format( + match.group(1) + ), + ) + + if not signonTime.isNull(): + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} has been online since {1}.").format( + match.group(1), signonTime.toString("yyyy-MM-dd, hh:mm:ss") + ), + ) + return True + + return False + + def __whoIsEnd(self, match): + """ + Private method to handle the end of WHOIS reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) end message + if match.group(1) == self.__whoIsNick: + self.__whoIsNick = "" + self.__addManagementMessage( + self.tr("Whois"), + self.tr("End of WHOIS list for {0}.").format(match.group(1)), + ) + return True + + return False + + def __whoIsIdentify(self, match): + """ + Private method to handle the WHOIS identify and identified replies. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) identified message + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is an identified user.").format(match.group(1)), + ) + return True + + return False + + def __whoIsHelper(self, match): + """ + Private method to handle the WHOIS helper reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) helper message + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is available for help.").format(match.group(1)), + ) + return True + + return False + + def __whoIsAccount(self, match): + """ + Private method to handle the WHOIS account reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) login name + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is logged in as {1}.").format( + match.group(1), match.group(2) + ), + ) + return True + + return False + + def __whoIsActually(self, match): + """ + Private method to handle the WHOIS actually reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) actual user@host + # group(3) actual IP + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is actually using the host {1} (IP: {2}).").format( + match.group(1), match.group(2), match.group(3) + ), + ) + return True + + return False + + def __whoIsSecure(self, match): + """ + Private method to handle the WHOIS secure reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is using a secure connection.").format(match.group(1)), + ) + return True + + return False + + def __whoIsConnection(self, match): + """ + Private method to handle the WHOIS connection reply. + + @param match match object that matched the pattern + @type re.Match + @return flag indicating whether the message was handled + @rtype bool + """ + # group(1) nick + # group(2) host name + # group(3) IP + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage( + self.tr("Whois"), + self.tr("{0} is connecting from {1} (IP: {2}).").format( + match.group(1), match.group(2), match.group(3) + ), + ) + return True + + return False + + def __setEditTopicButton(self): + """ + Private method to set the visibility of the Edit Topic button. + """ + itm = self.__findUser(self.__userName) + if itm: + self.editTopicButton.setVisible(itm.canChangeTopic()) + + @pyqtSlot() + def on_editTopicButton_clicked(self): + """ + Private slot to change the topic of the channel. + """ + topic, ok = QInputDialog.getText( + self, + self.tr("Edit Channel Topic"), + self.tr("Enter the topic for this channel:"), + QLineEdit.EchoMode.Normal, + self.topicLabel.text(), + ) + if ok and topic != "": + self.sendData.emit("TOPIC {0} :{1}".format(self.__name, topic)) + + @pyqtSlot(QUrl) + def on_messages_anchorClicked(self, url): + """ + Private slot to open links in the default browser. + + @param url URL to be opened + @type QUrl + """ + QDesktopServices.openUrl(url)