mirror of
https://github.com/Xevion/tcp-chat.git
synced 2025-12-06 11:16:39 -06:00
fix message adding/removing with textBox total refresh, compiled message formats, add proper html escaping, separate ReceiveWorker into worker.py
This commit is contained in:
126
client/gui.py
126
client/gui.py
@@ -1,15 +1,17 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import traceback
|
from pprint import pprint
|
||||||
|
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QEvent, QTimer
|
from PyQt5.QtCore import Qt, QEvent, QTimer
|
||||||
from PyQt5.QtWidgets import QMainWindow
|
from PyQt5.QtWidgets import QMainWindow
|
||||||
|
|
||||||
|
from sortedcontainers import SortedList
|
||||||
|
|
||||||
import constants
|
import constants
|
||||||
import helpers
|
import helpers
|
||||||
from client.MainWindow import Ui_MainWindow
|
from client.MainWindow import Ui_MainWindow
|
||||||
from client.dialog import NicknameDialog
|
from client.dialog import NicknameDialog
|
||||||
|
from client.worker import ReceiveWorker
|
||||||
|
|
||||||
IP = '127.0.0.1'
|
IP = '127.0.0.1'
|
||||||
PORT = 55555
|
PORT = 55555
|
||||||
@@ -18,87 +20,6 @@ logging.basicConfig(level=logging.DEBUG,
|
|||||||
format='[%(asctime)s] [%(levelname)s] [%(threadName)s] %(message)s')
|
format='[%(asctime)s] [%(levelname)s] [%(threadName)s] %(message)s')
|
||||||
logger = logging.getLogger('gui')
|
logger = logging.getLogger('gui')
|
||||||
|
|
||||||
HEADER_LENGTH = 10
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiveWorker(QThread):
|
|
||||||
messages = pyqtSignal(dict)
|
|
||||||
client_list = pyqtSignal(list)
|
|
||||||
error = pyqtSignal()
|
|
||||||
sent_nick = pyqtSignal()
|
|
||||||
logs = pyqtSignal(dict)
|
|
||||||
|
|
||||||
def __init__(self, client: socket.socket, nickname: str, parent=None):
|
|
||||||
QThread.__init__(self, parent)
|
|
||||||
self.client = client
|
|
||||||
self.nickname = nickname
|
|
||||||
self.__isRunning = True
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
self.__isRunning = False
|
|
||||||
|
|
||||||
def __extract_message(self, data) -> dict:
|
|
||||||
return {
|
|
||||||
'nickname': data['nickname'],
|
|
||||||
'message': data['content'],
|
|
||||||
'color': data['color'],
|
|
||||||
'time': data['time'],
|
|
||||||
'id': data['id']
|
|
||||||
}
|
|
||||||
|
|
||||||
def log(self, message: str, level: int = logging.INFO, error: Exception = None):
|
|
||||||
"""Send a log message out from this QThread to the MainThread"""
|
|
||||||
self.logs.emit(
|
|
||||||
{
|
|
||||||
'message': message,
|
|
||||||
'level': level,
|
|
||||||
'error': error
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
while self.__isRunning:
|
|
||||||
try:
|
|
||||||
raw_length = self.client.recv(HEADER_LENGTH).decode('utf-8')
|
|
||||||
if not raw_length:
|
|
||||||
continue
|
|
||||||
raw = self.client.recv(int(raw_length)).decode('utf-8')
|
|
||||||
if not raw:
|
|
||||||
continue
|
|
||||||
message = json.loads(raw)
|
|
||||||
|
|
||||||
if message['type'] == constants.Types.REQUEST:
|
|
||||||
self.log(f'Data[{int(raw_length)}] received, {message["type"]}/{message["request"]}.', level=logging.DEBUG)
|
|
||||||
else:
|
|
||||||
self.log(f'Data[{int(raw_length)}] received, {message["type"]}.', level=logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
if message['type'] == constants.Types.REQUEST:
|
|
||||||
if message['request'] == constants.Requests.REQUEST_NICK:
|
|
||||||
self.client.send(helpers.prepare_json(
|
|
||||||
{
|
|
||||||
'type': constants.Types.NICKNAME,
|
|
||||||
'nickname': self.nickname
|
|
||||||
}
|
|
||||||
))
|
|
||||||
self.sent_nick.emit()
|
|
||||||
elif message['type'] == constants.Types.MESSAGE:
|
|
||||||
self.messages.emit(self.__extract_message(message))
|
|
||||||
elif message['type'] == constants.Types.USER_LIST:
|
|
||||||
self.client_list.emit(message['users'])
|
|
||||||
elif message['type'] == constants.Types.MESSAGE_HISTORY:
|
|
||||||
for submessage in message['messages']:
|
|
||||||
self.messages.emit(self.__extract_message(submessage))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log(str(e), level=logging.CRITICAL, error=e)
|
|
||||||
self.error.emit()
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
self.log('Closing socket.', level=logging.INFO)
|
|
||||||
self.client.close()
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow, Ui_MainWindow):
|
class MainWindow(QMainWindow, Ui_MainWindow):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -137,7 +58,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.messageBox.setPlaceholderText('Type your message here...')
|
self.messageBox.setPlaceholderText('Type your message here...')
|
||||||
self.messageBox.installEventFilter(self)
|
self.messageBox.installEventFilter(self)
|
||||||
|
|
||||||
self.messages = []
|
self.messages = SortedList(key=lambda message: message['time'])
|
||||||
|
self.added_messages = []
|
||||||
|
self.max_message_id = -1
|
||||||
|
|
||||||
def log(self, log_data: dict) -> None:
|
def log(self, log_data: dict) -> None:
|
||||||
logger.log(level=log_data['level'], msg=log_data['message'], exc_info=log_data['error'])
|
logger.log(level=log_data['level'], msg=log_data['message'], exc_info=log_data['error'])
|
||||||
@@ -170,15 +93,40 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
return True
|
return True
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
def refresh_messages(self) -> None:
|
||||||
|
"""Completely refresh the chat box text."""
|
||||||
|
min_time = min(map(lambda msg: msg['time'], self.messages))
|
||||||
|
|
||||||
|
scrollbar = self.messageHistory.verticalScrollBar()
|
||||||
|
lastPosition = scrollbar.value()
|
||||||
|
atMaximum = lastPosition == scrollbar.maximum()
|
||||||
|
|
||||||
|
self.messageHistory.setText('<br>'.join(
|
||||||
|
msg['compiled'] for msg in self.messages
|
||||||
|
))
|
||||||
|
|
||||||
|
scrollbar.setValue(scrollbar.maximum() if atMaximum else lastPosition)
|
||||||
|
|
||||||
def addMessage(self, message: dict) -> None:
|
def addMessage(self, message: dict) -> None:
|
||||||
self.messages.append(message)
|
message_id = message['id']
|
||||||
self.messageHistory.append(
|
if message_id not in self.added_messages:
|
||||||
f'<<span style="color: {message["color"]}">{message["nickname"]}</span>> {message["message"]}')
|
message['compiled'] = helpers.formatted_message(message)
|
||||||
|
|
||||||
|
if 0 <= message_id < self.max_message_id:
|
||||||
|
logger.info('Refreshing entire chatbox...')
|
||||||
|
self.max_message_id = message_id
|
||||||
|
self.messages.add(message)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.max_message_id = message_id
|
||||||
|
self.added_messages.append(message_id)
|
||||||
|
self.messages.add(message)
|
||||||
|
|
||||||
|
self.refresh_messages()
|
||||||
|
|
||||||
def sendMessage(self, message: str) -> None:
|
def sendMessage(self, message: str) -> None:
|
||||||
message = message.strip()
|
message = message.strip()
|
||||||
if len(message) > 0:
|
if len(message) > 0:
|
||||||
logger.info(f'Sending message of length {len(message)}')
|
|
||||||
self.client.send(helpers.prepare_json(
|
self.client.send(helpers.prepare_json(
|
||||||
{
|
{
|
||||||
'type': constants.Types.MESSAGE,
|
'type': constants.Types.MESSAGE,
|
||||||
|
|||||||
87
client/worker.py
Normal file
87
client/worker.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
|
import constants
|
||||||
|
import helpers
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiveWorker(QThread):
|
||||||
|
messages = pyqtSignal(dict)
|
||||||
|
client_list = pyqtSignal(list)
|
||||||
|
error = pyqtSignal()
|
||||||
|
sent_nick = pyqtSignal()
|
||||||
|
logs = pyqtSignal(dict)
|
||||||
|
|
||||||
|
def __init__(self, client: socket.socket, nickname: str, parent=None):
|
||||||
|
QThread.__init__(self, parent)
|
||||||
|
self.client = client
|
||||||
|
self.nickname = nickname
|
||||||
|
self.__isRunning = True
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.__isRunning = False
|
||||||
|
|
||||||
|
def __extract_message(self, data) -> dict:
|
||||||
|
return {
|
||||||
|
'nickname': data['nickname'],
|
||||||
|
'message': data['content'],
|
||||||
|
'color': data['color'],
|
||||||
|
'time': data['time'],
|
||||||
|
'id': data['id']
|
||||||
|
}
|
||||||
|
|
||||||
|
def log(self, message: str, level: int = logging.INFO, error: Exception = None):
|
||||||
|
"""Send a log message out from this QThread to the MainThread"""
|
||||||
|
self.logs.emit(
|
||||||
|
{
|
||||||
|
'message': message,
|
||||||
|
'level': level,
|
||||||
|
'error': error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
while self.__isRunning:
|
||||||
|
try:
|
||||||
|
raw_length = self.client.recv(constants.HEADER_LENGTH).decode('utf-8')
|
||||||
|
if not raw_length:
|
||||||
|
continue
|
||||||
|
raw = self.client.recv(int(raw_length)).decode('utf-8')
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
message = json.loads(raw)
|
||||||
|
|
||||||
|
if message['type'] == constants.Types.REQUEST:
|
||||||
|
self.log(f'Data[{int(raw_length)}] received, {message["type"]}/{message["request"]}.',
|
||||||
|
level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
self.log(f'Data[{int(raw_length)}] received, {message["type"]}.', level=logging.DEBUG)
|
||||||
|
|
||||||
|
if message['type'] == constants.Types.REQUEST:
|
||||||
|
if message['request'] == constants.Requests.REQUEST_NICK:
|
||||||
|
self.client.send(helpers.prepare_json(
|
||||||
|
{
|
||||||
|
'type': constants.Types.NICKNAME,
|
||||||
|
'nickname': self.nickname
|
||||||
|
}
|
||||||
|
))
|
||||||
|
self.sent_nick.emit()
|
||||||
|
elif message['type'] == constants.Types.MESSAGE:
|
||||||
|
self.messages.emit(self.__extract_message(message))
|
||||||
|
elif message['type'] == constants.Types.USER_LIST:
|
||||||
|
self.client_list.emit(message['users'])
|
||||||
|
elif message['type'] == constants.Types.MESSAGE_HISTORY:
|
||||||
|
for submessage in message['messages']:
|
||||||
|
self.messages.emit(self.__extract_message(submessage))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(str(e), level=logging.CRITICAL, error=e)
|
||||||
|
self.error.emit()
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.log('Closing socket.', level=logging.INFO)
|
||||||
|
self.client.close()
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import html
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
@@ -57,3 +58,10 @@ def prepare_message_history(messages: List[Tuple[int, str, str, str, int]]) -> b
|
|||||||
def prepare_request(request: str) -> bytes:
|
def prepare_request(request: str) -> bytes:
|
||||||
"""Helper function for creating a request message."""
|
"""Helper function for creating a request message."""
|
||||||
return prepare_json({'type': constants.Types.REQUEST, 'request': request})
|
return prepare_json({'type': constants.Types.REQUEST, 'request': request})
|
||||||
|
|
||||||
|
|
||||||
|
def formatted_message(message: dict) -> str:
|
||||||
|
"""Given a message dict object, return a color formatted and GUI ready string."""
|
||||||
|
nick_esc = html.escape(message["nickname"])
|
||||||
|
msg_esc = html.escape(message["message"])
|
||||||
|
return f'<<span style="color: {message["color"]}">{nick_esc}</span>> {msg_esc}'
|
||||||
|
|||||||
Reference in New Issue
Block a user