Network/IRC/IrcWidget.py

changeset 2227
b7aceb255831
child 2232
47290dad6d0b
equal deleted inserted replaced
2225:0139003972cd 2227:b7aceb255831
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 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 PyQt4.QtCore import pyqtSlot, Qt, QByteArray
14 from PyQt4.QtGui import QWidget, QToolButton, QLabel
15 from PyQt4.QtNetwork import QTcpSocket, QAbstractSocket
16
17 from E5Gui import E5MessageBox
18
19 from .Ui_IrcWidget import Ui_IrcWidget
20
21 from .IrcNetworkManager import IrcNetworkManager
22 from .IrcChannelWidget import IrcChannelWidget
23
24 import Preferences
25 import UI.PixmapCache
26
27
28 class IrcWidget(QWidget, Ui_IrcWidget):
29 """
30 Class implementing the IRC window.
31 """
32 def __init__(self, parent=None):
33 """
34 Constructor
35
36 @param parent reference to the parent widget (QWidget)
37 """
38 super().__init__(parent)
39 self.setupUi(self)
40
41 self.__ircNetworkManager = IrcNetworkManager(self)
42
43 self.__leaveButton = QToolButton(self)
44 self.__leaveButton.setIcon(UI.PixmapCache.getIcon("ircCloseChannel.png"))
45 self.__leaveButton.setToolTip(self.trUtf8("Press to leave the current channel"))
46 self.__leaveButton.clicked[()].connect(self.__leaveChannel)
47 self.__leaveButton.setEnabled(False)
48 self.channelsWidget.setCornerWidget(self.__leaveButton, Qt.BottomRightCorner)
49 self.channelsWidget.setTabsClosable(False)
50
51 self.networkWidget.initialize(self.__ircNetworkManager)
52 self.networkWidget.connectNetwork.connect(self.__connectNetwork)
53 self.networkWidget.editNetwork.connect(self.__editNetwork)
54 self.networkWidget.joinChannel.connect(self.__joinChannel)
55 self.networkWidget.nickChanged.connect(self.__changeNick)
56
57 self.__channelList = []
58 self.__channelTypePrefixes = ""
59 self.__userName = ""
60 self.__nickIndex = -1
61 self.__nickName = ""
62 self.__server = None
63 self.__registering = False
64
65 self.__buffer = ""
66 self.__userPrefix = {}
67
68 # create TCP socket
69 self.__socket = QTcpSocket(self)
70 self.__socket.hostFound.connect(self.__hostFound)
71 self.__socket.connected.connect(self.__hostConnected)
72 self.__socket.disconnected.connect(self.__hostDisconnected)
73 self.__socket.readyRead.connect(self.__readyRead)
74 self.__socket.error.connect(self.__tcpError)
75
76 self.__patterns = [
77 # :foo.bar.net COMMAND some message
78 (re.compile(r""":([^ ]+)\s+([A-Z]+)\s+(.+)"""), self.__handleNamedMessage),
79 # :foo.bar.net 123 * :info
80 (re.compile(r""":([^ ]+)\s+(\d{3})\s+(.+)"""), self.__handleNumericMessage),
81 # PING :ping message
82 (re.compile(r"""PING\s+:(.*)"""), self.__ping),
83 ]
84 self.__prefixRe = re.compile(r""".*\sPREFIX=\((.*)\)([^ ]+).*""")
85 self.__chanTypesRe = re.compile(r""".*\sCHANTYPES=([^ ]+).*""")
86
87 ircPic = UI.PixmapCache.getPixmap("irc128.png")
88 self.__emptyLabel = QLabel()
89 self.__emptyLabel.setPixmap(ircPic)
90 self.__emptyLabel.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
91 self.channelsWidget.addTab(self.__emptyLabel, "")
92
93
94 def shutdown(self):
95 """
96 Public method to shut down the widget.
97
98 @return flag indicating successful shutdown (boolean)
99 """
100 if self.__server:
101 ok = E5MessageBox.yesNo(self,
102 self.trUtf8("Disconnect from Server"),
103 self.trUtf8("""<p>Do you really want to disconnect from"""
104 """ <b>{0}</b>?</p><p>All channels will be closed.</p>""")\
105 .format(self.__server.getServer()))
106 if ok:
107 self.__socket.blockSignals(True)
108
109 self.__send("QUIT :" + self.trUtf8("IRC for eric IDE"))
110 self.__socket.close()
111 self.__socket.deleteLater()
112 else:
113 ok = True
114
115 if ok:
116 self.__ircNetworkManager.close()
117 return ok
118
119 def __connectNetwork(self, name, connect):
120 """
121 Private slot to connect to or disconnect from the given network.
122
123 @param name name of the network to connect to (string)
124 @param connect flag indicating to connect (boolean)
125 """
126 if connect:
127 network = self.__ircNetworkManager.getNetwork(name)
128 self.__server = self.__ircNetworkManager.getServer(network.getServerName())
129 self.__userName = network.getIdentityName()
130 if self.__server:
131 self.networkWidget.addServerMessage(self.trUtf8("Info"),
132 self.trUtf8("Looking for server {0} (port {1})...").format(
133 self.__server.getServer(), self.__server.getPort()))
134 self.__socket.connectToHost(self.__server.getServer(),
135 self.__server.getPort())
136 else:
137 ok = E5MessageBox.yesNo(self,
138 self.trUtf8("Disconnect from Server"),
139 self.trUtf8("""<p>Do you really want to disconnect from"""
140 """ <b>{0}</b>?</p><p>All channels will be closed.</p>""")\
141 .format(self.__server.getServer()))
142 if ok:
143 self.networkWidget.addServerMessage(self.trUtf8("Info"),
144 self.trUtf8("Disconnecting from server {0}...").format(
145 self.__server.getServer()))
146 while self.__channelList:
147 channel = self.__channelList.pop()
148 self.channelsWidget.removeTab(self.channelsWidget.indexOf(channel))
149 channel.deleteLater()
150 channel = None
151 self.__send("QUIT :" + self.trUtf8("IRC for eric IDE"))
152 self.__socket.close()
153
154 def __editNetwork(self, name):
155 """
156 Private slot to edit the network configuration.
157
158 @param name name of the network to edit (string)
159 """
160 # TODO: implement this
161
162 def __joinChannel(self, name):
163 """
164 Private slot to join a channel.
165
166 @param name name of the channel (string)
167 """
168 # step 1: check, if this channel is already joined
169 for channel in self.__channelList:
170 if channel.name() == name:
171 return
172
173 channel = IrcChannelWidget(self)
174 channel.setName(name)
175 channel.setUserName(self.__nickName)
176 channel.setPartMessage(self.trUtf8("IRC for eric IDE"))
177 channel.setUserPrivilegePrefix(self.__userPrefix)
178
179 channel.sendData.connect(self.__send)
180 channel.channelClosed.connect(self.__closeChannel)
181
182 self.channelsWidget.addTab(channel, name)
183 self.__channelList.append(channel)
184
185 self.__send("JOIN " + name)
186 self.__send("MODE " + name)
187
188 emptyIndex = self.channelsWidget.indexOf(self.__emptyLabel)
189 if emptyIndex > -1:
190 self.channelsWidget.removeTab(emptyIndex)
191 self.__leaveButton.setEnabled(True)
192 self.channelsWidget.setTabsClosable(True)
193
194 @pyqtSlot()
195 def __leaveChannel(self):
196 """
197 Private slot to leave a channel and close the associated tab.
198 """
199 channel = self.channelsWidget.currentWidget()
200 channel.requestLeave()
201
202 def __closeChannel(self, name):
203 """
204 Private slot handling the closing of a channel.
205
206 @param name name of the closed channel (string)
207 """
208 for channel in self.__channelList:
209 if channel.name() == name:
210 self.channelsWidget.removeTab(self.channelsWidget.indexOf(channel))
211 self.__channelList.remove(channel)
212 channel.deleteLater()
213
214 if self.channelsWidget.count() == 0:
215 self.channelsWidget.addTab(self.__emptyLabel, "")
216 self.__leaveButton.setEnabled(False)
217 self.channelsWidget.setTabsClosable(False)
218
219 @pyqtSlot(int)
220 def on_channelsWidget_tabCloseRequested(self, index):
221 """
222 Private slot to close a channel by pressing the close button of
223 the channels widget.
224
225 @param index index of the tab to be closed (integer)
226 """
227 channel = self.channelsWidget.widget(index)
228 channel.requestLeave()
229
230 def __send(self, data):
231 """
232 Private slot to send data to the IRC server.
233
234 @param data data to be sent (string)
235 """
236 self.__socket.write(QByteArray("{0}\r\n".format(data).encode("utf-8")))
237
238 def __hostFound(self):
239 """
240 Private slot to indicate the host was found.
241 """
242 self.networkWidget.addServerMessage(self.trUtf8("Info"),
243 self.trUtf8("Server found,connecting..."))
244
245 def __hostConnected(self):
246 """
247 Private slot to log in to the server after the connection was established.
248 """
249 self.networkWidget.addServerMessage(self.trUtf8("Info"),
250 self.trUtf8("Connected,logging in..."))
251 self.networkWidget.setConnected(True)
252
253 self.__registering = True
254 serverPassword = self.__server.getPassword()
255 if serverPassword:
256 self.__send("PASS " + serverPassword)
257 nick = self.networkWidget.getNickname()
258 if not nick:
259 self.__nickIndex = 0
260 try:
261 nick = self.__ircNetworkManager.getIdentity(self.__userName)\
262 .getNickNames()[self.__nickIndex]
263 except IndexError:
264 nick = ""
265 if not nick:
266 nick = self.__userName
267 self.__nickName = nick
268 self.networkWidget.setNickName(nick)
269 self.__send("NICK " + nick)
270 self.__send("USER " + self.__userName + " 0 * :eric IDE chat")
271
272 def __hostDisconnected(self):
273 """
274 Private slot to indicate the host was disconnected.
275 """
276 self.networkWidget.addServerMessage(self.trUtf8("Info"),
277 self.trUtf8("Server disconnected."))
278 self.networkWidget.setConnected(False)
279 self.__server = None
280 self.__nickName = ""
281 self.__nickIndex = -1
282 self.__channelTypePrefixes = ""
283
284 def __readyRead(self):
285 """
286 Private slot to read data from the socket.
287 """
288 self.__buffer += str(self.__socket.readAll(),
289 Preferences.getSystem("IOEncoding"),
290 'replace')
291 if self.__buffer.endswith("\r\n"):
292 for line in self.__buffer.splitlines():
293 line = line.strip()
294 if line:
295 logging.debug("<IRC> " + line)
296 handled = False
297 # step 1: give channels a chance to handle the message
298 for channel in self.__channelList:
299 handled = channel.handleMessage(line)
300 if handled:
301 break
302 else:
303 # step 2: try to process the message ourselves
304 for patternRe, patternFunc in self.__patterns:
305 match = patternRe.match(line)
306 if match is not None:
307 if patternFunc(match):
308 break
309 else:
310 # Oops, the message wasn't handled
311 self.networkWidget.addErrorMessage(
312 self.trUtf8("Message Error"),
313 self.trUtf8("Unknown message received from server:"
314 "<br/>{0}").format(line))
315
316 self.__updateUsersCount()
317 self.__buffer = ""
318
319 def __handleNamedMessage(self, match):
320 """
321 Private method to handle a server message containing a message name.
322
323 @param reference to the match object
324 @return flag indicating, if the message was handled (boolean)
325 """
326 name = match.group(2)
327 if name == "NOTICE":
328 try:
329 msg = match.group(3).split(":", 1)[1]
330 except IndexError:
331 msg = match.group(3)
332 if "!" in match.group(1):
333 name = match.group(1).split("!", 1)[0]
334 msg = "-{0}- {1}".format(name, msg)
335 self.networkWidget.addServerMessage(self.trUtf8("Notice"), msg)
336 return True
337 elif name == "MODE":
338 self.__registering = False
339 if ":" in match.group(3):
340 # :detlev_ MODE detlev_ :+i
341 name, modes = match.group(3).split(" :")
342 sourceNick = match.group(1)
343 if not self.__isChannelName(name):
344 if name == self.__nickName:
345 if sourceNick == self.__nickName:
346 msg = self.trUtf8(
347 "You have set your personal modes to <b>[{0}]</b>")\
348 .format(modes)
349 else:
350 msg = self.trUtf8(
351 "{0} has changed your personal modes to <b>[{1}]</b>")\
352 .format(sourceNick, modes)
353 self.networkWidget.addServerMessage(
354 self.trUtf8("Mode"), msg, filterMsg=False)
355 return True
356 elif name == "PART":
357 nick = match.group(1).split("!", 1)[0]
358 if nick == self.__nickName:
359 channel = match.group(3).split(None, 1)[0]
360 self.networkWidget.addMessage(
361 self.trUtf8("You have left channel {0}.").format(channel))
362 return True
363 elif name == "NICK":
364 # :foo_!n=foo@foohost.bar.net NICK :newnick
365 oldNick = match.group(1).split("!", 1)[0]
366 newNick = match.group(3).split(":", 1)[1]
367 if oldNick == self.__nickName:
368 self.networkWidget.addMessage(
369 self.trUtf8("You are now known as {0}.").format(newNick))
370 self.__nickName = newNick
371 self.networkWidget.setNickName(newNick)
372 else:
373 self.networkWidget.addMessage(
374 self.trUtf8("User {0} is now known as {1}.").format(
375 oldNick, newNick))
376 return True
377
378 return False
379
380 def __handleNumericMessage(self, match):
381 """
382 Private method to handle a server message containing a numeric code.
383
384 @param reference to the match object
385 @return flag indicating, if the message was handled (boolean)
386 """
387 code = int(match.group(2))
388 if code < 400:
389 return self.__handleServerReply(code, match.group(1), match.group(3))
390 else:
391 return self.__handleServerError(code, match.group(1), match.group(3))
392
393 def __handleServerError(self, code, server, message):
394 """
395 Private slot to handle a server error reply.
396
397 @param code numerical code sent by the server (integer)
398 @param server name of the server (string)
399 @param message message sent by the server (string)
400 @return flag indicating, if the message was handled (boolean)
401 """
402 if code == 433:
403 if self.__registering:
404 self.__handleNickInUseLogin()
405 else:
406 self.__handleNickInUse()
407 else:
408 self.networkWidget.addServerMessage(self.trUtf8("Error"), message)
409
410 return True
411
412 def __handleServerReply(self, code, server, message):
413 """
414 Private slot to handle a server reply.
415
416 @param code numerical code sent by the server (integer)
417 @param server name of the server (string)
418 @param message message sent by the server (string)
419 @return flag indicating, if the message was handled (boolean)
420 """
421 # determine message type
422 if code in [1, 2, 3, 4]:
423 msgType = self.trUtf8("Welcome")
424 elif code == 5:
425 msgType = self.trUtf8("Support")
426 elif code in [250, 251, 252, 253, 254, 255, 265, 266]:
427 msgType = self.trUtf8("User")
428 elif code in [372, 375, 376]:
429 msgType = self.trUtf8("MOTD")
430 else:
431 msgType = self.trUtf8("Info ({0})").format(code)
432
433 # special treatment for some messages
434 if code == 375:
435 message = self.trUtf8("Message of the day")
436 elif code == 376:
437 message = self.trUtf8("End of message of the day")
438 elif code == 4:
439 parts = message.strip().split()
440 message = self.trUtf8("Server {0} (Version {1}), User-Modes: {2},"
441 " Channel-Modes: {3}").format(parts[1], parts[2], parts[3], parts[4])
442 elif code == 265:
443 parts = message.strip().split()
444 message = self.trUtf8("Current users on {0}: {1}, max. {2}").format(
445 server, parts[1], parts[2])
446 elif code == 266:
447 parts = message.strip().split()
448 message = self.trUtf8("Current users on the network: {0}, max. {1}").format(
449 parts[1], parts[2])
450 else:
451 first, message = message.split(None, 1)
452 if message.startswith(":"):
453 message = message[1:]
454 else:
455 message = message.replace(":", "", 1)
456
457 self.networkWidget.addServerMessage(msgType, message)
458
459 if code == 1:
460 # register with services after the welcome message
461 self.__registerWithServices()
462 elif code == 5:
463 # extract the user privilege prefixes
464 # ... PREFIX=(ov)@+ ...
465 m = self.__prefixRe.match(message)
466 if m:
467 self.__setUserPrivilegePrefix(m.group(1), m.group(2))
468 # extract the channel type prefixes
469 # ... CHANTYPES=# ...
470 m = self.__chanTypesRe.match(message)
471 if m:
472 self.__setChannelTypePrefixes(m.group(1))
473
474 return True
475
476 def __registerWithServices(self):
477 """
478 Private method to register to services.
479 """
480 identity = self.__ircNetworkManager.getIdentity(self.__userName)
481 service = identity.getName()
482 password = identity.getPassword()
483 if service and password:
484 self.__send("PRIVMSG " + service + " :identify " + password)
485
486 def __tcpError(self, error):
487 """
488 Private slot to handle errors reported by the TCP socket.
489
490 @param error error code reported by the socket
491 (QAbstractSocket.SocketError)
492 """
493 if error == QAbstractSocket.RemoteHostClosedError:
494 # ignore this one, it's a disconnect
495 pass
496 elif error == QAbstractSocket.HostNotFoundError:
497 self.networkWidget.addErrorMessage(self.trUtf8("Socket Error"),
498 self.trUtf8("The host was not found. Please check the host name"
499 " and port settings."))
500 elif error == QAbstractSocket.ConnectionRefusedError:
501 self.networkWidget.addErrorMessage(self.trUtf8("Socket Error"),
502 self.trUtf8("The connection was refused by the peer. Please check the"
503 " host name and port settings."))
504 else:
505 self.networkWidget.addErrorMessage(self.trUtf8("Socket Error"),
506 self.trUtf8("The following network error occurred:<br/>{0}").format(
507 self.__socket.errorString()))
508
509 def __setUserPrivilegePrefix(self, prefix1, prefix2):
510 """
511 Private method to set the user privilege prefix.
512
513 @param prefix1 first part of the prefix (string)
514 @param prefix2 indictors the first part gets mapped to (string)
515 """
516 # PREFIX=(ov)@+
517 # o = @ -> @ircbot , channel operator
518 # v = + -> +userName , voice operator
519 for i in range(len(prefix1)):
520 self.__userPrefix["+" + prefix1[i]] = prefix2[i]
521 self.__userPrefix["-" + prefix1[i]] = ""
522
523 def __ping(self, match):
524 """
525 Private method to handle a PING message.
526
527 @param reference to the match object
528 @return flag indicating, if the message was handled (boolean)
529 """
530 self.__send("PONG " + match.group(1))
531 return True
532
533 def __updateUsersCount(self):
534 """
535 Private method to update the users count on the channel tabs.
536 """
537 for channel in self.__channelList:
538 index = self.channelsWidget.indexOf(channel)
539 self.channelsWidget.setTabText(index,
540 self.trUtf8("{0} ({1})", "channel name, users count").format(
541 channel.name(), channel.getUsersCount()))
542
543 def __handleNickInUseLogin(self):
544 """
545 Private method to handle a 443 server error at login.
546 """
547 self.__nickIndex += 1
548 try:
549 nick = self.__ircNetworkManager.getIdentity(self.__userName)\
550 .getNickNames()[self.__nickIndex]
551 self.__nickName = nick
552 except IndexError:
553 self.networkWidget.addServerMessage(self.trUtf8("Critical"),
554 self.trUtf8("No nickname acceptable to the server configured"
555 " for <b>{0}</b>. Disconnecting...").format(self.__userName))
556 self.__connectNetwork("", False)
557 self.__nickName = ""
558 self.__nickIndex = -1
559 return
560
561 self.networkWidget.setNickName(nick)
562 self.__send("NICK " + nick)
563
564 def __handleNickInUse(self):
565 """
566 Private method to handle a 443 server error.
567 """
568 self.networkWidget.addServerMessage(self.trUtf8("Critical"),
569 self.trUtf8("The given nickname is already in use."))
570
571 def __changeNick(self, nick):
572 """
573 Private slot to use a new nick name.
574
575 @param nick nick name to use (str)
576 """
577 self.__send("NICK " + nick)
578
579 def __setChannelTypePrefixes(self, prefixes):
580 """
581 Private method to set the channel type prefixes.
582
583 @param prefixes channel prefix characters (string)
584 """
585 self.__channelTypePrefixes = prefixes
586
587 def __isChannelName(self, name):
588 """
589 Private method to check, if the given name is a channel name.
590
591 @return flag indicating a channel name (boolean)
592 """
593 if not name:
594 return False
595
596 if self.__channelTypePrefixes:
597 return name[0] in self.__channelTypePrefixes
598 else:
599 return name[0] in "#&"

eric ide

mercurial