--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Cooperation/CooperationClient.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the client of the cooperation package. +""" + +from __future__ import unicode_literals + +import collections + +from PyQt5.QtCore import QObject, pyqtSignal, QProcess, QRegExp +from PyQt5.QtNetwork import QHostInfo, QHostAddress, QAbstractSocket, \ + QNetworkInterface + +from .CooperationServer import CooperationServer +from .Connection import Connection + +import Preferences + + +class CooperationClient(QObject): + """ + Class implementing the client of the cooperation package. + + @signal newMessage(user, message) emitted after a new message has + arrived (string, string) + @signal newParticipant(nickname) emitted after a new participant joined + (string) + @signal participantLeft(nickname) emitted after a participant left (string) + @signal connectionError(message) emitted when a connection error occurs + (string) + @signal cannotConnect() emitted, if the initial connection fails + @signal editorCommand(hash, filename, message) emitted when an editor + command has been received (string, string, string) + """ + newMessage = pyqtSignal(str, str) + newParticipant = pyqtSignal(str) + participantLeft = pyqtSignal(str) + connectionError = pyqtSignal(str) + cannotConnect = pyqtSignal() + editorCommand = pyqtSignal(str, str, str) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(CooperationClient, self).__init__(parent) + + self.__chatWidget = parent + + self.__servers = [] + for networkInterface in QNetworkInterface.allInterfaces(): + for addressEntry in networkInterface.addressEntries(): + address = addressEntry.ip() + # fix scope of link local addresses + if address.toString().lower().startswith("fe80"): + address.setScopeId(networkInterface.humanReadableName()) + server = CooperationServer(address, self) + server.newConnection.connect(self.__newConnection) + self.__servers.append(server) + + self.__peers = collections.defaultdict(list) + + self.__initialConnection = None + + envVariables = ["USERNAME.*", "USER.*", "USERDOMAIN.*", + "HOSTNAME.*", "DOMAINNAME.*"] + environment = QProcess.systemEnvironment() + found = False + for envVariable in envVariables: + for env in environment: + if QRegExp(envVariable).exactMatch(env): + envList = env.split("=") + if len(envList) == 2: + self.__username = envList[1].strip() + found = True + break + + if found: + break + + if self.__username == "": + self.__username = self.tr("unknown") + + self.__listening = False + self.__serversErrorString = "" + + def chatWidget(self): + """ + Public method to get a reference to the chat widget. + + @return reference to the chat widget (ChatWidget) + """ + return self.__chatWidget + + def sendMessage(self, message): + """ + Public method to send a message. + + @param message message to be sent (string) + """ + if message == "": + return + + for connectionList in self.__peers.values(): + for connection in connectionList: + connection.sendMessage(message) + + def nickName(self): + """ + Public method to get the nick name. + + @return nick name (string) + """ + return "{0}@{1}@{2}".format( + self.__username, + QHostInfo.localHostName(), + self.__servers[0].serverPort() + ) + + def hasConnection(self, senderIp, senderPort=-1): + """ + Public method to check for an existing connection. + + @param senderIp address of the sender (QHostAddress) + @param senderPort port of the sender (integer) + @return flag indicating an existing connection (boolean) + """ + if senderPort == -1: + return senderIp in self.__peers + + if senderIp not in self.__peers: + return False + + for connection in self.__peers[senderIp]: + if connection.peerPort() == senderPort: + return True + + return False + + def hasConnections(self): + """ + Public method to check, if there are any connections established. + + @return flag indicating the presence of connections (boolean) + """ + for connectionList in self.__peers.values(): + if connectionList: + return True + + return False + + def removeConnection(self, connection): + """ + Public method to remove a connection. + + @param connection reference to the connection to be removed + (Connection) + """ + if connection.peerAddress() in self.__peers and \ + connection in self.__peers[connection.peerAddress()]: + self.__peers[connection.peerAddress()].remove(connection) + nick = connection.name() + if nick != "": + self.participantLeft.emit(nick) + + if connection.isValid(): + connection.abort() + + def disconnectConnections(self): + """ + Public slot to disconnect from the chat network. + """ + for connectionList in self.__peers.values(): + while connectionList: + self.removeConnection(connectionList[0]) + + def __newConnection(self, connection): + """ + Private slot to handle a new connection. + + @param connection reference to the new connection (Connection) + """ + connection.setParent(self) + connection.setClient(self) + connection.setGreetingMessage(self.__username, + self.__servers[0].serverPort()) + + connection.error.connect( + lambda err: self.__connectionError(err, connection)) + connection.disconnected.connect( + lambda: self.__disconnected(connection)) + connection.readyForUse.connect( + lambda: self.__readyForUse(connection)) + connection.rejected.connect(self.__connectionRejected) + + def __connectionRejected(self, msg): + """ + Private slot to handle the rejection of a connection. + + @param msg error message (string) + """ + self.connectionError.emit(msg) + + def __connectionError(self, socketError, connection): + """ + Private slot to handle a connection error. + + @param socketError reference to the error object + @type QAbstractSocket.SocketError + @param connection connection that caused the error + @type Connection + """ + if socketError != QAbstractSocket.RemoteHostClosedError: + if connection.peerPort() != 0: + msg = "* {0}:{1}\n{2}\n".format( + connection.peerAddress().toString(), + connection.peerPort(), + connection.errorString() + ) + else: + msg = "* {0}\n".format(connection.errorString()) + self.connectionError.emit(msg) + if connection == self.__initialConnection: + self.cannotConnect.emit() + self.removeConnection(connection) + + def __disconnected(self, connection): + """ + Private slot to handle the disconnection of a chat client. + + @param connection connection that was disconnected + @type Connection + """ + self.removeConnection(connection) + + def __readyForUse(self, connection): + """ + Private slot to handle a connection getting ready for use. + + @param connection connection that got ready for use + @type Connection + """ + if self.hasConnection(connection.peerAddress(), connection.peerPort()): + return + + connection.newMessage.connect(self.newMessage) + connection.getParticipants.connect( + lambda: self.__getParticipants(connection)) + connection.editorCommand.connect(self.editorCommand) + + self.__peers[connection.peerAddress()].append(connection) + nick = connection.name() + if nick != "": + self.newParticipant.emit(nick) + + if connection == self.__initialConnection: + connection.sendGetParticipants() + self.__initialConnection = None + + def connectToHost(self, host, port): + """ + Public method to connect to a host. + + @param host host to connect to (string) + @param port port to connect to (integer) + """ + self.__initialConnection = Connection(self) + self.__newConnection(self.__initialConnection) + self.__initialConnection.participants.connect( + self.__processParticipants) + self.__initialConnection.connectToHost(host, port) + + def __getParticipants(self, reqConnection): + """ + Private slot to handle the request for a list of participants. + + @param reqConnection reference to the connection to get + participants for + @type Connection + """ + participants = [] + for connectionList in self.__peers.values(): + for connection in connectionList: + if connection != reqConnection: + participants.append("{0}@{1}".format( + connection.peerAddress().toString(), + connection.serverPort())) + reqConnection.sendParticipants(participants) + + def __processParticipants(self, participants): + """ + Private slot to handle the receipt of a list of participants. + + @param participants list of participants (list of strings of + "host:port") + """ + for participant in participants: + host, port = participant.split("@") + port = int(port) + + if port == 0: + msg = self.tr("Illegal address: {0}@{1}\n").format( + host, port) + self.connectionError.emit(msg) + else: + if not self.hasConnection(QHostAddress(host), port): + connection = Connection(self) + self.__newConnection(connection) + connection.connectToHost(host, port) + + def sendEditorCommand(self, projectHash, filename, message): + """ + Public method to send an editor command. + + @param projectHash hash of the project (string) + @param filename project relative universal file name of + the sending editor (string) + @param message editor command to be sent (string) + """ + for connectionList in self.__peers.values(): + for connection in connectionList: + connection.sendEditorCommand(projectHash, filename, message) + + def __findConnections(self, nick): + """ + Private method to get a list of connection given a nick name. + + @param nick nick name in the format of self.nickName() (string) + @return list of references to the connection objects (list of + Connection) + """ + if "@" not in nick: + # nick given in wrong format + return [] + + user, host, port = nick.split("@") + senderIp = QHostAddress(host) + + if senderIp not in self.__peers: + return [] + + return self.__peers[senderIp][:] + + def kickUser(self, nick): + """ + Public method to kick a user by its nick name. + + @param nick nick name in the format of self.nickName() (string) + """ + for connection in self.__findConnections(nick): + connection.abort() + + def banUser(self, nick): + """ + Public method to ban a user by its nick name. + + @param nick nick name in the format of self.nickName() (string) + """ + Preferences.syncPreferences() + user = nick.rsplit("@")[0] + bannedUsers = Preferences.getCooperation("BannedUsers")[:] + if user not in bannedUsers: + bannedUsers.append(user) + Preferences.setCooperation("BannedUsers", bannedUsers) + + def banKickUser(self, nick): + """ + Public method to ban and kick a user by its nick name. + + @param nick nick name in the format of self.nickName() (string) + """ + self.banUser(nick) + self.kickUser(nick) + + def startListening(self, port=-1): + """ + Public method to start listening for new connections. + + @param port port to listen on (integer) + @return tuple giving a flag indicating success (boolean) and + the port the server listens on + """ + if self.__servers: + # do first server and determine free port + res, port = self.__servers[0].startListening(port, True) + if res and len(self.__servers) > 1: + for server in self.__servers[1:]: + res, port = server.startListening(port, False) + if not res: + self.__serversErrorString = server.errorString() + else: + self.__serversErrorString = self.__servers[0].errorString() + else: + res = False + self.__serversErrorString = self.tr("No servers present.") + + if res: + self.__serversErrorString = "" + self.__listening = res + return res, port + + def isListening(self): + """ + Public method to check, if the client is listening for connections. + + @return flag indicating the listening state (boolean) + """ + return self.__listening + + def close(self): + """ + Public method to close all connections and stop listening. + """ + for server in self.__servers: + server.close() + self.__listening = False + + def errorString(self): + """ + Public method to get a human readable error message about the last + server error. + + @return human readable error message about the last server error + (string) + """ + return self.__serversErrorString