src/eric7/Cooperation/CooperationClient.py

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

eric ide

mercurial