|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the IRC window. |
|
8 """ |
|
9 |
|
10 import enum |
|
11 import logging |
|
12 import re |
|
13 |
|
14 from PyQt6.QtCore import QByteArray, QDateTime, Qt, QTimer, pyqtSignal, pyqtSlot |
|
15 from PyQt6.QtNetwork import QAbstractSocket, QTcpSocket |
|
16 from PyQt6.QtWidgets import QLabel, QTabWidget, QToolButton, QWidget |
|
17 |
|
18 try: |
|
19 from PyQt6.QtNetwork import QSslConfiguration, QSslSocket |
|
20 |
|
21 from eric7.EricNetwork.EricSslErrorHandler import ( |
|
22 EricSslErrorHandler, |
|
23 EricSslErrorState, |
|
24 ) |
|
25 |
|
26 SSL_AVAILABLE = True |
|
27 except ImportError: |
|
28 SSL_AVAILABLE = False |
|
29 |
|
30 from eric7 import Preferences |
|
31 from eric7.__version__ import Version |
|
32 from eric7.EricWidgets import EricMessageBox |
|
33 from eric7.SystemUtilities import OSUtilities |
|
34 from eric7.UI.Info import Copyright |
|
35 |
|
36 from .IrcNetworkManager import IrcNetworkManager |
|
37 from .Ui_IrcWidget import Ui_IrcWidget |
|
38 |
|
39 |
|
40 class IrcConnectionState(enum.Enum): |
|
41 """ |
|
42 Class defining the connection states. |
|
43 """ |
|
44 |
|
45 Disconnected = 1 |
|
46 Connected = 2 |
|
47 Connecting = 3 |
|
48 |
|
49 |
|
50 class IrcWidget(QWidget, Ui_IrcWidget): |
|
51 """ |
|
52 Class implementing the IRC window. |
|
53 |
|
54 @signal autoConnected() emitted after an automatic connection was initiated |
|
55 """ |
|
56 |
|
57 autoConnected = pyqtSignal() |
|
58 |
|
59 def __init__(self, plugin, parent=None): |
|
60 """ |
|
61 Constructor |
|
62 |
|
63 @param plugin reference to the plug-in object |
|
64 @type MqttMonitorPlugin |
|
65 @param parent reference to the parent widget |
|
66 @type QWidget |
|
67 """ |
|
68 super().__init__(parent) |
|
69 self.setupUi(self) |
|
70 |
|
71 self.layout().setContentsMargins(0, 3, 0, 0) |
|
72 |
|
73 self.__plugin = plugin |
|
74 |
|
75 self.__ircNetworkManager = IrcNetworkManager(self) |
|
76 |
|
77 self.__leaveButton = QToolButton(self) |
|
78 self.__leaveButton.setIcon(self.__plugin.getIcon("ircCloseChannel")) |
|
79 self.__leaveButton.setToolTip(self.tr("Press to leave the current channel")) |
|
80 self.__leaveButton.clicked.connect(self.__leaveChannel) |
|
81 self.__leaveButton.setEnabled(False) |
|
82 self.channelsWidget.setCornerWidget( |
|
83 self.__leaveButton, Qt.Corner.BottomRightCorner |
|
84 ) |
|
85 self.channelsWidget.setTabsClosable(False) |
|
86 if not OSUtilities.isMacPlatform(): |
|
87 self.channelsWidget.setTabPosition(QTabWidget.TabPosition.South) |
|
88 |
|
89 height = self.height() |
|
90 self.splitter.setSizes([int(height * 0.6), int(height * 0.4)]) |
|
91 |
|
92 self.__channelList = [] |
|
93 self.__channelTypePrefixes = "" |
|
94 self.__userName = "" |
|
95 self.__identityName = "" |
|
96 self.__quitMessage = "" |
|
97 self.__nickIndex = -1 |
|
98 self.__nickName = "" |
|
99 self.__server = None |
|
100 self.__registering = False |
|
101 |
|
102 self.__connectionState = IrcConnectionState.Disconnected |
|
103 self.__sslErrorLock = False |
|
104 |
|
105 self.__buffer = "" |
|
106 self.__userPrefix = {} |
|
107 |
|
108 self.__socket = None |
|
109 if SSL_AVAILABLE: |
|
110 self.__sslErrorHandler = EricSslErrorHandler( |
|
111 Preferences.getSettings(), self |
|
112 ) |
|
113 else: |
|
114 self.__sslErrorHandler = None |
|
115 |
|
116 self.__patterns = [ |
|
117 # :foo_!n=foo@foohost.bar.net PRIVMSG bar_ :some long message |
|
118 (re.compile(r":([^!]+)!([^ ]+)\sPRIVMSG\s([^ ]+)\s:(.*)"), self.__query), |
|
119 # :foo.bar.net COMMAND some message |
|
120 (re.compile(r""":([^ ]+)\s+([A-Z]+)\s+(.+)"""), self.__handleNamedMessage), |
|
121 # :foo.bar.net 123 * :info |
|
122 (re.compile(r""":([^ ]+)\s+(\d{3})\s+(.+)"""), self.__handleNumericMessage), |
|
123 # PING :ping message |
|
124 (re.compile(r"""PING\s+:(.*)"""), self.__ping), |
|
125 ] |
|
126 self.__prefixRe = re.compile(r""".*\sPREFIX=\((.*)\)([^ ]+).*""") |
|
127 self.__chanTypesRe = re.compile(r""".*\sCHANTYPES=([^ ]+).*""") |
|
128 |
|
129 ircPic = self.__plugin.getPixmap("irc128") |
|
130 self.__emptyLabel = QLabel() |
|
131 self.__emptyLabel.setPixmap(ircPic) |
|
132 self.__emptyLabel.setAlignment( |
|
133 Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignHCenter |
|
134 ) |
|
135 self.channelsWidget.addTab(self.__emptyLabel, "") |
|
136 |
|
137 # all initialized, do connections now |
|
138 self.__ircNetworkManager.dataChanged.connect(self.__networkDataChanged) |
|
139 self.networkWidget.initialize(manager=self.__ircNetworkManager) |
|
140 self.networkWidget.connectNetwork.connect(self.__connectNetwork) |
|
141 self.networkWidget.editNetwork.connect(self.__editNetwork) |
|
142 self.networkWidget.joinChannel.connect(self.joinChannel) |
|
143 self.networkWidget.nickChanged.connect(self.__changeNick) |
|
144 self.networkWidget.sendData.connect(self.__send) |
|
145 self.networkWidget.away.connect(self.__away) |
|
146 self.networkWidget.autoConnected.connect(self.autoConnected) |
|
147 |
|
148 @pyqtSlot() |
|
149 def shutdown(self): |
|
150 """ |
|
151 Public slot to shut down the widget. |
|
152 """ |
|
153 if self.__server: |
|
154 self.__connectNetwork("", False, True) |
|
155 |
|
156 self.__ircNetworkManager.close() |
|
157 |
|
158 def autoConnect(self): |
|
159 """ |
|
160 Public method to initiate the IRC auto connection. |
|
161 """ |
|
162 self.networkWidget.autoConnect() |
|
163 |
|
164 def __connectNetwork(self, name, connect, silent): |
|
165 """ |
|
166 Private slot to connect to or disconnect from the given network. |
|
167 |
|
168 @param name name of the network to connect to |
|
169 @type str |
|
170 @param connect flag indicating to connect |
|
171 @type bool |
|
172 @param silent flag indicating a silent connect/disconnect |
|
173 @type bool |
|
174 """ |
|
175 if connect: |
|
176 network = self.__ircNetworkManager.getNetwork(name) |
|
177 if network: |
|
178 self.__server = network.getServer() |
|
179 self.__identityName = network.getIdentityName() |
|
180 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
181 if identity: |
|
182 self.__userName = identity.getIdent() |
|
183 self.__quitMessage = identity.getQuitMessage() |
|
184 if self.__server: |
|
185 useSSL = self.__server.useSSL() |
|
186 if useSSL and not SSL_AVAILABLE: |
|
187 EricMessageBox.critical( |
|
188 self, |
|
189 self.tr("SSL Connection"), |
|
190 self.tr( |
|
191 """An encrypted connection to the IRC""" |
|
192 """ network was requested but SSL is not""" |
|
193 """ available. Please change the server""" |
|
194 """ configuration.""" |
|
195 ), |
|
196 ) |
|
197 return |
|
198 |
|
199 if useSSL: |
|
200 # create SSL socket |
|
201 self.__socket = QSslSocket(self) |
|
202 self.__socket.encrypted.connect(self.__hostConnected) |
|
203 self.__socket.sslErrors.connect(self.__sslErrors) |
|
204 else: |
|
205 # create TCP socket |
|
206 self.__socket = QTcpSocket(self) |
|
207 self.__socket.connected.connect(self.__hostConnected) |
|
208 self.__socket.hostFound.connect(self.__hostFound) |
|
209 self.__socket.disconnected.connect(self.__hostDisconnected) |
|
210 self.__socket.readyRead.connect(self.__readyRead) |
|
211 self.__socket.errorOccurred.connect(self.__tcpError) |
|
212 |
|
213 self.__connectionState = IrcConnectionState.Connecting |
|
214 if useSSL: |
|
215 self.networkWidget.addServerMessage( |
|
216 self.tr("Info"), |
|
217 self.tr( |
|
218 "Looking for server {0} (port {1})" |
|
219 " using an SSL encrypted connection" |
|
220 "..." |
|
221 ).format( |
|
222 self.__server.getName(), self.__server.getPort() |
|
223 ), |
|
224 ) |
|
225 self.__socket.connectToHostEncrypted( |
|
226 self.__server.getName(), self.__server.getPort() |
|
227 ) |
|
228 else: |
|
229 self.networkWidget.addServerMessage( |
|
230 self.tr("Info"), |
|
231 self.tr("Looking for server {0} (port {1})...").format( |
|
232 self.__server.getName(), self.__server.getPort() |
|
233 ), |
|
234 ) |
|
235 self.__socket.connectToHost( |
|
236 self.__server.getName(), self.__server.getPort() |
|
237 ) |
|
238 else: |
|
239 if silent: |
|
240 ok = True |
|
241 else: |
|
242 ok = EricMessageBox.yesNo( |
|
243 self, |
|
244 self.tr("Disconnect from Server"), |
|
245 self.tr( |
|
246 """<p>Do you really want to disconnect from""" |
|
247 """ <b>{0}</b>?</p><p>All channels will be""" |
|
248 """ closed.</p>""" |
|
249 ).format(self.__server.getName()), |
|
250 ) |
|
251 if ok: |
|
252 if self.__server is not None: |
|
253 self.networkWidget.addServerMessage( |
|
254 self.tr("Info"), |
|
255 self.tr("Disconnecting from server {0}...").format( |
|
256 self.__server.getName() |
|
257 ), |
|
258 ) |
|
259 elif name: |
|
260 self.networkWidget.addServerMessage( |
|
261 self.tr("Info"), |
|
262 self.tr("Disconnecting from network {0}...").format(name), |
|
263 ) |
|
264 else: |
|
265 self.networkWidget.addServerMessage( |
|
266 self.tr("Info"), self.tr("Disconnecting from server.") |
|
267 ) |
|
268 self.__closeAllChannels() |
|
269 self.__send("QUIT :" + self.__quitMessage) |
|
270 if self.__socket: |
|
271 self.__socket.flush() |
|
272 self.__socket.close() |
|
273 if self.__socket: |
|
274 # socket is still existing |
|
275 self.__socket.deleteLater() |
|
276 self.__socket = None |
|
277 self.__userName = "" |
|
278 self.__identityName = "" |
|
279 self.__quitMessage = "" |
|
280 |
|
281 @pyqtSlot() |
|
282 def __editNetwork(self): |
|
283 """ |
|
284 Private slot to edit the network configuration. |
|
285 """ |
|
286 from .IrcNetworkListDialog import IrcNetworkListDialog |
|
287 |
|
288 dlg = IrcNetworkListDialog(manager=self.__ircNetworkManager, parent=self) |
|
289 dlg.exec() |
|
290 |
|
291 def __networkDataChanged(self): |
|
292 """ |
|
293 Private slot handling changes of the network and identity definitions. |
|
294 """ |
|
295 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
296 if identity: |
|
297 partMsg = identity.getPartMessage() |
|
298 for channel in self.__channelList: |
|
299 channel.setPartMessage(partMsg) |
|
300 |
|
301 def joinChannel(self, name, key=""): |
|
302 """ |
|
303 Public slot to join a channel. |
|
304 |
|
305 @param name name of the channel |
|
306 @type str |
|
307 @param key key of the channel |
|
308 @type str |
|
309 """ |
|
310 from .IrcChannelWidget import IrcChannelWidget |
|
311 |
|
312 # step 1: check, if this channel is already joined |
|
313 for channel in self.__channelList: |
|
314 if channel.name() == name: |
|
315 return |
|
316 |
|
317 channel = IrcChannelWidget(self) |
|
318 channel.setName(name) |
|
319 channel.setUserName(self.__nickName) |
|
320 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
321 if identity: |
|
322 channel.setPartMessage(identity.getPartMessage()) |
|
323 channel.setUserPrivilegePrefix(self.__userPrefix) |
|
324 channel.initAutoWho() |
|
325 |
|
326 channel.sendData.connect(self.__send) |
|
327 channel.sendCtcpRequest.connect(self.__sendCtcpRequest) |
|
328 channel.sendCtcpReply.connect(self.__sendCtcpReply) |
|
329 channel.channelClosed.connect(self.__closeChannel) |
|
330 channel.openPrivateChat.connect(self.__openPrivate) |
|
331 channel.awayCommand.connect(self.networkWidget.handleAwayCommand) |
|
332 channel.leaveChannels.connect(self.__leaveChannels) |
|
333 channel.leaveAllChannels.connect(self.__leaveAllChannels) |
|
334 |
|
335 self.channelsWidget.addTab(channel, name) |
|
336 self.__channelList.append(channel) |
|
337 self.channelsWidget.setCurrentWidget(channel) |
|
338 |
|
339 joinCommand = ["JOIN", name] |
|
340 if key: |
|
341 joinCommand.append(key) |
|
342 self.__send(" ".join(joinCommand)) |
|
343 self.__send("MODE " + name) |
|
344 |
|
345 emptyIndex = self.channelsWidget.indexOf(self.__emptyLabel) |
|
346 if emptyIndex > -1: |
|
347 self.channelsWidget.removeTab(emptyIndex) |
|
348 self.__leaveButton.setEnabled(True) |
|
349 self.channelsWidget.setTabsClosable(True) |
|
350 |
|
351 def __query(self, match): |
|
352 """ |
|
353 Private method to handle a new private connection. |
|
354 |
|
355 @param match reference to the match object |
|
356 @type re.Match |
|
357 @return flag indicating, if the message was handled |
|
358 @rtype bool |
|
359 """ |
|
360 # group(1) sender user name |
|
361 # group(2) sender user@host |
|
362 # group(3) target nick |
|
363 # group(4) message |
|
364 if match.group(4).startswith("\x01"): |
|
365 return self.__handleCtcp(match) |
|
366 |
|
367 self.__openPrivate(match.group(1)) |
|
368 # the above call sets the new channel as the current widget |
|
369 channel = self.channelsWidget.currentWidget() |
|
370 channel.addMessage(match.group(1), match.group(4)) |
|
371 channel.setPrivateInfo("{0} - {1}".format(match.group(1), match.group(2))) |
|
372 |
|
373 return True |
|
374 |
|
375 @pyqtSlot(str) |
|
376 def __openPrivate(self, name): |
|
377 """ |
|
378 Private slot to open a private chat with the given user. |
|
379 |
|
380 @param name name of the user |
|
381 @type str |
|
382 """ |
|
383 from .IrcChannelWidget import IrcChannelWidget |
|
384 |
|
385 channel = IrcChannelWidget(self) |
|
386 channel.setName(self.__nickName) |
|
387 channel.setUserName(self.__nickName) |
|
388 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
389 if identity: |
|
390 channel.setPartMessage(identity.getPartMessage()) |
|
391 channel.setUserPrivilegePrefix(self.__userPrefix) |
|
392 channel.setPrivate(True, name) |
|
393 channel.addUsers([name, self.__nickName]) |
|
394 |
|
395 channel.sendData.connect(self.__send) |
|
396 channel.sendCtcpRequest.connect(self.__sendCtcpRequest) |
|
397 channel.sendCtcpReply.connect(self.__sendCtcpReply) |
|
398 channel.channelClosed.connect(self.__closeChannel) |
|
399 channel.awayCommand.connect(self.networkWidget.handleAwayCommand) |
|
400 channel.leaveChannels.connect(self.__leaveChannels) |
|
401 channel.leaveAllChannels.connect(self.__leaveAllChannels) |
|
402 |
|
403 self.channelsWidget.addTab(channel, name) |
|
404 self.__channelList.append(channel) |
|
405 self.channelsWidget.setCurrentWidget(channel) |
|
406 |
|
407 @pyqtSlot() |
|
408 def __leaveChannel(self): |
|
409 """ |
|
410 Private slot to leave a channel and close the associated tab. |
|
411 """ |
|
412 channel = self.channelsWidget.currentWidget() |
|
413 channel.requestLeave() |
|
414 |
|
415 @pyqtSlot(list) |
|
416 def __leaveChannels(self, channelNames): |
|
417 """ |
|
418 Private slot to leave a list of channels and close their associated |
|
419 tabs. |
|
420 |
|
421 @param channelNames list of channels to leave |
|
422 @type list of str |
|
423 """ |
|
424 for channelName in channelNames: |
|
425 for channel in self.__channelList: |
|
426 if channel.name() == channelName: |
|
427 channel.leaveChannel() |
|
428 |
|
429 @pyqtSlot() |
|
430 def __leaveAllChannels(self): |
|
431 """ |
|
432 Private slot to leave all channels and close their tabs. |
|
433 """ |
|
434 while self.__channelList: |
|
435 channel = self.__channelList[0] |
|
436 channel.leaveChannel() |
|
437 |
|
438 def __closeAllChannels(self): |
|
439 """ |
|
440 Private method to close all channels. |
|
441 """ |
|
442 while self.__channelList: |
|
443 channel = self.__channelList.pop() |
|
444 self.channelsWidget.removeTab(self.channelsWidget.indexOf(channel)) |
|
445 channel.deleteLater() |
|
446 channel = None |
|
447 |
|
448 self.channelsWidget.addTab(self.__emptyLabel, "") |
|
449 self.__emptyLabel.show() |
|
450 self.__leaveButton.setEnabled(False) |
|
451 self.channelsWidget.setTabsClosable(False) |
|
452 |
|
453 def __closeChannel(self, name): |
|
454 """ |
|
455 Private slot handling the closing of a channel. |
|
456 |
|
457 @param name name of the closed channel |
|
458 @type str |
|
459 """ |
|
460 for channel in self.__channelList[:]: |
|
461 if channel.name() == name: |
|
462 self.channelsWidget.removeTab(self.channelsWidget.indexOf(channel)) |
|
463 self.__channelList.remove(channel) |
|
464 channel.deleteLater() |
|
465 |
|
466 if self.channelsWidget.count() == 0: |
|
467 self.channelsWidget.addTab(self.__emptyLabel, "") |
|
468 self.__emptyLabel.show() |
|
469 self.__leaveButton.setEnabled(False) |
|
470 self.channelsWidget.setTabsClosable(False) |
|
471 |
|
472 @pyqtSlot(int) |
|
473 def on_channelsWidget_tabCloseRequested(self, index): |
|
474 """ |
|
475 Private slot to close a channel by pressing the close button of |
|
476 the channels widget. |
|
477 |
|
478 @param index index of the tab to be closed |
|
479 @type int |
|
480 """ |
|
481 channel = self.channelsWidget.widget(index) |
|
482 channel.requestLeave() |
|
483 |
|
484 def __send(self, data): |
|
485 """ |
|
486 Private slot to send data to the IRC server. |
|
487 |
|
488 @param data data to be sent |
|
489 @type str |
|
490 """ |
|
491 if self.__socket: |
|
492 self.__socket.write(QByteArray("{0}\r\n".format(data).encode("utf-8"))) |
|
493 |
|
494 def __sendCtcpRequest(self, receiver, request, arguments): |
|
495 """ |
|
496 Private slot to send a CTCP request. |
|
497 |
|
498 @param receiver nick name of the receiver |
|
499 @type str |
|
500 @param request CTCP request to be sent |
|
501 @type str |
|
502 @param arguments arguments to be sent |
|
503 @type str |
|
504 """ |
|
505 request = request.upper() |
|
506 if request == "PING": |
|
507 arguments = "Eric IRC {0}".format(QDateTime.currentMSecsSinceEpoch()) |
|
508 |
|
509 self.__send("PRIVMSG {0} :\x01{1} {2}\x01".format(receiver, request, arguments)) |
|
510 |
|
511 def __sendCtcpReply(self, receiver, text): |
|
512 """ |
|
513 Private slot to send a CTCP reply. |
|
514 |
|
515 @param receiver nick name of the receiver |
|
516 @type str |
|
517 @param text text to be sent |
|
518 @type str |
|
519 """ |
|
520 self.__send("NOTICE {0} :\x01{1}\x01".format(receiver, text)) |
|
521 |
|
522 def __hostFound(self): |
|
523 """ |
|
524 Private slot to indicate the host was found. |
|
525 """ |
|
526 self.networkWidget.addServerMessage( |
|
527 self.tr("Info"), self.tr("Server found,connecting...") |
|
528 ) |
|
529 |
|
530 def __hostConnected(self): |
|
531 """ |
|
532 Private slot to log in to the server after the connection was |
|
533 established. |
|
534 """ |
|
535 self.networkWidget.addServerMessage( |
|
536 self.tr("Info"), self.tr("Connected,logging in...") |
|
537 ) |
|
538 self.networkWidget.setConnected(True) |
|
539 |
|
540 self.__registering = True |
|
541 serverPassword = self.__server.getPassword() |
|
542 if serverPassword: |
|
543 self.__send("PASS " + serverPassword) |
|
544 |
|
545 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
546 nick = self.networkWidget.getNickname() |
|
547 if not nick and identity: |
|
548 self.__nickIndex = 0 |
|
549 try: |
|
550 nick = identity.getNickNames()[self.__nickIndex] |
|
551 except IndexError: |
|
552 nick = "" |
|
553 if not nick: |
|
554 nick = self.__userName |
|
555 self.__nickName = nick |
|
556 self.networkWidget.setNickName(nick) |
|
557 if identity: |
|
558 realName = identity.getRealName() |
|
559 if not realName: |
|
560 realName = "eric IDE chat" |
|
561 self.__send("NICK " + nick) |
|
562 self.__send("USER " + self.__userName + " 0 * :" + realName) |
|
563 |
|
564 def __hostDisconnected(self): |
|
565 """ |
|
566 Private slot to indicate the host was disconnected. |
|
567 """ |
|
568 if self.networkWidget.isConnected(): |
|
569 self.__closeAllChannels() |
|
570 self.networkWidget.addServerMessage( |
|
571 self.tr("Info"), self.tr("Server disconnected.") |
|
572 ) |
|
573 self.networkWidget.setRegistered(False) |
|
574 self.networkWidget.setConnected(False) |
|
575 self.__server = None |
|
576 self.__nickName = "" |
|
577 self.__nickIndex = -1 |
|
578 self.__channelTypePrefixes = "" |
|
579 |
|
580 if self.__socket: |
|
581 self.__socket.deleteLater() |
|
582 self.__socket = None |
|
583 |
|
584 self.__connectionState = IrcConnectionState.Disconnected |
|
585 self.__sslErrorLock = False |
|
586 |
|
587 def __readyRead(self): |
|
588 """ |
|
589 Private slot to read data from the socket. |
|
590 """ |
|
591 if self.__socket: |
|
592 self.__buffer += str( |
|
593 self.__socket.readAll(), Preferences.getSystem("IOEncoding"), "replace" |
|
594 ) |
|
595 if self.__buffer.endswith("\r\n"): |
|
596 for line in self.__buffer.splitlines(): |
|
597 line = line.strip() |
|
598 if line: |
|
599 logging.getLogger(__name__).debug("<IRC> %s", line) |
|
600 handled = False |
|
601 # step 1: give channels a chance to handle the message |
|
602 for channel in self.__channelList: |
|
603 handled = channel.handleMessage(line) |
|
604 if handled: |
|
605 break |
|
606 else: |
|
607 # step 2: try to process the message ourselves |
|
608 for patternRe, patternFunc in self.__patterns: |
|
609 match = patternRe.match(line) |
|
610 if match is not None and patternFunc(match): |
|
611 break |
|
612 else: |
|
613 # Oops, the message wasn't handled |
|
614 self.networkWidget.addErrorMessage( |
|
615 self.tr("Message Error"), |
|
616 self.tr( |
|
617 "Unknown message received from server:" |
|
618 "<br/>{0}" |
|
619 ).format(line), |
|
620 ) |
|
621 |
|
622 self.__updateUsersCount() |
|
623 self.__buffer = "" |
|
624 |
|
625 def __handleCtcpReply(self, match): |
|
626 """ |
|
627 Private method to handle a server message containing a CTCP reply. |
|
628 |
|
629 @param match reference to the match object |
|
630 @type re.Match |
|
631 """ |
|
632 if "!" in match.group(1): |
|
633 sender = match.group(1).split("!", 1)[0] |
|
634 |
|
635 try: |
|
636 ctcpCommand = match.group(3).split(":", 1)[1] |
|
637 except IndexError: |
|
638 ctcpCommand = match.group(3) |
|
639 ctcpCommand = ctcpCommand[1:].split("\x01", 1)[0] |
|
640 if " " in ctcpCommand: |
|
641 ctcpReply, ctcpArg = ctcpCommand.split(" ", 1) |
|
642 else: |
|
643 ctcpReply, ctcpArg = ctcpCommand, "" |
|
644 ctcpReply = ctcpReply.upper() |
|
645 |
|
646 if ctcpReply == "PING" and ctcpArg.startswith("Eric IRC "): |
|
647 # it is a response to a ping request |
|
648 pingDateTime = int(ctcpArg.split()[-1]) |
|
649 latency = QDateTime.currentMSecsSinceEpoch() - pingDateTime |
|
650 self.networkWidget.addServerMessage( |
|
651 self.tr("CTCP"), |
|
652 self.tr( |
|
653 "Received CTCP-PING response from {0} with latency" |
|
654 " of {1} ms." |
|
655 ).format(sender, latency), |
|
656 ) |
|
657 else: |
|
658 self.networkWidget.addServerMessage( |
|
659 self.tr("CTCP"), |
|
660 self.tr("Received unknown CTCP-{0} response from {1}.").format( |
|
661 ctcpReply, sender |
|
662 ), |
|
663 ) |
|
664 |
|
665 def __handleNamedMessage(self, match): |
|
666 """ |
|
667 Private method to handle a server message containing a message name. |
|
668 |
|
669 @param match reference to the match object |
|
670 @type re.Match |
|
671 @return flag indicating, if the message was handled |
|
672 @rtype bool |
|
673 """ |
|
674 name = match.group(2) |
|
675 if name == "NOTICE": |
|
676 try: |
|
677 msg = match.group(3).split(":", 1)[1] |
|
678 except IndexError: |
|
679 msg = match.group(3) |
|
680 |
|
681 if msg.startswith("\x01"): |
|
682 self.__handleCtcpReply(match) |
|
683 return True |
|
684 |
|
685 if "!" in match.group(1): |
|
686 name = match.group(1).split("!", 1)[0] |
|
687 msg = "-{0}- {1}".format(name, msg) |
|
688 self.networkWidget.addServerMessage(self.tr("Notice"), msg) |
|
689 return True |
|
690 elif name == "MODE": |
|
691 self.__registering = False |
|
692 if ":" in match.group(3): |
|
693 # :foo MODE foo :+i |
|
694 name, modes = match.group(3).split(" :") |
|
695 sourceNick = match.group(1) |
|
696 if not self.isChannelName(name) and name == self.__nickName: |
|
697 if sourceNick == self.__nickName: |
|
698 msg = self.tr( |
|
699 "You have set your personal modes to <b>[{0}]</b>." |
|
700 ).format(modes) |
|
701 else: |
|
702 msg = self.tr( |
|
703 "{0} has changed your personal modes to <b>[{1}]</b>." |
|
704 ).format(sourceNick, modes) |
|
705 self.networkWidget.addServerMessage( |
|
706 self.tr("Mode"), msg, filterMsg=False |
|
707 ) |
|
708 return True |
|
709 elif name == "PART": |
|
710 nick = match.group(1).split("!", 1)[0] |
|
711 if nick == self.__nickName: |
|
712 channel = match.group(3).split(None, 1)[0] |
|
713 self.networkWidget.addMessage( |
|
714 self.tr("You have left channel {0}.").format(channel) |
|
715 ) |
|
716 return True |
|
717 elif name == "QUIT": |
|
718 # don't do anything with it here |
|
719 return True |
|
720 elif name == "NICK": |
|
721 # :foo_!n=foo@foohost.bar.net NICK :newnick |
|
722 oldNick = match.group(1).split("!", 1)[0] |
|
723 newNick = match.group(3).split(":", 1)[1] |
|
724 if oldNick == self.__nickName: |
|
725 self.networkWidget.addMessage( |
|
726 self.tr("You are now known as {0}.").format(newNick) |
|
727 ) |
|
728 self.__nickName = newNick |
|
729 self.networkWidget.setNickName(newNick) |
|
730 else: |
|
731 self.networkWidget.addMessage( |
|
732 self.tr("User {0} is now known as {1}.").format(oldNick, newNick) |
|
733 ) |
|
734 return True |
|
735 elif name == "PONG": |
|
736 nick = match.group(3).split(":", 1)[1] |
|
737 self.networkWidget.addMessage( |
|
738 self.tr("Received PONG from {0}").format(nick) |
|
739 ) |
|
740 return True |
|
741 elif name == "ERROR": |
|
742 self.networkWidget.addErrorMessage( |
|
743 self.tr("Server Error"), match.group(3).split(":", 1)[1] |
|
744 ) |
|
745 return True |
|
746 |
|
747 return False |
|
748 |
|
749 def __handleNumericMessage(self, match): |
|
750 """ |
|
751 Private method to handle a server message containing a numeric code. |
|
752 |
|
753 @param match reference to the match object |
|
754 @type re.Match |
|
755 @return flag indicating, if the message was handled |
|
756 @rtype bool |
|
757 """ |
|
758 code = int(match.group(2)) |
|
759 if code < 400: |
|
760 return self.__handleServerReply(code, match.group(1), match.group(3)) |
|
761 else: |
|
762 return self.__handleServerError(code, match.group(3)) |
|
763 |
|
764 def __handleServerError(self, code, message): |
|
765 """ |
|
766 Private slot to handle a server error reply. |
|
767 |
|
768 @param code numerical code sent by the server |
|
769 @type int |
|
770 @param message message sent by the server |
|
771 @type str |
|
772 @return flag indicating, if the message was handled |
|
773 @rtype bool |
|
774 """ |
|
775 if code == 433: |
|
776 if self.__registering: |
|
777 self.__handleNickInUseLogin() |
|
778 else: |
|
779 self.__handleNickInUse() |
|
780 else: |
|
781 self.networkWidget.addServerMessage(self.tr("Error"), message) |
|
782 |
|
783 return True |
|
784 |
|
785 def __handleServerReply(self, code, server, message): |
|
786 """ |
|
787 Private slot to handle a server reply. |
|
788 |
|
789 @param code numerical code sent by the server |
|
790 @type int |
|
791 @param server name of the server |
|
792 @type str |
|
793 @param message message sent by the server |
|
794 @type str |
|
795 @return flag indicating, if the message was handled |
|
796 @rtype bool |
|
797 """ |
|
798 # determine message type |
|
799 if code in [1, 2, 3, 4]: |
|
800 msgType = self.tr("Welcome") |
|
801 elif code == 5: |
|
802 msgType = self.tr("Support") |
|
803 elif code in [250, 251, 252, 253, 254, 255, 265, 266]: |
|
804 msgType = self.tr("User") |
|
805 elif code in [372, 375, 376]: |
|
806 msgType = self.tr("MOTD") |
|
807 elif code in [305, 306]: |
|
808 msgType = self.tr("Away") |
|
809 else: |
|
810 msgType = self.tr("Info ({0})").format(code) |
|
811 |
|
812 # special treatment for some messages |
|
813 if code == 375: |
|
814 message = self.tr("Message of the day") |
|
815 elif code == 376: |
|
816 message = self.tr("End of message of the day") |
|
817 elif code == 4: |
|
818 parts = message.strip().split() |
|
819 message = self.tr( |
|
820 "Server {0} (Version {1}), User-Modes: {2}, Channel-Modes: {3}" |
|
821 ).format(parts[1], parts[2], parts[3], parts[4]) |
|
822 elif code == 265: |
|
823 parts = message.strip().split() |
|
824 message = self.tr("Current users on {0}: {1}, max. {2}").format( |
|
825 server, parts[1], parts[2] |
|
826 ) |
|
827 elif code == 266: |
|
828 parts = message.strip().split() |
|
829 message = self.tr("Current users on the network: {0}, max. {1}").format( |
|
830 parts[1], parts[2] |
|
831 ) |
|
832 elif code == 305: |
|
833 message = self.tr("You are no longer marked as being away.") |
|
834 elif code == 306: |
|
835 message = self.tr("You have been marked as being away.") |
|
836 else: |
|
837 _first, message = message.split(None, 1) |
|
838 if message.startswith(":"): |
|
839 message = message[1:] |
|
840 else: |
|
841 message = message.replace(":", "", 1) |
|
842 |
|
843 self.networkWidget.addServerMessage(msgType, message) |
|
844 |
|
845 if code == 1: |
|
846 # register with services after the welcome message |
|
847 self.__connectionState = IrcConnectionState.Connected |
|
848 self.__registerWithServices() |
|
849 self.networkWidget.setRegistered(True) |
|
850 QTimer.singleShot(1000, self.__autoJoinChannels) |
|
851 elif code == 5: |
|
852 # extract the user privilege prefixes |
|
853 # ... PREFIX=(ov)@+ ... |
|
854 m = self.__prefixRe.match(message) |
|
855 if m: |
|
856 self.__setUserPrivilegePrefix(m.group(1), m.group(2)) |
|
857 # extract the channel type prefixes |
|
858 # ... CHANTYPES=# ... |
|
859 m = self.__chanTypesRe.match(message) |
|
860 if m: |
|
861 self.__setChannelTypePrefixes(m.group(1)) |
|
862 |
|
863 return True |
|
864 |
|
865 def __registerWithServices(self): |
|
866 """ |
|
867 Private method to register to services. |
|
868 """ |
|
869 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
870 if identity: |
|
871 service = identity.getServiceName() |
|
872 password = identity.getPassword() |
|
873 if service and password: |
|
874 self.__send("PRIVMSG " + service + " :identify " + password) |
|
875 |
|
876 def __autoJoinChannels(self): |
|
877 """ |
|
878 Private slot to join channels automatically once a server got |
|
879 connected. |
|
880 """ |
|
881 for channel in self.networkWidget.getNetworkChannels(): |
|
882 if channel.autoJoin(): |
|
883 name = channel.getName() |
|
884 key = channel.getKey() |
|
885 self.joinChannel(name, key) |
|
886 |
|
887 def __tcpError(self, error): |
|
888 """ |
|
889 Private slot to handle errors reported by the TCP socket. |
|
890 |
|
891 @param error error code reported by the socket |
|
892 @type QAbstractSocket.SocketError |
|
893 """ |
|
894 if error == QAbstractSocket.SocketError.RemoteHostClosedError: |
|
895 # ignore this one, it's a disconnect |
|
896 if self.__sslErrorLock: |
|
897 self.networkWidget.addErrorMessage( |
|
898 self.tr("SSL Error"), |
|
899 self.tr( |
|
900 """Connection to server {0} (port {1}) lost while""" |
|
901 """ waiting for user response to an SSL error.""" |
|
902 ).format(self.__server.getName(), self.__server.getPort()), |
|
903 ) |
|
904 self.__connectionState = IrcConnectionState.Disconnected |
|
905 elif error == QAbstractSocket.SocketError.HostNotFoundError: |
|
906 self.networkWidget.addErrorMessage( |
|
907 self.tr("Socket Error"), |
|
908 self.tr( |
|
909 "The host was not found. Please check the host name" |
|
910 " and port settings." |
|
911 ), |
|
912 ) |
|
913 elif error == QAbstractSocket.SocketError.ConnectionRefusedError: |
|
914 self.networkWidget.addErrorMessage( |
|
915 self.tr("Socket Error"), |
|
916 self.tr( |
|
917 "The connection was refused by the peer. Please check the" |
|
918 " host name and port settings." |
|
919 ), |
|
920 ) |
|
921 elif error == QAbstractSocket.SocketError.SslHandshakeFailedError: |
|
922 self.networkWidget.addErrorMessage( |
|
923 self.tr("Socket Error"), self.tr("The SSL handshake failed.") |
|
924 ) |
|
925 else: |
|
926 if self.__socket: |
|
927 self.networkWidget.addErrorMessage( |
|
928 self.tr("Socket Error"), |
|
929 self.tr("The following network error occurred:<br/>{0}").format( |
|
930 self.__socket.errorString() |
|
931 ), |
|
932 ) |
|
933 else: |
|
934 self.networkWidget.addErrorMessage( |
|
935 self.tr("Socket Error"), self.tr("A network error occurred.") |
|
936 ) |
|
937 |
|
938 def __sslErrors(self, errors): |
|
939 """ |
|
940 Private slot to handle SSL errors. |
|
941 |
|
942 @param errors list of SSL errors |
|
943 @type list of QSslError |
|
944 """ |
|
945 ignored, defaultChanged = self.__sslErrorHandler.sslErrors( |
|
946 errors, self.__server.getName(), self.__server.getPort() |
|
947 ) |
|
948 if ignored == EricSslErrorState.NOT_IGNORED: |
|
949 self.networkWidget.addErrorMessage( |
|
950 self.tr("SSL Error"), |
|
951 self.tr( |
|
952 """Could not connect to {0} (port {1}) using an SSL""" |
|
953 """ encrypted connection. Either the server does not""" |
|
954 """ support SSL (did you use the correct port?) or""" |
|
955 """ you rejected the certificate.""" |
|
956 ).format(self.__server.getName(), self.__server.getPort()), |
|
957 ) |
|
958 self.__socket.close() |
|
959 else: |
|
960 if defaultChanged: |
|
961 self.__socket.setSslConfiguration( |
|
962 QSslConfiguration.defaultConfiguration() |
|
963 ) |
|
964 if ignored == EricSslErrorState.USER_IGNORED: |
|
965 self.networkWidget.addErrorMessage( |
|
966 self.tr("SSL Error"), |
|
967 self.tr( |
|
968 """The SSL certificate for the server {0} (port {1})""" |
|
969 """ failed the authenticity check. SSL errors""" |
|
970 """ were accepted by you.""" |
|
971 ).format(self.__server.getName(), self.__server.getPort()), |
|
972 ) |
|
973 if self.__connectionState == IrcConnectionState.Connecting: |
|
974 self.__socket.ignoreSslErrors() |
|
975 |
|
976 def __setUserPrivilegePrefix(self, prefix1, prefix2): |
|
977 """ |
|
978 Private method to set the user privilege prefix. |
|
979 |
|
980 @param prefix1 first part of the prefix |
|
981 @type str |
|
982 @param prefix2 indictors the first part gets mapped to |
|
983 @type str |
|
984 """ |
|
985 # PREFIX=(ov)@+ |
|
986 # o = @ -> @ircbot , channel operator |
|
987 # v = + -> +userName , voice operator |
|
988 for i in range(len(prefix1)): |
|
989 self.__userPrefix["+" + prefix1[i]] = prefix2[i] |
|
990 self.__userPrefix["-" + prefix1[i]] = "" |
|
991 |
|
992 def __ping(self, match): |
|
993 """ |
|
994 Private method to handle a PING message. |
|
995 |
|
996 @param match reference to the match object |
|
997 @type re.Match |
|
998 @return flag indicating, if the message was handled |
|
999 @rtype bool |
|
1000 """ |
|
1001 self.__send("PONG " + match.group(1)) |
|
1002 return True |
|
1003 |
|
1004 def __handleCtcp(self, match): |
|
1005 """ |
|
1006 Private method to handle a CTCP command. |
|
1007 |
|
1008 @param match reference to the match object |
|
1009 @type re.Match |
|
1010 @return flag indicating, if the message was handled |
|
1011 @rtype bool |
|
1012 """ |
|
1013 # group(1) sender user name |
|
1014 # group(2) sender user@host |
|
1015 # group(3) target nick |
|
1016 # group(4) message |
|
1017 if match.group(4).startswith("\x01"): |
|
1018 ctcpCommand = match.group(4)[1:].split("\x01", 1)[0] |
|
1019 if " " in ctcpCommand: |
|
1020 ctcpRequest, ctcpArg = ctcpCommand.split(" ", 1) |
|
1021 else: |
|
1022 ctcpRequest, ctcpArg = ctcpCommand, "" |
|
1023 ctcpRequest = ctcpRequest.lower() |
|
1024 if ctcpRequest == "version": |
|
1025 if Version.startswith("@@"): |
|
1026 vers = "" |
|
1027 else: |
|
1028 vers = " " + Version |
|
1029 msg = "Eric IRC client{0}, {1}".format(vers, Copyright) |
|
1030 self.networkWidget.addServerMessage( |
|
1031 self.tr("CTCP"), |
|
1032 self.tr("Received Version request from {0}.").format( |
|
1033 match.group(1) |
|
1034 ), |
|
1035 ) |
|
1036 self.__sendCtcpReply(match.group(1), "VERSION " + msg) |
|
1037 elif ctcpRequest == "ping": |
|
1038 self.networkWidget.addServerMessage( |
|
1039 self.tr("CTCP"), |
|
1040 self.tr( |
|
1041 "Received CTCP-PING request from {0}, sending answer." |
|
1042 ).format(match.group(1)), |
|
1043 ) |
|
1044 self.__sendCtcpReply(match.group(1), "PING {0}".format(ctcpArg)) |
|
1045 elif ctcpRequest == "clientinfo": |
|
1046 self.networkWidget.addServerMessage( |
|
1047 self.tr("CTCP"), |
|
1048 self.tr( |
|
1049 "Received CTCP-CLIENTINFO request from {0}, sending answer." |
|
1050 ).format(match.group(1)), |
|
1051 ) |
|
1052 self.__sendCtcpReply( |
|
1053 match.group(1), "CLIENTINFO CLIENTINFO PING VERSION" |
|
1054 ) |
|
1055 else: |
|
1056 self.networkWidget.addServerMessage( |
|
1057 self.tr("CTCP"), |
|
1058 self.tr("Received unknown CTCP-{0} request from {1}.").format( |
|
1059 ctcpRequest, match.group(1) |
|
1060 ), |
|
1061 ) |
|
1062 return True |
|
1063 |
|
1064 return False |
|
1065 |
|
1066 def __updateUsersCount(self): |
|
1067 """ |
|
1068 Private method to update the users count on the channel tabs. |
|
1069 """ |
|
1070 for channel in self.__channelList: |
|
1071 index = self.channelsWidget.indexOf(channel) |
|
1072 self.channelsWidget.setTabText( |
|
1073 index, |
|
1074 self.tr("{0} ({1})", "channel name, users count").format( |
|
1075 channel.name(), channel.getUsersCount() |
|
1076 ), |
|
1077 ) |
|
1078 |
|
1079 def __handleNickInUseLogin(self): |
|
1080 """ |
|
1081 Private method to handle a 443 server error at login. |
|
1082 """ |
|
1083 self.__nickIndex += 1 |
|
1084 try: |
|
1085 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
1086 if identity: |
|
1087 nick = identity.getNickNames()[self.__nickIndex] |
|
1088 self.__nickName = nick |
|
1089 else: |
|
1090 self.__connectNetwork("", False, True) |
|
1091 self.__nickName = "" |
|
1092 self.__nickIndex = -1 |
|
1093 return |
|
1094 except IndexError: |
|
1095 self.networkWidget.addServerMessage( |
|
1096 self.tr("Critical"), |
|
1097 self.tr( |
|
1098 "No nickname acceptable to the server configured" |
|
1099 " for <b>{0}</b>. Disconnecting..." |
|
1100 ).format(self.__userName), |
|
1101 filterMsg=False, |
|
1102 ) |
|
1103 self.__connectNetwork("", False, True) |
|
1104 self.__nickName = "" |
|
1105 self.__nickIndex = -1 |
|
1106 return |
|
1107 |
|
1108 self.networkWidget.setNickName(nick) |
|
1109 self.__send("NICK " + nick) |
|
1110 |
|
1111 def __handleNickInUse(self): |
|
1112 """ |
|
1113 Private method to handle a 443 server error. |
|
1114 """ |
|
1115 self.networkWidget.addServerMessage( |
|
1116 self.tr("Critical"), self.tr("The given nickname is already in use.") |
|
1117 ) |
|
1118 |
|
1119 def __changeNick(self, nick): |
|
1120 """ |
|
1121 Private slot to use a new nick name. |
|
1122 |
|
1123 @param nick nick name to use |
|
1124 @type str |
|
1125 """ |
|
1126 if nick and nick != self.__nickName: |
|
1127 self.__send("NICK " + nick) |
|
1128 |
|
1129 def __setChannelTypePrefixes(self, prefixes): |
|
1130 """ |
|
1131 Private method to set the channel type prefixes. |
|
1132 |
|
1133 @param prefixes channel prefix characters |
|
1134 @type str |
|
1135 """ |
|
1136 self.__channelTypePrefixes = prefixes |
|
1137 |
|
1138 def isChannelName(self, name): |
|
1139 """ |
|
1140 Public method to check, if the given name is a channel name. |
|
1141 |
|
1142 @param name name to check |
|
1143 @type str |
|
1144 @return flag indicating a channel name |
|
1145 @rtype bool |
|
1146 """ |
|
1147 if not name: |
|
1148 return False |
|
1149 |
|
1150 if self.__channelTypePrefixes: |
|
1151 return name[0] in self.__channelTypePrefixes |
|
1152 else: |
|
1153 return name[0] in "#&" |
|
1154 |
|
1155 def __away(self, isAway): |
|
1156 """ |
|
1157 Private slot handling the change of the away state. |
|
1158 |
|
1159 @param isAway flag indicating the current away state |
|
1160 @type bool |
|
1161 """ |
|
1162 if isAway and self.__identityName: |
|
1163 identity = self.__ircNetworkManager.getIdentity(self.__identityName) |
|
1164 if identity and identity.rememberAwayPosition(): |
|
1165 for channel in self.__channelList: |
|
1166 channel.setMarkerLine() |