src/eric7/Cooperation/CooperationClient.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the client of the cooperation package.
8 """
9
10 import collections
11
12 from PyQt6.QtCore import QObject, pyqtSignal, QProcess
13 from PyQt6.QtNetwork import (
14 QHostInfo, QHostAddress, QAbstractSocket, QNetworkInterface
15 )
16
17 from .CooperationServer import CooperationServer
18 from .Connection import Connection
19
20 import Preferences
21
22
23 class CooperationClient(QObject):
24 """
25 Class implementing the client of the cooperation package.
26
27 @signal newMessage(user, message) emitted after a new message has
28 arrived (string, string)
29 @signal newParticipant(nickname) emitted after a new participant joined
30 (string)
31 @signal participantLeft(nickname) emitted after a participant left (string)
32 @signal connectionError(message) emitted when a connection error occurs
33 (string)
34 @signal cannotConnect() emitted, if the initial connection fails
35 @signal editorCommand(hash, filename, message) emitted when an editor
36 command has been received (string, string, string)
37 """
38 newMessage = pyqtSignal(str, str)
39 newParticipant = pyqtSignal(str)
40 participantLeft = pyqtSignal(str)
41 connectionError = pyqtSignal(str)
42 cannotConnect = pyqtSignal()
43 editorCommand = pyqtSignal(str, str, str)
44
45 def __init__(self, parent=None):
46 """
47 Constructor
48
49 @param parent reference to the parent object (QObject)
50 """
51 super().__init__(parent)
52
53 self.__chatWidget = parent
54
55 self.__servers = []
56 for networkInterface in QNetworkInterface.allInterfaces():
57 for addressEntry in networkInterface.addressEntries():
58 address = addressEntry.ip()
59 # fix scope of link local addresses
60 if address.toString().lower().startswith("fe80"):
61 address.setScopeId(networkInterface.humanReadableName())
62 server = CooperationServer(address, self)
63 server.newConnection.connect(self.__newConnection)
64 self.__servers.append(server)
65
66 self.__peers = collections.defaultdict(list)
67
68 self.__initialConnection = None
69
70 envVariables = ["USERNAME", "USERDOMAIN", "USER",
71 "HOSTNAME", "DOMAINNAME"]
72 environment = QProcess.systemEnvironment()
73 found = False
74 for envVariable in envVariables:
75 for env in environment:
76 if env.startswith(envVariable):
77 envList = env.split("=")
78 if len(envList) == 2:
79 self.__username = envList[1].strip()
80 found = True
81 break
82
83 if found:
84 break
85
86 if self.__username == "":
87 self.__username = self.tr("unknown")
88
89 self.__listening = False
90 self.__serversErrorString = ""
91
92 def chatWidget(self):
93 """
94 Public method to get a reference to the chat widget.
95
96 @return reference to the chat widget (ChatWidget)
97 """
98 return self.__chatWidget
99
100 def sendMessage(self, message):
101 """
102 Public method to send a message.
103
104 @param message message to be sent (string)
105 """
106 if message == "":
107 return
108
109 for connectionList in self.__peers.values():
110 for connection in connectionList:
111 connection.sendMessage(message)
112
113 def nickName(self):
114 """
115 Public method to get the nick name.
116
117 @return nick name (string)
118 """
119 return "{0}@{1}@{2}".format(
120 self.__username,
121 QHostInfo.localHostName(),
122 self.__servers[0].serverPort()
123 )
124
125 def hasConnection(self, senderIp, senderPort=-1):
126 """
127 Public method to check for an existing connection.
128
129 @param senderIp address of the sender (QHostAddress)
130 @param senderPort port of the sender (integer)
131 @return flag indicating an existing connection (boolean)
132 """
133 if senderPort == -1:
134 return senderIp in self.__peers
135
136 if senderIp not in self.__peers:
137 return False
138
139 return any(connection.peerPort() == senderPort
140 for connection in self.__peers[senderIp])
141
142 def hasConnections(self):
143 """
144 Public method to check, if there are any connections established.
145
146 @return flag indicating the presence of connections (boolean)
147 """
148 return any(bool(connectionList)
149 for connectionList in self.__peers.values())
150
151 def removeConnection(self, connection):
152 """
153 Public method to remove a connection.
154
155 @param connection reference to the connection to be removed
156 (Connection)
157 """
158 if (connection.peerAddress() in self.__peers and
159 connection in self.__peers[connection.peerAddress()]):
160 self.__peers[connection.peerAddress()].remove(connection)
161 nick = connection.name()
162 if nick != "":
163 self.participantLeft.emit(nick)
164
165 if connection.isValid():
166 connection.abort()
167
168 def disconnectConnections(self):
169 """
170 Public slot to disconnect from the chat network.
171 """
172 for connectionList in self.__peers.values():
173 while connectionList:
174 self.removeConnection(connectionList[0])
175
176 def __newConnection(self, connection):
177 """
178 Private slot to handle a new connection.
179
180 @param connection reference to the new connection (Connection)
181 """
182 connection.setParent(self)
183 connection.setClient(self)
184 connection.setGreetingMessage(self.__username,
185 self.__servers[0].serverPort())
186
187 connection.error.connect(
188 lambda err: self.__connectionError(err, connection))
189 connection.disconnected.connect(
190 lambda: self.__disconnected(connection))
191 connection.readyForUse.connect(
192 lambda: self.__readyForUse(connection))
193 connection.rejected.connect(self.__connectionRejected)
194
195 def __connectionRejected(self, msg):
196 """
197 Private slot to handle the rejection of a connection.
198
199 @param msg error message (string)
200 """
201 self.connectionError.emit(msg)
202
203 def __connectionError(self, socketError, connection):
204 """
205 Private slot to handle a connection error.
206
207 @param socketError reference to the error object
208 @type QAbstractSocket.SocketError
209 @param connection connection that caused the error
210 @type Connection
211 """
212 if socketError != QAbstractSocket.SocketError.RemoteHostClosedError:
213 if connection.peerPort() != 0:
214 msg = "* {0}:{1}\n{2}\n".format(
215 connection.peerAddress().toString(),
216 connection.peerPort(),
217 connection.errorString()
218 )
219 else:
220 msg = "* {0}\n".format(connection.errorString())
221 self.connectionError.emit(msg)
222 if connection == self.__initialConnection:
223 self.cannotConnect.emit()
224 self.removeConnection(connection)
225
226 def __disconnected(self, connection):
227 """
228 Private slot to handle the disconnection of a chat client.
229
230 @param connection connection that was disconnected
231 @type Connection
232 """
233 self.removeConnection(connection)
234
235 def __readyForUse(self, connection):
236 """
237 Private slot to handle a connection getting ready for use.
238
239 @param connection connection that got ready for use
240 @type Connection
241 """
242 if self.hasConnection(connection.peerAddress(), connection.peerPort()):
243 return
244
245 connection.newMessage.connect(self.newMessage)
246 connection.getParticipants.connect(
247 lambda: self.__getParticipants(connection))
248 connection.editorCommand.connect(self.editorCommand)
249
250 self.__peers[connection.peerAddress()].append(connection)
251 nick = connection.name()
252 if nick != "":
253 self.newParticipant.emit(nick)
254
255 if connection == self.__initialConnection:
256 connection.sendGetParticipants()
257 self.__initialConnection = None
258
259 def connectToHost(self, host, port):
260 """
261 Public method to connect to a host.
262
263 @param host host to connect to (string)
264 @param port port to connect to (integer)
265 """
266 self.__initialConnection = Connection(self)
267 self.__newConnection(self.__initialConnection)
268 self.__initialConnection.participants.connect(
269 self.__processParticipants)
270 self.__initialConnection.connectToHost(host, port)
271
272 def __getParticipants(self, reqConnection):
273 """
274 Private slot to handle the request for a list of participants.
275
276 @param reqConnection reference to the connection to get
277 participants for
278 @type Connection
279 """
280 participants = []
281 for connectionList in self.__peers.values():
282 for connection in connectionList:
283 if connection != reqConnection:
284 participants.append("{0}@{1}".format(
285 connection.peerAddress().toString(),
286 connection.serverPort()))
287 reqConnection.sendParticipants(participants)
288
289 def __processParticipants(self, participants):
290 """
291 Private slot to handle the receipt of a list of participants.
292
293 @param participants list of participants (list of strings of
294 "host:port")
295 """
296 for participant in participants:
297 host, port = participant.split("@")
298 port = int(port)
299
300 if port == 0:
301 msg = self.tr("Illegal address: {0}@{1}\n").format(
302 host, port)
303 self.connectionError.emit(msg)
304 else:
305 if not self.hasConnection(QHostAddress(host), port):
306 connection = Connection(self)
307 self.__newConnection(connection)
308 connection.connectToHost(host, port)
309
310 def sendEditorCommand(self, projectHash, filename, message):
311 """
312 Public method to send an editor command.
313
314 @param projectHash hash of the project (string)
315 @param filename project relative universal file name of
316 the sending editor (string)
317 @param message editor command to be sent (string)
318 """
319 for connectionList in self.__peers.values():
320 for connection in connectionList:
321 connection.sendEditorCommand(projectHash, filename, message)
322
323 def __findConnections(self, nick):
324 """
325 Private method to get a list of connection given a nick name.
326
327 @param nick nick name in the format of self.nickName() (string)
328 @return list of references to the connection objects (list of
329 Connection)
330 """
331 if "@" not in nick:
332 # nick given in wrong format
333 return []
334
335 user, host, port = nick.split("@")
336 senderIp = QHostAddress(host)
337
338 if senderIp not in self.__peers:
339 return []
340
341 return self.__peers[senderIp][:]
342
343 def kickUser(self, nick):
344 """
345 Public method to kick a user by its nick name.
346
347 @param nick nick name in the format of self.nickName() (string)
348 """
349 for connection in self.__findConnections(nick):
350 connection.abort()
351
352 def banUser(self, nick):
353 """
354 Public method to ban a user by its nick name.
355
356 @param nick nick name in the format of self.nickName() (string)
357 """
358 Preferences.syncPreferences()
359 user = nick.rsplit("@")[0]
360 bannedUsers = Preferences.getCooperation("BannedUsers")[:]
361 if user not in bannedUsers:
362 bannedUsers.append(user)
363 Preferences.setCooperation("BannedUsers", bannedUsers)
364
365 def banKickUser(self, nick):
366 """
367 Public method to ban and kick a user by its nick name.
368
369 @param nick nick name in the format of self.nickName() (string)
370 """
371 self.banUser(nick)
372 self.kickUser(nick)
373
374 def startListening(self, port=-1):
375 """
376 Public method to start listening for new connections.
377
378 @param port port to listen on (integer)
379 @return tuple giving a flag indicating success (boolean) and
380 the port the server listens on
381 """
382 if self.__servers:
383 # do first server and determine free port
384 res, port = self.__servers[0].startListening(port, True)
385 if res and len(self.__servers) > 1:
386 for server in self.__servers[1:]:
387 res, port = server.startListening(port, False)
388 if not res:
389 self.__serversErrorString = server.errorString()
390 else:
391 self.__serversErrorString = self.__servers[0].errorString()
392 else:
393 res = False
394 self.__serversErrorString = self.tr("No servers present.")
395
396 if res:
397 self.__serversErrorString = ""
398 self.__listening = res
399 return res, port
400
401 def isListening(self):
402 """
403 Public method to check, if the client is listening for connections.
404
405 @return flag indicating the listening state (boolean)
406 """
407 return self.__listening
408
409 def close(self):
410 """
411 Public method to close all connections and stop listening.
412 """
413 for server in self.__servers:
414 server.close()
415 self.__listening = False
416
417 def errorString(self):
418 """
419 Public method to get a human readable error message about the last
420 server error.
421
422 @return human readable error message about the last server error
423 (string)
424 """
425 return self.__serversErrorString

eric ide

mercurial