eric7/Network/IRC/IrcChannelWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8259
2bbec88047dd
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2021 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 re
13
14 from PyQt5.QtCore import (
15 pyqtSlot, pyqtSignal, QDateTime, QPoint, QFileInfo, QTimer, QUrl,
16 QCoreApplication
17 )
18 from PyQt5.QtGui import QIcon, QPainter, QTextCursor, QDesktopServices
19 from PyQt5.QtWidgets import (
20 QWidget, QListWidgetItem, QMenu, QApplication, QInputDialog, QLineEdit
21 )
22
23 from E5Gui import E5MessageBox, E5FileDialog
24 from E5Gui.E5Application import e5App
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 = e5App().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([height * 0.3, 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.freenode.net 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.freenode.net 301 foo_ bar :Gone away for now
309 (re.compile(r":.*\s301\s([^ ]+)\s([^ ]+)\s:(.+)"),
310 self.__userAway),
311 # :sturgeon.freenode.net 315 foo_ #eric-ide :End of /WHO list.
312 (re.compile(r":.*\s315\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoEnd),
313 # :zelazny.freenode.net 324 foo_ #eric-ide +cnt
314 (re.compile(r":.*\s324\s.*\s([^ ]+)\s(.+)"), self.__channelModes),
315 # :zelazny.freenode.net 328 foo_ #eric-ide :http://www.bugger.com/
316 (re.compile(r":.*\s328\s.*\s([^ ]+)\s:(.+)"), self.__channelUrl),
317 # :zelazny.freenode.net 329 foo_ #eric-ide 1353001005
318 (re.compile(r":.*\s329\s.*\s([^ ]+)\s(.+)"),
319 self.__channelCreated),
320 # :zelazny.freenode.net 332 foo_ #eric-ide :eric support channel
321 (re.compile(r":.*\s332\s.*\s([^ ]+)\s:(.*)"), self.__setTopic),
322 # :zelazny.freenode.net foo_ 333 #eric-ide foo 1353089020
323 (re.compile(r":.*\s333\s.*\s([^ ]+)\s([^ ]+)\s(\d+)"),
324 self.__topicCreated),
325 # :cameron.freenode.net 352 detlev_ #eric-ide ~foo foohost.bar.net
326 # cameron.freenode.net 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.freenode.net 353 foo_ @ #eric-ide :@user1 +user2 user3
331 (re.compile(r":.*\s353\s.*\s.\s([^ ]+)\s:(.*)"), self.__userList),
332 # :sturgeon.freenode.net 354 foo_ 42 ChanServ H@
333 (re.compile(r":.*\s354\s[^ ]+\s42\s([^ ]+)\s(.*)"),
334 self.__autoWhoEntry),
335 # :zelazny.freenode.net 366 foo_ #eric-ide :End of /NAMES list.
336 (re.compile(r":.*\s366\s.*\s([^ ]+)\s:(.*)"), self.__ignore),
337 # :sturgeon.freenode.net 704 foo_ index :Help topics available:
338 (re.compile(r":.*\s70[456]\s[^ ]+\s([^ ]+)\s:(.*)"), self.__help),
339
340 # WHOIS replies
341 # :sturgeon.freenode.net 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.freenode.net 319 foo_ bar :@#eric-ide
346 (re.compile(r":.*\s319\s[^ ]+\s([^ ]+)\s:(.*)"),
347 self.__whoIsChannels),
348 # :sturgeon.freenode.net 312 foo_ bar sturgeon.freenode.net :London
349 (re.compile(r":.*\s312\s[^ ]+\s([^ ]+)\s([^ ]+)\s:(.*)"),
350 self.__whoIsServer),
351 # :sturgeon.freenode.net 671 foo_ bar :is using a secure connection
352 (re.compile(r":.*\s671\s[^ ]+\s([^ ]+)\s:.*"), self.__whoIsSecure),
353 # :sturgeon.freenode.net 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.freenode.net 330 foo_ bar bar :is logged in as
358 (re.compile(r":.*\s330\s[^ ]+\s([^ ]+)\s([^ ]+)\s:.*"),
359 self.__whoIsAccount),
360 # :sturgeon.freenode.net 318 foo_ bar :End of /WHOIS list.
361 (re.compile(r":.*\s318\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsEnd),
362 # :sturgeon.freenode.net 307 foo_ bar :is an identified user
363 (re.compile(r":.*\s307\s[^ ]+\s([^ ]+)\s:(.*)"),
364 self.__whoIsIdentify),
365 # :sturgeon.freenode.net 320 foo_ bar :is an identified user
366 (re.compile(r":.*\s320\s[^ ]+\s([^ ]+)\s:(.*)"),
367 self.__whoIsIdentify),
368 # :sturgeon.freenode.net 310 foo_ bar :is available for help
369 (re.compile(r":.*\s310\s[^ ]+\s([^ ]+)\s:(.*)"),
370 self.__whoIsHelper),
371 # :sturgeon.freenode.net 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.freenode.net 313 foo_ bar :is an IRC Operator
376 (re.compile(r":.*\s313\s[^ ]+\s([^ ]+)\s:(.*)"),
377 self.__whoIsOperator),
378 # :sturgeon.freenode.net 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 E5MessageBox.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 = E5MessageBox.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), QDateTime.fromTime_t(int(match.group(3)))
898 .toString("yyyy-MM-dd hh:mm")))
899 return True
900
901 return False
902
903 def __channelUrl(self, match):
904 """
905 Private method to handle a channel URL message.
906
907 @param match match object that matched the pattern
908 @return flag indicating whether the message was handled (boolean)
909 """
910 if match.group(1).lower() == self.__name.lower():
911 self.__addManagementMessage(
912 IrcChannelWidget.MessageIndicator,
913 ircFilter(self.tr("Channel URL: {0}").format(
914 match.group(2))))
915 return True
916
917 return False
918
919 def __channelModes(self, match):
920 """
921 Private method to handle a message reporting the channel modes.
922
923 @param match match object that matched the pattern
924 @return flag indicating whether the message was handled (boolean)
925 """
926 if match.group(1).lower() == self.__name.lower():
927 modesDict = getChannelModesDict()
928 modesParameters = match.group(2).split()
929 modeString = modesParameters.pop(0)
930 modes = []
931 for modeChar in modeString:
932 if modeChar == "+":
933 continue
934 elif modeChar == "k":
935 parameter = modesParameters.pop(0)
936 modes.append(self.tr(
937 "password protected ({0})").format(parameter))
938 elif modeChar == "l":
939 parameter = modesParameters.pop(0)
940 modes.append(self.tr(
941 "limited to %n user(s)", "", int(parameter)))
942 elif modeChar in modesDict:
943 modes.append(modesDict[modeChar])
944 else:
945 modes.append(modeChar)
946
947 self.__addManagementMessage(
948 IrcChannelWidget.MessageIndicator,
949 self.tr("Channel modes: {0}.").format(", ".join(modes)))
950
951 return True
952
953 return False
954
955 def __channelCreated(self, match):
956 """
957 Private method to handle a channel created message.
958
959 @param match match object that matched the pattern
960 @return flag indicating whether the message was handled (boolean)
961 """
962 if match.group(1).lower() == self.__name.lower():
963 self.__addManagementMessage(
964 IrcChannelWidget.MessageIndicator,
965 self.tr("This channel was created on {0}.").format(
966 QDateTime.fromTime_t(int(match.group(2)))
967 .toString("yyyy-MM-dd hh:mm")))
968 return True
969
970 return False
971
972 def __updateChannelModes(self, match):
973 """
974 Private method to handle a message reporting the channel modes.
975
976 @param match match object that matched the pattern
977 @return flag indicating whether the message was handled (boolean)
978 """
979 # group(1) user or server
980 # group(2) channel
981 # group(3) modes and parameter list
982 if match.group(2).lower() == self.__name.lower():
983 nick = match.group(1)
984 modesParameters = match.group(3).split()
985 modeString = modesParameters.pop(0)
986 isPlus = True
987 message = ""
988 for mode in modeString:
989 if mode == "+":
990 isPlus = True
991 continue
992 elif mode == "-":
993 isPlus = False
994 continue
995 elif mode == "a":
996 if isPlus:
997 message = self.tr(
998 "{0} sets the channel mode to 'anonymous'."
999 ).format(nick)
1000 else:
1001 message = self.tr(
1002 "{0} removes the 'anonymous' mode from the"
1003 " channel.").format(nick)
1004 elif mode == "b":
1005 if isPlus:
1006 message = self.tr(
1007 "{0} sets a ban on {1}.").format(
1008 nick, modesParameters.pop(0))
1009 else:
1010 message = self.tr(
1011 "{0} removes the ban on {1}.").format(
1012 nick, modesParameters.pop(0))
1013 elif mode == "c":
1014 if isPlus:
1015 message = self.tr(
1016 "{0} sets the channel mode to 'no colors"
1017 " allowed'.").format(nick)
1018 else:
1019 message = self.tr(
1020 "{0} sets the channel mode to 'allow color"
1021 " codes'.").format(nick)
1022 elif mode == "e":
1023 if isPlus:
1024 message = self.tr(
1025 "{0} sets a ban exception on {1}.").format(
1026 nick, modesParameters.pop(0))
1027 else:
1028 message = self.tr(
1029 "{0} removes the ban exception on {1}.").format(
1030 nick, modesParameters.pop(0))
1031 elif mode == "i":
1032 if isPlus:
1033 message = self.tr(
1034 "{0} sets the channel mode to 'invite only'."
1035 ).format(nick)
1036 else:
1037 message = self.tr(
1038 "{0} removes the 'invite only' mode from the"
1039 " channel.").format(nick)
1040 elif mode == "k":
1041 if isPlus:
1042 message = self.tr(
1043 "{0} sets the channel key to '{1}'.").format(
1044 nick, modesParameters.pop(0))
1045 else:
1046 message = self.tr(
1047 "{0} removes the channel key.").format(nick)
1048 elif mode == "l":
1049 if isPlus:
1050 message = self.tr(
1051 "{0} sets the channel limit to %n nick(s).", "",
1052 int(modesParameters.pop(0))).format(nick)
1053 else:
1054 message = self.tr(
1055 "{0} removes the channel limit.").format(nick)
1056 elif mode == "m":
1057 if isPlus:
1058 message = self.tr(
1059 "{0} sets the channel mode to 'moderated'."
1060 ).format(nick)
1061 else:
1062 message = self.tr(
1063 "{0} sets the channel mode to 'unmoderated'."
1064 ).format(nick)
1065 elif mode == "n":
1066 if isPlus:
1067 message = self.tr(
1068 "{0} sets the channel mode to 'no messages from"
1069 " outside'.").format(nick)
1070 else:
1071 message = self.tr(
1072 "{0} sets the channel mode to 'allow messages"
1073 " from outside'.").format(nick)
1074 elif mode == "p":
1075 if isPlus:
1076 message = self.tr(
1077 "{0} sets the channel mode to 'private'."
1078 ).format(nick)
1079 else:
1080 message = self.tr(
1081 "{0} sets the channel mode to 'public'."
1082 ).format(nick)
1083 elif mode == "q":
1084 if isPlus:
1085 message = self.tr(
1086 "{0} sets the channel mode to 'quiet'."
1087 ).format(nick)
1088 else:
1089 message = self.tr(
1090 "{0} removes the 'quiet' mode from the channel."
1091 ).format(nick)
1092 elif mode == "r":
1093 continue
1094 elif mode == "s":
1095 if isPlus:
1096 message = self.tr(
1097 "{0} sets the channel mode to 'secret'."
1098 ).format(nick)
1099 else:
1100 message = self.tr(
1101 "{0} sets the channel mode to 'visible'."
1102 ).format(nick)
1103 elif mode == "t":
1104 if isPlus:
1105 message = self.tr(
1106 "{0} switches on 'topic protection'.").format(nick)
1107 else:
1108 message = self.tr(
1109 "{0} switches off 'topic protection'."
1110 ).format(nick)
1111 elif mode == "I":
1112 if isPlus:
1113 message = self.tr(
1114 "{0} sets invitation mask {1}.").format(
1115 nick, modesParameters.pop(0))
1116 else:
1117 message = self.tr(
1118 "{0} removes the invitation mask {1}.").format(
1119 nick, modesParameters.pop(0))
1120
1121 self.__addManagementMessage(self.tr("Mode"), message)
1122
1123 return True
1124
1125 return False
1126
1127 def __setUserPrivilege(self, match):
1128 """
1129 Private method to handle a change of user privileges for the channel.
1130
1131 @param match match object that matched the pattern
1132 @return flag indicating whether the message was handled (boolean)
1133 """
1134 if match.group(2).lower() == self.__name.lower():
1135 itm = self.__findUser(match.group(4))
1136 if itm:
1137 itm.changePrivilege(match.group(3))
1138 self.__setEditTopicButton()
1139 self.__addManagementMessage(
1140 IrcChannelWidget.MessageIndicator,
1141 self.tr("{0} sets mode for {1}: {2}.").format(
1142 match.group(1), match.group(4), match.group(3)))
1143 return True
1144
1145 return False
1146
1147 def __ignore(self, match):
1148 """
1149 Private method to handle a channel message we are not interested in.
1150
1151 @param match match object that matched the pattern
1152 @return flag indicating whether the message was handled (boolean)
1153 """
1154 if match.group(1).lower() == self.__name.lower():
1155 return True
1156
1157 return False
1158
1159 def __help(self, match):
1160 """
1161 Private method to handle a help message.
1162
1163 @param match match object that matched the pattern
1164 @return flag indicating whether the message was handled (boolean)
1165 """
1166 self.__addManagementMessage(
1167 self.tr("Help"),
1168 "{0} {1}".format(match.group(1), ircFilter(match.group(2))))
1169 return True
1170
1171 def __handleCtcp(self, match):
1172 """
1173 Private method to handle a CTCP channel command.
1174
1175 @param match reference to the match object
1176 @return flag indicating, if the message was handled (boolean)
1177 """
1178 # group(1) sender user name
1179 # group(2) sender user@host
1180 # group(3) target nick
1181 # group(4) message
1182 if match.group(4).startswith("\x01"):
1183 ctcpCommand = match.group(4)[1:].split("\x01", 1)[0]
1184 if " " in ctcpCommand:
1185 ctcpRequest, ctcpArg = ctcpCommand.split(" ", 1)
1186 else:
1187 ctcpRequest, ctcpArg = ctcpCommand, ""
1188 ctcpRequest = ctcpRequest.lower()
1189 if ctcpRequest == "version":
1190 msg = "Eric IRC client {0}, {1}".format(Version, Copyright)
1191 self.__addManagementMessage(
1192 self.tr("CTCP"),
1193 self.tr("Received Version request from {0}.").format(
1194 match.group(1)))
1195 self.sendCtcpReply.emit(match.group(1), "VERSION " + msg)
1196 elif ctcpRequest == "ping":
1197 self.__addManagementMessage(
1198 self.tr("CTCP"),
1199 self.tr(
1200 "Received CTCP-PING request from {0},"
1201 " sending answer.").format(match.group(1)))
1202 self.sendCtcpReply.emit(
1203 match.group(1), "PING {0}".format(ctcpArg))
1204 elif ctcpRequest == "clientinfo":
1205 self.__addManagementMessage(
1206 self.tr("CTCP"),
1207 self.tr(
1208 "Received CTCP-CLIENTINFO request from {0},"
1209 " sending answer.").format(match.group(1)))
1210 self.sendCtcpReply.emit(
1211 match.group(1), "CLIENTINFO CLIENTINFO PING VERSION")
1212 else:
1213 self.__addManagementMessage(
1214 self.tr("CTCP"),
1215 self.tr("Received unknown CTCP-{0} request from {1}.")
1216 .format(ctcpRequest, match.group(1)))
1217 return True
1218
1219 return False
1220
1221 def setUserPrivilegePrefix(self, prefixes):
1222 """
1223 Public method to set the user privilege to prefix mapping.
1224
1225 @param prefixes dictionary with privilege as key and prefix as value
1226 """
1227 self.__prefixToPrivilege = {}
1228 for privilege, prefix in prefixes.items():
1229 if prefix:
1230 self.__prefixToPrivilege[prefix] = privilege
1231
1232 def __findUser(self, name):
1233 """
1234 Private method to find the user in the list of users.
1235
1236 @param name user name to search for (string)
1237 @return reference to the list entry (QListWidgetItem)
1238 """
1239 for row in range(self.usersList.count()):
1240 itm = self.usersList.item(row)
1241 if itm.name() == name:
1242 return itm
1243
1244 return None
1245
1246 def __extractPrivilege(self, name):
1247 """
1248 Private method to extract the user privileges out of the name.
1249
1250 @param name user name and prefixes (string)
1251 @return list of privileges and user name (list of string, string)
1252 """
1253 privileges = []
1254 while name[0] in self.__prefixToPrivilege:
1255 prefix = name[0]
1256 privileges.append(self.__prefixToPrivilege[prefix])
1257 name = name[1:]
1258 if name[0] == ",":
1259 name = name[1:]
1260
1261 return privileges, name
1262
1263 def __addManagementMessage(self, indicator, message):
1264 """
1265 Private method to add a channel management message to the list.
1266
1267 @param indicator indicator to be shown (string)
1268 @param message message to be shown (string)
1269 """
1270 if indicator == self.JoinIndicator:
1271 color = Preferences.getIrc("JoinChannelColour")
1272 elif indicator == self.LeaveIndicator:
1273 color = Preferences.getIrc("LeaveChannelColour")
1274 else:
1275 color = Preferences.getIrc("ChannelInfoColour")
1276 self.__appendMessage(
1277 '<font color="{0}">{1} <b>[</b>{2}<b>]</b> {3}</font>'.format(
1278 color, ircTimestamp(), indicator, message))
1279
1280 def __appendMessage(self, message):
1281 """
1282 Private slot to append a message.
1283
1284 @param message message to be appended (string)
1285 """
1286 if (
1287 self.__hidden and
1288 self.__markerLine == "" and
1289 Preferences.getIrc("MarkPositionWhenHidden")
1290 ):
1291 self.setMarkerLine()
1292 self.messages.append(message)
1293
1294 def setMarkerLine(self):
1295 """
1296 Public method to draw a line to mark the current position.
1297 """
1298 self.unsetMarkerLine()
1299 self.__markerLine = (
1300 '<span style=" color:{0}; background-color:{1};">{2}</span>'
1301 .format(Preferences.getIrc("MarkerLineForegroundColour"),
1302 Preferences.getIrc("MarkerLineBackgroundColour"),
1303 self.tr('--- New From Here ---'))
1304 )
1305 self.messages.append(self.__markerLine)
1306
1307 def unsetMarkerLine(self):
1308 """
1309 Public method to remove the marker line.
1310 """
1311 if self.__markerLine:
1312 txt = self.messages.toHtml()
1313 if txt.endswith(self.__markerLine + "</p></body></html>"):
1314 # remove empty last paragraph
1315 pos = txt.rfind("<p")
1316 txt = txt[:pos] + "</body></html>"
1317 else:
1318 txt = txt.replace(self.__markerLine, "")
1319 self.messages.setHtml(txt)
1320 self.__markerLine = ""
1321 self.messages.moveCursor(QTextCursor.MoveOperation.End)
1322
1323 def __clearMessages(self):
1324 """
1325 Private slot to clear the contents of the messages display.
1326 """
1327 self.messages.clear()
1328
1329 def __copyMessages(self):
1330 """
1331 Private slot to copy the selection of the messages display to the
1332 clipboard.
1333 """
1334 self.messages.copy()
1335
1336 def __copyAllMessages(self):
1337 """
1338 Private slot to copy the contents of the messages display to the
1339 clipboard.
1340 """
1341 txt = self.messages.toPlainText()
1342 if txt:
1343 cb = QApplication.clipboard()
1344 cb.setText(txt)
1345
1346 def __cutAllMessages(self):
1347 """
1348 Private slot to cut the contents of the messages display to the
1349 clipboard.
1350 """
1351 txt = self.messages.toPlainText()
1352 if txt:
1353 cb = QApplication.clipboard()
1354 cb.setText(txt)
1355 self.messages.clear()
1356
1357 def __saveMessages(self):
1358 """
1359 Private slot to save the contents of the messages display.
1360 """
1361 hasText = not self.messages.document().isEmpty()
1362 if hasText:
1363 if Utilities.isWindowsPlatform():
1364 htmlExtension = "htm"
1365 else:
1366 htmlExtension = "html"
1367 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
1368 self,
1369 self.tr("Save Messages"),
1370 "",
1371 self.tr(
1372 "HTML Files (*.{0});;Text Files (*.txt);;All Files (*)")
1373 .format(htmlExtension),
1374 None,
1375 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
1376 if fname:
1377 ext = QFileInfo(fname).suffix()
1378 if not ext:
1379 ex = selectedFilter.split("(*")[1].split(")")[0]
1380 if ex:
1381 fname += ex
1382 ext = QFileInfo(fname).suffix()
1383 if QFileInfo(fname).exists():
1384 res = E5MessageBox.yesNo(
1385 self,
1386 self.tr("Save Messages"),
1387 self.tr("<p>The file <b>{0}</b> already exists."
1388 " Overwrite it?</p>").format(fname),
1389 icon=E5MessageBox.Warning)
1390 if not res:
1391 return
1392 fname = Utilities.toNativeSeparators(fname)
1393
1394 try:
1395 txt = (
1396 self.messages.toHtml()
1397 if ext.lower() in ["htm", "html"] else
1398 self.messages.toPlainText()
1399 )
1400 with open(fname, "w", encoding="utf-8") as f:
1401 f.write(txt)
1402 except OSError as err:
1403 E5MessageBox.critical(
1404 self,
1405 self.tr("Error saving Messages"),
1406 self.tr(
1407 """<p>The messages contents could not be written"""
1408 """ to <b>{0}</b></p><p>Reason: {1}</p>""")
1409 .format(fname, str(err)))
1410
1411 def __initMessagesMenu(self):
1412 """
1413 Private slot to initialize the context menu of the messages pane.
1414 """
1415 self.__messagesMenu = QMenu(self)
1416 self.__copyMessagesAct = self.__messagesMenu.addAction(
1417 UI.PixmapCache.getIcon("editCopy"),
1418 self.tr("Copy"), self.__copyMessages)
1419 self.__messagesMenu.addSeparator()
1420 self.__cutAllMessagesAct = self.__messagesMenu.addAction(
1421 UI.PixmapCache.getIcon("editCut"),
1422 self.tr("Cut all"), self.__cutAllMessages)
1423 self.__copyAllMessagesAct = self.__messagesMenu.addAction(
1424 UI.PixmapCache.getIcon("editCopy"),
1425 self.tr("Copy all"), self.__copyAllMessages)
1426 self.__messagesMenu.addSeparator()
1427 self.__clearMessagesAct = self.__messagesMenu.addAction(
1428 UI.PixmapCache.getIcon("editDelete"),
1429 self.tr("Clear"), self.__clearMessages)
1430 self.__messagesMenu.addSeparator()
1431 self.__saveMessagesAct = self.__messagesMenu.addAction(
1432 UI.PixmapCache.getIcon("fileSave"),
1433 self.tr("Save"), self.__saveMessages)
1434 self.__messagesMenu.addSeparator()
1435 self.__setMarkerMessagesAct = self.__messagesMenu.addAction(
1436 self.tr("Mark Current Position"), self.setMarkerLine)
1437 self.__unsetMarkerMessagesAct = self.__messagesMenu.addAction(
1438 self.tr("Remove Position Marker"),
1439 self.unsetMarkerLine)
1440
1441 self.on_messages_copyAvailable(False)
1442
1443 @pyqtSlot(bool)
1444 def on_messages_copyAvailable(self, yes):
1445 """
1446 Private slot to react to text selection/deselection of the messages
1447 edit.
1448
1449 @param yes flag signaling the availability of selected text (boolean)
1450 """
1451 self.__copyMessagesAct.setEnabled(yes)
1452
1453 @pyqtSlot(QPoint)
1454 def on_messages_customContextMenuRequested(self, pos):
1455 """
1456 Private slot to show the context menu of the messages pane.
1457
1458 @param pos the position of the mouse pointer (QPoint)
1459 """
1460 enable = not self.messages.document().isEmpty()
1461 self.__cutAllMessagesAct.setEnabled(enable)
1462 self.__copyAllMessagesAct.setEnabled(enable)
1463 self.__saveMessagesAct.setEnabled(enable)
1464 self.__setMarkerMessagesAct.setEnabled(self.__markerLine == "")
1465 self.__unsetMarkerMessagesAct.setEnabled(self.__markerLine != "")
1466 self.__messagesMenu.popup(self.messages.mapToGlobal(pos))
1467
1468 def __whoIs(self):
1469 """
1470 Private slot to get information about the selected user.
1471 """
1472 self.__whoIsNick = self.usersList.selectedItems()[0].text()
1473 self.sendData.emit("WHOIS " + self.__whoIsNick)
1474
1475 def __openPrivateChat(self):
1476 """
1477 Private slot to open a chat with the selected user.
1478 """
1479 user = self.usersList.selectedItems()[0].text()
1480 self.openPrivateChat.emit(user)
1481
1482 def __sendUserMessage(self):
1483 """
1484 Private slot to send a private message to a specific user.
1485 """
1486 from E5Gui import E5TextInputDialog
1487
1488 user = self.usersList.selectedItems()[0].text()
1489 ok, message = E5TextInputDialog.getText(
1490 self, self.tr("Send Message"),
1491 self.tr("Enter the message to be sent:"),
1492 minimumWidth=400)
1493 if ok and message:
1494 self.__processUserMessage("/MSG {0} {1}".format(user, message))
1495
1496 def __sendUserQuery(self):
1497 """
1498 Private slot to send a query message to a specific user.
1499 """
1500 from E5Gui import E5TextInputDialog
1501
1502 user = self.usersList.selectedItems()[0].text()
1503 ok, message = E5TextInputDialog.getText(
1504 self, self.tr("Send Query"),
1505 self.tr("Enter the message to be sent:"),
1506 minimumWidth=400)
1507 if ok and message:
1508 self.__processUserMessage("/QUERY {0} {1}".format(user, message))
1509
1510 def __sendUserNotice(self):
1511 """
1512 Private slot to send a notice message to a specific user.
1513 """
1514 from E5Gui import E5TextInputDialog
1515
1516 user = self.usersList.selectedItems()[0].text()
1517 ok, message = E5TextInputDialog.getText(
1518 self, self.tr("Send Notice"),
1519 self.tr("Enter the message to be sent:"),
1520 minimumWidth=400)
1521 if ok and message:
1522 self.__processUserMessage("/NOTICE {0} {1}".format(user, message))
1523
1524 def __pingUser(self):
1525 """
1526 Private slot to send a ping to a specific user.
1527 """
1528 user = self.usersList.selectedItems()[0].text()
1529 self.__processUserMessage("/PING {0}".format(user))
1530
1531 def __ignoreUser(self):
1532 """
1533 Private slot to ignore a specific user.
1534 """
1535 user = self.usersList.selectedItems()[0].text()
1536 self.__processUserMessage("/IGNORE {0}".format(user))
1537
1538 def __initUsersMenu(self):
1539 """
1540 Private slot to initialize the users list context menu.
1541 """
1542 self.__usersMenu = QMenu(self)
1543 self.__whoIsAct = self.__usersMenu.addAction(
1544 self.tr("Who Is"), self.__whoIs)
1545 self.__usersMenu.addSeparator()
1546 self.__privateChatAct = self.__usersMenu.addAction(
1547 self.tr("Private Chat"), self.__openPrivateChat)
1548 self.__usersMenu.addSeparator()
1549 self.__sendUserMessageAct = self.__usersMenu.addAction(
1550 self.tr("Send Message"), self.__sendUserMessage)
1551 self.__sendUserQueryAct = self.__usersMenu.addAction(
1552 self.tr("Send Query"), self.__sendUserQuery)
1553 self.__sendUserNoticeAct = self.__usersMenu.addAction(
1554 self.tr("Send Notice"), self.__sendUserNotice)
1555 self.__usersMenu.addSeparator()
1556 self.__pingUserAct = self.__usersMenu.addAction(
1557 self.tr("Send Ping"), self.__pingUser)
1558 self.__ignoreUserAct = self.__usersMenu.addAction(
1559 self.tr("Ignore User"), self.__ignoreUser)
1560 self.__usersMenu.addSeparator()
1561 self.__usersListRefreshAct = self.__usersMenu.addAction(
1562 self.tr("Refresh"), self.__sendAutoWhoCommand)
1563
1564 @pyqtSlot(QPoint)
1565 def on_usersList_customContextMenuRequested(self, pos):
1566 """
1567 Private slot to show the context menu of the users list.
1568
1569 @param pos the position of the mouse pointer (QPoint)
1570 """
1571 enable = len(self.usersList.selectedItems()) > 0
1572 enablePrivate = enable and not self.__private
1573 itm = self.usersList.itemAt(pos)
1574 if itm and enablePrivate:
1575 enablePrivate = itm.text().lower() not in [
1576 "chanserv", self.__userName.lower()]
1577 self.__whoIsAct.setEnabled(enable)
1578 self.__privateChatAct.setEnabled(enablePrivate)
1579 self.__usersListRefreshAct.setEnabled(
1580 self.usersList.count() <= Preferences.getIrc("AutoUserInfoMax"))
1581 self.__usersMenu.popup(self.usersList.mapToGlobal(pos))
1582
1583 def hideEvent(self, evt):
1584 """
1585 Protected method handling hide events.
1586
1587 @param evt reference to the hide event (QHideEvent)
1588 """
1589 self.__hidden = True
1590
1591 def showEvent(self, evt):
1592 """
1593 Protected method handling show events.
1594
1595 @param evt reference to the show event (QShowEvent)
1596 """
1597 self.__hidden = False
1598
1599 def initAutoWho(self):
1600 """
1601 Public method to initialize the Auto Who system.
1602 """
1603 if Preferences.getIrc("AutoUserInfoLookup"):
1604 self.__autoWhoTimer.setInterval(
1605 Preferences.getIrc("AutoUserInfoInterval") * 1000)
1606 self.__autoWhoTimer.start()
1607
1608 @pyqtSlot()
1609 def __sendAutoWhoCommand(self):
1610 """
1611 Private slot to send the WHO command to update the users list.
1612 """
1613 if self.usersList.count() <= Preferences.getIrc("AutoUserInfoMax"):
1614 self.__autoWhoRequested = True
1615 self.sendData.emit(self.__autoWhoTemplate.format(self.__name))
1616
1617 def __autoWhoEntry(self, match):
1618 """
1619 Private method to handle a WHO entry returned by the server as
1620 requested automatically.
1621
1622 @param match match object that matched the pattern
1623 @return flag indicating whether the message was handled (boolean)
1624 """
1625 # group(1) nick
1626 # group(2) user flags
1627 if self.__autoWhoRequested:
1628 itm = self.__findUser(match.group(1))
1629 if itm:
1630 itm.parseWhoFlags(match.group(2))
1631 return True
1632
1633 return False
1634
1635 def __whoEnd(self, match):
1636 """
1637 Private method to handle the end of the WHO list.
1638
1639 @param match match object that matched the pattern
1640 @return flag indicating whether the message was handled (boolean)
1641 """
1642 if match.group(1).lower() == self.__name.lower():
1643 if self.__autoWhoRequested:
1644 self.__autoWhoRequested = False
1645 self.initAutoWho()
1646 else:
1647 self.__addManagementMessage(
1648 self.tr("Who"),
1649 self.tr("End of WHO list for {0}.").format(
1650 match.group(1)))
1651 return True
1652
1653 return False
1654
1655 def __whoEntry(self, match):
1656 """
1657 Private method to handle a WHO entry returned by the server as
1658 requested manually.
1659
1660 @param match match object that matched the pattern
1661 @return flag indicating whether the message was handled (boolean)
1662 """
1663 # group(1) channel
1664 # group(2) user
1665 # group(3) host
1666 # group(4) nick
1667 # group(5) user flags
1668 # group(6) real name
1669 if match.group(1).lower() == self.__name.lower():
1670 away = (
1671 self.tr(" (Away)")
1672 if match.group(5).startswith("G") else ""
1673 )
1674 self.__addManagementMessage(
1675 self.tr("Who"),
1676 self.tr("{0} is {1}@{2} ({3}){4}").format(
1677 match.group(4), match.group(2), match.group(3),
1678 match.group(6), away))
1679 return True
1680
1681 return False
1682
1683 def __whoIsUser(self, match):
1684 """
1685 Private method to handle the WHOIS user reply.
1686
1687 @param match match object that matched the pattern
1688 @return flag indicating whether the message was handled (boolean)
1689 """
1690 # group(1) nick
1691 # group(2) user
1692 # group(3) host
1693 # group(4) real name
1694 if match.group(1) == self.__whoIsNick:
1695 realName = match.group(4).replace("<", "&lt;").replace(">", "&gt;")
1696 self.__addManagementMessage(
1697 self.tr("Whois"),
1698 self.tr("{0} is {1}@{2} ({3}).").format(
1699 match.group(1), match.group(2), match.group(3), realName))
1700 return True
1701
1702 return False
1703
1704 def __whoIsChannels(self, match):
1705 """
1706 Private method to handle the WHOIS channels reply.
1707
1708 @param match match object that matched the pattern
1709 @return flag indicating whether the message was handled (boolean)
1710 """
1711 # group(1) nick
1712 # group(2) channels
1713 if match.group(1) == self.__whoIsNick:
1714 userChannels = []
1715 voiceChannels = []
1716 opChannels = []
1717 halfopChannels = []
1718 ownerChannels = []
1719 adminChannels = []
1720
1721 # generate the list of channels the user is in
1722 channelList = match.group(2).split()
1723 for channel in channelList:
1724 if channel.startswith(("*", "&")):
1725 adminChannels.append(channel[1:])
1726 elif (
1727 channel.startswith(("!", "~")) and
1728 self.__ircWidget.isChannelName(channel[1:])
1729 ):
1730 ownerChannels.append(channel[1:])
1731 elif channel.startswith("@+"):
1732 opChannels.append(channel[2:])
1733 elif channel.startswith("@"):
1734 opChannels.append(channel[1:])
1735 elif channel.startswith("%"):
1736 halfopChannels.append(channel[1:])
1737 elif channel.startswith("+"):
1738 voiceChannels.append(channel[1:])
1739 else:
1740 userChannels.append(channel)
1741
1742 # show messages
1743 if userChannels:
1744 self.__addManagementMessage(
1745 self.tr("Whois"),
1746 self.tr("{0} is a user on channels: {1}").format(
1747 match.group(1), " ".join(userChannels)))
1748 if voiceChannels:
1749 self.__addManagementMessage(
1750 self.tr("Whois"),
1751 self.tr("{0} has voice on channels: {1}").format(
1752 match.group(1), " ".join(voiceChannels)))
1753 if halfopChannels:
1754 self.__addManagementMessage(
1755 self.tr("Whois"),
1756 self.tr("{0} is a halfop on channels: {1}").format(
1757 match.group(1), " ".join(halfopChannels)))
1758 if opChannels:
1759 self.__addManagementMessage(
1760 self.tr("Whois"),
1761 self.tr("{0} is an operator on channels: {1}").format(
1762 match.group(1), " ".join(opChannels)))
1763 if ownerChannels:
1764 self.__addManagementMessage(
1765 self.tr("Whois"),
1766 self.tr("{0} is owner of channels: {1}").format(
1767 match.group(1), " ".join(ownerChannels)))
1768 if adminChannels:
1769 self.__addManagementMessage(
1770 self.tr("Whois"),
1771 self.tr("{0} is admin on channels: {1}").format(
1772 match.group(1), " ".join(adminChannels)))
1773 return True
1774
1775 return False
1776
1777 def __whoIsServer(self, match):
1778 """
1779 Private method to handle the WHOIS server reply.
1780
1781 @param match match object that matched the pattern
1782 @return flag indicating whether the message was handled (boolean)
1783 """
1784 # group(1) nick
1785 # group(2) server
1786 # group(3) server info
1787 if match.group(1) == self.__whoIsNick:
1788 self.__addManagementMessage(
1789 self.tr("Whois"),
1790 self.tr("{0} is online via {1} ({2}).").format(
1791 match.group(1), match.group(2), match.group(3)))
1792 return True
1793
1794 return False
1795
1796 def __whoIsOperator(self, match):
1797 """
1798 Private method to handle the WHOIS operator reply.
1799
1800 @param match match object that matched the pattern
1801 @return flag indicating whether the message was handled (boolean)
1802 """
1803 # group(1) nick
1804 # group(2) message
1805 if match.group(1) == self.__whoIsNick:
1806 if match.group(2).lower().startswith("is an irc operator"):
1807 self.__addManagementMessage(
1808 self.tr("Whois"),
1809 self.tr("{0} is an IRC Operator.").format(
1810 match.group(1)))
1811 else:
1812 self.__addManagementMessage(
1813 self.tr("Whois"),
1814 "{0} {1}".format(match.group(1), match.group(2)))
1815 return True
1816
1817 return False
1818
1819 def __whoIsIdle(self, match):
1820 """
1821 Private method to handle the WHOIS idle reply.
1822
1823 @param match match object that matched the pattern
1824 @return flag indicating whether the message was handled (boolean)
1825 """
1826 # group(1) nick
1827 # group(2) idle seconds
1828 # group(3) signon time
1829 if match.group(1) == self.__whoIsNick:
1830 seconds = int(match.group(2))
1831 minutes = seconds // 60
1832 hours = minutes // 60
1833 days = hours // 24
1834
1835 signonTimestamp = int(match.group(3))
1836 signonTime = QDateTime()
1837 signonTime.setTime_t(signonTimestamp)
1838
1839 if days:
1840 daysString = self.tr("%n day(s)", "", days)
1841 hoursString = self.tr("%n hour(s)", "", hours)
1842 minutesString = self.tr("%n minute(s)", "", minutes)
1843 secondsString = self.tr("%n second(s)", "", seconds)
1844 self.__addManagementMessage(
1845 self.tr("Whois"),
1846 self.tr(
1847 "{0} has been idle for {1}, {2}, {3}, and {4}.",
1848 "{0} = name of person, {1} = (x days),"
1849 " {2} = (x hours), {3} = (x minutes),"
1850 " {4} = (x seconds)").format(
1851 match.group(1), daysString, hoursString, minutesString,
1852 secondsString))
1853 elif hours:
1854 hoursString = self.tr("%n hour(s)", "", hours)
1855 minutesString = self.tr("%n minute(s)", "", minutes)
1856 secondsString = self.tr("%n second(s)", "", seconds)
1857 self.__addManagementMessage(
1858 self.tr("Whois"),
1859 self.tr(
1860 "{0} has been idle for {1}, {2}, and {3}.",
1861 "{0} = name of person, {1} = (x hours), "
1862 "{2} = (x minutes), {3} = (x seconds)")
1863 .format(match.group(1), hoursString, minutesString,
1864 secondsString))
1865 elif minutes:
1866 minutesString = self.tr("%n minute(s)", "", minutes)
1867 secondsString = self.tr("%n second(s)", "", seconds)
1868 self.__addManagementMessage(
1869 self.tr("Whois"),
1870 self.tr(
1871 "{0} has been idle for {1} and {2}.",
1872 "{0} = name of person, {1} = (x minutes), "
1873 "{3} = (x seconds)")
1874 .format(match.group(1), minutesString, secondsString))
1875 else:
1876 self.__addManagementMessage(
1877 self.tr("Whois"),
1878 self.tr(
1879 "{0} has been idle for %n second(s).", "",
1880 seconds).format(match.group(1)))
1881
1882 if not signonTime.isNull():
1883 self.__addManagementMessage(
1884 self.tr("Whois"),
1885 self.tr("{0} has been online since {1}.").format(
1886 match.group(1),
1887 signonTime.toString("yyyy-MM-dd, hh:mm:ss")))
1888 return True
1889
1890 return False
1891
1892 def __whoIsEnd(self, match):
1893 """
1894 Private method to handle the end of WHOIS reply.
1895
1896 @param match match object that matched the pattern
1897 @return flag indicating whether the message was handled (boolean)
1898 """
1899 # group(1) nick
1900 # group(2) end message
1901 if match.group(1) == self.__whoIsNick:
1902 self.__whoIsNick = ""
1903 self.__addManagementMessage(
1904 self.tr("Whois"),
1905 self.tr("End of WHOIS list for {0}.").format(
1906 match.group(1)))
1907 return True
1908
1909 return False
1910
1911 def __whoIsIdentify(self, match):
1912 """
1913 Private method to handle the WHOIS identify and identified replies.
1914
1915 @param match match object that matched the pattern
1916 @return flag indicating whether the message was handled (boolean)
1917 """
1918 # group(1) nick
1919 # group(2) identified message
1920 if match.group(1) == self.__whoIsNick:
1921 self.__addManagementMessage(
1922 self.tr("Whois"),
1923 self.tr("{0} is an identified user.").format(
1924 match.group(1)))
1925 return True
1926
1927 return False
1928
1929 def __whoIsHelper(self, match):
1930 """
1931 Private method to handle the WHOIS helper reply.
1932
1933 @param match match object that matched the pattern
1934 @return flag indicating whether the message was handled (boolean)
1935 """
1936 # group(1) nick
1937 # group(2) helper message
1938 if match.group(1) == self.__whoIsNick:
1939 self.__addManagementMessage(
1940 self.tr("Whois"),
1941 self.tr("{0} is available for help.").format(
1942 match.group(1)))
1943 return True
1944
1945 return False
1946
1947 def __whoIsAccount(self, match):
1948 """
1949 Private method to handle the WHOIS account reply.
1950
1951 @param match match object that matched the pattern
1952 @return flag indicating whether the message was handled (boolean)
1953 """
1954 # group(1) nick
1955 # group(2) login name
1956 if match.group(1) == self.__whoIsNick:
1957 self.__addManagementMessage(
1958 self.tr("Whois"),
1959 self.tr("{0} is logged in as {1}.").format(
1960 match.group(1), match.group(2)))
1961 return True
1962
1963 return False
1964
1965 def __whoIsActually(self, match):
1966 """
1967 Private method to handle the WHOIS actually reply.
1968
1969 @param match match object that matched the pattern
1970 @return flag indicating whether the message was handled (boolean)
1971 """
1972 # group(1) nick
1973 # group(2) actual user@host
1974 # group(3) actual IP
1975 if match.group(1) == self.__whoIsNick:
1976 self.__addManagementMessage(
1977 self.tr("Whois"),
1978 self.tr(
1979 "{0} is actually using the host {1} (IP: {2}).").format(
1980 match.group(1), match.group(2), match.group(3)))
1981 return True
1982
1983 return False
1984
1985 def __whoIsSecure(self, match):
1986 """
1987 Private method to handle the WHOIS secure reply.
1988
1989 @param match match object that matched the pattern
1990 @return flag indicating whether the message was handled (boolean)
1991 """
1992 # group(1) nick
1993 if match.group(1) == self.__whoIsNick:
1994 self.__addManagementMessage(
1995 self.tr("Whois"),
1996 self.tr("{0} is using a secure connection.").format(
1997 match.group(1)))
1998 return True
1999
2000 return False
2001
2002 def __whoIsConnection(self, match):
2003 """
2004 Private method to handle the WHOIS connection reply.
2005
2006 @param match match object that matched the pattern
2007 @return flag indicating whether the message was handled (boolean)
2008 """
2009 # group(1) nick
2010 # group(2) host name
2011 # group(3) IP
2012 if match.group(1) == self.__whoIsNick:
2013 self.__addManagementMessage(
2014 self.tr("Whois"),
2015 self.tr("{0} is connecting from {1} (IP: {2}).").format(
2016 match.group(1), match.group(2), match.group(3)))
2017 return True
2018
2019 return False
2020
2021 def __setEditTopicButton(self):
2022 """
2023 Private method to set the visibility of the Edit Topic button.
2024 """
2025 itm = self.__findUser(self.__userName)
2026 if itm:
2027 self.editTopicButton.setVisible(itm.canChangeTopic())
2028
2029 @pyqtSlot()
2030 def on_editTopicButton_clicked(self):
2031 """
2032 Private slot to change the topic of the channel.
2033 """
2034 topic, ok = QInputDialog.getText(
2035 self,
2036 self.tr("Edit Channel Topic"),
2037 self.tr("Enter the topic for this channel:"),
2038 QLineEdit.EchoMode.Normal,
2039 self.topicLabel.text())
2040 if ok and topic != "":
2041 self.sendData.emit("TOPIC {0} :{1}".format(
2042 self.__name, topic))
2043
2044 @pyqtSlot(QUrl)
2045 def on_messages_anchorClicked(self, url):
2046 """
2047 Private slot to open links in the default browser.
2048
2049 @param url URL to be opened (QUrl)
2050 """
2051 QDesktopServices.openUrl(url)

eric ide

mercurial