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:
Xevion
2021-01-22 11:19:22 -06:00
parent 35bff4b320
commit 82a408072d
3 changed files with 132 additions and 89 deletions

View File

@@ -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'&lt;<span style="color: {message["color"]}">{message["nickname"]}</span>&gt; {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
View 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()

View File

@@ -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'&lt;<span style="color: {message["color"]}">{nick_esc}</span>&gt; {msg_esc}'