Add support of TOTP and password hash (WeeChat >= 2.9)
This commit is contained in:
		| @@ -45,6 +45,7 @@ In QWeeChat, click on connect and enter fields: | ||||
| - `server`: the IP address or hostname of your machine with WeeChat running | ||||
| - `port`: the relay port (defined in WeeChat) | ||||
| - `password`: the relay password (defined in WeeChat) | ||||
| - `totp`: the Time-Based One-Time Password (optional, to set if required by WeeChat) | ||||
|  | ||||
| Options can be changed in file `~/.config/qweechat/qweechat.conf`. | ||||
|  | ||||
|   | ||||
| @@ -32,35 +32,91 @@ class ConnectionDialog(QtWidgets.QDialog): | ||||
|         super().__init__(*args) | ||||
|         self.values = values | ||||
|         self.setModal(True) | ||||
|         self.setWindowTitle('Connect to WeeChat') | ||||
|  | ||||
|         grid = QtWidgets.QGridLayout() | ||||
|         grid.setSpacing(10) | ||||
|  | ||||
|         self.fields = {} | ||||
|         for line, field in enumerate(('server', 'port', 'password', 'lines')): | ||||
|             grid.addWidget(QtWidgets.QLabel(field.capitalize()), line, 0) | ||||
|             line_edit = QtWidgets.QLineEdit() | ||||
|             line_edit.setFixedWidth(200) | ||||
|             if field == 'password': | ||||
|                 line_edit.setEchoMode(QtWidgets.QLineEdit.Password) | ||||
|             if field == 'lines': | ||||
|                 validator = QtGui.QIntValidator(0, 2147483647, self) | ||||
|                 line_edit.setValidator(validator) | ||||
|                 line_edit.setFixedWidth(80) | ||||
|             line_edit.insert(self.values[field]) | ||||
|             grid.addWidget(line_edit, line, 1) | ||||
|             self.fields[field] = line_edit | ||||
|             if field == 'port': | ||||
|                 ssl = QtWidgets.QCheckBox('SSL') | ||||
|                 ssl.setChecked(self.values['ssl'] == 'on') | ||||
|                 grid.addWidget(ssl, line, 2) | ||||
|                 self.fields['ssl'] = ssl | ||||
|         focus = None | ||||
|  | ||||
|         # server | ||||
|         grid.addWidget(QtWidgets.QLabel('<b>Server</b>'), 0, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         value = self.values.get('server', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 0, 1) | ||||
|         self.fields['server'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'server' | ||||
|  | ||||
|         # port / SSL | ||||
|         grid.addWidget(QtWidgets.QLabel('<b>Port</b>'), 1, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         value = self.values.get('port', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 1, 1) | ||||
|         self.fields['port'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'port' | ||||
|  | ||||
|         ssl = QtWidgets.QCheckBox('SSL') | ||||
|         ssl.setChecked(self.values['ssl'] == 'on') | ||||
|         grid.addWidget(ssl, 1, 2) | ||||
|         self.fields['ssl'] = ssl | ||||
|  | ||||
|         # password | ||||
|         grid.addWidget(QtWidgets.QLabel('<b>Password</b>'), 2, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         line_edit.setEchoMode(QtWidgets.QLineEdit.Password) | ||||
|         value = self.values.get('password', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 2, 1) | ||||
|         self.fields['password'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'password' | ||||
|  | ||||
|         # TOTP (Time-Based One-Time Password) | ||||
|         label = QtWidgets.QLabel('TOTP') | ||||
|         label.setToolTip('Time-Based One-Time Password (6 digits)') | ||||
|         grid.addWidget(label, 3, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setPlaceholderText('6 digits') | ||||
|         validator = QtGui.QIntValidator(0, 999999, self) | ||||
|         line_edit.setValidator(validator) | ||||
|         line_edit.setFixedWidth(80) | ||||
|         value = self.values.get('totp', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 3, 1) | ||||
|         self.fields['totp'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'totp' | ||||
|  | ||||
|         # lines | ||||
|         grid.addWidget(QtWidgets.QLabel('Lines'), 4, 0) | ||||
|         line_edit = QtWidgets.QLineEdit() | ||||
|         line_edit.setFixedWidth(200) | ||||
|         validator = QtGui.QIntValidator(0, 2147483647, self) | ||||
|         line_edit.setValidator(validator) | ||||
|         line_edit.setFixedWidth(80) | ||||
|         value = self.values.get('lines', '') | ||||
|         line_edit.insert(value) | ||||
|         grid.addWidget(line_edit, 4, 1) | ||||
|         self.fields['lines'] = line_edit | ||||
|         if not focus and not value: | ||||
|             focus = 'lines' | ||||
|  | ||||
|         self.dialog_buttons = QtWidgets.QDialogButtonBox() | ||||
|         self.dialog_buttons.setStandardButtons( | ||||
|             QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) | ||||
|         self.dialog_buttons.rejected.connect(self.close) | ||||
|  | ||||
|         grid.addWidget(self.dialog_buttons, 4, 0, 1, 2) | ||||
|         grid.addWidget(self.dialog_buttons, 5, 0, 1, 2) | ||||
|         self.setLayout(grid) | ||||
|         self.show() | ||||
|  | ||||
|         if focus: | ||||
|             self.fields[focus].setFocus() | ||||
|   | ||||
| @@ -10,8 +10,9 @@ Files: weechat.png, bullet_green_8x8.png, bullet_yellow_8x8.png | ||||
|  | ||||
|  | ||||
| Files: application-exit.png, dialog-close.png, dialog-ok-apply.png, | ||||
|        dialog-warning.png, document-save.png, edit-find.png, help-about.png, | ||||
|        network-connect.png, network-disconnect.png, preferences-other.png | ||||
|        dialog-password.png, dialog-warning.png, document-save.png, | ||||
|        edit-find.png, help-about.png, network-connect.png, | ||||
|        network-disconnect.png, preferences-other.png | ||||
|  | ||||
|   Files come from Debian package "oxygen-icon-theme": | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								qweechat/data/icons/dialog-password.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								qweechat/data/icons/dialog-password.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 713 B | 
| @@ -22,19 +22,38 @@ | ||||
|  | ||||
| """I/O with WeeChat/relay.""" | ||||
|  | ||||
| import hashlib | ||||
| import secrets | ||||
| import struct | ||||
|  | ||||
| from PySide6 import QtCore, QtNetwork | ||||
|  | ||||
| from qweechat import config | ||||
| from qweechat.debug import DebugDialog | ||||
|  | ||||
|  | ||||
| _PROTO_INIT_CMD = [ | ||||
|     # initialize with the password | ||||
|     'init password=%(password)s', | ||||
| # list of supported hash algorithms on our side | ||||
| # (the hash algorithm will be negotiated with the remote WeeChat) | ||||
| _HASH_ALGOS_LIST = [ | ||||
|     'plain', | ||||
|     'sha256', | ||||
|     'sha512', | ||||
|     'pbkdf2+sha256', | ||||
|     'pbkdf2+sha512', | ||||
| ] | ||||
| _HASH_ALGOS = ':'.join(_HASH_ALGOS_LIST) | ||||
|  | ||||
| _PROTO_SYNC_CMDS = [ | ||||
| # handshake with remote WeeChat (before init) | ||||
| _PROTO_HANDSHAKE = f'(handshake) handshake password_hash_algo={_HASH_ALGOS}\n' | ||||
|  | ||||
| # initialize with the password (plain text) | ||||
| _PROTO_INIT_PWD = 'init password=%(password)s%(totp)s\n' | ||||
|  | ||||
| # initialize with the hashed password | ||||
| _PROTO_INIT_HASH = ('init password_hash=' | ||||
|                     '%(algo)s:%(salt)s%(iter)s:%(hash)s%(totp)s\n') | ||||
|  | ||||
| _PROTO_SYNC = [ | ||||
|     # get buffers | ||||
|     '(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,' | ||||
|     'type,nicklist,title,local_variables', | ||||
| @@ -49,26 +68,33 @@ _PROTO_SYNC_CMDS = [ | ||||
|  | ||||
| STATUS_DISCONNECTED = 'disconnected' | ||||
| STATUS_CONNECTING = 'connecting' | ||||
| STATUS_AUTHENTICATING = 'authenticating' | ||||
| STATUS_CONNECTED = 'connected' | ||||
|  | ||||
| NETWORK_STATUS = { | ||||
|     'disconnected': { | ||||
|     STATUS_DISCONNECTED: { | ||||
|         'label': 'Disconnected', | ||||
|         'color': '#aa0000', | ||||
|         'icon': 'dialog-close.png', | ||||
|     }, | ||||
|     'connecting': { | ||||
|     STATUS_CONNECTING: { | ||||
|         'label': 'Connecting…', | ||||
|         'color': '#ff7f00', | ||||
|         'color': '#dd5f00', | ||||
|         'icon': 'dialog-warning.png', | ||||
|     }, | ||||
|     'connected': { | ||||
|     STATUS_AUTHENTICATING: { | ||||
|         'label': 'Authenticating…', | ||||
|         'color': '#007fff', | ||||
|         'icon': 'dialog-password.png', | ||||
|     }, | ||||
|     STATUS_CONNECTED: { | ||||
|         'label': 'Connected', | ||||
|         'color': 'green', | ||||
|         'icon': 'dialog-ok-apply.png', | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| class Network(QtCore.QObject): | ||||
|     """I/O with WeeChat/relay.""" | ||||
|  | ||||
| @@ -77,10 +103,9 @@ class Network(QtCore.QObject): | ||||
|  | ||||
|     def __init__(self, *args): | ||||
|         super().__init__(*args) | ||||
|         self._server = None | ||||
|         self._port = None | ||||
|         self._ssl = None | ||||
|         self._password = None | ||||
|         self._init_connection() | ||||
|         self.debug_lines = [] | ||||
|         self.debug_dialog = None | ||||
|         self._lines = config.CONFIG_DEFAULT_RELAY_LINES | ||||
|         self._buffer = QtCore.QByteArray() | ||||
|         self._socket = QtNetwork.QSslSocket() | ||||
| @@ -88,22 +113,91 @@ class Network(QtCore.QObject): | ||||
|         self._socket.readyRead.connect(self._socket_read) | ||||
|         self._socket.disconnected.connect(self._socket_disconnected) | ||||
|  | ||||
|     def _init_connection(self): | ||||
|         self.status = STATUS_DISCONNECTED | ||||
|         self._server = None | ||||
|         self._port = None | ||||
|         self._ssl = None | ||||
|         self._password = None | ||||
|         self._totp = None | ||||
|         self._handshake_received = False | ||||
|         self._handshake_timer = None | ||||
|         self._handshake_timer = False | ||||
|         self._pwd_hash_algo = None | ||||
|         self._pwd_hash_iter = 0 | ||||
|         self._server_nonce = None | ||||
|  | ||||
|     def set_status(self, status): | ||||
|         """Set current status.""" | ||||
|         self.status = status | ||||
|         self.statusChanged.emit(status, None) | ||||
|  | ||||
|     def pbkdf2(self, hash_name, salt): | ||||
|         """Return hashed password with PBKDF2-HMAC.""" | ||||
|         return hashlib.pbkdf2_hmac( | ||||
|             hash_name, | ||||
|             password=self._password.encode('utf-8'), | ||||
|             salt=salt, | ||||
|             iterations=self._pwd_hash_iter, | ||||
|         ).hex() | ||||
|  | ||||
|     def _build_init_command(self): | ||||
|         """Build the init command to send to WeeChat.""" | ||||
|         cmd = '\n'.join(_PROTO_INIT_CMD) + '\n' | ||||
|         return cmd % {'password': self._password} | ||||
|         totp = f',totp={self._totp}' if self._totp else '' | ||||
|         if self._pwd_hash_algo == 'plain': | ||||
|             cmd = _PROTO_INIT_PWD % { | ||||
|                 'password': self._password, | ||||
|                 'totp': totp, | ||||
|             } | ||||
|         else: | ||||
|             client_nonce = secrets.token_bytes(16) | ||||
|             salt = self._server_nonce + client_nonce | ||||
|             pwd_hash = None | ||||
|             iterations = '' | ||||
|             if self._pwd_hash_algo == 'pbkdf2+sha512': | ||||
|                 pwd_hash = self.pbkdf2('sha512', salt) | ||||
|                 iterations = f':{self._pwd_hash_iter}' | ||||
|             elif self._pwd_hash_algo == 'pbkdf2+sha256': | ||||
|                 pwd_hash = self.pbkdf2('sha256', salt) | ||||
|                 iterations = f':{self._pwd_hash_iter}' | ||||
|             elif self._pwd_hash_algo == 'sha512': | ||||
|                 pwd = salt + self._password.encode('utf-8') | ||||
|                 pwd_hash = hashlib.sha512(pwd).hexdigest() | ||||
|             elif self._pwd_hash_algo == 'sha256': | ||||
|                 pwd = salt + self._password.encode('utf-8') | ||||
|                 pwd_hash = hashlib.sha256(pwd).hexdigest() | ||||
|             if not pwd_hash: | ||||
|                 return None | ||||
|             cmd = _PROTO_INIT_HASH % { | ||||
|                 'algo': self._pwd_hash_algo, | ||||
|                 'salt': bytearray(salt).hex(), | ||||
|                 'iter': iterations, | ||||
|                 'hash': pwd_hash, | ||||
|                 'totp': totp, | ||||
|             } | ||||
|         return cmd | ||||
|  | ||||
|     def _build_sync_command(self): | ||||
|         """Build the sync commands to send to WeeChat.""" | ||||
|         cmd =  '\n'.join(_PROTO_SYNC_CMDS) + '\n' | ||||
|         cmd =  '\n'.join(_PROTO_SYNC) + '\n' | ||||
|         return cmd % {'lines': self._lines} | ||||
|  | ||||
|     def handshake_timer_expired(self): | ||||
|         if self.status == STATUS_AUTHENTICATING: | ||||
|             self._pwd_hash_algo = 'plain' | ||||
|             self.send_to_weechat(self._build_init_command()) | ||||
|             self.sync_weechat() | ||||
|             self.set_status(STATUS_CONNECTED) | ||||
|  | ||||
|     def _socket_connected(self): | ||||
|         """Slot: socket connected.""" | ||||
|         self.statusChanged.emit(STATUS_CONNECTED, None) | ||||
|         if self._password: | ||||
|             cmd = self._build_init_command() + self._build_sync_command() | ||||
|             self.send_to_weechat(cmd) | ||||
|         self.set_status(STATUS_AUTHENTICATING) | ||||
|         self.send_to_weechat(_PROTO_HANDSHAKE) | ||||
|         self._handshake_timer = QtCore.QTimer() | ||||
|         self._handshake_timer.setSingleShot(True) | ||||
|         self._handshake_timer.setInterval(2000) | ||||
|         self._handshake_timer.timeout.connect(self.handshake_timer_expired) | ||||
|         self._handshake_timer.start() | ||||
|  | ||||
|     def _socket_read(self): | ||||
|         """Slot: data available on socket.""" | ||||
| @@ -129,11 +223,10 @@ class Network(QtCore.QObject): | ||||
|  | ||||
|     def _socket_disconnected(self): | ||||
|         """Slot: socket disconnected.""" | ||||
|         self._server = None | ||||
|         self._port = None | ||||
|         self._ssl = None | ||||
|         self._password = "" | ||||
|         self.statusChanged.emit(STATUS_DISCONNECTED, None) | ||||
|         if self._handshake_timer: | ||||
|             self._handshake_timer.stop() | ||||
|         self._init_connection() | ||||
|         self.set_status(STATUS_DISCONNECTED) | ||||
|  | ||||
|     def is_connected(self): | ||||
|         """Return True if the socket is connected, False otherwise.""" | ||||
| @@ -143,7 +236,7 @@ class Network(QtCore.QObject): | ||||
|         """Return True if SSL is used, False otherwise.""" | ||||
|         return self._ssl | ||||
|  | ||||
|     def connect_weechat(self, server, port, ssl, password, lines): | ||||
|     def connect_weechat(self, server, port, ssl, password, totp, lines): | ||||
|         """Connect to WeeChat.""" | ||||
|         self._server = server | ||||
|         try: | ||||
| @@ -152,6 +245,7 @@ class Network(QtCore.QObject): | ||||
|             self._port = 0 | ||||
|         self._ssl = ssl | ||||
|         self._password = password | ||||
|         self._totp = totp | ||||
|         try: | ||||
|             self._lines = int(lines) | ||||
|         except ValueError: | ||||
| @@ -165,23 +259,40 @@ class Network(QtCore.QObject): | ||||
|             self._socket.connectToHostEncrypted(self._server, self._port) | ||||
|         else: | ||||
|             self._socket.connectToHost(self._server, self._port) | ||||
|         self.statusChanged.emit(STATUS_CONNECTING, "") | ||||
|         self.set_status(STATUS_CONNECTING) | ||||
|  | ||||
|     def disconnect_weechat(self): | ||||
|         """Disconnect from WeeChat.""" | ||||
|         if self._socket.state() == QtNetwork.QAbstractSocket.UnconnectedState: | ||||
|             self.set_status(STATUS_DISCONNECTED) | ||||
|             return | ||||
|         if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState: | ||||
|             self.send_to_weechat('quit\n') | ||||
|             self._socket.waitForBytesWritten(1000) | ||||
|         else: | ||||
|             self.statusChanged.emit(STATUS_DISCONNECTED, None) | ||||
|             self.set_status(STATUS_DISCONNECTED) | ||||
|         self._socket.abort() | ||||
|  | ||||
|     def send_to_weechat(self, message): | ||||
|         """Send a message to WeeChat.""" | ||||
|         self.debug_print(0, '<==', message, forcecolor='#AA0000') | ||||
|         self._socket.write(message.encode('utf-8')) | ||||
|  | ||||
|     def init_with_handshake(self, response): | ||||
|         """Initialize with WeeChat using the handshake response.""" | ||||
|         self._pwd_hash_algo = response['password_hash_algo'] | ||||
|         self._pwd_hash_iter = int(response['password_hash_iterations']) | ||||
|         self._server_nonce = bytearray.fromhex(response['nonce']) | ||||
|         if self._pwd_hash_algo: | ||||
|             cmd = self._build_init_command() | ||||
|             if cmd: | ||||
|                 self.send_to_weechat(cmd) | ||||
|                 self.sync_weechat() | ||||
|                 self.set_status(STATUS_CONNECTED) | ||||
|                 return | ||||
|         # failed to initialize: disconnect | ||||
|         self.disconnect_weechat() | ||||
|  | ||||
|     def desync_weechat(self): | ||||
|         """Desynchronize from WeeChat.""" | ||||
|         self.send_to_weechat('desync\n') | ||||
| @@ -211,3 +322,35 @@ class Network(QtCore.QObject): | ||||
|             'password': self._password, | ||||
|             'lines': str(self._lines), | ||||
|         } | ||||
|  | ||||
|     def debug_print(self, *args, **kwargs): | ||||
|         """Display a debug message.""" | ||||
|         self.debug_lines.append((args, kwargs)) | ||||
|         if self.debug_dialog: | ||||
|             self.debug_dialog.chat.display(*args, **kwargs) | ||||
|  | ||||
|     def _debug_dialog_closed(self, result): | ||||
|         """Called when debug dialog is closed.""" | ||||
|         self.debug_dialog = None | ||||
|  | ||||
|     def debug_input_text_sent(self, text): | ||||
|         """Send debug buffer input to WeeChat.""" | ||||
|         if self.network.is_connected(): | ||||
|             text = str(text) | ||||
|             pos = text.find(')') | ||||
|             if text.startswith('(') and pos >= 0: | ||||
|                 text = '(debug_%s)%s' % (text[1:pos], text[pos+1:]) | ||||
|             else: | ||||
|                 text = '(debug) %s' % text | ||||
|             self.network.debug_print(0, '<==', text, forcecolor='#AA0000') | ||||
|             self.network.send_to_weechat(text + '\n') | ||||
|  | ||||
|     def open_debug_dialog(self): | ||||
|         """Open a dialog with debug messages.""" | ||||
|         if not self.debug_dialog: | ||||
|             self.debug_dialog = DebugDialog() | ||||
|             self.debug_dialog.input.textSent.connect( | ||||
|                 self.debug_input_text_sent) | ||||
|             self.debug_dialog.finished.connect(self._debug_dialog_closed) | ||||
|             self.debug_dialog.display_lines(self.debug_lines) | ||||
|             self.debug_dialog.chat.scroll_bottom() | ||||
|   | ||||
							
								
								
									
										57
									
								
								qweechat/preferences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								qweechat/preferences.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # preferences.py - preferences dialog box | ||||
| # | ||||
| # Copyright (C) 2011-2021 Sébastien Helleu <flashcode@flashtux.org> | ||||
| # | ||||
| # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | ||||
| # | ||||
| # QWeeChat is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # QWeeChat is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
|  | ||||
| """Preferences dialog box.""" | ||||
|  | ||||
| from PySide6 import QtCore, QtWidgets as QtGui | ||||
|  | ||||
|  | ||||
| class PreferencesDialog(QtGui.QDialog): | ||||
|     """Preferences dialog.""" | ||||
|  | ||||
|     def __init__(self, *args): | ||||
|         QtGui.QDialog.__init__(*(self,) + args) | ||||
|         self.setModal(True) | ||||
|         self.setWindowTitle('Preferences') | ||||
|  | ||||
|         close_button = QtGui.QPushButton('Close') | ||||
|         close_button.pressed.connect(self.close) | ||||
|  | ||||
|         hbox = QtGui.QHBoxLayout() | ||||
|         hbox.addStretch(1) | ||||
|         hbox.addWidget(close_button) | ||||
|         hbox.addStretch(1) | ||||
|  | ||||
|         vbox = QtGui.QVBoxLayout() | ||||
|  | ||||
|         label = QtGui.QLabel('Not yet implemented!') | ||||
|         label.setAlignment(QtCore.Qt.AlignHCenter) | ||||
|         vbox.addWidget(label) | ||||
|  | ||||
|         label = QtGui.QLabel('') | ||||
|         label.setAlignment(QtCore.Qt.AlignHCenter) | ||||
|         vbox.addWidget(label) | ||||
|  | ||||
|         vbox.addLayout(hbox) | ||||
|  | ||||
|         self.setLayout(vbox) | ||||
|         self.show() | ||||
| @@ -40,21 +40,18 @@ from pkg_resources import resource_filename | ||||
| from PySide6 import QtCore, QtGui, QtWidgets | ||||
|  | ||||
| from qweechat import config | ||||
| from qweechat.weechat import protocol | ||||
| from qweechat.network import Network, STATUS_DISCONNECTED, NETWORK_STATUS | ||||
| from qweechat.connection import ConnectionDialog | ||||
| from qweechat.buffer import BufferListWidget, Buffer | ||||
| from qweechat.debug import DebugDialog | ||||
| from qweechat.about import AboutDialog | ||||
| from qweechat.buffer import BufferListWidget, Buffer | ||||
| from qweechat.connection import ConnectionDialog | ||||
| from qweechat.network import Network, STATUS_DISCONNECTED, NETWORK_STATUS | ||||
| from qweechat.preferences import PreferencesDialog | ||||
| from qweechat.weechat import protocol | ||||
|  | ||||
|  | ||||
| APP_NAME = 'QWeeChat' | ||||
| AUTHOR = 'Sébastien Helleu' | ||||
| WEECHAT_SITE = 'https://weechat.org/' | ||||
|  | ||||
| # number of lines in buffer for debug window | ||||
| DEBUG_NUM_LINES = 50 | ||||
|  | ||||
|  | ||||
| class MainWindow(QtWidgets.QMainWindow): | ||||
|     """Main window.""" | ||||
| @@ -67,9 +64,6 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|         self.resize(1000, 600) | ||||
|         self.setWindowTitle(APP_NAME) | ||||
|  | ||||
|         self.debug_dialog = None | ||||
|         self.debug_lines = [] | ||||
|  | ||||
|         self.about_dialog = None | ||||
|         self.connection_dialog = None | ||||
|         self.preferences_dialog = None | ||||
| @@ -101,26 +95,47 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|         # actions for menu and toolbar | ||||
|         actions_def = { | ||||
|             'connect': [ | ||||
|                 'network-connect.png', 'Connect to WeeChat', | ||||
|                 'Ctrl+O', self.open_connection_dialog], | ||||
|                 'network-connect.png', | ||||
|                 'Connect to WeeChat', | ||||
|                 'Ctrl+O', | ||||
|                 self.open_connection_dialog, | ||||
|             ], | ||||
|             'disconnect': [ | ||||
|                 'network-disconnect.png', 'Disconnect from WeeChat', | ||||
|                 'Ctrl+D', self.network.disconnect_weechat], | ||||
|                 'network-disconnect.png', | ||||
|                 'Disconnect from WeeChat', | ||||
|                 'Ctrl+D', | ||||
|                 self.network.disconnect_weechat, | ||||
|             ], | ||||
|             'debug': [ | ||||
|                 'edit-find.png', 'Debug console window', | ||||
|                 'Ctrl+B', self.open_debug_dialog], | ||||
|                 'edit-find.png', | ||||
|                 'Open debug console window', | ||||
|                 'Ctrl+B', | ||||
|                 self.network.open_debug_dialog, | ||||
|             ], | ||||
|             'preferences': [ | ||||
|                 'preferences-other.png', 'Preferences', | ||||
|                 'Ctrl+P', self.open_preferences_dialog], | ||||
|                 'preferences-other.png', | ||||
|                 'Change preferences', | ||||
|                 'Ctrl+P', | ||||
|                 self.open_preferences_dialog, | ||||
|             ], | ||||
|             'about': [ | ||||
|                 'help-about.png', 'About', | ||||
|                 'Ctrl+H', self.open_about_dialog], | ||||
|                 'help-about.png', | ||||
|                 'About QWeeChat', | ||||
|                 'Ctrl+H', | ||||
|                 self.open_about_dialog, | ||||
|             ], | ||||
|             'save connection': [ | ||||
|                 'document-save.png', 'Save connection configuration', | ||||
|                 'Ctrl+S', self.save_connection], | ||||
|                 'document-save.png', | ||||
|                 'Save connection configuration', | ||||
|                 'Ctrl+S', | ||||
|                 self.save_connection, | ||||
|             ], | ||||
|             'quit': [ | ||||
|                 'application-exit.png', 'Quit application', | ||||
|                 'Ctrl+Q', self.close], | ||||
|                 'application-exit.png', | ||||
|                 'Quit application', | ||||
|                 'Ctrl+Q', | ||||
|                 self.close, | ||||
|             ], | ||||
|         } | ||||
|         self.actions = {} | ||||
|         for name, action in list(actions_def.items()): | ||||
| @@ -128,7 +143,7 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|                 QtGui.QIcon( | ||||
|                     resource_filename(__name__, 'data/icons/%s' % action[0])), | ||||
|                 name.capitalize(), self) | ||||
|             self.actions[name].setStatusTip(action[1]) | ||||
|             self.actions[name].setToolTip(f'{action[1]} ({action[2]})') | ||||
|             self.actions[name].setShortcut(action[2]) | ||||
|             self.actions[name].triggered.connect(action[3]) | ||||
|  | ||||
| @@ -168,16 +183,18 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|  | ||||
|         # open debug dialog | ||||
|         if self.config.getboolean('look', 'debug'): | ||||
|             self.open_debug_dialog() | ||||
|             self.network.open_debug_dialog() | ||||
|  | ||||
|         # auto-connect to relay | ||||
|         if self.config.getboolean('relay', 'autoconnect'): | ||||
|             self.network.connect_weechat(self.config.get('relay', 'server'), | ||||
|                                          self.config.get('relay', 'port'), | ||||
|                                          self.config.getboolean('relay', | ||||
|                                                                 'ssl'), | ||||
|                                          self.config.get('relay', 'password'), | ||||
|                                          self.config.get('relay', 'lines')) | ||||
|             self.network.connect_weechat( | ||||
|                 server=self.config.get('relay', 'server'), | ||||
|                 port=self.config.get('relay', 'port'), | ||||
|                 ssl=self.config.getboolean('relay', 'ssl'), | ||||
|                 password=self.config.get('relay', 'password'), | ||||
|                 totp=None, | ||||
|                 lines=self.config.get('relay', 'lines'), | ||||
|             ) | ||||
|  | ||||
|         self.show() | ||||
|  | ||||
| @@ -192,14 +209,12 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|         if self.network.is_connected(): | ||||
|             message = 'input %s %s\n' % (full_name, text) | ||||
|             self.network.send_to_weechat(message) | ||||
|             self.debug_display(0, '<==', message, forcecolor='#AA0000') | ||||
|             self.network.debug_print(0, '<==', message, forcecolor='#AA0000') | ||||
|  | ||||
|     def open_preferences_dialog(self): | ||||
|         """Open a dialog with preferences.""" | ||||
|         # TODO: implement the preferences dialog box | ||||
|         messages = ['Not yet implemented!', | ||||
|                     ''] | ||||
|         self.preferences_dialog = AboutDialog('Preferences', messages, self) | ||||
|         self.preferences_dialog = PreferencesDialog(self) | ||||
|  | ||||
|     def save_connection(self): | ||||
|         """Save connection configuration.""" | ||||
| @@ -208,39 +223,6 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|             for option in options: | ||||
|                 self.config.set('relay', option, options[option]) | ||||
|  | ||||
|     def debug_display(self, *args, **kwargs): | ||||
|         """Display a debug message.""" | ||||
|         self.debug_lines.append((args, kwargs)) | ||||
|         self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:] | ||||
|         if self.debug_dialog: | ||||
|             self.debug_dialog.chat.display(*args, **kwargs) | ||||
|  | ||||
|     def open_debug_dialog(self): | ||||
|         """Open a dialog with debug messages.""" | ||||
|         if not self.debug_dialog: | ||||
|             self.debug_dialog = DebugDialog(self) | ||||
|             self.debug_dialog.input.textSent.connect( | ||||
|                 self.debug_input_text_sent) | ||||
|             self.debug_dialog.finished.connect(self._debug_dialog_closed) | ||||
|             self.debug_dialog.display_lines(self.debug_lines) | ||||
|             self.debug_dialog.chat.scroll_bottom() | ||||
|  | ||||
|     def debug_input_text_sent(self, text): | ||||
|         """Send debug buffer input to WeeChat.""" | ||||
|         if self.network.is_connected(): | ||||
|             text = str(text) | ||||
|             pos = text.find(')') | ||||
|             if text.startswith('(') and pos >= 0: | ||||
|                 text = '(debug_%s)%s' % (text[1:pos], text[pos+1:]) | ||||
|             else: | ||||
|                 text = '(debug) %s' % text | ||||
|             self.debug_display(0, '<==', text, forcecolor='#AA0000') | ||||
|             self.network.send_to_weechat(text + '\n') | ||||
|  | ||||
|     def _debug_dialog_closed(self, result): | ||||
|         """Called when debug dialog is closed.""" | ||||
|         self.debug_dialog = None | ||||
|  | ||||
|     def open_about_dialog(self): | ||||
|         """Open a dialog with info about QWeeChat.""" | ||||
|         self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self) | ||||
| @@ -257,18 +239,20 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|     def connect_weechat(self): | ||||
|         """Connect to WeeChat.""" | ||||
|         self.network.connect_weechat( | ||||
|             self.connection_dialog.fields['server'].text(), | ||||
|             self.connection_dialog.fields['port'].text(), | ||||
|             self.connection_dialog.fields['ssl'].isChecked(), | ||||
|             self.connection_dialog.fields['password'].text(), | ||||
|             int(self.connection_dialog.fields['lines'].text())) | ||||
|             server=self.connection_dialog.fields['server'].text(), | ||||
|             port=self.connection_dialog.fields['port'].text(), | ||||
|             ssl=self.connection_dialog.fields['ssl'].isChecked(), | ||||
|             password=self.connection_dialog.fields['password'].text(), | ||||
|             totp=self.connection_dialog.fields['totp'].text(), | ||||
|             lines=int(self.connection_dialog.fields['lines'].text()), | ||||
|         ) | ||||
|         self.connection_dialog.close() | ||||
|  | ||||
|     def _network_status_changed(self, status, extra): | ||||
|         """Called when the network status has changed.""" | ||||
|         if self.config.getboolean('look', 'statusbar'): | ||||
|             self.statusBar().showMessage(status) | ||||
|         self.debug_display(0, '', status, forcecolor='#0000AA') | ||||
|         self.network.debug_print(0, '', status, forcecolor='#0000AA') | ||||
|         self.network_status_set(status) | ||||
|  | ||||
|     def network_status_set(self, status): | ||||
| @@ -296,30 +280,40 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|  | ||||
|     def _network_weechat_msg(self, message): | ||||
|         """Called when a message is received from WeeChat.""" | ||||
|         self.debug_display(0, '==>', | ||||
|                            'message (%d bytes):\n%s' | ||||
|                            % (len(message), | ||||
|                               protocol.hex_and_ascii(message.data(), 20)), | ||||
|                            forcecolor='#008800') | ||||
|         self.network.debug_print( | ||||
|             0, '==>', | ||||
|             'message (%d bytes):\n%s' | ||||
|             % (len(message), | ||||
|                protocol.hex_and_ascii(message.data(), 20)), | ||||
|             forcecolor='#008800', | ||||
|         ) | ||||
|         try: | ||||
|             proto = protocol.Protocol() | ||||
|             message = proto.decode(message.data()) | ||||
|             if message.uncompressed: | ||||
|                 self.debug_display( | ||||
|                 self.network.debug_print( | ||||
|                     0, '==>', | ||||
|                     'message uncompressed (%d bytes):\n%s' | ||||
|                     % (message.size_uncompressed, | ||||
|                        protocol.hex_and_ascii(message.uncompressed, 20)), | ||||
|                     forcecolor='#008800') | ||||
|             self.debug_display(0, '', 'Message: %s' % message) | ||||
|             self.network.debug_print(0, '', 'Message: %s' % message) | ||||
|             self.parse_message(message) | ||||
|         except Exception:  # noqa: E722 | ||||
|             print('Error while decoding message from WeeChat:\n%s' | ||||
|                   % traceback.format_exc()) | ||||
|             self.network.disconnect_weechat() | ||||
|  | ||||
|     def _parse_handshake(self, message): | ||||
|         """Parse a WeeChat message with handshake response.""" | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'htb': | ||||
|                 continue | ||||
|             self.network.init_with_handshake(obj.value) | ||||
|             break | ||||
|  | ||||
|     def _parse_listbuffers(self, message): | ||||
|         """Parse a WeeChat with list of buffers.""" | ||||
|         """Parse a WeeChat message with list of buffers.""" | ||||
|         for obj in message.objects: | ||||
|             if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer': | ||||
|                 continue | ||||
| @@ -462,7 +456,9 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|     def parse_message(self, message): | ||||
|         """Parse a WeeChat message.""" | ||||
|         if message.msgid.startswith('debug'): | ||||
|             self.debug_display(0, '', '(debug message, ignored)') | ||||
|             self.network.debug_print(0, '', '(debug message, ignored)') | ||||
|         elif message.msgid == 'handshake': | ||||
|             self._parse_handshake(message) | ||||
|         elif message.msgid == 'listbuffers': | ||||
|             self._parse_listbuffers(message) | ||||
|         elif message.msgid in ('listlines', '_buffer_line_added'): | ||||
| @@ -526,8 +522,8 @@ class MainWindow(QtWidgets.QMainWindow): | ||||
|     def closeEvent(self, event): | ||||
|         """Called when QWeeChat window is closed.""" | ||||
|         self.network.disconnect_weechat() | ||||
|         if self.debug_dialog: | ||||
|             self.debug_dialog.close() | ||||
|         if self.network.debug_dialog: | ||||
|             self.network.debug_dialog.close() | ||||
|         config.write(self.config) | ||||
|         QtWidgets.QMainWindow.closeEvent(self, event) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user