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