Add support of TOTP and password hash (WeeChat >= 2.9)

This commit is contained in:
Sébastien Helleu 2021-11-14 18:40:26 +01:00
parent cca2a0fa68
commit 3b0947c9ef
7 changed files with 389 additions and 135 deletions

View file

@ -45,6 +45,7 @@ In QWeeChat, click on connect and enter fields:
- `server`: the IP address or hostname of your machine with WeeChat running - `server`: the IP address or hostname of your machine with WeeChat running
- `port`: the relay port (defined in WeeChat) - `port`: the relay port (defined in WeeChat)
- `password`: the relay password (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`. Options can be changed in file `~/.config/qweechat/qweechat.conf`.

View file

@ -32,35 +32,91 @@ class ConnectionDialog(QtWidgets.QDialog):
super().__init__(*args) super().__init__(*args)
self.values = values self.values = values
self.setModal(True) self.setModal(True)
self.setWindowTitle('Connect to WeeChat')
grid = QtWidgets.QGridLayout() grid = QtWidgets.QGridLayout()
grid.setSpacing(10) grid.setSpacing(10)
self.fields = {} self.fields = {}
for line, field in enumerate(('server', 'port', 'password', 'lines')): focus = None
grid.addWidget(QtWidgets.QLabel(field.capitalize()), line, 0)
line_edit = QtWidgets.QLineEdit() # server
line_edit.setFixedWidth(200) grid.addWidget(QtWidgets.QLabel('<b>Server</b>'), 0, 0)
if field == 'password': line_edit = QtWidgets.QLineEdit()
line_edit.setEchoMode(QtWidgets.QLineEdit.Password) line_edit.setFixedWidth(200)
if field == 'lines': value = self.values.get('server', '')
validator = QtGui.QIntValidator(0, 2147483647, self) line_edit.insert(value)
line_edit.setValidator(validator) grid.addWidget(line_edit, 0, 1)
line_edit.setFixedWidth(80) self.fields['server'] = line_edit
line_edit.insert(self.values[field]) if not focus and not value:
grid.addWidget(line_edit, line, 1) focus = 'server'
self.fields[field] = line_edit
if field == 'port': # port / SSL
ssl = QtWidgets.QCheckBox('SSL') grid.addWidget(QtWidgets.QLabel('<b>Port</b>'), 1, 0)
ssl.setChecked(self.values['ssl'] == 'on') line_edit = QtWidgets.QLineEdit()
grid.addWidget(ssl, line, 2) line_edit.setFixedWidth(200)
self.fields['ssl'] = ssl 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 = QtWidgets.QDialogButtonBox()
self.dialog_buttons.setStandardButtons( self.dialog_buttons.setStandardButtons(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.dialog_buttons.rejected.connect(self.close) 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.setLayout(grid)
self.show() self.show()
if focus:
self.fields[focus].setFocus()

View file

@ -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, Files: application-exit.png, dialog-close.png, dialog-ok-apply.png,
dialog-warning.png, document-save.png, edit-find.png, help-about.png, dialog-password.png, dialog-warning.png, document-save.png,
network-connect.png, network-disconnect.png, preferences-other.png edit-find.png, help-about.png, network-connect.png,
network-disconnect.png, preferences-other.png
Files come from Debian package "oxygen-icon-theme": Files come from Debian package "oxygen-icon-theme":

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

View file

@ -22,19 +22,38 @@
"""I/O with WeeChat/relay.""" """I/O with WeeChat/relay."""
import hashlib
import secrets
import struct import struct
from PySide6 import QtCore, QtNetwork from PySide6 import QtCore, QtNetwork
from qweechat import config from qweechat import config
from qweechat.debug import DebugDialog
_PROTO_INIT_CMD = [ # list of supported hash algorithms on our side
# initialize with the password # (the hash algorithm will be negotiated with the remote WeeChat)
'init password=%(password)s', _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 # get buffers
'(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,' '(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,'
'type,nicklist,title,local_variables', 'type,nicklist,title,local_variables',
@ -49,26 +68,33 @@ _PROTO_SYNC_CMDS = [
STATUS_DISCONNECTED = 'disconnected' STATUS_DISCONNECTED = 'disconnected'
STATUS_CONNECTING = 'connecting' STATUS_CONNECTING = 'connecting'
STATUS_AUTHENTICATING = 'authenticating'
STATUS_CONNECTED = 'connected' STATUS_CONNECTED = 'connected'
NETWORK_STATUS = { NETWORK_STATUS = {
'disconnected': { STATUS_DISCONNECTED: {
'label': 'Disconnected', 'label': 'Disconnected',
'color': '#aa0000', 'color': '#aa0000',
'icon': 'dialog-close.png', 'icon': 'dialog-close.png',
}, },
'connecting': { STATUS_CONNECTING: {
'label': 'Connecting…', 'label': 'Connecting…',
'color': '#ff7f00', 'color': '#dd5f00',
'icon': 'dialog-warning.png', 'icon': 'dialog-warning.png',
}, },
'connected': { STATUS_AUTHENTICATING: {
'label': 'Authenticating…',
'color': '#007fff',
'icon': 'dialog-password.png',
},
STATUS_CONNECTED: {
'label': 'Connected', 'label': 'Connected',
'color': 'green', 'color': 'green',
'icon': 'dialog-ok-apply.png', 'icon': 'dialog-ok-apply.png',
}, },
} }
class Network(QtCore.QObject): class Network(QtCore.QObject):
"""I/O with WeeChat/relay.""" """I/O with WeeChat/relay."""
@ -77,10 +103,9 @@ class Network(QtCore.QObject):
def __init__(self, *args): def __init__(self, *args):
super().__init__(*args) super().__init__(*args)
self._server = None self._init_connection()
self._port = None self.debug_lines = []
self._ssl = None self.debug_dialog = None
self._password = None
self._lines = config.CONFIG_DEFAULT_RELAY_LINES self._lines = config.CONFIG_DEFAULT_RELAY_LINES
self._buffer = QtCore.QByteArray() self._buffer = QtCore.QByteArray()
self._socket = QtNetwork.QSslSocket() self._socket = QtNetwork.QSslSocket()
@ -88,22 +113,91 @@ class Network(QtCore.QObject):
self._socket.readyRead.connect(self._socket_read) self._socket.readyRead.connect(self._socket_read)
self._socket.disconnected.connect(self._socket_disconnected) 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): def _build_init_command(self):
"""Build the init command to send to WeeChat.""" """Build the init command to send to WeeChat."""
cmd = '\n'.join(_PROTO_INIT_CMD) + '\n' totp = f',totp={self._totp}' if self._totp else ''
return cmd % {'password': self._password} 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): def _build_sync_command(self):
"""Build the sync commands to send to WeeChat.""" """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} 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): def _socket_connected(self):
"""Slot: socket connected.""" """Slot: socket connected."""
self.statusChanged.emit(STATUS_CONNECTED, None) self.set_status(STATUS_AUTHENTICATING)
if self._password: self.send_to_weechat(_PROTO_HANDSHAKE)
cmd = self._build_init_command() + self._build_sync_command() self._handshake_timer = QtCore.QTimer()
self.send_to_weechat(cmd) 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): def _socket_read(self):
"""Slot: data available on socket.""" """Slot: data available on socket."""
@ -129,11 +223,10 @@ class Network(QtCore.QObject):
def _socket_disconnected(self): def _socket_disconnected(self):
"""Slot: socket disconnected.""" """Slot: socket disconnected."""
self._server = None if self._handshake_timer:
self._port = None self._handshake_timer.stop()
self._ssl = None self._init_connection()
self._password = "" self.set_status(STATUS_DISCONNECTED)
self.statusChanged.emit(STATUS_DISCONNECTED, None)
def is_connected(self): def is_connected(self):
"""Return True if the socket is connected, False otherwise.""" """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 True if SSL is used, False otherwise."""
return self._ssl 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.""" """Connect to WeeChat."""
self._server = server self._server = server
try: try:
@ -152,6 +245,7 @@ class Network(QtCore.QObject):
self._port = 0 self._port = 0
self._ssl = ssl self._ssl = ssl
self._password = password self._password = password
self._totp = totp
try: try:
self._lines = int(lines) self._lines = int(lines)
except ValueError: except ValueError:
@ -165,23 +259,40 @@ class Network(QtCore.QObject):
self._socket.connectToHostEncrypted(self._server, self._port) self._socket.connectToHostEncrypted(self._server, self._port)
else: else:
self._socket.connectToHost(self._server, self._port) self._socket.connectToHost(self._server, self._port)
self.statusChanged.emit(STATUS_CONNECTING, "") self.set_status(STATUS_CONNECTING)
def disconnect_weechat(self): def disconnect_weechat(self):
"""Disconnect from WeeChat.""" """Disconnect from WeeChat."""
if self._socket.state() == QtNetwork.QAbstractSocket.UnconnectedState: if self._socket.state() == QtNetwork.QAbstractSocket.UnconnectedState:
self.set_status(STATUS_DISCONNECTED)
return return
if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState: if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
self.send_to_weechat('quit\n') self.send_to_weechat('quit\n')
self._socket.waitForBytesWritten(1000) self._socket.waitForBytesWritten(1000)
else: else:
self.statusChanged.emit(STATUS_DISCONNECTED, None) self.set_status(STATUS_DISCONNECTED)
self._socket.abort() self._socket.abort()
def send_to_weechat(self, message): def send_to_weechat(self, message):
"""Send a message to WeeChat.""" """Send a message to WeeChat."""
self.debug_print(0, '<==', message, forcecolor='#AA0000')
self._socket.write(message.encode('utf-8')) 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): def desync_weechat(self):
"""Desynchronize from WeeChat.""" """Desynchronize from WeeChat."""
self.send_to_weechat('desync\n') self.send_to_weechat('desync\n')
@ -211,3 +322,35 @@ class Network(QtCore.QObject):
'password': self._password, 'password': self._password,
'lines': str(self._lines), '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
View 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()

View file

@ -40,21 +40,18 @@ from pkg_resources import resource_filename
from PySide6 import QtCore, QtGui, QtWidgets from PySide6 import QtCore, QtGui, QtWidgets
from qweechat import config 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.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' APP_NAME = 'QWeeChat'
AUTHOR = 'Sébastien Helleu' AUTHOR = 'Sébastien Helleu'
WEECHAT_SITE = 'https://weechat.org/' WEECHAT_SITE = 'https://weechat.org/'
# number of lines in buffer for debug window
DEBUG_NUM_LINES = 50
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
"""Main window.""" """Main window."""
@ -67,9 +64,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.resize(1000, 600) self.resize(1000, 600)
self.setWindowTitle(APP_NAME) self.setWindowTitle(APP_NAME)
self.debug_dialog = None
self.debug_lines = []
self.about_dialog = None self.about_dialog = None
self.connection_dialog = None self.connection_dialog = None
self.preferences_dialog = None self.preferences_dialog = None
@ -101,26 +95,47 @@ class MainWindow(QtWidgets.QMainWindow):
# actions for menu and toolbar # actions for menu and toolbar
actions_def = { actions_def = {
'connect': [ 'connect': [
'network-connect.png', 'Connect to WeeChat', 'network-connect.png',
'Ctrl+O', self.open_connection_dialog], 'Connect to WeeChat',
'Ctrl+O',
self.open_connection_dialog,
],
'disconnect': [ 'disconnect': [
'network-disconnect.png', 'Disconnect from WeeChat', 'network-disconnect.png',
'Ctrl+D', self.network.disconnect_weechat], 'Disconnect from WeeChat',
'Ctrl+D',
self.network.disconnect_weechat,
],
'debug': [ 'debug': [
'edit-find.png', 'Debug console window', 'edit-find.png',
'Ctrl+B', self.open_debug_dialog], 'Open debug console window',
'Ctrl+B',
self.network.open_debug_dialog,
],
'preferences': [ 'preferences': [
'preferences-other.png', 'Preferences', 'preferences-other.png',
'Ctrl+P', self.open_preferences_dialog], 'Change preferences',
'Ctrl+P',
self.open_preferences_dialog,
],
'about': [ 'about': [
'help-about.png', 'About', 'help-about.png',
'Ctrl+H', self.open_about_dialog], 'About QWeeChat',
'Ctrl+H',
self.open_about_dialog,
],
'save connection': [ 'save connection': [
'document-save.png', 'Save connection configuration', 'document-save.png',
'Ctrl+S', self.save_connection], 'Save connection configuration',
'Ctrl+S',
self.save_connection,
],
'quit': [ 'quit': [
'application-exit.png', 'Quit application', 'application-exit.png',
'Ctrl+Q', self.close], 'Quit application',
'Ctrl+Q',
self.close,
],
} }
self.actions = {} self.actions = {}
for name, action in list(actions_def.items()): for name, action in list(actions_def.items()):
@ -128,7 +143,7 @@ class MainWindow(QtWidgets.QMainWindow):
QtGui.QIcon( QtGui.QIcon(
resource_filename(__name__, 'data/icons/%s' % action[0])), resource_filename(__name__, 'data/icons/%s' % action[0])),
name.capitalize(), self) 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].setShortcut(action[2])
self.actions[name].triggered.connect(action[3]) self.actions[name].triggered.connect(action[3])
@ -168,16 +183,18 @@ class MainWindow(QtWidgets.QMainWindow):
# open debug dialog # open debug dialog
if self.config.getboolean('look', 'debug'): if self.config.getboolean('look', 'debug'):
self.open_debug_dialog() self.network.open_debug_dialog()
# auto-connect to relay # auto-connect to relay
if self.config.getboolean('relay', 'autoconnect'): if self.config.getboolean('relay', 'autoconnect'):
self.network.connect_weechat(self.config.get('relay', 'server'), self.network.connect_weechat(
self.config.get('relay', 'port'), server=self.config.get('relay', 'server'),
self.config.getboolean('relay', port=self.config.get('relay', 'port'),
'ssl'), ssl=self.config.getboolean('relay', 'ssl'),
self.config.get('relay', 'password'), password=self.config.get('relay', 'password'),
self.config.get('relay', 'lines')) totp=None,
lines=self.config.get('relay', 'lines'),
)
self.show() self.show()
@ -192,14 +209,12 @@ class MainWindow(QtWidgets.QMainWindow):
if self.network.is_connected(): if self.network.is_connected():
message = 'input %s %s\n' % (full_name, text) message = 'input %s %s\n' % (full_name, text)
self.network.send_to_weechat(message) 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): def open_preferences_dialog(self):
"""Open a dialog with preferences.""" """Open a dialog with preferences."""
# TODO: implement the preferences dialog box # TODO: implement the preferences dialog box
messages = ['Not yet implemented!', self.preferences_dialog = PreferencesDialog(self)
'']
self.preferences_dialog = AboutDialog('Preferences', messages, self)
def save_connection(self): def save_connection(self):
"""Save connection configuration.""" """Save connection configuration."""
@ -208,39 +223,6 @@ class MainWindow(QtWidgets.QMainWindow):
for option in options: for option in options:
self.config.set('relay', option, options[option]) 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): def open_about_dialog(self):
"""Open a dialog with info about QWeeChat.""" """Open a dialog with info about QWeeChat."""
self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self) self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self)
@ -257,18 +239,20 @@ class MainWindow(QtWidgets.QMainWindow):
def connect_weechat(self): def connect_weechat(self):
"""Connect to WeeChat.""" """Connect to WeeChat."""
self.network.connect_weechat( self.network.connect_weechat(
self.connection_dialog.fields['server'].text(), server=self.connection_dialog.fields['server'].text(),
self.connection_dialog.fields['port'].text(), port=self.connection_dialog.fields['port'].text(),
self.connection_dialog.fields['ssl'].isChecked(), ssl=self.connection_dialog.fields['ssl'].isChecked(),
self.connection_dialog.fields['password'].text(), password=self.connection_dialog.fields['password'].text(),
int(self.connection_dialog.fields['lines'].text())) totp=self.connection_dialog.fields['totp'].text(),
lines=int(self.connection_dialog.fields['lines'].text()),
)
self.connection_dialog.close() self.connection_dialog.close()
def _network_status_changed(self, status, extra): def _network_status_changed(self, status, extra):
"""Called when the network status has changed.""" """Called when the network status has changed."""
if self.config.getboolean('look', 'statusbar'): if self.config.getboolean('look', 'statusbar'):
self.statusBar().showMessage(status) self.statusBar().showMessage(status)
self.debug_display(0, '', status, forcecolor='#0000AA') self.network.debug_print(0, '', status, forcecolor='#0000AA')
self.network_status_set(status) self.network_status_set(status)
def network_status_set(self, status): def network_status_set(self, status):
@ -296,30 +280,40 @@ class MainWindow(QtWidgets.QMainWindow):
def _network_weechat_msg(self, message): def _network_weechat_msg(self, message):
"""Called when a message is received from WeeChat.""" """Called when a message is received from WeeChat."""
self.debug_display(0, '==>', self.network.debug_print(
'message (%d bytes):\n%s' 0, '==>',
% (len(message), 'message (%d bytes):\n%s'
protocol.hex_and_ascii(message.data(), 20)), % (len(message),
forcecolor='#008800') protocol.hex_and_ascii(message.data(), 20)),
forcecolor='#008800',
)
try: try:
proto = protocol.Protocol() proto = protocol.Protocol()
message = proto.decode(message.data()) message = proto.decode(message.data())
if message.uncompressed: if message.uncompressed:
self.debug_display( self.network.debug_print(
0, '==>', 0, '==>',
'message uncompressed (%d bytes):\n%s' 'message uncompressed (%d bytes):\n%s'
% (message.size_uncompressed, % (message.size_uncompressed,
protocol.hex_and_ascii(message.uncompressed, 20)), protocol.hex_and_ascii(message.uncompressed, 20)),
forcecolor='#008800') forcecolor='#008800')
self.debug_display(0, '', 'Message: %s' % message) self.network.debug_print(0, '', 'Message: %s' % message)
self.parse_message(message) self.parse_message(message)
except Exception: # noqa: E722 except Exception: # noqa: E722
print('Error while decoding message from WeeChat:\n%s' print('Error while decoding message from WeeChat:\n%s'
% traceback.format_exc()) % traceback.format_exc())
self.network.disconnect_weechat() 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): 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: for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer': if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue continue
@ -462,7 +456,9 @@ class MainWindow(QtWidgets.QMainWindow):
def parse_message(self, message): def parse_message(self, message):
"""Parse a WeeChat message.""" """Parse a WeeChat message."""
if message.msgid.startswith('debug'): 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': elif message.msgid == 'listbuffers':
self._parse_listbuffers(message) self._parse_listbuffers(message)
elif message.msgid in ('listlines', '_buffer_line_added'): elif message.msgid in ('listlines', '_buffer_line_added'):
@ -526,8 +522,8 @@ class MainWindow(QtWidgets.QMainWindow):
def closeEvent(self, event): def closeEvent(self, event):
"""Called when QWeeChat window is closed.""" """Called when QWeeChat window is closed."""
self.network.disconnect_weechat() self.network.disconnect_weechat()
if self.debug_dialog: if self.network.debug_dialog:
self.debug_dialog.close() self.network.debug_dialog.close()
config.write(self.config) config.write(self.config)
QtWidgets.QMainWindow.closeEvent(self, event) QtWidgets.QMainWindow.closeEvent(self, event)