src/eric7/Network/IRC/IrcChannelWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9153
506e35e424d5
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the IRC channel widget.
8 """
9
10 from itertools import zip_longest
11
12 import pathlib
13 import re
14
15 from PyQt6.QtCore import (
16 pyqtSlot, pyqtSignal, QDateTime, QPoint, QTimer, QUrl, QCoreApplication
17 )
18 from PyQt6.QtGui import QIcon, QPainter, QTextCursor, QDesktopServices
19 from PyQt6.QtWidgets import (
20 QWidget, QListWidgetItem, QMenu, QApplication, QInputDialog, QLineEdit
21 )
22
23 from EricWidgets import EricMessageBox, EricFileDialog
24 from EricWidgets.EricApplication import ericApp
25
26 from .Ui_IrcChannelWidget import Ui_IrcChannelWidget
27
28 from .IrcUtilities import ircFilter, ircTimestamp, getChannelModesDict
29
30 import Utilities
31 import UI.PixmapCache
32 import Preferences
33
34 from UI.Info import Version, Copyright
35
36
37 class IrcUserItem(QListWidgetItem):
38 """
39 Class implementing a list widget item containing an IRC channel user.
40 """
41 Normal = 0x00 # no privileges
42 Operator = 0x01 # channel operator
43 Voice = 0x02 # voice operator
44 Admin = 0x04 # administrator
45 Halfop = 0x08 # half operator
46 Owner = 0x10 # channel owner
47 Away = 0x80 # user away
48
49 PrivilegeMapping = {
50 "a": Away,
51 "o": Operator,
52 "O": Owner,
53 "v": Voice,
54
55 }
56
57 def __init__(self, name, parent=None):
58 """
59 Constructor
60
61 @param name string with user name and privilege prefix (string)
62 @param parent reference to the parent widget (QListWidget or
63 QListWidgetItem)
64 """
65 super().__init__(name, parent)
66
67 self.__privilege = IrcUserItem.Normal
68 self.__name = name
69 self.__ignored = False
70
71 self.__setText()
72 self.__setIcon()
73
74 def name(self):
75 """
76 Public method to get the user name.
77
78 @return user name (string)
79 """
80 return self.__name
81
82 def setName(self, name):
83 """
84 Public method to set a new nick name.
85
86 @param name new nick name for the user (string)
87 """
88 self.__name = name
89 self.__setText()
90
91 def changePrivilege(self, privilege):
92 """
93 Public method to set or unset a user privilege.
94
95 @param privilege privilege to set or unset (string)
96 """
97 oper = privilege[0]
98 priv = privilege[1]
99 if priv in IrcUserItem.PrivilegeMapping:
100 if oper == "+":
101 self.__privilege |= IrcUserItem.PrivilegeMapping[priv]
102 elif oper == "-":
103 self.__privilege &= ~IrcUserItem.PrivilegeMapping[priv]
104 self.__setIcon()
105
106 def clearPrivileges(self):
107 """
108 Public method to clear the user privileges.
109 """
110 self.__privilege = IrcUserItem.Normal
111 self.__setIcon()
112
113 def __setText(self):
114 """
115 Private method to set the user item text.
116 """
117 if self.__ignored:
118 self.setText(QCoreApplication.translate(
119 "IrcUserItem",
120 "{0} (ignored)").format(self.__name))
121 else:
122 self.setText(self.__name)
123
124 def __setIcon(self):
125 """
126 Private method to set the icon dependent on user privileges.
127 """
128 # step 1: determine the icon
129 if self.__privilege & IrcUserItem.Voice:
130 icon = UI.PixmapCache.getIcon("ircVoice")
131 elif self.__privilege & IrcUserItem.Owner:
132 icon = UI.PixmapCache.getIcon("ircOwner")
133 elif self.__privilege & IrcUserItem.Operator:
134 icon = UI.PixmapCache.getIcon("ircOp")
135 elif self.__privilege & IrcUserItem.Halfop:
136 icon = UI.PixmapCache.getIcon("ircHalfop")
137 elif self.__privilege & IrcUserItem.Admin:
138 icon = UI.PixmapCache.getIcon("ircAdmin")
139 else:
140 icon = UI.PixmapCache.getIcon("ircNormal")
141 if self.__privilege & IrcUserItem.Away:
142 icon = self.__awayIcon(icon)
143
144 # step 2: set the icon
145 self.setIcon(icon)
146
147 def __awayIcon(self, icon):
148 """
149 Private method to convert an icon to an away icon.
150
151 @param icon icon to be converted (QIcon)
152 @return away icon (QIcon)
153 """
154 pix1 = icon.pixmap(16, 16)
155 pix2 = UI.PixmapCache.getPixmap("ircAway")
156 painter = QPainter(pix1)
157 painter.drawPixmap(0, 0, pix2)
158 painter.end()
159 return QIcon(pix1)
160
161 def parseWhoFlags(self, flags):
162 """
163 Public method to parse the user flags reported by a WHO command.
164
165 @param flags user flags as reported by WHO (string)
166 """
167 # H The user is not away.
168 # G The user is set away.
169 # * The user is an IRC operator.
170 # @ The user is a channel op in the channel listed in the first field.
171 # + The user is voiced in the channel listed.
172 if flags.endswith("@"):
173 privilege = IrcUserItem.Operator
174 elif flags.endswith("+"):
175 privilege = IrcUserItem.Voice
176 else:
177 privilege = IrcUserItem.Normal
178 if "*" in flags:
179 privilege = IrcUserItem.Admin
180 if flags.startswith("G"):
181 privilege |= IrcUserItem.Away
182 self.__privilege = privilege
183 self.__setIcon()
184
185 def canChangeTopic(self):
186 """
187 Public method to check, if the user is allowed to change the topic.
188
189 @return flag indicating that the topic can be changed (boolean)
190 """
191 return(bool(self.__privilege & IrcUserItem.Operator) or
192 bool(self.__privilege & IrcUserItem.Admin) or
193 bool(self.__privilege & IrcUserItem.Owner))
194
195 def setIgnored(self, ignored):
196 """
197 Public method to set the user status to ignored.
198
199 @param ignored flag indicating the new ignored status
200 @type bool
201 """
202 self.__ignored = ignored
203 self.__setText()
204
205 def isIgnored(self):
206 """
207 Public method to check, if this user is ignored.
208
209 @return flag indicating the ignored status
210 @rtype bool
211 """
212 return self.__ignored
213
214
215 class IrcChannelWidget(QWidget, Ui_IrcChannelWidget):
216 """
217 Class implementing the IRC channel widget.
218
219 @signal sendData(str) emitted to send a message to the channel
220 @signal sendCtcpRequest(str, str, str) emitted to send a CTCP request
221 @signal sendCtcpReply(str, str) emitted to send a CTCP reply
222 @signal channelClosed(str) emitted after the user has left the channel
223 @signal openPrivateChat(str) emitted to open a "channel" for private
224 messages
225 @signal awayCommand(str) emitted to set the away status via the /away
226 command
227 @signal leaveChannels(list) emitted to leave a list of channels
228 @signal leaveAllChannels() emitted to leave all channels
229 """
230 sendData = pyqtSignal(str)
231 sendCtcpRequest = pyqtSignal(str, str, str)
232 sendCtcpReply = pyqtSignal(str, str)
233 channelClosed = pyqtSignal(str)
234 openPrivateChat = pyqtSignal(str)
235 awayCommand = pyqtSignal(str)
236 leaveChannels = pyqtSignal(list)
237 leaveAllChannels = pyqtSignal()
238
239 UrlRe = re.compile(
240 r"""((?:http|ftp|https):\/\/[\w\-_]+(?:\.[\w\-_]+)+"""
241 r"""(?:[\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?)""")
242
243 JoinIndicator = "--&gt;"
244 LeaveIndicator = "&lt;--"
245 MessageIndicator = "***"
246
247 def __init__(self, parent=None):
248 """
249 Constructor
250
251 @param parent reference to the parent widget (QWidget)
252 """
253 super().__init__(parent)
254 self.setupUi(self)
255
256 self.__ui = ericApp().getObject("UserInterface")
257 self.__ircWidget = parent
258
259 self.editTopicButton.setIcon(
260 UI.PixmapCache.getIcon("ircEditTopic"))
261 self.editTopicButton.hide()
262
263 height = self.usersList.height() + self.messages.height()
264 self.splitter.setSizes([int(height * 0.3), int(height * 0.7)])
265
266 self.__initMessagesMenu()
267 self.__initUsersMenu()
268
269 self.__name = ""
270 self.__userName = ""
271 self.__partMessage = ""
272 self.__prefixToPrivilege = {}
273 self.__private = False
274 self.__privatePartner = ""
275 self.__whoIsNick = ""
276
277 self.__markerLine = ""
278 self.__hidden = True
279
280 self.__serviceNamesLower = ["nickserv", "chanserv", "memoserv"]
281
282 self.__patterns = [
283 # :foo_!n=foo@foohost.bar.net PRIVMSG #eric-ide :some long message
284 # :foo_!n=foo@foohost.bar.net PRIVMSG bar_ :some long message
285 (re.compile(r":([^!]+)!([^ ]+)\sPRIVMSG\s([^ ]+)\s:(.*)"),
286 self.__message),
287 # :foo_!n=foo@foohost.bar.net JOIN :#eric-ide
288 (re.compile(r":([^!]+)!([^ ]+)\sJOIN\s:?([^ ]+)"),
289 self.__userJoin),
290 # :foo_!n=foo@foohost.bar.net PART #eric-ide :part message
291 (re.compile(r":([^!]+).*\sPART\s([^ ]+)\s:(.*)"), self.__userPart),
292 # :foo_!n=foo@foohost.bar.net PART #eric-ide
293 (re.compile(r":([^!]+).*\sPART\s([^ ]+)\s*"), self.__userPart),
294 # :foo_!n=foo@foohost.bar.net QUIT :quit message
295 (re.compile(r":([^!]+).*\sQUIT\s:(.*)"), self.__userQuit),
296 # :foo_!n=foo@foohost.bar.net QUIT
297 (re.compile(r":([^!]+).*\sQUIT\s*"), self.__userQuit),
298 # :foo_!n=foo@foohost.bar.net NICK :newnick
299 (re.compile(r":([^!]+).*\sNICK\s:(.*)"), self.__userNickChange),
300 # :foo_!n=foo@foohost.bar.net MODE #eric-ide +o foo_
301 (re.compile(r":([^!]+).*\sMODE\s([^ ]+)\s([+-][ovO]+)\s([^ ]+).*"),
302 self.__setUserPrivilege),
303 # :cameron.libera.chat MODE #eric-ide +ns
304 (re.compile(r":([^ ]+)\sMODE\s([^ ]+)\s(.+)"),
305 self.__updateChannelModes),
306 # :foo_!n=foo@foohost.bar.net TOPIC #eric-ide :eric - Python IDE
307 (re.compile(r":.*\sTOPIC\s([^ ]+)\s:(.*)"), self.__setTopic),
308 # :sturgeon.libera.chat 301 foo_ bar :Gone away for now
309 (re.compile(r":.*\s301\s([^ ]+)\s([^ ]+)\s:(.+)"),
310 self.__userAway),
311 # :sturgeon.libera.chat 315 foo_ #eric-ide :End of /WHO list.
312 (re.compile(r":.*\s315\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoEnd),
313 # :zelazny.libera.chat 324 foo_ #eric-ide +cnt
314 (re.compile(r":.*\s324\s.*\s([^ ]+)\s(.+)"), self.__channelModes),
315 # :zelazny.libera.chat 328 foo_ #eric-ide :http://www.bugger.com/
316 (re.compile(r":.*\s328\s.*\s([^ ]+)\s:(.+)"), self.__channelUrl),
317 # :zelazny.libera.chat 329 foo_ #eric-ide 1353001005
318 (re.compile(r":.*\s329\s.*\s([^ ]+)\s(.+)"),
319 self.__channelCreated),
320 # :zelazny.libera.chat 332 foo_ #eric-ide :eric support channel
321 (re.compile(r":.*\s332\s.*\s([^ ]+)\s:(.*)"), self.__setTopic),
322 # :zelazny.libera.chat foo_ 333 #eric-ide foo 1353089020
323 (re.compile(r":.*\s333\s.*\s([^ ]+)\s([^ ]+)\s(\d+)"),
324 self.__topicCreated),
325 # :cameron.libera.chat 352 detlev_ #eric-ide ~foo foohost.bar.net
326 # cameron.libera.chat foo_ H :0 Foo Bar
327 (re.compile(
328 r":.*\s352\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s[^ ]+\s([^ ]+)"
329 r"\s([^ ]+)\s:\d+\s(.*)"), self.__whoEntry),
330 # :zelazny.libera.chat 353 foo_ @ #eric-ide :@user1 +user2 user3
331 (re.compile(r":.*\s353\s.*\s.\s([^ ]+)\s:(.*)"), self.__userList),
332 # :sturgeon.libera.chat 354 foo_ 42 ChanServ H@
333 (re.compile(r":.*\s354\s[^ ]+\s42\s([^ ]+)\s(.*)"),
334 self.__autoWhoEntry),
335 # :zelazny.libera.chat 366 foo_ #eric-ide :End of /NAMES list.
336 (re.compile(r":.*\s366\s.*\s([^ ]+)\s:(.*)"), self.__ignore),
337 # :sturgeon.libera.chat 704 foo_ index :Help topics available:
338 (re.compile(r":.*\s70[456]\s[^ ]+\s([^ ]+)\s:(.*)"), self.__help),
339
340 # WHOIS replies
341 # :sturgeon.libera.chat 311 foo_ bar ~bar barhost.foo.net * :Bar
342 (re.compile(
343 r":.*\s311\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s\*\s:(.*)"),
344 self.__whoIsUser),
345 # :sturgeon.libera.chat 319 foo_ bar :@#eric-ide
346 (re.compile(r":.*\s319\s[^ ]+\s([^ ]+)\s:(.*)"),
347 self.__whoIsChannels),
348 # :sturgeon.libera.chat 312 foo_ bar sturgeon.libera.chat :London
349 (re.compile(r":.*\s312\s[^ ]+\s([^ ]+)\s([^ ]+)\s:(.*)"),
350 self.__whoIsServer),
351 # :sturgeon.libera.chat 671 foo_ bar :is using a secure connection
352 (re.compile(r":.*\s671\s[^ ]+\s([^ ]+)\s:.*"), self.__whoIsSecure),
353 # :sturgeon.libera.chat 317 foo_ bar 3758 1355046912 :seconds
354 # idle, signon time
355 (re.compile(r":.*\s317\s[^ ]+\s([^ ]+)\s(\d+)\s(\d+)\s:.*"),
356 self.__whoIsIdle),
357 # :sturgeon.libera.chat 330 foo_ bar bar :is logged in as
358 (re.compile(r":.*\s330\s[^ ]+\s([^ ]+)\s([^ ]+)\s:.*"),
359 self.__whoIsAccount),
360 # :sturgeon.libera.chat 318 foo_ bar :End of /WHOIS list.
361 (re.compile(r":.*\s318\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsEnd),
362 # :sturgeon.libera.chat 307 foo_ bar :is an identified user
363 (re.compile(r":.*\s307\s[^ ]+\s([^ ]+)\s:(.*)"),
364 self.__whoIsIdentify),
365 # :sturgeon.libera.chat 320 foo_ bar :is an identified user
366 (re.compile(r":.*\s320\s[^ ]+\s([^ ]+)\s:(.*)"),
367 self.__whoIsIdentify),
368 # :sturgeon.libera.chat 310 foo_ bar :is available for help
369 (re.compile(r":.*\s310\s[^ ]+\s([^ ]+)\s:(.*)"),
370 self.__whoIsHelper),
371 # :sturgeon.libera.chat 338 foo_ bar real.ident@real.host
372 # 12.34.56.78 :Actual user@host, Actual IP
373 (re.compile(r":.*\s338\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s:.*"),
374 self.__whoIsActually),
375 # :sturgeon.libera.chat 313 foo_ bar :is an IRC Operator
376 (re.compile(r":.*\s313\s[^ ]+\s([^ ]+)\s:(.*)"),
377 self.__whoIsOperator),
378 # :sturgeon.libera.chat 378 foo_ bar :is connecting from
379 # *@mnch-4d044d5a.pool.mediaWays.net 77.4.77.90
380 (re.compile(r":.*\s378\s[^ ]+\s([^ ]+)\s:.*\s([^ ]+)\s([^ ]+)"),
381 self.__whoIsConnection),
382 ]
383
384 self.__autoWhoTemplate = "WHO {0} %tnf,42"
385 self.__autoWhoTimer = QTimer()
386 self.__autoWhoTimer.setSingleShot(True)
387 self.__autoWhoTimer.timeout.connect(self.__sendAutoWhoCommand)
388 self.__autoWhoRequested = False
389
390 @pyqtSlot()
391 def on_messageEdit_returnPressed(self):
392 """
393 Private slot to send a message to the channel.
394 """
395 msg = self.messageEdit.text()
396 if msg:
397 self.__processUserMessage(msg)
398
399 def __processUserMessage(self, msg):
400 """
401 Private method to process a message entered by the user or via the
402 user list context menu.
403
404 @param msg message to be processed
405 @type str
406 """
407 self.messages.append(
408 '<font color="{0}">{2} <b>&lt;</b><font color="{1}">{3}</font>'
409 '<b>&gt;</b> {4}</font>'.format(
410 Preferences.getIrc("ChannelMessageColour"),
411 Preferences.getIrc("OwnNickColour"),
412 ircTimestamp(), self.__userName,
413 Utilities.html_encode(msg)))
414
415 if msg.startswith("/"):
416 if self.__private:
417 EricMessageBox.information(
418 self,
419 self.tr("Send Message"),
420 self.tr(
421 """Messages starting with a '/' are not allowed"""
422 """ in private chats."""))
423 else:
424 sendData = True
425 # flag set to False, if command was handled
426
427 msgList = msg.split()
428 cmd = msgList[0][1:].upper()
429 if cmd in ["MSG", "QUERY"]:
430 cmd = "PRIVMSG"
431 if len(msgList) > 1:
432 if (
433 msgList[1].strip().lower() in
434 self.__serviceNamesLower
435 ):
436 msg = (
437 "PRIVMSG " +
438 msgList[1].strip().lower() +
439 " :" + " ".join(msgList[2:])
440 )
441 else:
442 msg = "PRIVMSG {0} :{1}".format(
443 msgList[1], " ".join(msgList[2:]))
444 else:
445 msgList[0] = cmd
446 msg = " ".join(msgList)
447 elif cmd == "NOTICE":
448 if len(msgList) > 2:
449 msg = "NOTICE {0} :{1}".format(
450 msgList[1], " ".join(msgList[2:]))
451 else:
452 msg = "NOTICE {0}".format(" ".join(msgList[1:]))
453 elif cmd == "PING":
454 receiver = msgList[1]
455 msg = "PING {0} "
456 self.sendCtcpRequest.emit(receiver, "PING", "")
457 sendData = False
458 elif cmd == "IGNORE":
459 sendData = False
460 if len(msgList) > 1:
461 if msgList[1] == "-r":
462 ignored = False
463 userNamesList = msgList[2:]
464 else:
465 ignored = True
466 userNamesList = msgList[1:]
467 else:
468 userNamesList = []
469 userNames = ",".join(
470 u.rstrip(",") for u in userNamesList).split(",")
471 for userName in userNames:
472 itm = self.__findUser(userName)
473 if itm:
474 itm.setIgnored(ignored)
475 elif cmd == "UNIGNORE":
476 sendData = False
477 if len(msgList) > 1:
478 userNamesList = msgList[1:]
479 else:
480 userNamesList = []
481 userNames = ",".join(
482 u.rstrip(",") for u in userNamesList).split(",")
483 for userName in userNames:
484 itm = self.__findUser(userName)
485 if itm:
486 itm.setIgnored(False)
487 elif cmd == "AWAY":
488 sendData = False
489 if len(msgList) > 1:
490 msg = " ".join(msgList[1:])
491 else:
492 msg = ""
493 self.awayCommand.emit(msg)
494 elif cmd == "JOIN":
495 sendData = False
496 if len(msgList) > 1:
497 channels = msgList[1].split(",")
498 if len(msgList) > 2:
499 keys = msgList[2].split(",")
500 else:
501 keys = []
502 for channel, key in zip_longest(
503 channels, keys, fillvalue=""):
504 self.__ircWidget.joinChannel(channel, key)
505 elif cmd == "PART":
506 sendData = False
507 if len(msgList) == 1:
508 self.leaveChannel()
509 else:
510 self.leaveChannels.emit(msgList[1:])
511 elif cmd == "PARTALL":
512 sendData = False
513 self.leaveAllChannels.emit()
514 else:
515 msg = msg[1:]
516 if sendData:
517 self.sendData.emit(msg)
518 else:
519 if self.__private:
520 self.sendData.emit(
521 "PRIVMSG " + self.__privatePartner + " :" + msg)
522 else:
523 self.sendData.emit(
524 "PRIVMSG " + self.__name + " :" + msg)
525
526 self.messageEdit.clear()
527 self.unsetMarkerLine()
528
529 def requestLeave(self):
530 """
531 Public method to leave the channel.
532 """
533 ok = EricMessageBox.yesNo(
534 self,
535 self.tr("Leave IRC channel"),
536 self.tr(
537 """Do you really want to leave the IRC channel"""
538 """ <b>{0}</b>?""").format(self.__name))
539 if ok:
540 self.leaveChannel()
541
542 def leaveChannel(self):
543 """
544 Public slot to leave the channel.
545 """
546 if not self.__private:
547 self.sendData.emit(
548 "PART " + self.__name + " :" + self.__partMessage)
549 self.channelClosed.emit(self.__name)
550
551 def name(self):
552 """
553 Public method to get the name of the channel.
554
555 @return name of the channel (string)
556 """
557 return self.__name
558
559 def setName(self, name):
560 """
561 Public method to set the name of the channel.
562
563 @param name of the channel (string)
564 """
565 self.__name = name
566
567 def getUsersCount(self):
568 """
569 Public method to get the users count of the channel.
570
571 @return users count of the channel (integer)
572 """
573 return self.usersList.count()
574
575 def userName(self):
576 """
577 Public method to get the nick name of the user.
578
579 @return nick name of the user (string)
580 """
581 return self.__userName
582
583 def setUserName(self, name):
584 """
585 Public method to set the user name for the channel.
586
587 @param name user name for the channel (string)
588 """
589 self.__userName = name
590
591 def partMessage(self):
592 """
593 Public method to get the part message.
594
595 @return part message (string)
596 """
597 return self.__partMessage
598
599 def setPartMessage(self, message):
600 """
601 Public method to set the part message.
602
603 @param message message to be used for PART messages (string)
604 """
605 self.__partMessage = message
606
607 def setPrivate(self, private, partner=""):
608 """
609 Public method to set the private chat mode.
610
611 @param private flag indicating private chat mode (boolean)
612 @param partner name of the partner user (string)
613 """
614 self.__private = private
615 self.__privatePartner = partner
616 self.editTopicButton.setEnabled(private)
617
618 def setPrivateInfo(self, infoText):
619 """
620 Public method to set some info text for private chat mode.
621
622 @param infoText info text to be shown (string)
623 """
624 if self.__private:
625 self.topicLabel.setText(infoText)
626
627 def handleMessage(self, line):
628 """
629 Public method to handle the message sent by the server.
630
631 @param line server message (string)
632 @return flag indicating, if the message was handled (boolean)
633 """
634 for patternRe, patternFunc in self.__patterns:
635 match = patternRe.match(line)
636 if match is not None and patternFunc(match):
637 return True
638
639 return False
640
641 def __message(self, match):
642 """
643 Private method to handle messages to the channel.
644
645 @param match match object that matched the pattern
646 @return flag indicating whether the message was handled (boolean)
647 """
648 # group(1) sender user name
649 # group(2) sender user@host
650 # group(3) target nick
651 # group(4) message
652 if match.group(3).lower() == self.__name.lower():
653 senderName = match.group(1)
654 itm = self.__findUser(senderName)
655 if itm and itm.isIgnored():
656 # user should be ignored
657 return True
658
659 if match.group(4).startswith("\x01"):
660 return self.__handleCtcp(match)
661
662 self.addMessage(senderName, match.group(4))
663 if self.__private and not self.topicLabel.text():
664 self.setPrivateInfo(
665 "{0} - {1}".format(match.group(1), match.group(2)))
666 return True
667
668 return False
669
670 def addMessage(self, sender, msg):
671 """
672 Public method to add a message from external.
673
674 @param sender nick name of the sender (string)
675 @param msg message received from sender (string)
676 """
677 self.__appendMessage(
678 '<font color="{0}">{2} <b>&lt;</b><font color="{1}">{3}</font>'
679 '<b>&gt;</b> {4}</font>'.format(
680 Preferences.getIrc("ChannelMessageColour"),
681 Preferences.getIrc("NickColour"),
682 ircTimestamp(), sender, ircFilter(msg)))
683 if Preferences.getIrc("ShowNotifications"):
684 if Preferences.getIrc("NotifyMessage"):
685 self.__ui.showNotification(
686 UI.PixmapCache.getPixmap("irc48"),
687 self.tr("Channel Message"), msg)
688 elif (
689 Preferences.getIrc("NotifyNick") and
690 self.__userName.lower() in msg.lower()
691 ):
692 self.__ui.showNotification(
693 UI.PixmapCache.getPixmap("irc48"),
694 self.tr("Nick mentioned"), msg)
695
696 def addUsers(self, users):
697 """
698 Public method to add users to the channel.
699
700 @param users list of user names to add (list of string)
701 """
702 for user in users:
703 itm = self.__findUser(user)
704 if itm is None:
705 IrcUserItem(user, self.usersList)
706
707 def __userJoin(self, match):
708 """
709 Private method to handle a user joining the channel.
710
711 @param match match object that matched the pattern
712 @return flag indicating whether the message was handled (boolean)
713 """
714 if match.group(3).lower() == self.__name.lower():
715 if self.__userName != match.group(1):
716 IrcUserItem(match.group(1), self.usersList)
717 msg = self.tr(
718 "{0} has joined the channel {1} ({2}).").format(
719 match.group(1), self.__name, match.group(2))
720 self.__addManagementMessage(
721 IrcChannelWidget.JoinIndicator, msg)
722 else:
723 msg = self.tr(
724 "You have joined the channel {0} ({1}).").format(
725 self.__name, match.group(2))
726 self.__addManagementMessage(
727 IrcChannelWidget.JoinIndicator, msg)
728 if (
729 Preferences.getIrc("ShowNotifications") and
730 Preferences.getIrc("NotifyJoinPart")
731 ):
732 self.__ui.showNotification(
733 UI.PixmapCache.getPixmap("irc48"),
734 self.tr("Join Channel"), msg)
735 return True
736
737 return False
738
739 def __userPart(self, match):
740 """
741 Private method to handle a user leaving the channel.
742
743 @param match match object that matched the pattern
744 @return flag indicating whether the message was handled (boolean)
745 """
746 if match.group(2).lower() == self.__name.lower():
747 itm = self.__findUser(match.group(1))
748 self.usersList.takeItem(self.usersList.row(itm))
749 del itm
750 if match.lastindex == 2:
751 msg = self.tr("{0} has left {1}.").format(
752 match.group(1), self.__name)
753 nmsg = msg
754 self.__addManagementMessage(
755 IrcChannelWidget.LeaveIndicator, msg)
756 else:
757 msg = self.tr("{0} has left {1}: {2}.").format(
758 match.group(1), self.__name, ircFilter(match.group(3)))
759 nmsg = self.tr("{0} has left {1}: {2}.").format(
760 match.group(1), self.__name, match.group(3))
761 self.__addManagementMessage(
762 IrcChannelWidget.LeaveIndicator, msg)
763 if (
764 Preferences.getIrc("ShowNotifications") and
765 Preferences.getIrc("NotifyJoinPart")
766 ):
767 self.__ui.showNotification(
768 UI.PixmapCache.getPixmap("irc48"),
769 self.tr("Leave Channel"), nmsg)
770 return True
771
772 return False
773
774 def __userQuit(self, match):
775 """
776 Private method to handle a user logging off the server.
777
778 @param match match object that matched the pattern
779 @return flag indicating whether the message was handled (boolean)
780 """
781 itm = self.__findUser(match.group(1))
782 if itm:
783 self.usersList.takeItem(self.usersList.row(itm))
784 del itm
785 if match.lastindex == 1:
786 msg = self.tr("{0} has quit {1}.").format(
787 match.group(1), self.__name)
788 self.__addManagementMessage(
789 IrcChannelWidget.MessageIndicator, msg)
790 else:
791 msg = self.tr("{0} has quit {1}: {2}.").format(
792 match.group(1), self.__name, ircFilter(match.group(2)))
793 self.__addManagementMessage(
794 IrcChannelWidget.MessageIndicator, msg)
795 if (
796 Preferences.getIrc("ShowNotifications") and
797 Preferences.getIrc("NotifyJoinPart")
798 ):
799 self.__ui.showNotification(
800 UI.PixmapCache.getPixmap("irc48"),
801 self.tr("Quit"), msg)
802
803 # always return False for other channels and server to process
804 return False
805
806 def __userNickChange(self, match):
807 """
808 Private method to handle a nickname change of a user.
809
810 @param match match object that matched the pattern
811 @return flag indicating whether the message was handled (boolean)
812 """
813 itm = self.__findUser(match.group(1))
814 if itm:
815 itm.setName(match.group(2))
816 if match.group(1) == self.__userName:
817 self.__addManagementMessage(
818 IrcChannelWidget.MessageIndicator,
819 self.tr("You are now known as {0}.").format(
820 match.group(2)))
821 self.__userName = match.group(2)
822 else:
823 self.__addManagementMessage(
824 IrcChannelWidget.MessageIndicator,
825 self.tr("User {0} is now known as {1}.").format(
826 match.group(1), match.group(2)))
827
828 # always return False for other channels and server to process
829 return False
830
831 def __userList(self, match):
832 """
833 Private method to handle the receipt of a list of users of the channel.
834
835 @param match match object that matched the pattern
836 @return flag indicating whether the message was handled (boolean)
837 """
838 if match.group(1).lower() == self.__name.lower():
839 users = match.group(2).split()
840 for user in users:
841 userPrivileges, userName = self.__extractPrivilege(user)
842 itm = self.__findUser(userName)
843 if itm is None:
844 itm = IrcUserItem(userName, self.usersList)
845 for privilege in userPrivileges:
846 itm.changePrivilege(privilege)
847
848 self.__setEditTopicButton()
849 return True
850
851 return False
852
853 def __userAway(self, match):
854 """
855 Private method to handle a topic change of the channel.
856
857 @param match match object that matched the pattern
858 @return flag indicating whether the message was handled (boolean)
859 """
860 if match.group(1).lower() == self.__name.lower():
861 self.__addManagementMessage(
862 self.tr("Away"),
863 self.tr("{0} is away: {1}").format(
864 match.group(2), match.group(3)))
865 return True
866
867 return False
868
869 def __setTopic(self, match):
870 """
871 Private method to handle a topic change of the channel.
872
873 @param match match object that matched the pattern
874 @return flag indicating whether the message was handled (boolean)
875 """
876 if match.group(1).lower() == self.__name.lower():
877 self.topicLabel.setText(match.group(2))
878 self.__addManagementMessage(
879 IrcChannelWidget.MessageIndicator,
880 ircFilter(self.tr('The channel topic is: "{0}".').format(
881 match.group(2))))
882 return True
883
884 return False
885
886 def __topicCreated(self, match):
887 """
888 Private method to handle a topic created message.
889
890 @param match match object that matched the pattern
891 @return flag indicating whether the message was handled (boolean)
892 """
893 if match.group(1).lower() == self.__name.lower():
894 self.__addManagementMessage(
895 IrcChannelWidget.MessageIndicator,
896 self.tr("The topic was set by {0} on {1}.").format(
897 match.group(2),
898 QDateTime.fromSecsSinceEpoch(int(match.group(3)))
899 .toString("yyyy-MM-dd hh:mm")))
900 return True
901
902 return False
903
904 def __channelUrl(self, match):
905 """
906 Private method to handle a channel URL message.
907
908 @param match match object that matched the pattern
909 @return flag indicating whether the message was handled (boolean)
910 """
911 if match.group(1).lower() == self.__name.lower():
912 self.__addManagementMessage(
913 IrcChannelWidget.MessageIndicator,
914 ircFilter(self.tr("Channel URL: {0}").format(
915 match.group(2))))
916 return True
917
918 return False
919
920 def __channelModes(self, match):
921 """
922 Private method to handle a message reporting the channel modes.
923
924 @param match match object that matched the pattern
925 @return flag indicating whether the message was handled (boolean)
926 """
927 if match.group(1).lower() == self.__name.lower():
928 modesDict = getChannelModesDict()
929 modesParameters = match.group(2).split()
930 modeString = modesParameters.pop(0)
931 modes = []
932 for modeChar in modeString:
933 if modeChar == "+":
934 continue
935 elif modeChar == "k":
936 parameter = modesParameters.pop(0)
937 modes.append(self.tr(
938 "password protected ({0})").format(parameter))
939 elif modeChar == "l":
940 parameter = modesParameters.pop(0)
941 modes.append(self.tr(
942 "limited to %n user(s)", "", int(parameter)))
943 elif modeChar in modesDict:
944 modes.append(modesDict[modeChar])
945 else:
946 modes.append(modeChar)
947
948 self.__addManagementMessage(
949 IrcChannelWidget.MessageIndicator,
950 self.tr("Channel modes: {0}.").format(", ".join(modes)))
951
952 return True
953
954 return False
955
956 def __channelCreated(self, match):
957 """
958 Private method to handle a channel created message.
959
960 @param match match object that matched the pattern
961 @return flag indicating whether the message was handled (boolean)
962 """
963 if match.group(1).lower() == self.__name.lower():
964 self.__addManagementMessage(
965 IrcChannelWidget.MessageIndicator,
966 self.tr("This channel was created on {0}.").format(
967 QDateTime.fromSecsSinceEpoch(int(match.group(2)))
968 .toString("yyyy-MM-dd hh:mm")))
969 return True
970
971 return False
972
973 def __updateChannelModes(self, match):
974 """
975 Private method to handle a message reporting the channel modes.
976
977 @param match match object that matched the pattern
978 @return flag indicating whether the message was handled (boolean)
979 """
980 # group(1) user or server
981 # group(2) channel
982 # group(3) modes and parameter list
983 if match.group(2).lower() == self.__name.lower():
984 nick = match.group(1)
985 modesParameters = match.group(3).split()
986 modeString = modesParameters.pop(0)
987 isPlus = True
988 message = ""
989 for mode in modeString:
990 if mode == "+":
991 isPlus = True
992 continue
993 elif mode == "-":
994 isPlus = False
995 continue
996 elif mode == "a":
997 if isPlus:
998 message = self.tr(
999 "{0} sets the channel mode to 'anonymous'."
1000 ).format(nick)
1001 else:
1002 message = self.tr(
1003 "{0} removes the 'anonymous' mode from the"
1004 " channel.").format(nick)
1005 elif mode == "b":
1006 if isPlus:
1007 message = self.tr(
1008 "{0} sets a ban on {1}.").format(
1009 nick, modesParameters.pop(0))
1010 else:
1011 message = self.tr(
1012 "{0} removes the ban on {1}.").format(
1013 nick, modesParameters.pop(0))
1014 elif mode == "c":
1015 if isPlus:
1016 message = self.tr(
1017 "{0} sets the channel mode to 'no colors"
1018 " allowed'.").format(nick)
1019 else:
1020 message = self.tr(
1021 "{0} sets the channel mode to 'allow color"
1022 " codes'.").format(nick)
1023 elif mode == "e":
1024 if isPlus:
1025 message = self.tr(
1026 "{0} sets a ban exception on {1}.").format(
1027 nick, modesParameters.pop(0))
1028 else:
1029 message = self.tr(
1030 "{0} removes the ban exception on {1}.").format(
1031 nick, modesParameters.pop(0))
1032 elif mode == "i":
1033 if isPlus:
1034 message = self.tr(
1035 "{0} sets the channel mode to 'invite only'."
1036 ).format(nick)
1037 else:
1038 message = self.tr(
1039 "{0} removes the 'invite only' mode from the"
1040 " channel.").format(nick)
1041 elif mode == "k":
1042 if isPlus:
1043 message = self.tr(
1044 "{0} sets the channel key to '{1}'.").format(
1045 nick, modesParameters.pop(0))
1046 else:
1047 message = self.tr(
1048 "{0} removes the channel key.").format(nick)
1049 elif mode == "l":
1050 if isPlus:
1051 message = self.tr(
1052 "{0} sets the channel limit to %n nick(s).", "",
1053 int(modesParameters.pop(0))).format(nick)
1054 else:
1055 message = self.tr(
1056 "{0} removes the channel limit.").format(nick)
1057 elif mode == "m":
1058 if isPlus:
1059 message = self.tr(
1060 "{0} sets the channel mode to 'moderated'."
1061 ).format(nick)
1062 else:
1063 message = self.tr(
1064 "{0} sets the channel mode to 'unmoderated'."
1065 ).format(nick)
1066 elif mode == "n":
1067 if isPlus:
1068 message = self.tr(
1069 "{0} sets the channel mode to 'no messages from"
1070 " outside'.").format(nick)
1071 else:
1072 message = self.tr(
1073 "{0} sets the channel mode to 'allow messages"
1074 " from outside'.").format(nick)
1075 elif mode == "p":
1076 if isPlus:
1077 message = self.tr(
1078 "{0} sets the channel mode to 'private'."
1079 ).format(nick)
1080 else:
1081 message = self.tr(
1082 "{0} sets the channel mode to 'public'."
1083 ).format(nick)
1084 elif mode == "q":
1085 if isPlus:
1086 message = self.tr(
1087 "{0} sets the channel mode to 'quiet'."
1088 ).format(nick)
1089 else:
1090 message = self.tr(
1091 "{0} removes the 'quiet' mode from the channel."
1092 ).format(nick)
1093 elif mode == "r":
1094 continue
1095 elif mode == "s":
1096 if isPlus:
1097 message = self.tr(
1098 "{0} sets the channel mode to 'secret'."
1099 ).format(nick)
1100 else:
1101 message = self.tr(
1102 "{0} sets the channel mode to 'visible'."
1103 ).format(nick)
1104 elif mode == "t":
1105 if isPlus:
1106 message = self.tr(
1107 "{0} switches on 'topic protection'.").format(nick)
1108 else:
1109 message = self.tr(
1110 "{0} switches off 'topic protection'."
1111 ).format(nick)
1112 elif mode == "I":
1113 if isPlus:
1114 message = self.tr(
1115 "{0} sets invitation mask {1}.").format(
1116 nick, modesParameters.pop(0))
1117 else:
1118 message = self.tr(
1119 "{0} removes the invitation mask {1}.").format(
1120 nick, modesParameters.pop(0))
1121
1122 self.__addManagementMessage(self.tr("Mode"), message)
1123
1124 return True
1125
1126 return False
1127
1128 def __setUserPrivilege(self, match):
1129 """
1130 Private method to handle a change of user privileges for the channel.
1131
1132 @param match match object that matched the pattern
1133 @return flag indicating whether the message was handled (boolean)
1134 """
1135 if match.group(2).lower() == self.__name.lower():
1136 itm = self.__findUser(match.group(4))
1137 if itm:
1138 itm.changePrivilege(match.group(3))
1139 self.__setEditTopicButton()
1140 self.__addManagementMessage(
1141 IrcChannelWidget.MessageIndicator,
1142 self.tr("{0} sets mode for {1}: {2}.").format(
1143 match.group(1), match.group(4), match.group(3)))
1144 return True
1145
1146 return False
1147
1148 def __ignore(self, match):
1149 """
1150 Private method to handle a channel message we are not interested in.
1151
1152 @param match match object that matched the pattern
1153 @return flag indicating whether the message was handled (boolean)
1154 """
1155 if match.group(1).lower() == self.__name.lower():
1156 return True
1157
1158 return False
1159
1160 def __help(self, match):
1161 """
1162 Private method to handle a help message.
1163
1164 @param match match object that matched the pattern
1165 @return flag indicating whether the message was handled (boolean)
1166 """
1167 self.__addManagementMessage(
1168 self.tr("Help"),
1169 "{0} {1}".format(match.group(1), ircFilter(match.group(2))))
1170 return True
1171
1172 def __handleCtcp(self, match):
1173 """
1174 Private method to handle a CTCP channel command.
1175
1176 @param match reference to the match object
1177 @return flag indicating, if the message was handled (boolean)
1178 """
1179 # group(1) sender user name
1180 # group(2) sender user@host
1181 # group(3) target nick
1182 # group(4) message
1183 if match.group(4).startswith("\x01"):
1184 ctcpCommand = match.group(4)[1:].split("\x01", 1)[0]
1185 if " " in ctcpCommand:
1186 ctcpRequest, ctcpArg = ctcpCommand.split(" ", 1)
1187 else:
1188 ctcpRequest, ctcpArg = ctcpCommand, ""
1189 ctcpRequest = ctcpRequest.lower()
1190 if ctcpRequest == "version":
1191 msg = "Eric IRC client {0}, {1}".format(Version, Copyright)
1192 self.__addManagementMessage(
1193 self.tr("CTCP"),
1194 self.tr("Received Version request from {0}.").format(
1195 match.group(1)))
1196 self.sendCtcpReply.emit(match.group(1), "VERSION " + msg)
1197 elif ctcpRequest == "ping":
1198 self.__addManagementMessage(
1199 self.tr("CTCP"),
1200 self.tr(
1201 "Received CTCP-PING request from {0},"
1202 " sending answer.").format(match.group(1)))
1203 self.sendCtcpReply.emit(
1204 match.group(1), "PING {0}".format(ctcpArg))
1205 elif ctcpRequest == "clientinfo":
1206 self.__addManagementMessage(
1207 self.tr("CTCP"),
1208 self.tr(
1209 "Received CTCP-CLIENTINFO request from {0},"
1210 " sending answer.").format(match.group(1)))
1211 self.sendCtcpReply.emit(
1212 match.group(1), "CLIENTINFO CLIENTINFO PING VERSION")
1213 else:
1214 self.__addManagementMessage(
1215 self.tr("CTCP"),
1216 self.tr("Received unknown CTCP-{0} request from {1}.")
1217 .format(ctcpRequest, match.group(1)))
1218 return True
1219
1220 return False
1221
1222 def setUserPrivilegePrefix(self, prefixes):
1223 """
1224 Public method to set the user privilege to prefix mapping.
1225
1226 @param prefixes dictionary with privilege as key and prefix as value
1227 """
1228 self.__prefixToPrivilege = {}
1229 for privilege, prefix in prefixes.items():
1230 if prefix:
1231 self.__prefixToPrivilege[prefix] = privilege
1232
1233 def __findUser(self, name):
1234 """
1235 Private method to find the user in the list of users.
1236
1237 @param name user name to search for (string)
1238 @return reference to the list entry (QListWidgetItem)
1239 """
1240 for row in range(self.usersList.count()):
1241 itm = self.usersList.item(row)
1242 if itm.name() == name:
1243 return itm
1244
1245 return None
1246
1247 def __extractPrivilege(self, name):
1248 """
1249 Private method to extract the user privileges out of the name.
1250
1251 @param name user name and prefixes (string)
1252 @return list of privileges and user name (list of string, string)
1253 """
1254 privileges = []
1255 while name[0] in self.__prefixToPrivilege:
1256 prefix = name[0]
1257 privileges.append(self.__prefixToPrivilege[prefix])
1258 name = name[1:]
1259 if name[0] == ",":
1260 name = name[1:]
1261
1262 return privileges, name
1263
1264 def __addManagementMessage(self, indicator, message):
1265 """
1266 Private method to add a channel management message to the list.
1267
1268 @param indicator indicator to be shown (string)
1269 @param message message to be shown (string)
1270 """
1271 if indicator == self.JoinIndicator:
1272 color = Preferences.getIrc("JoinChannelColour")
1273 elif indicator == self.LeaveIndicator:
1274 color = Preferences.getIrc("LeaveChannelColour")
1275 else:
1276 color = Preferences.getIrc("ChannelInfoColour")
1277 self.__appendMessage(
1278 '<font color="{0}">{1} <b>[</b>{2}<b>]</b> {3}</font>'.format(
1279 color, ircTimestamp(), indicator, message))
1280
1281 def __appendMessage(self, message):
1282 """
1283 Private slot to append a message.
1284
1285 @param message message to be appended (string)
1286 """
1287 if (
1288 self.__hidden and
1289 self.__markerLine == "" and
1290 Preferences.getIrc("MarkPositionWhenHidden")
1291 ):
1292 self.setMarkerLine()
1293 self.messages.append(message)
1294
1295 def setMarkerLine(self):
1296 """
1297 Public method to draw a line to mark the current position.
1298 """
1299 self.unsetMarkerLine()
1300 self.__markerLine = (
1301 '<span style=" color:{0}; background-color:{1};">{2}</span>'
1302 .format(Preferences.getIrc("MarkerLineForegroundColour"),
1303 Preferences.getIrc("MarkerLineBackgroundColour"),
1304 self.tr('--- New From Here ---'))
1305 )
1306 self.messages.append(self.__markerLine)
1307
1308 def unsetMarkerLine(self):
1309 """
1310 Public method to remove the marker line.
1311 """
1312 if self.__markerLine:
1313 txt = self.messages.toHtml()
1314 if txt.endswith(self.__markerLine + "</p></body></html>"):
1315 # remove empty last paragraph
1316 pos = txt.rfind("<p")
1317 txt = txt[:pos] + "</body></html>"
1318 else:
1319 txt = txt.replace(self.__markerLine, "")
1320 self.messages.setHtml(txt)
1321 self.__markerLine = ""
1322 self.messages.moveCursor(QTextCursor.MoveOperation.End)
1323
1324 def __clearMessages(self):
1325 """
1326 Private slot to clear the contents of the messages display.
1327 """
1328 self.messages.clear()
1329
1330 def __copyMessages(self):
1331 """
1332 Private slot to copy the selection of the messages display to the
1333 clipboard.
1334 """
1335 self.messages.copy()
1336
1337 def __copyAllMessages(self):
1338 """
1339 Private slot to copy the contents of the messages display to the
1340 clipboard.
1341 """
1342 txt = self.messages.toPlainText()
1343 if txt:
1344 cb = QApplication.clipboard()
1345 cb.setText(txt)
1346
1347 def __cutAllMessages(self):
1348 """
1349 Private slot to cut the contents of the messages display to the
1350 clipboard.
1351 """
1352 txt = self.messages.toPlainText()
1353 if txt:
1354 cb = QApplication.clipboard()
1355 cb.setText(txt)
1356 self.messages.clear()
1357
1358 def __saveMessages(self):
1359 """
1360 Private slot to save the contents of the messages display.
1361 """
1362 hasText = not self.messages.document().isEmpty()
1363 if hasText:
1364 if Utilities.isWindowsPlatform():
1365 htmlExtension = "htm"
1366 else:
1367 htmlExtension = "html"
1368 fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
1369 self,
1370 self.tr("Save Messages"),
1371 "",
1372 self.tr(
1373 "HTML Files (*.{0});;Text Files (*.txt);;All Files (*)")
1374 .format(htmlExtension),
1375 None,
1376 EricFileDialog.DontConfirmOverwrite)
1377 if fname:
1378 fpath = pathlib.Path(fname)
1379 if not fpath.suffix:
1380 ex = selectedFilter.split("(*")[1].split(")")[0]
1381 if ex:
1382 fpath = fpath.with_suffix(ex)
1383 if fpath.exists():
1384 res = EricMessageBox.yesNo(
1385 self,
1386 self.tr("Save Messages"),
1387 self.tr("<p>The file <b>{0}</b> already exists."
1388 " Overwrite it?</p>").format(fpath),
1389 icon=EricMessageBox.Warning)
1390 if not res:
1391 return
1392
1393 try:
1394 txt = (
1395 self.messages.toHtml()
1396 if fpath.suffix.lower() in [".htm", ".html"] else
1397 self.messages.toPlainText()
1398 )
1399 with fpath.open("w", encoding="utf-8") as f:
1400 f.write(txt)
1401 except OSError as err:
1402 EricMessageBox.critical(
1403 self,
1404 self.tr("Error saving Messages"),
1405 self.tr(
1406 """<p>The messages contents could not be written"""
1407 """ to <b>{0}</b></p><p>Reason: {1}</p>""")
1408 .format(fpath, str(err)))
1409
1410 def __initMessagesMenu(self):
1411 """
1412 Private slot to initialize the context menu of the messages pane.
1413 """
1414 self.__messagesMenu = QMenu(self)
1415 self.__copyMessagesAct = self.__messagesMenu.addAction(
1416 UI.PixmapCache.getIcon("editCopy"),
1417 self.tr("Copy"), self.__copyMessages)
1418 self.__messagesMenu.addSeparator()
1419 self.__cutAllMessagesAct = self.__messagesMenu.addAction(
1420 UI.PixmapCache.getIcon("editCut"),
1421 self.tr("Cut all"), self.__cutAllMessages)
1422 self.__copyAllMessagesAct = self.__messagesMenu.addAction(
1423 UI.PixmapCache.getIcon("editCopy"),
1424 self.tr("Copy all"), self.__copyAllMessages)
1425 self.__messagesMenu.addSeparator()
1426 self.__clearMessagesAct = self.__messagesMenu.addAction(
1427 UI.PixmapCache.getIcon("editDelete"),
1428 self.tr("Clear"), self.__clearMessages)
1429 self.__messagesMenu.addSeparator()
1430 self.__saveMessagesAct = self.__messagesMenu.addAction(
1431 UI.PixmapCache.getIcon("fileSave"),
1432 self.tr("Save"), self.__saveMessages)
1433 self.__messagesMenu.addSeparator()
1434 self.__setMarkerMessagesAct = self.__messagesMenu.addAction(
1435 self.tr("Mark Current Position"), self.setMarkerLine)
1436 self.__unsetMarkerMessagesAct = self.__messagesMenu.addAction(
1437 self.tr("Remove Position Marker"),
1438 self.unsetMarkerLine)
1439
1440 self.on_messages_copyAvailable(False)
1441
1442 @pyqtSlot(bool)
1443 def on_messages_copyAvailable(self, yes):
1444 """
1445 Private slot to react to text selection/deselection of the messages
1446 edit.
1447
1448 @param yes flag signaling the availability of selected text (boolean)
1449 """
1450 self.__copyMessagesAct.setEnabled(yes)
1451
1452 @pyqtSlot(QPoint)
1453 def on_messages_customContextMenuRequested(self, pos):
1454 """
1455 Private slot to show the context menu of the messages pane.
1456
1457 @param pos the position of the mouse pointer (QPoint)
1458 """
1459 enable = not self.messages.document().isEmpty()
1460 self.__cutAllMessagesAct.setEnabled(enable)
1461 self.__copyAllMessagesAct.setEnabled(enable)
1462 self.__saveMessagesAct.setEnabled(enable)
1463 self.__setMarkerMessagesAct.setEnabled(self.__markerLine == "")
1464 self.__unsetMarkerMessagesAct.setEnabled(self.__markerLine != "")
1465 self.__messagesMenu.popup(self.messages.mapToGlobal(pos))
1466
1467 def __whoIs(self):
1468 """
1469 Private slot to get information about the selected user.
1470 """
1471 self.__whoIsNick = self.usersList.selectedItems()[0].text()
1472 self.sendData.emit("WHOIS " + self.__whoIsNick)
1473
1474 def __openPrivateChat(self):
1475 """
1476 Private slot to open a chat with the selected user.
1477 """
1478 user = self.usersList.selectedItems()[0].text()
1479 self.openPrivateChat.emit(user)
1480
1481 def __sendUserMessage(self):
1482 """
1483 Private slot to send a private message to a specific user.
1484 """
1485 from EricWidgets import EricTextInputDialog
1486
1487 user = self.usersList.selectedItems()[0].text()
1488 ok, message = EricTextInputDialog.getText(
1489 self, self.tr("Send Message"),
1490 self.tr("Enter the message to be sent:"),
1491 minimumWidth=400)
1492 if ok and message:
1493 self.__processUserMessage("/MSG {0} {1}".format(user, message))
1494
1495 def __sendUserQuery(self):
1496 """
1497 Private slot to send a query message to a specific user.
1498 """
1499 from EricWidgets import EricTextInputDialog
1500
1501 user = self.usersList.selectedItems()[0].text()
1502 ok, message = EricTextInputDialog.getText(
1503 self, self.tr("Send Query"),
1504 self.tr("Enter the message to be sent:"),
1505 minimumWidth=400)
1506 if ok and message:
1507 self.__processUserMessage("/QUERY {0} {1}".format(user, message))
1508
1509 def __sendUserNotice(self):
1510 """
1511 Private slot to send a notice message to a specific user.
1512 """
1513 from EricWidgets import EricTextInputDialog
1514
1515 user = self.usersList.selectedItems()[0].text()
1516 ok, message = EricTextInputDialog.getText(
1517 self, self.tr("Send Notice"),
1518 self.tr("Enter the message to be sent:"),
1519 minimumWidth=400)
1520 if ok and message:
1521 self.__processUserMessage("/NOTICE {0} {1}".format(user, message))
1522
1523 def __pingUser(self):
1524 """
1525 Private slot to send a ping to a specific user.
1526 """
1527 user = self.usersList.selectedItems()[0].text()
1528 self.__processUserMessage("/PING {0}".format(user))
1529
1530 def __ignoreUser(self):
1531 """
1532 Private slot to ignore a specific user.
1533 """
1534 user = self.usersList.selectedItems()[0].text()
1535 self.__processUserMessage("/IGNORE {0}".format(user))
1536
1537 def __initUsersMenu(self):
1538 """
1539 Private slot to initialize the users list context menu.
1540 """
1541 self.__usersMenu = QMenu(self)
1542 self.__whoIsAct = self.__usersMenu.addAction(
1543 self.tr("Who Is"), self.__whoIs)
1544 self.__usersMenu.addSeparator()
1545 self.__privateChatAct = self.__usersMenu.addAction(
1546 self.tr("Private Chat"), self.__openPrivateChat)
1547 self.__usersMenu.addSeparator()
1548 self.__sendUserMessageAct = self.__usersMenu.addAction(
1549 self.tr("Send Message"), self.__sendUserMessage)
1550 self.__sendUserQueryAct = self.__usersMenu.addAction(
1551 self.tr("Send Query"), self.__sendUserQuery)
1552 self.__sendUserNoticeAct = self.__usersMenu.addAction(
1553 self.tr("Send Notice"), self.__sendUserNotice)
1554 self.__usersMenu.addSeparator()
1555 self.__pingUserAct = self.__usersMenu.addAction(
1556 self.tr("Send Ping"), self.__pingUser)
1557 self.__ignoreUserAct = self.__usersMenu.addAction(
1558 self.tr("Ignore User"), self.__ignoreUser)
1559 self.__usersMenu.addSeparator()
1560 self.__usersListRefreshAct = self.__usersMenu.addAction(
1561 self.tr("Refresh"), self.__sendAutoWhoCommand)
1562
1563 @pyqtSlot(QPoint)
1564 def on_usersList_customContextMenuRequested(self, pos):
1565 """
1566 Private slot to show the context menu of the users list.
1567
1568 @param pos the position of the mouse pointer (QPoint)
1569 """
1570 enable = len(self.usersList.selectedItems()) > 0
1571 enablePrivate = enable and not self.__private
1572 itm = self.usersList.itemAt(pos)
1573 if itm and enablePrivate:
1574 enablePrivate = itm.text().lower() not in [
1575 "chanserv", self.__userName.lower()]
1576 self.__whoIsAct.setEnabled(enable)
1577 self.__privateChatAct.setEnabled(enablePrivate)
1578 self.__usersListRefreshAct.setEnabled(
1579 self.usersList.count() <= Preferences.getIrc("AutoUserInfoMax"))
1580 self.__usersMenu.popup(self.usersList.mapToGlobal(pos))
1581
1582 def hideEvent(self, evt):
1583 """
1584 Protected method handling hide events.
1585
1586 @param evt reference to the hide event (QHideEvent)
1587 """
1588 self.__hidden = True
1589
1590 def showEvent(self, evt):
1591 """
1592 Protected method handling show events.
1593
1594 @param evt reference to the show event (QShowEvent)
1595 """
1596 self.__hidden = False
1597
1598 def initAutoWho(self):
1599 """
1600 Public method to initialize the Auto Who system.
1601 """
1602 if Preferences.getIrc("AutoUserInfoLookup"):
1603 self.__autoWhoTimer.setInterval(
1604 Preferences.getIrc("AutoUserInfoInterval") * 1000)
1605 self.__autoWhoTimer.start()
1606
1607 @pyqtSlot()
1608 def __sendAutoWhoCommand(self):
1609 """
1610 Private slot to send the WHO command to update the users list.
1611 """
1612 if self.usersList.count() <= Preferences.getIrc("AutoUserInfoMax"):
1613 self.__autoWhoRequested = True
1614 self.sendData.emit(self.__autoWhoTemplate.format(self.__name))
1615
1616 def __autoWhoEntry(self, match):
1617 """
1618 Private method to handle a WHO entry returned by the server as
1619 requested automatically.
1620
1621 @param match match object that matched the pattern
1622 @return flag indicating whether the message was handled (boolean)
1623 """
1624 # group(1) nick
1625 # group(2) user flags
1626 if self.__autoWhoRequested:
1627 itm = self.__findUser(match.group(1))
1628 if itm:
1629 itm.parseWhoFlags(match.group(2))
1630 return True
1631
1632 return False
1633
1634 def __whoEnd(self, match):
1635 """
1636 Private method to handle the end of the WHO list.
1637
1638 @param match match object that matched the pattern
1639 @return flag indicating whether the message was handled (boolean)
1640 """
1641 if match.group(1).lower() == self.__name.lower():
1642 if self.__autoWhoRequested:
1643 self.__autoWhoRequested = False
1644 self.initAutoWho()
1645 else:
1646 self.__addManagementMessage(
1647 self.tr("Who"),
1648 self.tr("End of WHO list for {0}.").format(
1649 match.group(1)))
1650 return True
1651
1652 return False
1653
1654 def __whoEntry(self, match):
1655 """
1656 Private method to handle a WHO entry returned by the server as
1657 requested manually.
1658
1659 @param match match object that matched the pattern
1660 @return flag indicating whether the message was handled (boolean)
1661 """
1662 # group(1) channel
1663 # group(2) user
1664 # group(3) host
1665 # group(4) nick
1666 # group(5) user flags
1667 # group(6) real name
1668 if match.group(1).lower() == self.__name.lower():
1669 away = (
1670 self.tr(" (Away)")
1671 if match.group(5).startswith("G") else ""
1672 )
1673 self.__addManagementMessage(
1674 self.tr("Who"),
1675 self.tr("{0} is {1}@{2} ({3}){4}").format(
1676 match.group(4), match.group(2), match.group(3),
1677 match.group(6), away))
1678 return True
1679
1680 return False
1681
1682 def __whoIsUser(self, match):
1683 """
1684 Private method to handle the WHOIS user reply.
1685
1686 @param match match object that matched the pattern
1687 @return flag indicating whether the message was handled (boolean)
1688 """
1689 # group(1) nick
1690 # group(2) user
1691 # group(3) host
1692 # group(4) real name
1693 if match.group(1) == self.__whoIsNick:
1694 realName = match.group(4).replace("<", "&lt;").replace(">", "&gt;")
1695 self.__addManagementMessage(
1696 self.tr("Whois"),
1697 self.tr("{0} is {1}@{2} ({3}).").format(
1698 match.group(1), match.group(2), match.group(3), realName))
1699 return True
1700
1701 return False
1702
1703 def __whoIsChannels(self, match):
1704 """
1705 Private method to handle the WHOIS channels reply.
1706
1707 @param match match object that matched the pattern
1708 @return flag indicating whether the message was handled (boolean)
1709 """
1710 # group(1) nick
1711 # group(2) channels
1712 if match.group(1) == self.__whoIsNick:
1713 userChannels = []
1714 voiceChannels = []
1715 opChannels = []
1716 halfopChannels = []
1717 ownerChannels = []
1718 adminChannels = []
1719
1720 # generate the list of channels the user is in
1721 channelList = match.group(2).split()
1722 for channel in channelList:
1723 if channel.startswith(("*", "&")):
1724 adminChannels.append(channel[1:])
1725 elif (
1726 channel.startswith(("!", "~")) and
1727 self.__ircWidget.isChannelName(channel[1:])
1728 ):
1729 ownerChannels.append(channel[1:])
1730 elif channel.startswith("@+"):
1731 opChannels.append(channel[2:])
1732 elif channel.startswith("@"):
1733 opChannels.append(channel[1:])
1734 elif channel.startswith("%"):
1735 halfopChannels.append(channel[1:])
1736 elif channel.startswith("+"):
1737 voiceChannels.append(channel[1:])
1738 else:
1739 userChannels.append(channel)
1740
1741 # show messages
1742 if userChannels:
1743 self.__addManagementMessage(
1744 self.tr("Whois"),
1745 self.tr("{0} is a user on channels: {1}").format(
1746 match.group(1), " ".join(userChannels)))
1747 if voiceChannels:
1748 self.__addManagementMessage(
1749 self.tr("Whois"),
1750 self.tr("{0} has voice on channels: {1}").format(
1751 match.group(1), " ".join(voiceChannels)))
1752 if halfopChannels:
1753 self.__addManagementMessage(
1754 self.tr("Whois"),
1755 self.tr("{0} is a halfop on channels: {1}").format(
1756 match.group(1), " ".join(halfopChannels)))
1757 if opChannels:
1758 self.__addManagementMessage(
1759 self.tr("Whois"),
1760 self.tr("{0} is an operator on channels: {1}").format(
1761 match.group(1), " ".join(opChannels)))
1762 if ownerChannels:
1763 self.__addManagementMessage(
1764 self.tr("Whois"),
1765 self.tr("{0} is owner of channels: {1}").format(
1766 match.group(1), " ".join(ownerChannels)))
1767 if adminChannels:
1768 self.__addManagementMessage(
1769 self.tr("Whois"),
1770 self.tr("{0} is admin on channels: {1}").format(
1771 match.group(1), " ".join(adminChannels)))
1772 return True
1773
1774 return False
1775
1776 def __whoIsServer(self, match):
1777 """
1778 Private method to handle the WHOIS server reply.
1779
1780 @param match match object that matched the pattern
1781 @return flag indicating whether the message was handled (boolean)
1782 """
1783 # group(1) nick
1784 # group(2) server
1785 # group(3) server info
1786 if match.group(1) == self.__whoIsNick:
1787 self.__addManagementMessage(
1788 self.tr("Whois"),
1789 self.tr("{0} is online via {1} ({2}).").format(
1790 match.group(1), match.group(2), match.group(3)))
1791 return True
1792
1793 return False
1794
1795 def __whoIsOperator(self, match):
1796 """
1797 Private method to handle the WHOIS operator reply.
1798
1799 @param match match object that matched the pattern
1800 @return flag indicating whether the message was handled (boolean)
1801 """
1802 # group(1) nick
1803 # group(2) message
1804 if match.group(1) == self.__whoIsNick:
1805 if match.group(2).lower().startswith("is an irc operator"):
1806 self.__addManagementMessage(
1807 self.tr("Whois"),
1808 self.tr("{0} is an IRC Operator.").format(
1809 match.group(1)))
1810 else:
1811 self.__addManagementMessage(
1812 self.tr("Whois"),
1813 "{0} {1}".format(match.group(1), match.group(2)))
1814 return True
1815
1816 return False
1817
1818 def __whoIsIdle(self, match):
1819 """
1820 Private method to handle the WHOIS idle reply.
1821
1822 @param match match object that matched the pattern
1823 @return flag indicating whether the message was handled (boolean)
1824 """
1825 # group(1) nick
1826 # group(2) idle seconds
1827 # group(3) signon time
1828 if match.group(1) == self.__whoIsNick:
1829 seconds = int(match.group(2))
1830 minutes = seconds // 60
1831 hours = minutes // 60
1832 days = hours // 24
1833
1834 signonTimestamp = int(match.group(3))
1835 signonTime = QDateTime()
1836 signonTime.setTime_t(signonTimestamp)
1837
1838 if days:
1839 daysString = self.tr("%n day(s)", "", days)
1840 hoursString = self.tr("%n hour(s)", "", hours)
1841 minutesString = self.tr("%n minute(s)", "", minutes)
1842 secondsString = self.tr("%n second(s)", "", seconds)
1843 self.__addManagementMessage(
1844 self.tr("Whois"),
1845 self.tr(
1846 "{0} has been idle for {1}, {2}, {3}, and {4}.",
1847 "{0} = name of person, {1} = (x days),"
1848 " {2} = (x hours), {3} = (x minutes),"
1849 " {4} = (x seconds)").format(
1850 match.group(1), daysString, hoursString, minutesString,
1851 secondsString))
1852 elif hours:
1853 hoursString = self.tr("%n hour(s)", "", hours)
1854 minutesString = self.tr("%n minute(s)", "", minutes)
1855 secondsString = self.tr("%n second(s)", "", seconds)
1856 self.__addManagementMessage(
1857 self.tr("Whois"),
1858 self.tr(
1859 "{0} has been idle for {1}, {2}, and {3}.",
1860 "{0} = name of person, {1} = (x hours), "
1861 "{2} = (x minutes), {3} = (x seconds)")
1862 .format(match.group(1), hoursString, minutesString,
1863 secondsString))
1864 elif minutes:
1865 minutesString = self.tr("%n minute(s)", "", minutes)
1866 secondsString = self.tr("%n second(s)", "", seconds)
1867 self.__addManagementMessage(
1868 self.tr("Whois"),
1869 self.tr(
1870 "{0} has been idle for {1} and {2}.",
1871 "{0} = name of person, {1} = (x minutes), "
1872 "{3} = (x seconds)")
1873 .format(match.group(1), minutesString, secondsString))
1874 else:
1875 self.__addManagementMessage(
1876 self.tr("Whois"),
1877 self.tr(
1878 "{0} has been idle for %n second(s).", "",
1879 seconds).format(match.group(1)))
1880
1881 if not signonTime.isNull():
1882 self.__addManagementMessage(
1883 self.tr("Whois"),
1884 self.tr("{0} has been online since {1}.").format(
1885 match.group(1),
1886 signonTime.toString("yyyy-MM-dd, hh:mm:ss")))
1887 return True
1888
1889 return False
1890
1891 def __whoIsEnd(self, match):
1892 """
1893 Private method to handle the end of WHOIS reply.
1894
1895 @param match match object that matched the pattern
1896 @return flag indicating whether the message was handled (boolean)
1897 """
1898 # group(1) nick
1899 # group(2) end message
1900 if match.group(1) == self.__whoIsNick:
1901 self.__whoIsNick = ""
1902 self.__addManagementMessage(
1903 self.tr("Whois"),
1904 self.tr("End of WHOIS list for {0}.").format(
1905 match.group(1)))
1906 return True
1907
1908 return False
1909
1910 def __whoIsIdentify(self, match):
1911 """
1912 Private method to handle the WHOIS identify and identified replies.
1913
1914 @param match match object that matched the pattern
1915 @return flag indicating whether the message was handled (boolean)
1916 """
1917 # group(1) nick
1918 # group(2) identified message
1919 if match.group(1) == self.__whoIsNick:
1920 self.__addManagementMessage(
1921 self.tr("Whois"),
1922 self.tr("{0} is an identified user.").format(
1923 match.group(1)))
1924 return True
1925
1926 return False
1927
1928 def __whoIsHelper(self, match):
1929 """
1930 Private method to handle the WHOIS helper reply.
1931
1932 @param match match object that matched the pattern
1933 @return flag indicating whether the message was handled (boolean)
1934 """
1935 # group(1) nick
1936 # group(2) helper message
1937 if match.group(1) == self.__whoIsNick:
1938 self.__addManagementMessage(
1939 self.tr("Whois"),
1940 self.tr("{0} is available for help.").format(
1941 match.group(1)))
1942 return True
1943
1944 return False
1945
1946 def __whoIsAccount(self, match):
1947 """
1948 Private method to handle the WHOIS account reply.
1949
1950 @param match match object that matched the pattern
1951 @return flag indicating whether the message was handled (boolean)
1952 """
1953 # group(1) nick
1954 # group(2) login name
1955 if match.group(1) == self.__whoIsNick:
1956 self.__addManagementMessage(
1957 self.tr("Whois"),
1958 self.tr("{0} is logged in as {1}.").format(
1959 match.group(1), match.group(2)))
1960 return True
1961
1962 return False
1963
1964 def __whoIsActually(self, match):
1965 """
1966 Private method to handle the WHOIS actually reply.
1967
1968 @param match match object that matched the pattern
1969 @return flag indicating whether the message was handled (boolean)
1970 """
1971 # group(1) nick
1972 # group(2) actual user@host
1973 # group(3) actual IP
1974 if match.group(1) == self.__whoIsNick:
1975 self.__addManagementMessage(
1976 self.tr("Whois"),
1977 self.tr(
1978 "{0} is actually using the host {1} (IP: {2}).").format(
1979 match.group(1), match.group(2), match.group(3)))
1980 return True
1981
1982 return False
1983
1984 def __whoIsSecure(self, match):
1985 """
1986 Private method to handle the WHOIS secure reply.
1987
1988 @param match match object that matched the pattern
1989 @return flag indicating whether the message was handled (boolean)
1990 """
1991 # group(1) nick
1992 if match.group(1) == self.__whoIsNick:
1993 self.__addManagementMessage(
1994 self.tr("Whois"),
1995 self.tr("{0} is using a secure connection.").format(
1996 match.group(1)))
1997 return True
1998
1999 return False
2000
2001 def __whoIsConnection(self, match):
2002 """
2003 Private method to handle the WHOIS connection reply.
2004
2005 @param match match object that matched the pattern
2006 @return flag indicating whether the message was handled (boolean)
2007 """
2008 # group(1) nick
2009 # group(2) host name
2010 # group(3) IP
2011 if match.group(1) == self.__whoIsNick:
2012 self.__addManagementMessage(
2013 self.tr("Whois"),
2014 self.tr("{0} is connecting from {1} (IP: {2}).").format(
2015 match.group(1), match.group(2), match.group(3)))
2016 return True
2017
2018 return False
2019
2020 def __setEditTopicButton(self):
2021 """
2022 Private method to set the visibility of the Edit Topic button.
2023 """
2024 itm = self.__findUser(self.__userName)
2025 if itm:
2026 self.editTopicButton.setVisible(itm.canChangeTopic())
2027
2028 @pyqtSlot()
2029 def on_editTopicButton_clicked(self):
2030 """
2031 Private slot to change the topic of the channel.
2032 """
2033 topic, ok = QInputDialog.getText(
2034 self,
2035 self.tr("Edit Channel Topic"),
2036 self.tr("Enter the topic for this channel:"),
2037 QLineEdit.EchoMode.Normal,
2038 self.topicLabel.text())
2039 if ok and topic != "":
2040 self.sendData.emit("TOPIC {0} :{1}".format(
2041 self.__name, topic))
2042
2043 @pyqtSlot(QUrl)
2044 def on_messages_anchorClicked(self, url):
2045 """
2046 Private slot to open links in the default browser.
2047
2048 @param url URL to be opened (QUrl)
2049 """
2050 QDesktopServices.openUrl(url)

eric ide

mercurial